# copyright (C) 1997-2006 Jean-Luc Fontaine (mailto:jfontain@free.fr)
# this program is free software: please read the COPYRIGHT file enclosed in this package or use the Help Copyright menu

# $Id: myreplication.tcl,v 1.15 2006/03/16 21:29:56 jfontain Exp $


package provide myreplication [lindex {$Revision: 1.15 $} 1]
package require miscellaneous 1


namespace eval myreplication {

    array set data {
        updates 0
        0,label host 0,type dictionary 0,message {database server host name (or IP address) or ODBC DSN (Data Source Name)}
        1,label id 1,type integer 1,message {unique identifier of the server instance in the community of replication partners (all values in this column should be different)}
        2,label delay 2,type integer
            2,message {replication delay in seconds (shows how late the server is in the replication process)}
        3,label role 3,type ascii 3,message {the database server role (master or slave or both)}
        4,label master 4,type dictionary
            4,message {the master database server host name (or IP address) (only if a slave, empty for a master)}
        5,label running 5,type ascii 5,message {whether the slave thread is running (only if a slave, empty for a master)}
        6,label log 6,type dictionary
            6,message {the binary update log file name (the non-index part of which should be common to all servers)}
        7,label position 7,type integer 7,message {the replication position in the binary log}
        8,label error 8,type ascii 8,message {the latest replication related error message} 8,anchor left
        pollTimes {20 10 30 60 120 300}
        persistent 0
        switches {--dsns 1 --hosts 1 --password 1 --user 1}
    }
    set file [open myreplication.htm]
    set data(helpText) [read $file]                                                           ;# initialize HTML help data from file
    close $file

    proc initialize {optionsName} {
        upvar 1 $optionsName options
        variable odbc
        variable connections
        variable data

        set user $::tcl_platform(user)                                                                                 ;# by default
        catch {set user $options(--user)}
        if {[info exists options(--dsns)]} {                                             ;# ODBC mode id data source names specified
            set odbc 1
            package require tclodbc 2                               ;# so that it works with both UNIX 2.2.1 et Windows 2.3 versions
            if {[info exists options(--hosts)]} {
                error {--hosts option incompatible with ODBC mode}
            }
            set hosts [split $options(--dsns) ,]                                         ;# voluntary confusion between DSN and host
        } else {
            set odbc 0
            package require mysqltcl
            if {![info exists options(--hosts)]} {
                error {either --dsns or --hosts option must be specified}
            }
            set hosts [split $options(--hosts) ,]
        }
        set connections {}
        set row 0
        foreach host $hosts {
            if {$odbc} {
                set arguments [list $host]                                                                       ;# host name or DSN
                catch {lappend arguments $user}
                catch {lappend arguments $options(--password)}
                set connection [eval database odbc$row $arguments]                                   ;# use a unique connection name
                $connection {show master status}                                      ;# check that user has the required privileges
                $connection {show slave status}
            } else {
                catch {unset port}
                scan $host {%[^:]:%u} host port                                  ;# port may be specified using the host:port syntax
                if {[info exists port] && [string equal $host localhost]} {
                    error {specifying port useless with local socket connection (localhost as host)}
                }
                set arguments [list -host $host]
                catch {lappend arguments -user $user}
                catch {lappend arguments -password $options(--password)}
                catch {lappend arguments -port $port}
                set connection [eval mysqlconnect $arguments]
                set host [lindex [mysqlinfo $connection host] 0]           ;# work around mysqltcl 3 return value: "host via TCP/IP"
                mysqlsel $connection {show master status} -list                       ;# check that user has the required privileges
                mysqlsel $connection {show slave status} -list
            }
            set data($row,0) $host
            reset $row                                                                                        ;# initialize row data
            lappend connections $connection
            incr row
        }
    }

    # expected results (tested on 4.0.1 servers):
    #
    # - on a master:
    # mysql> show master status;
    # +-------------------+----------+--------------+------------------+
    # | File              | Position | Binlog_do_db | Binlog_ignore_db |
    # +-------------------+----------+--------------+------------------+
    # | localhost-bin.006 | 79       |              |                  |
    # +-------------------+----------+--------------+------------------+
    # mysql> show slave status;
    # +-------------+-------------+-------------+---------------+----------+-----+---------------+-----------------+-
    # | Master_Host | Master_User | Master_Port | Connect_retry | Log_File | Pos | Slave_Running | Replicate_do_db |
    # +-------------+-------------+-------------+---------------+----------+-----+---------------+-----------------+-
    # |             |             | 0           | 0             |          | 0   | No            |                 |
    # +-------------+-------------+-------------+---------------+----------+-----+---------------+-----------------+-
    # -+---------------------+------------+------------+--------------+--------------+
    #  | Replicate_ignore_db | Last_errno | Last_error | Skip_counter | Last_log_seq |
    # -----------------------+------------+------------+--------------+--------------+
    #  |                     | 0          |            | 0            | 0            |
    # -----------------------+------------+------------+--------------+--------------+
    #
    # - on a standalone server;
    # mysql> show master status;
    # +------+----------+--------------+------------------+
    # | File | Position | Binlog_do_db | Binlog_ignore_db |
    # +------+----------+--------------+------------------+
    # | NULL | NULL     | NULL         | NULL             |
    # +------+----------+--------------+------------------+
    # mysql> show slave status; (same as master)
    #
    # - on a slave:
    # mysql> show master status; (same as master)
    # mysql> show slave status;
    # +-------------+-------------+-------------+---------------+-------------------+-----+---------------+-----------------+-
    # | Master_Host | Master_User | Master_Port | Connect_retry | Log_File          | Pos | Slave_Running | Replicate_do_db |
    # +-------------+-------------+-------------+---------------+-------------------+-----+---------------+-----------------+-
    # | 127.0.0.1   | root        | 3306        | 60            | localhost-bin.004 | 79  | No            |                 |
    # +-------------+-------------+-------------+---------------+-------------------+-----+---------------+-----------------+-
    # -+---------------------+------------+----------------------------------------------------------+--------------+--------------+
    #  | Replicate_ignore_db | Last_errno | Last_error                                               | Skip_counter | Last_log_seq |
    # -+---------------------+------------+----------------------------------------------------------+--------------+--------------+
    #  |                     | 1146       | error 'Table 'test.t' doesn't exist' on query 'insert... | 0            | 3219651128   |
    # -+---------------------+------------+----------------------------------------------------------+--------------+--------------+


    proc update {} {
        variable connections
        variable last
        variable data

        set row 0
        foreach connection $connections {
            set delay($row) $data($row,2)                                                ;# save delay for calculations further down
            reset $row                                                                            ;# in case there is a server error
            if {\
                [catch {set masterList [result $connection {show master status}]} message] ||\
                [catch {set slaveList [result $connection {show slave status}]} message] ||\
                [catch {set id [lindex [result $connection {show variables like 'server_id'}] end]} message]\
            } {                                                                                           ;# problem reaching server
### we could try to reconnect here every so often ###
                flashMessage "error: $message"
                continue                                                                              ;# no use trying anything else
            }
            set notMaster [catch {result $connection {show master logs}}]
            set masterFile [lindex $masterList 0]
            set slaveFile [lindex $slaveList 4]
            if {$notMaster} {
                if {[string length $slaveFile] > 0} {                                                       ;# server has slave role
                    set role slave
                } else {
                    set role {}                                                              ;# standalone (should not be monitored)
                }
            } else {                                                                                       ;# server has master role
                if {[string length $masterFile] > 0} {
                    set role master
                    if {[string length $slaveFile] > 0} {                                              ;# server also has slave role
                        set role both
                    }
                } else {
                    set role {}                                                                               ;# should never happen
                }
            }
            if {[string length $role] == 0} continue                                               ;# ignore non replicating servers
            set data($row,1) $id
            set data($row,3) $role
            # at this point, role must be either slave, master or both
            switch $role {
                slave - both {
                    set data($row,4) [lindex $slaveList 0]                                                            ;# master host
                    set data($row,5) [string tolower [lindex $slaveList 6]]                           ;# running (display yes or no)
                    set file $slaveFile
                    set data($row,7) [lindex $slaveList 5]                                                               ;# position
                    set data($row,8) [lindex $slaveList 10]                                                         ;# error message
                }
                master {
                    set file $masterFile
                    set data($row,7) [lindex $masterList 1]                                                              ;# position
                }
            }
            set data($row,6) $file
            if {[regexp {(.+)\.(\d{3})$} $file dummy name index($row)]} {                         ;# log filename format: string.NNN
                set log($name) {}                                                          ;# remember in order to detect duplicates
            } else {
                flashMessage "error: host $data($row,0) invalid log filename: $file"
            }
            incr row
        }
        set clock [expr {[clock clicks -milliseconds]/1000.0}]            ;# store current clock in seconds after any timeout errors
        if {[info exists last(clock)]} {
            set period [expr {round($clock-$last(clock))}]
        }
        if {[array size log] > 1} {
            flashMessage "error: multiple log file names: cannot determine reference server(s)"
        } elseif {[info exists period]} {                                                       ;# delay calculations require period
            set maximumIndex -2147483648                                                               ;# maximum log filename index
            set maximumPosition -2147483648                      ;# maximum log position for the log filename with the maximum index
            set limit $row
            for {set row 0} {$row < $limit} {incr row} {
                if {[catch {set number $index($row)}]} continue                                            ;# ignore invalid entries
                if {$number >= $maximumIndex} {
                    set maximumIndex $number
                    if {$data($row,7) > $maximumPosition} {
                        set maximumPosition $data($row,7)
                    }
                }
            }
            for {set row 0} {$row < $limit} {incr row} {
                if {[catch {set number $index($row)}]} continue                                            ;# ignore invalid entries
                if {[string equal $delay($row) ?]} {set delay($row) 0}              ;# eventually initialize delay if it was unknown
                if {$number < $maximumIndex} {                                                                     ;# server is late
                    incr delay($row) $period
                } else {
                    if {$data($row,7) < $maximumPosition} {                                                        ;# server is late
                        incr delay($row) $period
                    } else {
                        set delay($row) 0                                                                            ;# synchronized
                    }
                }
                set data($row,2) $delay($row)                                                         ;# finally set displayed value
            }
        }

        set last(clock) $clock
        incr data(updates)
    }

    proc result {connection query} {
        variable odbc

        if {$odbc} {
            return [lindex [$connection $query] 0]
        } else {
            return [mysqlsel $connection $query -flatlist]
        }
    }

    proc reset {row} {                                                                  ;# fill row with unknown values, except host
        variable data

        array set data [list $row,1 ? $row,2 ? $row,3 {} $row,4 {} $row,5 {} $row,6 {} $row,7 ? $row,8 {}]
    }

    proc terminate {} {
        variable odbc
        variable connections

        # ignore errors since connections may not exist in case or errors in the initialize stage:
        if {$odbc} {
            catch {foreach connection $connections {catch {$connection disconnect}}}
        } else {
            catch {foreach connection $connections {catch {mysqlclose $connection}}}
        }
    }

}
