在分配子项后,父进程中的发布到RabbitMQ会无效地失败

时间:2017-02-14 04:20:35

标签: perl rabbitmq fork

我正在使用Net :: AMQP :: RabbitMQ和fork()遇到令人困惑的行为。如果我......

  1. 在父进程中建立与RabbitMQ的连接
  2. 发布消息
  3. 分叉一个孩子并等待它退出(孩子睡觉)
  4. 发布消息
  5. ...第二条消息实际上并未发送到RabbitMQ(并且不会抛出任何错误)。我做了很多测试,包括在发送之前检查$connection->is_connected()。我的实验中有一些有趣的花絮:

    • 如果我在发布第二条消息之前尝试打开新频道,则$connection->open_channel( $newChannelId )来电会挂起。
    • 如果我允许孩子在父母发布第二条消息时继续跑步(并等到事后waitpid),则表示已成功发送。

    我正在寻找一种方法来检测当分叉子项退出时此连接不再有效,并强制断开连接/重新连接。我在系统中各种其他模块使用的perl模块中缓存连接,但我不知道是否/何时其他模块fork()并行工作。我无法可靠地设置$SIG{CHLD}处理程序并在收到信号时断开连接,因为其他代码可能会覆盖我的处理程序。我唯一的防弹选项是丢弃缓存并连接每条消息,但这会大大降低发布速度(降低30倍)。

    此脚本演示了此问题(发布到名为'广播'的主题交换):

    #!/usr/bin/perl
    use strict;
    use Net::AMQP::RabbitMQ;
    use JSON -support_by_pp;
    
    my $connection;
    my $channelId = 0;
    
    sub sendToRabbit {
      my ($message) = @_;
      print "Sending message $message->{message}\n";
      my $contentType = 'application/json';
      my $payload = encode_json $message;
      $connection->publish($channelId, 'test.route', $payload, { exchange => 'broadcast', force_utf8_in_header_strings => 1 }, { 'content_type' => $contentType });
      print "Sent!\n";
    }
    
    sub main {
      print "Starting...\n";
    
      $connection = Net::AMQP::RabbitMQ->new();
      $connection->connect('localhost', { user => 'guest', password => 'guest', port => 5672 });
      $connection->channel_open(++$channelId);
      print "Connected!\n";
    
      # send first message
      sendToRabbit({ message => 'body 1' });
    
      # fork child
      my $child = fork();
      if(!$child) {
        # child
        sleep(1);
        print "child exiting...\n";
        exit(0);
      }
      else {
        # parent
        waitpid($child, 0);
      }
      print "parent continuing...\n";
    
      # send second message - this will not be actually sent.  
      sendToRabbit({ message => 'body 2' });
    
      # allow I/O to settle...
      sleep(1);
    }
    
    main();
    

    编辑:解决方案

    感谢 ikegami 揭示解决方案!

    在我的RabbitMQ管理对象中,我已经在connect()例程中注入了一些代码,这些例程允许我选择性地跳过不自己调用connect()的分叉子代的析构函数。这似乎有预期的效果。

    # Connect to RabbitMQ and create a channel
    sub connect {
      my ($self) = @_;
    
      $self->{pid} = $$;
    
      # if we redefined the destructor and connect is called, we need to revert
      # it so it can be redefined again properly
      no warnings qw( redefine );
      if($self->{original_destructor}) {
        # reset original destructor
        *Net::AMQP::RabbitMQ::DESTROY = $self->{original_destructor};
        delete $self->{original_destructor};
      }
    
      # define alternate destructor so forked children that do not call "connect" do
      # not destroy our connection
      {
         $self->debug("Overridding constructor...");
         $self->{original_destructor} = Net::AMQP::RabbitMQ->can('DESTROY');
         # only destroy the connection if the current pid is the owner's pid
         my $new_destructor = sub { if($self->{pid} eq $$) { $self->debug("Destroying $_[0]!\n"); $self->{original_destructor}->(); } };
         *Net::AMQP::RabbitMQ::DESTROY = $new_destructor;
      }
    
      my $connection = Net::AMQP::RabbitMQ->new();
      $connection->connect('localhost', { user => $self->{username}, password => $self->{password}, port => $PORT, vhost => $VHOST });
      $self->{connection} = $connection;
      $self->{channel} = $self->createChannel();
    
      1;
    }
    

1 个答案:

答案 0 :(得分:3)

子节点是父节点的克隆,父节点与子节点共享的文件句柄。作为父母的副本,孩子有$connection的副本。当子进入时,该对象被销毁,调用它的析构函数,向RabbitMQ发送命令以关闭连接。

你可以通过添加

来看到这一点
{
   my $old_destructor = Net::AMQP::RabbitMQ->can('DESTROY');
   my $new_destructor = sub { print("Destroying $_[0]!\n"); $old_destructor->(); };
   no warnings qw( redefine );
   *Net::AMQP::RabbitMQ::DESTROY = $new_destructor;
}

可能的解决方案:

  • 将子代码移动到单独的文件,并exec该文件。
  • 将子代码移动到一个子代码中,当使用" secret"参数,并让子项通过exec使用该参数重新启动。
  • 尽快创建孩子。具体来说,在创建RabbitMQ连接之前创建它。
  • 创建一个孩子来做RabbitMQ。
  • 使用线程而不是子进程。

PS - 不要自己编写fork + exec代码。至少使用open3

sub spawn {
  open(local *CHILD_STDIN, '<', '/dev/null') or die $!;
  return open3('<&CHILD_STDIN', '>&STDOUT', '>&STDERR', @_);
}

sendToRabbit({ message => 'body 1' });

my $pid = spawn('child.pl');    
waitpid($pid, 0);

sendToRabbit({ message => 'body 2' });