简体   繁体   English

我可以在带有AMQP的Rails 3中使用请求/答复-RPC模式吗?

[英]Can I use a Request / Reply - RPC pattern in Rails 3 with AMQP?

For reasons similar to the ones in this discussion , I'm experimenting with messaging in lieu of REST for a synchronous RPC call from one Rails 3 application to another. 出于与本次讨论类似的原因,我正在尝试使用消息传递来代替REST,以实现从一个Rails 3应用程序到另一个Rails 3应用程序的同步RPC调用。 Both apps are running on thin. 这两个应用程序都在精简版上运行。

The "server" application has a config/initializers/amqp.rb file based on the Request / Reply pattern in the rubyamqp.info documentation : 基于rubyamqp.info文档中的“请求/回复”模式,“服务器”应用程序具有config/initializers/amqp.rb文件:

require "amqp"

EventMachine.next_tick do
  connection = AMQP.connect ENV['CLOUDAMQP_URL'] || 'amqp://guest:guest@localhost'
  channel    = AMQP::Channel.new(connection)

  requests_queue = channel.queue("amqpgem.examples.services.time", :exclusive => true, :auto_delete => true)
  requests_queue.subscribe(:ack => true) do |metadata, payload|
    puts "[requests] Got a request #{metadata.message_id}. Sending a reply..."
    channel.default_exchange.publish(Time.now.to_s,
                                     :routing_key    => metadata.reply_to,
                                     :correlation_id => metadata.message_id,
                                     :mandatory      => true)
    metadata.ack
  end

  Signal.trap("INT") { connection.close { EventMachine.stop } }
end

In the 'client' application, I'd like to render the results of a synchronous call to the 'server' in a view. 在“客户端”应用程序中,我想在视图中呈现对“服务器”的同步调用的结果。 I realize this is a bit outside the comfort zone of an inherently asynchronous library like the amqp gem, but I'm wondering if there's a way to make it work. 我意识到这有点超出了像amqp gem这样的固有异步库的舒适范围,但是我想知道是否有办法使它工作。 Here is my client config/initializers/amqp.rb : 这是我的客户端config/initializers/amqp.rb

require 'amqp'

EventMachine.next_tick do
  AMQP.connection = AMQP.connect 'amqp://guest:guest@localhost'  
  Signal.trap("INT") { AMQP.connection.close { EventMachine.stop } }
end

Here is the controller: 这是控制器:

require "amqp"

class WelcomeController < ApplicationController
  def index    
    puts "[request] Sending a request..."

    WelcomeController.channel.default_exchange.publish("get.time",
      :routing_key => "amqpgem.examples.services.time",
      :message_id  => Kernel.rand(10101010).to_s,
      :reply_to    => WelcomeController.replies_queue.name)

    WelcomeController.replies_queue.subscribe do |metadata, payload|
      puts "[response] Response for #{metadata.correlation_id}: #{payload.inspect}"
      @message = payload.inspect
    end   
  end

  def self.channel
    @channel ||= AMQP::Channel.new(AMQP.connection)
  end

  def self.replies_queue
    @replies_queue ||= channel.queue("reply", :exclusive => true, :auto_delete => true)
  end
end

When I start both applications on different ports and visit the welcome#index view. 当我在不同端口上启动两个应用程序并访问welcome#index视图时。 @message is nil in the view, since the result has not yet returned. @message在视图中为nil,因为结果尚未返回。 The result arrives a few milliseconds after the view is rendered and is displayed on the console: 结果在呈现视图后几毫秒到达,并显示在控制台上:

$ thin start
>> Using rack adapter
>> Thin web server (v1.5.0 codename Knife)
>> Maximum connections set to 1024
>> Listening on 0.0.0.0:3000, CTRL+C to stop
[request] Sending a request...
[response] Response for 3877031: "2012-11-27 22:04:28 -0600"

No surprise here: subscribe is clearly not meant for synchronous calls. 毫不奇怪: subscribe显然并不意味着同步呼叫。 What is surprising is that I can't find a synchronous alternative in the AMQP gem source code or in any documentation online. 令人惊讶的是,在AMQP gem源代码或任何在线文档中找不到同步替代方法。 Is there an alternative to subscribe that will give me the RPC behavior I want? 是否有其他subscribe方式可以使我获得所需的RPC行为? Given that there are other parts of the system in which I'd want to use legitimately asynchronous calls, the bunny gem didn't seem like the right tool for the job. 鉴于我想在系统的其他部分中使用合法的异步调用,因此, bunny gem似乎不适合该工作。 Should I give it another look? 我应该再看一遍吗?

edit in response to Sam Stokes 编辑以回应Sam Stokes

Thanks to Sam for the pointer to throw :async / async.callback. 感谢Sam提供了抛出:async / async.callback的指针。 I hadn't seen this technique before and this is exactly the kind of thing I was trying to learn with this experiment in the first place. 我以前从未见过这种技术,而这恰恰是我尝试通过该实验学习的那种东西。 send_response.finish is gone in Rails 3, but I was able to get his example to work for at least one request with a minor change: 在Rails 3中, send_response.finish消失了,但是我能够通过最小的更改使他的示例至少可以处理一个请求:

render :text => @message
rendered_response = response.prepare!

Subsequent requests fail with !! Unexpected error while processing request: deadlock; recursive locking 后续请求失败!! Unexpected error while processing request: deadlock; recursive locking !! Unexpected error while processing request: deadlock; recursive locking !! Unexpected error while processing request: deadlock; recursive locking . !! Unexpected error while processing request: deadlock; recursive locking This may have been what Sam was getting at with the comment about getting ActionController to allow concurrent requests, but the cited gist only works for Rails 2. Adding config.allow_concurrency = true in development.rb gets rid of this error in Rails 3, but leads to This queue already has default consumer. 这可能是Sam在获得有关使ActionController允许并发请求的评论时正在讨论的内容,但是引用的要点仅适用于Rails2。在development.rb中添加config.allow_concurrency = true消除了Rails 3中的此错误,但是导致This queue already has default consumer. from AMQP. 来自AMQP。

I think this yak is sufficiently shaven. 我认为这种牛已经足够剃光了。 ;-) ;-)

While interesting, this is clearly overkill for simple RPC. 尽管很有趣,但是对于简单的RPC来说显然是多余的。 Something like this Sinatra streaming example seems a more appropriate use case for client interaction with replies. 像这个Sinatra流示例一样,对于客户端与回复的交互来说,似乎是一个更合适的用例。 Tenderlove also has a blog post about an upcoming way to stream events in Rails 4 that could work with AMQP. Tenderlove也有一篇博客文章,介绍了一种即将在Rails 4中流式传输事件的方法,该方法可以与AMQP一起使用。

As Sam points out in his discussion of the HTTP alternative, REST / HTTP makes perfect sense for the RPC portion of my system that involves two Rails apps. 正如Sam在对HTTP替代方案的讨论中所指出的那样,REST / HTTP对于涉及两个Rails应用程序的系统的RPC部分是非常有意义的。 There are other parts of the system involving more classic asynchronous event publishing to Clojure apps. 系统的其他部分涉及将更多经典的异步事件发布到Clojure应用程序。 For these, the Rails app need only publish events in fire-and-forget fashion, so AMQP will work fine there using my original code without the reply queue. 对于这些,Rails应用程序只需要以即发即弃的方式发布事件,因此AMQP可以在没有回复队列的情况下使用我的原始代码在这里正常工作。

You can get the behaviour you want - have the client make a simple HTTP request, to which your web app responds asynchronously - but you need more tricks. 您可以得到想要的行为-让客户端发出一个简单的HTTP请求,您的Web应用程序将对该请求进行异步响应-但您需要更多技巧。 You need to use Thin's support for asynchronous responses: 您需要使用Thin支持异步响应:

require "amqp"

class WelcomeController < ApplicationController
  def index
    puts "[request] Sending a request..."

    WelcomeController.channel.default_exchange.publish("get.time",
      :routing_key => "amqpgem.examples.services.time",
      :message_id  => Kernel.rand(10101010).to_s,
      :reply_to    => WelcomeController.replies_queue.name)

    WelcomeController.replies_queue.subscribe do |metadata, payload|
      puts "[response] Response for #{metadata.correlation_id}: #{payload.inspect}"
      @message = payload.inspect

      # Trigger Rails response rendering now we have the message.
      # Tested in Rails 2.3; may or may not work in Rails 3.x.
      rendered_response = send_response.finish

      # Pass the response to Thin and make it complete the request.
      # env['async.callback'] expects a Rack-style response triple:
      # [status, headers, body]
      request.env['async.callback'].call(rendered_response)
    end

    # This unwinds the call stack, skipping the normal Rails response
    # rendering, all the way back up to Thin, which catches it and
    # interprets as "I'll give you the response later by calling
    # env['async.callback']".
    throw :async
  end

  def self.channel
    @channel ||= AMQP::Channel.new(AMQP.connection)
  end

  def self.replies_queue
    @replies_queue ||= channel.queue("reply", :exclusive => true, :auto_delete => true)
  end
end

As far as the client is concerned, the result is indistinguishable from your web app blocking on a synchronous call before returning the response; 就客户端而言,在返回响应之前,结果与您的Web应用在同步调用上的阻塞没有区别。 but now your web app can process many such requests concurrently. 但是现在您的Web应用程序可以同时处理许多此类请求。

CAUTION! 警告!

Async Rails is an advanced technique; 异步Rails是一项高级技术。 you need to know what you're doing. 您需要知道自己在做什么。 Some parts of Rails do not take kindly to having their call stack abruptly dismantled. Rails的某些部分并不善于突然取消其调用堆栈。 The throw will bypass any Rack middlewares that don't know to catch and rethrow it ( here is a rather old partial solution ). throw将绕过任何不知道要捕获并重新抛出的Rack中间件( 这是一个相当古老的部分解决方案 )。 ActiveSupport's development-mode class reloading will reload your app's classes after the throw , without waiting for the response, which can cause very confusing breakage if your callback refers to a class that has since been reloaded. ActiveSupport的开发模式类重载将在throw之后重载您应用程序的类,而无需等待响应,如果您的回调引用此后已重载的类,则可能导致令人困惑的破坏。 You'll also need to ask ActionController nicely to allow concurrent requests. 您还需要很好地询问ActionController以允许并发请求。

Request/response 要求/回应

You're also going to need to match up requests and responses. 您还需要匹配请求和响应。 As it stands, if Request 1 arrives, and then Request 2 arrives before Request 1 gets a response, then it's undefined which request would receive Response 1 (messages on a queue are distributed round-robin between the consumers subscribed to the queue). 就目前而言,如果请求1到达,然后请求2在请求1得到响应之前到达,那么尚不确定哪个请求将接收到响应1(队列中的消息在订阅该队列的使用者之间轮流分发)。

You could do this by inspecting the correlation_id (which you'll have to explicitly set, by the way - RabbitMQ won't do it for you!) and re-enqueuing the message if it's not the response you were waiting for. 您可以通过检查correlation_id(顺便说一句,您必须显式设置-RabbitMQ不会为您设置!)并重新排队消息(如果不是您正在等待的响应)来完成此操作。 My approach was to create a persistent Publisher object which would keep track of open requests, listen for all responses, and lookup the appropriate callback to invoke based on the correlation_id. 我的方法是创建一个持久的Publisher对象,该对象将跟踪打开的请求,侦听所有响应,并根据correlation_id查找适当的回调以进行调用。

Alternative: just use HTTP 替代方法:仅使用HTTP

You're really solving two different (and tricky!) problems here: persuading Rails/thin to process requests asynchronously, and implementing request-response semantics on top of AMQP's publish-subscribe model. 您实际上在这里解决了两个不同的问题(而且很棘手!):说服Rails / thin异步处理请求,并在AMQP的发布-订阅模型之上实现请求-响应语义。 Given you said this is for calling between two Rails apps, why not just use HTTP, which already has the request-response semantics you need? 假设您说这是为了在两个Rails应用程序之间调用,为什么不只使用HTTP,HTTP已经具有您所需的请求-响应语义? That way you only have to solve the first problem. 这样,您只需要解决第一个问题。 You can still get concurrent request processing if you use a non-blocking HTTP client library, such as em-http-request . 如果使用非阻塞HTTP客户端库(例如em-http-request) ,则仍然可以进行并发请求处理。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 Rails:如何在 a.select() 请求上使用 .to_sql - Rails: How can I use .to_sql on a .select() request Rails:我可以使用模板模式将可执行代码注入控制器并使用丰富的子类化吗? - Rails: Can I use Template Pattern to inject executable code into controller and use rich subclassing? 为什么我可以在rails4和rails5中使用request.path,在哪里记录了它? - Why can I use request.path in rails4 and rails5, where is it documented? Rails 5,不能在控制器中使用请求吗? - Rails 5, can't use request in controller? 我可以使用curl或curb张贴到使用Rails的跨站点请求伪造保护的表单吗? - Can I use curl or curb to POST to a form that uses rails' Cross-site request forgery protection? 为什么我仍然可以在Rails Rspec Type Request中使用Capybara方法 - Why I still can use Capybara method inside Rails Rspec Type Request 我可以在Rails中以XML响应HTML请求吗? - Can I respond with XML to an HTML request in Rails? 如何在Ruby on Rails中分析请求? - How can I profile a request in Ruby on Rails? 如何从Rails页面中请求Rails页面? - How can I request a Rails page from within a Rails page? 无法从 Sneakers worker 连接到 Rails 数据库(RabbitMQ RPC 调用) - Can not connect to Rails database from Sneakers worker (RabbitMQ RPC call)
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM