From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on dcvr.yhbt.net X-Spam-Level: X-Spam-ASN: X-Spam-Status: No, score=-4.0 required=3.0 tests=ALL_TRUSTED,BAYES_00 shortcircuit=no autolearn=ham autolearn_force=no version=3.4.0 Received: from localhost (dcvr.yhbt.net [127.0.0.1]) by dcvr.yhbt.net (Postfix) with ESMTP id 7BB922018F for ; Wed, 15 Jun 2016 00:37:44 +0000 (UTC) From: Eric Wong To: meta@public-inbox.org Subject: [PATCH 8/9] emergency: implement new emergency Maildir delivery Date: Wed, 15 Jun 2016 00:37:41 +0000 Message-Id: <20160615003742.22538-9-e@80x24.org> In-Reply-To: <20160615003742.22538-1-e@80x24.org> References: <20160615003742.22538-1-e@80x24.org> List-Id: This is transactional and hopefully safer in case we hit SIGSEGV or SIGKILL during processing, as the tmp/ copy will remain on the FS even if DESTROY/END handlers are not called. --- lib/PublicInbox/Emergency.pm | 96 ++++++++++++++++++++++++++++++++++++++++++++ t/emergency.t | 53 ++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 lib/PublicInbox/Emergency.pm create mode 100644 t/emergency.t diff --git a/lib/PublicInbox/Emergency.pm b/lib/PublicInbox/Emergency.pm new file mode 100644 index 0000000..e402d30 --- /dev/null +++ b/lib/PublicInbox/Emergency.pm @@ -0,0 +1,96 @@ +# Copyright (C) 2016 all contributors +# License: AGPL-3.0+ +# +# Emergency Maildir delivery for MDA +package PublicInbox::Emergency; +use strict; +use warnings; +use Fcntl qw(:DEFAULT SEEK_SET); +use Sys::Hostname qw(hostname); +use IO::Handle; + +sub new { + my ($class, $dir) = @_; + + foreach (qw(new tmp cur)) { + my $d = "$dir/$_"; + next if -d $d; + require File::Path; + File::Path::mkpath($d); # croaks on fatal errors + } + bless { dir => $dir, files => {}, t => 0, cnt => 0 }, $class; +} + +sub _fn_in { + my ($self, $dir) = @_; + my @host = split(/\./, hostname); + my $now = time; + if ($self->{t} != $now) { + $self->{t} = $now; + $self->{cnt} = 0; + } else { + $self->{cnt}++; + } + + my $f; + do { + $f = "$self->{dir}/$dir/$self->{t}.$$"."_$self->{cnt}.$host[0]"; + $self->{cnt}++; + } while (-e $f); + $f; +} + +sub prepare { + my ($self, $strref) = @_; + + die "already in transaction: $self->{tmp}" if $self->{tmp}; + my ($tmp, $fh); + do { + $tmp = _fn_in($self, 'tmp'); + $! = undef; + } while (!sysopen($fh, $tmp, O_CREAT|O_EXCL|O_RDWR) && $!{EEXIST}); + print $fh $$strref or die "write failed: $!"; + $fh->flush or die "flush failed: $!"; + $fh->autoflush(1); + $self->{fh} = $fh; + $self->{tmp} = $tmp; +} + +sub abort { + my ($self) = @_; + delete $self->{fh}; + my $tmp = delete $self->{tmp} or return; + + unlink($tmp) or warn "Failed to unlink $tmp: $!"; + undef; +} + +sub fh { + my ($self) = @_; + my $fh = $self->{fh} or die "{fh} not open!\n"; + seek($fh, 0, SEEK_SET) or die "seek(fh) failed: $!"; + sysseek($fh, 0, SEEK_SET) or die "sysseek(fh) failed: $!"; + $fh; +} + +sub commit { + my ($self) = @_; + + delete $self->{fh}; + my $tmp = delete $self->{tmp} or return; + my $new; + do { + $new = _fn_in($self, 'new'); + } while (!link($tmp, $new) && $!{EEXIST}); + my @sn = stat($new) or die "stat $new failed: $!"; + my @st = stat($tmp) or die "stat $tmp failed: $!"; + if ($st[0] == $sn[0] && $st[1] == $sn[1]) { + unlink($tmp) or warn "Failed to unlink $tmp: $!"; + } else { + warn "stat($new) and stat($tmp) differ"; + } +} + +sub DESTROY { commit($_[0]) } + +1; diff --git a/t/emergency.t b/t/emergency.t new file mode 100644 index 0000000..e480338 --- /dev/null +++ b/t/emergency.t @@ -0,0 +1,53 @@ +# Copyright (C) 2016 all contributors +# License: AGPL-3.0+ +use strict; +use warnings; +use Test::More; +use File::Temp qw/tempdir/; +my $tmpdir = tempdir('emergency-XXXXXX', TMPDIR => 1, CLEANUP => 1); +use_ok 'PublicInbox::Emergency'; + +{ + my $md = "$tmpdir/a"; + my $em = PublicInbox::Emergency->new($md); + ok(-d $md, 'Maildir a auto-created'); + my @tmp = <$md/tmp/*>; + is(scalar @tmp, 0, 'no temporary files exist, yet'); + $em->prepare(\"BLAH"); + @tmp = <$md/tmp/*>; + is(scalar @tmp, 1, 'globbed one temporary file'); + open my $fh, '<', $tmp[0] or die "failed to open: $!"; + is("BLAH", <$fh>, 'wrote contents to temporary location'); + my @new = <$md/new/*>; + is(scalar @new, 0, 'no new files exist, yet'); + $em = undef; + @tmp = <$md/tmp/*>; + is(scalar @tmp, 0, 'temporary file no longer exists'); + @new = <$md/new/*>; + is(scalar @new, 1, 'globbed one new file'); + open $fh, '<', $new[0] or die "failed to open: $!"; + is("BLAH", <$fh>, 'wrote contents to new location'); +} +{ + my $md = "$tmpdir/b"; + my $em = PublicInbox::Emergency->new($md); + ok(-d $md, 'Maildir b auto-created'); + my @tmp = <$md/tmp/*>; + is(scalar @tmp, 0, 'no temporary files exist, yet'); + $em->prepare(\"BLAH"); + @tmp = <$md/tmp/*>; + is(scalar @tmp, 1, 'globbed one temporary file'); + open my $fh, '<', $tmp[0] or die "failed to open: $!"; + is("BLAH", <$fh>, 'wrote contents to temporary location'); + my @new = <$md/new/*>; + is(scalar @new, 0, 'no new files exist, yet'); + is(sysread($em->fh, my $buf, 9), 4, 'read file handle exposed'); + is($buf, 'BLAH', 'got expected data'); + $em->abort; + @tmp = <$md/tmp/*>; + is(scalar @tmp, 0, 'temporary file no longer exists'); + @new = <$md/new/*>; + is(scalar @new , 0, 'new file no longer exists'); +} + +done_testing();