Featured image of post ログとエラーハンドリングによる堅牢化

ログとエラーハンドリングによる堅牢化

失敗時のリトライ、進捗ログ、エラーレポート機能を追加し、本番運用レベルのバックアップツールを完成させます。

これまでで機能的には充実したバックアップツールができましたが、本番運用するにはまだ足りないものがあります。それは「信頼性」です。

途中でコピーに失敗したら? 何が起きたか後から追跡できるか?

今回は、ログ出力と例外処理を追加し、ツールを堅牢化(Robustness)します。

前回: 圧縮機能の追加 | 目次 | 次回: 振り返りと発展

ログ機能の追加

print 文での出力は手軽ですが、本番では不十分です。Log::Dispatch などを使うのが一般的ですが、今回はツール自体にログ機能を組み込みます。

Backup::Engine にロガーを持たせましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package Backup::Logger;
use Moo;
use Time::Piece;

has log_file => (is => 'ro');

sub info {
    my ($self, $msg) = @_;
    $self->_write("INFO", $msg);
}

sub error {
    my ($self, $msg) = @_;
    $self->_write("ERROR", $msg);
}

sub _write {
    my ($self, $level, $msg) = @_;
    my $t = localtime->strftime('%Y-%m-%d %H:%M:%S');
    my $line = "[$t] [$level] $msg\n";
    print $line; # 標準出力にも出す
    if ($self->log_file) {
        open my $fh, '>>', $self->log_file or return;
        print $fh $line;
        close $fh;
    }
}
1;

Engineへの組み込みと例外処理

Engine側では Try::Tiny を使ってエラーを捕捉し、確実にログに残すようにします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package Backup::Engine;
use Moo;
use Try::Tiny;
# ... (以前のuse) ...
use Backup::Logger;

has logger => (is => 'lazy', builder => sub { Backup::Logger->new });

# ...

sub run {
    my $self = shift;
    
    $self->logger->info("Backup started using " . ref($self->strategy));
    
    try {
        $self->prepare;
        my $files = $self->scan;
        
        $self->strategy->execute($self, $files);
        
        $self->logger->info("Backup completed successfully.");
    }
    catch {
        my $e = $_;
        $self->logger->error("Backup FAILED: $e");
    };
}

Strategy側でのエラーハンドリング

Strategy側でも、個々のファイルのコピー失敗で全体を止めないように配慮します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
sub execute {
    my ($self, $engine, $files) = @_;
    my $errors = 0;
    
    foreach my $file (@$files) {
        try {
            # ... コピー処理 ...
            copy($file, $dest) or die "Copy failed: $!";
        }
        catch {
            $engine->logger->error("Failed to process $file: $_");
            $errors++;
        };
    }
    
    if ($errors > 0) {
        $engine->logger->error("Completed with $errors errors.");
    }
}

リトライ機能(発展)

ネットワークドライブへのバックアップなどでは、一過性のエラーが発生しがちです。Decoratorパターンを使って「リトライ機能付きStrategy」を作ることもできますが、今回はシンプルにサブルーチン再試行のイディオムを紹介します。

1
2
3
4
5
use Sub::Retry;

retry 3, 1, sub {
    copy($file, $dest) or die;
};

このような堅牢化コードを入れることで、夜間バッチで無人で動かしても安心なツールになります。

次回は最終回。これまでの設計を振り返り、さらなる発展の可能性を探ります。

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。