4

I have a script that writes out to a file on a frequent basis using AnyEvent. I've written the following sample to illustrate the issue I'm facing.

#!/usr/bin/perl

use strict;
use warnings;

use AnyEvent;
use AnyEvent::Handle;

my $outputFile = 'out_test.log';
open my $out, ">>", $outputFile or die "Can't open output\n";

my $data = "test string"x50000 . "\n";

my $out_ready = AnyEvent->condvar;
my $out_hdl; $out_hdl = AnyEvent::Handle->new(
    fh => $out,
    on_error => sub {
        my ($hdl, $fatal, $msg) = @_;
        AE::log error => $msg;
        $hdl->destroy;
        $out_ready->send;
    }
);

my $timer = AnyEvent->timer(
    after => 0,
    interval => 5,
    cb => sub {
        $out_hdl->push_write($data);
    }
);

$out_ready->recv;

This works well, but the file size gets to be enormous after a while. We use logrotate for problems like this so I created the following logrotate configuration file.

/path/to/out_test.log {
        size 2M
        copytruncate
        rotate 4
}

This also works well, and any time the above output file exceeds 2M it is rotated to out_test.log.1. However, when out_test.log is written to immediately after rotation, the file size is the same as the rotated log file. This behavior and what I'm experiencing is explained here: https://serverfault.com/a/221343

While I understand the issue, I don't know how to fix the problem in the sample Perl code I provided.

I don't have to implement log rotation via logrotate, but it would be preferred. If it's simple to implement in the script I can do that, but it would be nice if I could make the above sample play nice with logrotate. Any help or comments are appreciated. Thanks!

EDIT

Based on the answers below I was able to get things working with the monkeypatch ikegami provided as well as leveraging native perl I/O as per Marc Lehmann's advice. My example code looks like this and works well. Additionally this removes the requirement for the copytruncate directive in logrotate.

#!/usr/bin/perl

use strict;
use warnings;

use AnyEvent;
use AnyEvent::Handle;

my $outputFile = 'out_test.log';
open my $out, ">>", $outputFile or die "Can't open output\n";

my $data = "test string"x50000 . "\n";

my $cv = AnyEvent::condvar();
my $timer = AnyEvent->timer(
    after => 0,
    interval => 5,
    cb => sub {
        open my $out, ">>", $outputFile or die "Can't open output\n";
        print $out $data;
        close $out; 
    }
);

$cv->recv;
Community
  • 1
  • 1
Sgt B
  • 1,211
  • 2
  • 11
  • 21

2 Answers2

2

Normally, writing to a handle opened for append handle first seeks to the end of the file.

If the file was open(2)ed with O_APPEND, the file offset is first set to the end of the file before writing. The adjustment of the file offset and the write operation are performed as an atomic step.

But you're not seeing that with AnyEvent::Handle. The following demonstrates the problem:

$ perl -e'
   use strict;
   use warnings;

   use AE               qw( );
   use AnyEvent::Handle qw( );

   sub wait_for_drain {
      my ($hdl) = @_;
      my $drained = AE::cv();
      $hdl->on_drain($drained);
      $drained->recv();
   }


   my $qfn = "log";
   unlink($qfn);

   open(my $fh, ">>", $qfn) or die $!;
   $fh->autoflush(1);

   my $hdl = AnyEvent::Handle->new(
      fh => $fh,
      on_error => sub {
         my ($hdl, $fatal, $msg) = @_;
         if ($fatal) { die($msg); } else { warn($msg); }
      },
   );

   $hdl->push_write("abc\n");
   $hdl->push_write("def\n");
   wait_for_drain($hdl);
   print(-s $qfn, "\n");

   truncate($qfn, 0);
   print(-s $qfn, "\n");

   $hdl->push_write("ghi\n");
   wait_for_drain($hdl);
   print(-s $qfn, "\n");
'
8
0
12

While the following illustrates the behaviour you should be seeing:

$ perl -e'
   use strict;
   use warnings;

   my $qfn = "log";
   unlink($qfn);

   open(my $fh, ">>", $qfn) or die $!;
   $fh->autoflush(1);

   print($fh "abc\n");
   print($fh "def\n");
   print(-s $qfn, "\n");

   truncate($qfn, 0);
   print(-s $qfn, "\n");

   print($fh "ghi\n");
   print(-s $qfn, "\n");
'
8
0
4

The problem is that AnyEvent::Handle clobbers some of the handle's flag. The AnyEvent code above boils down to the following:

$ perl -e'
   use strict;
   use warnings;

   use Fcntl qw( F_SETFL O_NONBLOCK );

   my $qfn = "log";
   unlink($qfn);

   open(my $fh, ">>", $qfn) or die $!;
   $fh->autoflush(1);

   fcntl($fh, F_SETFL, O_NONBLOCK);

   print($fh "abc\n");
   print($fh "def\n");
   print(-s $qfn, "\n");

   truncate($qfn, 0);
   print(-s $qfn, "\n");

   print($fh "ghi\n");
   print(-s $qfn, "\n");
'
8
0
12

The following is what AnyEvent::Handle should be doing instead:

$ perl -e'
   use strict;
   use warnings;

   use Fcntl qw( F_GETFL F_SETFL O_NONBLOCK );

   my $qfn = "log";
   unlink($qfn);

   open(my $fh, ">>", $qfn) or die $!;
   $fh->autoflush(1);

   my $flags = fcntl($fh, F_GETFL, 0)
      or die($!);

   fcntl($fh, F_SETFL, $flags | O_NONBLOCK)
      or die($!);

   print($fh "abc\n");
   print($fh "def\n");
   print(-s $qfn, "\n");

   truncate($qfn, 0);
   print(-s $qfn, "\n");

   print($fh "ghi\n");
   print(-s $qfn, "\n");
'
8
0
4

I have submitted a bug report, but the author of the module is unwilling to fix the bug, so I'm forced to recommend the rather awful practice of monkey patching. Add the following to your program:

use AnyEvent       qw( );
use AnyEvent::Util qw( );
use Fcntl          qw( );

BEGIN {
   if (!AnyEvent::WIN32) {
      my $fixed_fh_nonblocking = sub($$) {
         my $flags = fcntl($_[0], Fcntl::F_GETFL, 0)
             or return;

         $flags = $_[1]
            ? $flags | AnyEvent::O_NONBLOCK
            : $flags & ~AnyEvent::O_NONBLOCK;

         fcntl($_[0], AnyEvent::F_SETFL, $flags);
      };

      no warnings "redefine";
      *AnyEvent::Util::fh_nonblocking = $fixed_fh_nonblocking;
   }
}

With this fix, your program will work correctly

$ perl -e'
   use strict;
   use warnings;

   use AE               qw( );
   use AnyEvent         qw( );
   use AnyEvent::Handle qw( );
   use AnyEvent::Util   qw( );
   use Fcntl            qw( );

   BEGIN {
      if (!AnyEvent::WIN32) {
         my $fixed_fh_nonblocking = sub($$) {
            my $flags = fcntl($_[0], Fcntl::F_GETFL, 0)
                or return;

            $flags = $_[1]
               ? $flags | AnyEvent::O_NONBLOCK
               : $flags & ~AnyEvent::O_NONBLOCK;

            fcntl($_[0], AnyEvent::F_SETFL, $flags);
         };

         no warnings "redefine";
         *AnyEvent::Util::fh_nonblocking = $fixed_fh_nonblocking;
      }
   }

   sub wait_for_drain {
      my ($hdl) = @_;
      my $drained = AE::cv();
      $hdl->on_drain($drained);
      $drained->recv();
   }


   my $qfn = "log";
   unlink($qfn);

   open(my $fh, ">>", $qfn) or die $!;
   $fh->autoflush(1);

   my $hdl = AnyEvent::Handle->new(
      fh => $fh,
      on_error => sub {
         my ($hdl, $fatal, $msg) = @_;
         if ($fatal) { die($msg); } else { warn($msg); }
      },
   );

   $hdl->push_write("abc\n");
   $hdl->push_write("def\n");
   wait_for_drain($hdl);
   print(-s $qfn, "\n");

   truncate($qfn, 0);
   print(-s $qfn, "\n");

   $hdl->push_write("ghi\n");
   wait_for_drain($hdl);
   print(-s $qfn, "\n");
'
8
0
4
ikegami
  • 367,544
  • 15
  • 269
  • 518
  • This is great, thanks again ikegami. I've implemented your patch into my program and it works precisely as you described with the desired results. Thanks for submitting a bug report as well. This really had me scratching my head, so I very much appreciate your help. – Sgt B Jun 26 '15 at 21:59
  • Note that this answer is wrong, and monkeypatching things will only hide the issue. See my answer for an explanation. – Remember Monica Jun 28 '15 at 09:28
  • @Marc Lehmann, 1) AE is definitely calling `SET_FL` incorrectly. 2) I always recommend against monkeypatching, but I compromised my ethics since I wasn't planning on submitting the bug report since I didn't want to deal with you. It sounds like you're not going to fix the bug anyway? 3) Even though AE::H uses non-blocking handles and library-level buffering, it can still handle the OP's use-case. It can even handle concurrency (`my $guard = lock_handle($hdl, LOCK_EX, sub { my ($lock, $hdl) = @_; $hdl->push_write($s); $hdl->on_drain(sub{ $lock->unlock(); }); }); `). – ikegami Jun 28 '15 at 17:19
  • 1
    1) all we have is your assertion, without any evidence. asserting things doesn't make them true. 2) I didn't argue with your ethics, I argued with your bad advice. Asking unfair loaded questions is definitely immoral though. 3) It doesn't matter if the OP's code *sometimes* might happen to work - the bug might bite at any time, and your answer is simply bad advice. It's his choice, of course, but I prefer writing correct code over code that seems to work sometimes, and when I advise others, I prefer to teach them correct solutions rather than hacks. – Remember Monica Jun 29 '15 at 19:57
  • I also think it is a good idea to warn others about your bad answer, simply so that other people don't fall into the trap and assume using AnyEvent::Handle with files is in any way a good programming technique. It will only cause grief. It's the responsible thing to do, really. – Remember Monica Jun 29 '15 at 20:00
  • 1) Changing flags is not a bug. Calling it "clobbering" doesn't make it a bug either. Name a few of those flags that can be "clobbered" but shouldn't? 2) It seems you don't understand what a loaded question is - see https://en.wikipedia.org/wiki/Loaded_question for an explanation. To spell it out for you, any answer to your question requires accepting your "bug" claims as true. If this is contested, as in this case, it is impossible to answer, and trying to trick people to accept your claims this way is unfair. If you are honestly interested in an answer ask an honest question not a trick one. – Remember Monica Jun 29 '15 at 20:08
  • @ikegami you confuse me with the OP - only the OP can fix bugs in his code. Your answer is wrong and misleading already - the OP doesn't need to fix his code for this to be true. If you plan to adjust your question to be more correct, you could do so at any time. – Remember Monica Jun 29 '15 at 20:10
  • @Marc Lehmann, Changing flags is not a bug, but unintentionally changing flags you know nothing about is. Why don't know name all of those flags you changed if it was intentional? Re "you confuse me with the OP", Not even close. – ikegami Jun 29 '15 at 20:31
  • @ikegami: let's not get carried away here. Fact is that the OPs code relies on explicitly unsupported behaviour, and this is the reason why it doesn't work. Whether AE _also_ has a bug _elsewhere_ is immaterial to his problem, and suggesting a hack is simply bad advice. The right thing to do is to fix the bug causing his problems, and worry about unrelated issues elsewhere. Imagining flags neither you nor I don't know about (and don't exist) isn't leading anywhere, and certainly doesn't help anybody running into this same problem. – Remember Monica Jun 29 '15 at 20:49
  • @Marc Lehmann, The module supports it just fine. It provides all the needed primitives. the bug is imposing a completely artificial limitation. – ikegami Jun 29 '15 at 20:50
  • @ikegami: I cannot understand your definition of "support" then - the module documents that it doesn't support it, and OP asked his question here because it doesn't work in practise either. I also don't think this discussion is improving this question, so unless you move it elsewhere, I will not comment further, as the important things have all been said. – Remember Monica Jun 29 '15 at 21:07
  • @Marc Lehmann, Actually, it doesn't. I think you are referring to "This is a helper module to make it easier to do event-based I/O on stream-based filehandles (sockets, pipes, and other stream things)." 1) It doesn't say it doesn't work on non-streams, 2) It would be lying if it did. – ikegami Jun 29 '15 at 21:10
2

ikegamis answer is very misleading - your code contains a bug, namely using AnyEvent::Handle for file I/O, which is undocumented and unsupported behaviour. The "bug" that ikegami perceives results from using AnyEvent::Handle on an illegal file handle.

While you can try to rely on undocumented behaviour and monkeypatch things and hope it will magically work, you will likely keep running into problems as long as you use AnyEvent::Handle for non-stream filehandles, so I would sugets fixing the actual bug.

If you want to do event-based file I/O, then you should look into AnyEvent::IO (and install a suitable backend such as IO::AIO). Otherwise, you should use the normal perl I/O functions (built-ins, IO:: classes and so on) to access files.

Update: the deeper reason that AnyEvent::Handle doesn't work on files is that ultimately it makes no sense, because the notion of non-blocking I/O doesn't apply to files in a useful way, so using AnyEvent::Handle only adds overhead.

Remember Monica
  • 3,897
  • 1
  • 24
  • 31
  • If I understand correctly, I should remove sections of code that refer to the file I/O operations using AnyEvent and replace them with the builtin perl I/O functions. Simply put, I would open a file handle in the timer's callback, write my data, and close the file handle. Thus eliminating the perceived need to use AnyEvent::Handle for that. This works in my example code above and is working in my production program as well. A side effect of this is the ability to discard the requirement for the copytruncate in logrotate. (see new code snippet above for clarification). Thanks! – Sgt B Jul 01 '15 at 22:49
  • 1
    Yes - the idea behind AnyEvent::Handle is to allow event-based non-blocking I/O, where some outside source pushes data towards you. This model doesn't make sense for disks (or even network filesystems), where you explicitly have to request data. Disks usually have pretty bounded I/O times, too, making it often not necessary to handle disk I/O via events - the disk usually delivers in tens of milliseconds, the network might take hours or more for new data. If disk I/O is an issue and slows down your program, AnyEvent::IO (+IO::AIO) is the right module, or maybe IO::AIO directly. – Remember Monica Jul 27 '15 at 19:34
  • 1
    Otherwise, built-in perl I/O is the right thing to use for files. In some cases, it can be convenient to use AnyEvent::Handle (e.g. when dealing with STDIN/STDOUT, which might be a pipe, a network socket, or a file, and you don't know which one), which is why AnyEvent::Handle doesn't refuse to work with these file handles, but generally, you should only use AnyEvent::Handle for cases where your handle could be a socket, pipe or another streaming source. In these cases, it will also work with files properly then. – Remember Monica Jul 27 '15 at 19:39