postgres LISTEN / NOTIFY rails

时间:2013-05-06 19:10:03

标签: ruby-on-rails postgresql asynchronous push-notification

Ryan Bates在this episode讨论推送通知时提到了Postgres的LISTEN / NOTIFY功能,但我还没有找到任何关于如何在我的rails应用程序中实现LISTEN / NOTIFY的提示。

以下是pg适配器内部wait_for_notify函数的文档,但我无法弄清楚它的具体用途是什么。

我们是否需要直接点击connection适配器的pg变量?

1 个答案:

答案 0 :(得分:51)

您正在使用wait_for_notify方法查找正确的位置,但由于ActiveRecord显然不提供使用它的API,因此您需要获取基础PG :: Connection对象(或其中一个,如果你正在运行多线程设置),ActiveRecord正在使用它与Postgres交谈。

获得连接后,只需执行所需的LISTEN语句,然后将一个块(以及一个可选的超时时间)传递给wait_for_notify。请注意,这将阻止当前线程,并独占Postgres连接,直到达到超时或发生NOTIFY为止(因此您不希望在Web请求中执行此操作)。当另一个进程在您正在侦听的其中一个通道上发出NOTIFY时,将使用三个参数调用该块 - 通知的通道,触发NOTIFY的Postgres后端的pid ,以及伴随NOTIFY的有效负载(如果有的话)。

我在很长一段时间内没有使用ActiveRecord,所以可能有一种更简洁的方法来做到这一点,但这似乎在4.0.0.beta1中正常工作:

# Be sure to check out a connection, so we stay thread-safe.
ActiveRecord::Base.connection_pool.with_connection do |connection|
  # connection is the ActiveRecord::ConnectionAdapters::PostgreSQLAdapter object
  conn = connection.instance_variable_get(:@connection)
  # conn is the underlying PG::Connection object, and exposes #wait_for_notify

  begin
    conn.async_exec "LISTEN channel1"
    conn.async_exec "LISTEN channel2"

    # This will block until a NOTIFY is issued on one of these two channels.
    conn.wait_for_notify do |channel, pid, payload|
      puts "Received a NOTIFY on channel #{channel}"
      puts "from PG backend #{pid}"
      puts "saying #{payload}"
    end

    # Note that you'll need to call wait_for_notify again if you want to pick
    # up further notifications. This time, bail out if we don't get a
    # notification within half a second.
    conn.wait_for_notify(0.5) do |channel, pid, payload|
      puts "Received a second NOTIFY on channel #{channel}"
      puts "from PG backend #{pid}"
      puts "saying #{payload}"
    end
  ensure
    # Don't want the connection to still be listening once we return
    # it to the pool - could result in weird behavior for the next
    # thread to check it out.
    conn.async_exec "UNLISTEN *"
  end
end

有关更一般用法的示例,请参阅Sequel's implementation

编辑添加:这是对正在发生的事情的另一种描述。这可能不是幕后的确切实现,但它似乎足以描述这种行为。

Postgres会保留每个连接的通知列表。当您使用连接执行LISTEN channel_name时,您告诉Postgres应该将该通道上的任何通知推送到此连接的列表(多个连接可以收听同一个通道,因此单个通知最终会被推送很多名单)。一个连接可以同时LISTEN到多个频道,并且任何频道的通知都将被推送到同一个列表。

wait_for_notify所做的是从连接列表中弹出最早的通知并将其信息传递给块 - 或者,如果列表为空,则休眠直到通知可用并为此做同样的事情(或直到达到超时,在这种情况下它只返回nil)。由于wait_for_notify仅处理单个通知,因此如果您要处理多个通知,则必须重复调用它。

当您UNLISTEN channel_nameUNLISTEN *时,Postgres会停止将这些通知推送到您的连接列表中,但已经推送到该列表的那些通知将保留在那里,并且wait_for_notify仍将返回它们它接下来被称为。这可能会导致一个问题,即在wait_for_notify之后但在UNLISTEN之前累积的通知会在另一个线程检出该连接时仍然存在。在这种情况下,在UNLISTEN之后,您可能希望在短时间内调用wait_for_notify,直到它返回nil。但是,除非你为了许多不同的目的大量使用LISTENNOTIFY,否则它可能不值得担心。

我在上面添加了一个更好的链接到Sequel的实现,我建议看一下。这很简单。