#!/usr/bin/perl

use strict;
use warnings;

=head1 DESCRIPTION

direct-pt-osc -- скрипт для генерации команд pt-online-schema-change в Директ-подобно устроенных БД

Знает про Директовые шардированные БД, берет актуальных мастеров и порты из db-config.

Можно использовать как подсказку и запустить все вручную, 
а можно передать bash'у, можно велеть запустить все в tmux (--tmux-exec).
      
=head1 OPTIONS

    -c, --conf <path>
        обязательно
        путь к файлу db-config
        для продакшена Директа /etc/yandex-direct/db-config.json

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

    --db <db>
        БД: ppc:all, ppcdcit, monitor и т.д.

    -u, --user <username>
        пользователь для коннекта к mysql
        по умолчанию берется из db-config.json

    -p, --pass <password>
        пароль для коннекта к mysql
        по умолчанию берется из db-config.json

    --osc-dry-run 
        если указан, будет сгенерирована команда pt-osc --dry-run, иначе (по умолчанию) --execute

    --tmux
        сгенерировать команды для запуска всего в новой сессии tmux
        команды pt-osc пишутся в /tmp/pt-osc-<DB>-<дата>
        если указать --tmux-exec -- все на самом деле запустится в tmux

    --tmux-exec
        запустить сгенерированные команды в новой сессии tmux
        tmux после окончания альтеров сам не закончится, надо прибить руками

    --no-tmux-attach
        не присоединятся к tmux автоматически

    --critical-load <param>
        как есть подставляется в --critical-load, подробнее см. man pt-online-schema-change
        По умолчанию Threads_running=1000

    --max-load <param>
        как есть подставляется в --max-load, подробнее см man pt-online-schema-change
        По умолчанию Threads_running=100

    --alter-foreign-keys-method <rebuild_constraints|drop_swap|none|auto>
        как обрабатывать ссылки foreign key на альтеримую таблицу
        как есть передается в pt-osc, подробнее см. man pt-online-schema-change
        по-умолчанию - rebuild_constraints + --set-vars foreign_key_checks=0

    --max-lag <seconds>
        как есть подставляется в --max-lag, подробнее см. man pt-online-schema-change
        по умолчанию 300

    --check-interval <seconds>
        как есть подставляется в --check-interval, подробнее см. pt-online-schema-change
        по умолчанию 1

    --no-auto-finish 
        не переименовывать таблицы после копирования данных
        Полезно для очень долгих альтеров, чтобы не терять многочасовое копирование из-за долгих селектов в таблице.
        Если выполняется такой запрос, после окончания копирования надо: 
        1. проверить, что не висят долгие запросы к таблице (если висят -- дождаться окончания или прибить, если можно ими пожертвовать)
        2. rename table orig to old, new to orig
        3. drop triggers
        4. drop table old

=head1 EXAMPLES

  Самое простое: 
  direct-pt-osc --db ppc:all --conf /etc/yandex-direct/db-config.json 'ALTER TABLE clients_options ADD COLUMN non_resident BOOL DEFAULT 0;'

  Сгенерировать dry-run'ы:
  direct-pt-osc --conf /etc/yandex-direct/db-config.json --db ppc:all 'ALTER TABLE camp_options ADD COLUMN `is_related_keywords_enabled` tinyint(1) unsigned NOT NULL DEFAULT "0"' --osc-dry-run

  Использовать альтернативного пользователя: 
  direct-pt-osc --conf /etc/yandex-direct/db-config.json --db ppc:all 'ALTER TABLE camp_options ADD COLUMN `is_related_keywords_enabled` tinyint(1) unsigned NOT NULL DEFAULT "0"' -u secret_user -p secret_pass

  Сгенерировать команды для запуска в tmux:
  direct-pt-osc --conf /etc/yandex-direct/db-config.json --db ppc:all 'ALTER TABLE camp_options ADD COLUMN `is_related_keywords_enabled` tinyint(1) unsigned NOT NULL DEFAULT "0"' -u secret_user -p secret_pass --tmux

  Запустить параллельно на всех шардах (в tmux):
  direct-pt-osc --conf /etc/yandex-direct/db-config.json --db ppc:all 'ALTER TABLE camp_options ADD COLUMN `is_related_keywords_enabled` tinyint(1) unsigned NOT NULL DEFAULT "0"' -u secret_user -p secret_pass --tmux-exec

  Для dev7:
  direct-pt-osc --conf /var/www/beta.lena-san.8880/etc/db-config.dev7.json --db ppc:all 'ALTER TABLE camp_options ADD COLUMN `is_related_keywords_enabled` tinyint(1) unsigned NOT NULL DEFAULT "0"' 

=head1 TODO

  * --tmux-clean -- удалить файлы со скриптами и логами 

=cut

use Data::Dumper;
use Getopt::Long;
use POSIX qw(strftime);
use YAML;

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


run() unless caller();

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

    $Yandex::DBTools::CONFIG_FILE = $O{conf_file};

    my @shards = ();
    if ( $O{db} =~ /^([^:]+):all(:.*)?$/ ){
        my ($db, $slave) = ($1, $2);
        $slave ||= '';
        @shards = map {"$_$slave"} @{Yandex::DBShards::get_shard_dbnames($db,"", shard => "all")->{dbnames}}
    } else {
        @shards = ($O{db});
    }
    die "empty shard list, stop" unless @shards > 0; 

    $query =~ s/'/"/g;
    $query =~ s/\s+/ /g;
    $query =~ s/[;\s]+$//;
    unless ($query =~ /(?:alter) *table *([^ ]+) *(.*)$/si){
        die "can't parse query";
    }
    my ($table, $short_query) = ($1, $2);

    for ($table) {
        s/^`//;
        s/`$//;
    }

    my $shard = 0;
    my $now = strftime("%Y%m%d-%H%M%S", localtime);
    my $tmux_session = '';
    my @cmds;
    for my $db ( @shards ){
        $shard++;

        $db =~ /^([^:]+)/;
        my $database = $1;
        my $cfg = get_db_config($db);
        my $host = ref $cfg->{host} ? $cfg->{host}->[0] : $cfg->{host};
        my $port = $cfg->{port};
        my $user = $O{mysql_user} || $cfg->{user};
        my $pass = $O{mysql_password} || $cfg->{pass};

        my $base_osc_query = "pt-online-schema-change t=$table,h=$host,P=$port,u=$user,p=$pass,D=$database --alter '$short_query'";
        my $no_swap_table_params = $O{no_auto_finish} ? "--no-swap-tables --no-drop-new-table --no-drop-triggers" : "";
        my $load_treshold_params = "--critical-load $O{critical_load} --max-load $O{max_load}";
        my $foreign_key_params = $O{alter_foreign_keys_method} 
                ? "--alter-foreign-keys-method $O{alter_foreign_keys_method}"
                : "--alter-foreign-keys-method rebuild_constraints --set-vars foreign_key_checks=0";
        my $check_alter_params = $O{no_check_alter} ? "--no-check-alter" : "";
        my $check_replica_params = "--recurse 1 --recursion-method hosts --nocheck-replication-filters --max-lag $O{max_lag} --check-interval $O{check_interval}";

        my $cmd;
        if ($O{generate_osc_dry_run}) {
            $cmd = "date; $base_osc_query $load_treshold_params $foreign_key_params $no_swap_table_params $check_alter_params $check_replica_params --dry-run; echo \$?; date\n";
        } elsif ($O{generate_osc_execute}) {
            $cmd = "date; $base_osc_query $load_treshold_params $foreign_key_params $no_swap_table_params $check_alter_params $check_replica_params --execute; echo \$?; date\n"; 
        }

        if ( $O{tmux} ) {
            $tmux_session ||= "pt-osc-$database-$now";

            open(my $fh, '>', "/tmp/$tmux_session:$shard.cmd");
            print $fh $cmd;
            print $fh "exec bash -i\n";

            if ($shard == 1) {
                push @cmds, qq(tmux new-session -d -s $tmux_session\n);
                push @cmds, qq(tmux rename-window -t $tmux_session dummy\n);
            }
            push @cmds, qq(tmux new-window -t $tmux_session -n $database:$shard 'bash -ex /tmp/$tmux_session:$shard.cmd 2>&1 | tee /tmp/$tmux_session:$shard.log'\n);
        } else {
            push @cmds, $cmd;
        }
    }
    if ( $O{tmux} ) {
        push @cmds, qq(tmux kill-window -t $tmux_session:dummy\n);
    }

    my $no_swap_message = $O{no_auto_finish} ? "\n# ИСПОЛЬЗУЕТСЯ НЕЗАКОНЧЕННЫЙ ЗАПРОС (--no-swap-tables --no-drop-new-table --no-drop-triggers)\n# после окончания копирования данных надо вручную переимновать таблицы и дропнуть триггеры, смотри справку, зови на помощь\n" : "";
    if ( !$O{tmux} ){
        # просто печатаем команды pt-osc
        print qq!
# Команды для выполнения альтера через pt-online-schema-change
# Можно добавить --tmux, тогда будут сгенерированы команды для запуска альтеров (всех параллельно) в новой сессии tmux
# Можно добавить --tmux-exec, тогда сгенерированные команды на самом деле будут запущены в новой сессии tmux
# Если альтер нельзя выполнять параллельно на разных шардах, надо явно указывать шард, в котором выполнить альтер: --db ppc:2
#
# Если не знаешь, что такое pt-online-schema-change и применим ли он в конкретном случае -- сначала изучи или спроси кого-нибудь. 
# Никаких гарантий, что данные Директа не пострадают, не дается.

$no_swap_message
set -ex

!;
        for my $c (@cmds){
            print "$c\n";
        }
    } elsif ( $O{tmux} && !$O{tmux_exec} ) {
        # команды для запуска в tmux
        print "# Команды для запуска в tmux:\n";
        for my $c (@cmds){
            print "$c\n";
        }
        print "# Чтобы на самом деле запустить выполнение, добавь ключ --tmux-exec (можно вместо --tmux) или передай результат в bash через pipe (direct-pt-osc ... |bash)\n";
        print "# Список сессий tmux:\n# tmux ls\n# Присоединиться:\n# tmux attach -t <session>\n$no_swap_message";
    } elsif ( $O{tmux} && $O{tmux_exec} ){
        # запускаем все в tmux
        print "Команды запускаются в tmux... Присоединиться к сессии можно будет так:\ntmux attach -t $tmux_session\n$no_swap_message";
        for my $c (@cmds){
            print "$c";
            system($c);
            print ( $? >> 8 == 0 ? "...success\n":"...fail\n");
        }
        if ( $O{no_tmux_attach} ) {
            print "\nГотово, можно присоединяться:\ntmux attach -t $tmux_session\n";
        } else {
            print "\nГотово, присоединяемся ...\ntmux attach -t $tmux_session\n";
            sleep 1;
            exec 'tmux', 'attach', '-t', "$tmux_session";
        }
    } else {
        # непредвиденное сочетание ключей
        die "impossible!";
    }

    exit 0;
}


sub parse_options
{
    my %O = (
        query => '',
        db => '',
        conf_file => '',
        cmd => 'execute',
        max_load => 'Threads_running=100',
        critical_load => 'Threads_running=1000',
        max_lag => 300,
        check_interval => 1,
    );

    GetOptions(
        "help" => sub {
            system("podselect -section NAME -section DESCRIPTION -section OPTIONS -section EXAMPLES $0 | pod2text-utf8"); 
            exit 0;
        },
        "c|conf=s" => \$O{conf_file},
        "query=s" => \$O{query},
        "db=s" => \$O{db},
        "u|user=s" => \$O{mysql_user},
        "p|password=s" => \$O{mysql_password},
        "osc-dry-run" => \$O{osc_dry_run},
        "no-auto-finish" => \$O{no_auto_finish},
        "critical-load=s" => \$O{critical_load},
        "max-load=s" => \$O{max_load},
        "alter-foreign-keys-method=s" => \$O{alter_foreign_keys_method},
        "max-lag=i" => \$O{max_lag},
        "check_interval=i" => \$O{check_interval},
        "tmux" => \$O{tmux},
        "tmux-exec" => \$O{tmux_exec},
        "no-tmux-attach" => \$O{no_tmux_attach},
        "no-check-alter" => \$O{no_check_alter},
    ) || die "can't parse options, stop";

    $O{tmux} = 1 if $O{tmux_exec};

    if ( @ARGV > 2 ){
        die "too many arguments: @ARGV, stop";
    } elsif ( @ARGV == 2 ) {
        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 conf, stop (try $0 --help)" unless $O{conf_file};

    $O{generate_osc_execute} = 0;
    $O{generate_osc_dry_run} = 0;
    if ( $O{osc_dry_run} ){
        $O{generate_osc_dry_run} = 1;
    } else {
        $O{generate_osc_execute} = 1;
    }

    return \%O;
}

