#!/usr/bin/perl

use strict;
use warnings;

=head1 DESCRIPTION

=encoding utf8

    Скрипт для безопасного выполнения "онлайновых" альтеров

    Смотрит на information_schema.processlist; 
    ждет, когда нет долгих запросов в таблицу, которую собираемся альтерить;
    выставляет max_statement_time, запускает альтер
      
=head1 OPTIONS

    --query
        исходный запрос (альтер)
        если указан - (дефис) -- читает запрос из stdin
        если не указан ключ --query -- запросом считается первый параметр скрипта

    -H, --host <hostname>
        хост

    -P, --port <port>
        порт

    -u, --user <username>
        пользователь для коннекта к mysql

    -p, --pass <password>
        пароль для коннекта к mysql

=head1 EXAMPLES

  Самое простое: 
  
    ... TODO ...

  direct-live-alter  -t 300min 'alter table users drop column test_01, algorithm=inplace, lock=none'

=head1 TODO

  * примеры в описании 
  * проверять, что algorithm=inplace, lock=none (или добавлять самостоятельно?)


=cut

use Data::Dumper;
use Getopt::Long qw(:config no_ignore_case);
use POSIX qw(strftime);
use YAML;
#use Carp::Always;

use Yandex::DBTools;
use Yandex::DBShards;

$|++;

run() unless caller();

sub run
{
    my %O = %{parse_options()};
    my $query = $O{query};

    die "can't parse table name, stop\n" unless $query =~ /^\s*alter\s+table\s+([^\s]+)/i;
    my $table = $1;
    $table =~ s/`//g;
    if ( $table =~ /^(.*)\.(.*)$/ ){
        ($O{database}, $table) = ($1, $2);
    }
    die "unknown database, stop" unless $O{database};

    print "query: $O{query}\ntable: $table\ndatabase: $O{database}\ntimeout: $O{timeout_minutes} minutes\n";
    sleep 2;

    $Yandex::DBTools::DONT_SEND_LETTERS = 1;
    %Yandex::DBTools::DB_CONFIG = (
        CHILDS => {
            my_db => {
                'AutoCommit' => '1',
                'CHILDS' => {
                    '_' => {},
                },
                'connect_timeout' => '4',
                'host' => $O{mysql_host},
                'pass' => $O{mysql_password},
                'port' => $O{mysql_port},
                'user' => $O{mysql_user},
                'utf8' => '1',
                'db'   => $O{database},
            },
        },
    );

    my $try_alter = 0;
    my $try_total = -1;

    my $start_ts = time;

    while (++$try_total <= $O{tries}){
        $try_alter++;
        my $try_sleep = 0;
        my $ok = 0;
        while ( ++$try_total <= $O{tries} ){
            $try_sleep++;
            my $working_time = int((time - $start_ts )/60);
            die "timeout $O{timeout_minutes} minutes exceeded" if $working_time >= $O{timeout_minutes};
            my $status_str = "$working_time min elapsed, global iteration #$try_total, alter #$try_alter, local iteration #$try_sleep";
            print "\n".localtime."\n($status_str) Checking for long-running queries...\n";
            # в 5.7 будет metadata_locks
            my $queries = get_all_sql("my_db", 'select * from information_schema.processlist where TIME > ? and command != "Sleep" and info rlike ?', $O{dangerous_query_duration}, $table); 
            if ( @$queries == 0 ){
                print "\nlooks good, no long-running queries found\n\n";
                $ok = 1;
                last;
            } else {
                print "found long-running queries:\n";
                my @fields = qw/ID USER HOST DB COMMAND TIME STATE ROWS_SENT ROWS_EXAMINED INFO/;
                print join("\t", @fields)."\n";
                for my $q ( @$queries ){
                    print join("\t", map {substr($q->{$_}, 0, 120)} @fields)."\n";
                }
                print "total: ".(scalar @$queries)." queries\n";

                print "($status_str) sleeping for $O{sleep} seconds...\n";
                sleep $O{sleep};
            }
        }

        die "max tries count ($O{tries}) exceeded, stop" unless $ok;

        print "(attempt #${try_alter}) let's do '$O{query}'...\n\n";
        my $res;
        eval{ 
            do_sql("my_db", "set max_statement_time=$O{max_statement_time};"); 
            $res = do_sql("my_db", "$O{query};");
        };
        my $error = $@;
        if ( !$error ){
            print "SUCCESS (code ".($res+0).")\n" ;
            last;
        } 
        print "error:\n$@\n";
        if ( $error !~ /Query execution was interrupted/i ){
            die "fatal error, stop\n";
        }
        print "let's try again\n";
        print "sleeping for $O{sleep} seconds...\n";
        sleep $O{sleep};
    }

    exit 0;
}


sub parse_options
{
    my %O = (
        query => '',
        db => '',
        conf_file => '',
        cmd => 'execute',
        max_statement_time => 5 * 1000,
        dangerous_query_duration => 5,
        tries => 100_000,
        sleep => 10,
        timeout_str => "60m",
    );

    GetOptions(
        "help" => sub {
            system("podselect -section NAME -section DESCRIPTION -section OPTIONS -section EXAMPLES $0 | pod2text"); 
            exit 0;
        },
        "H|host=s" => \$O{mysql_host},
        "P|port=s" => \$O{mysql_port},
        "query=s" => \$O{query},
        "tries=s" => \$O{tries},
        "t|timeout=s" => \$O{timeout_str},
        "u|user=s" => \$O{mysql_user},
        "p|password=s" => \$O{mysql_password},
        "db|database=s" => \$O{database},
    ) || die "can't parse options, stop";

    if ( @ARGV > 1 ){
        die "too many arguments: @ARGV, stop";
    } elsif ( @ARGV == 1 ){
        $O{query} = $ARGV[0];
    } else {
    }

    if ($O{query} eq '-'){
        $O{query} = join ' ', <STDIN>;
    }

    die "empty query, stop" unless $O{query};
    die "empty host, stop" unless $O{mysql_host};
    die "empty port, stop" unless $O{mysql_port};
    die "empty user, stop" unless $O{mysql_user};

    if ( $O{timeout_str} =~ /^([0-9]+)(m|min)$/ ){
        $O{timeout_minutes} = $1;
    } else {
        die "can't parse timeout, stop";
    }

    die "incorrect timeout, stop" if $O{timeout_minutes} < 5 or $O{timeout_minutes} > 1000;

    return \%O;
}

