I have an interesting situation that I need to fulfill. I need to have an EventMachine loop that sits and waits for messages in an AMQP queue but then interrupts that loop in order to send out a message to a separate AMQP queue on a regular interval. I'm new to EventMachine and this is what I have so far, except the EventMachine loop doesn't send the necessary message.
Right now I've made two procs:
listen_loop = Proc.new {
AMQP.start(connection_config) do |connection|
AMQP::Channel.new(connection) do |channel|
channel.queue("queue1", :exclusive => false, :durable => true) do |requests_queue|
requests_queue.once_declared do
consumer = AMQP::Consumer.new(channel, requests_queue).consume
consumer.on_delivery do |metadata, payload|
puts "[requests] Got a request #{metadata.message_id}. Sending a reply to #{metadata.reply_to}..."
response = "responding"
channel.default_exchange.publish(response,
:routing_key => metadata.reply_to,
:correlation_id => metadata.message_id,
:mandatory => true)
metadata.ack
end
end
end
end
end
Signal.trap("INT") { AMQP.stop { EM.stop } }
Signal.trap("TERM") { AMQP.stop { EM.stop } }
}
send_message = Proc.new {
AMQP.start(connection_config) do |connection|
channel = AMQP::Channel.new(connection)
queue = channel.queue('queue2')
channel.default_exchange.publish("hello world", :routing_key => queue.name)
EM.add_timer(0.5) do
connection.close do
EM.stop{ exit }
end
end
end
}
And then I have my EventMachine Loop:
EM.run do
EM.add_periodic_timer(5) { send_message.call }
listen_loop.call
end
I am able to receive messages in the listen loop but I am unable to send off any of the messages on the regular interval.
Figured out what I was doing wrong. The message loop wasn't able to open up a new connection to the RabbitMQ server because it was already connected. Consolidated everything into a single EventMachine loop and reused the connection and it works.
For those curious it looks like this:
EM.run do
AMQP.start(connection_config) do |connection|
channel = AMQP::Channel.new(connection)
EM.add_periodic_timer(5) { channel.default_exchange.publish("foo", :routing_key => 'queue2') }
queue = channel.queue("queue1", :exclusive => false, :durable => true)
channel.prefetch(1)
queue.subscribe(:ack => true) do |metadata, payload|
puts "[requests] Got a request #{metadata.message_id}. Sending a reply to #{metadata.reply_to}..."
response = "bar"
channel.default_exchange.publish(response,
:routing_key => metadata.reply_to,
:correlation_id => metadata.message_id,
:mandatory => true)
metadata.ack
end
end
Signal.trap("INT") { AMQP.stop { EM.stop } }
Signal.trap("TERM") { AMQP.stop { EM.stop } }
end
Related
I am new to ruby. I am trying to implement a chat client using em-websocket. I have the following code:
EventMachine::WebSocket.start(host: '0.0.0.0', port: 8080) do |websock|
websock.onopen do
puts 'New Connection Opened'
cookies = CGI::Cookie::parse( websock.request["cookie"])
person = Person.where(['token = ?', cookies["token"]]).first
unless person
websock.close(code = nil, body = {Error: "Invalid Token"}.to_json) unless person
return
end
puts "#{person.name} authenticated!"
person=person.attributes.merge(websock.attributes) # this doesn't work
# Subscribe the new user to the GrindServer.realtime_channel with the callback function for the push action
new_user = GrindServer.realtime_channel.subscribe { |msg| websock.send msg }
GrindServer.online_people << person
# Add the new user to the user list
#users[websock.object_id] = new_user
# Push the last messages to the user
# message.all.each do |message|
# websock.send message.to_json
# end
# puts GrindServer.realtime_channel.inspect
# Broadcast the notification to all users
onlinepeople = []
GrindServer.online_people.each do |onli|
onlinepeople << person
end
# Send last 10 messages to the newly connected user
websock.send Message.where({ receiver_id: [0, person.id}).last(10).to_json
GrindServer.realtime_channel.push ({
'id' => 0,
'sender_id' => 0,
'messagetext' => "#{person.name} joined. <$<^<#<#{#users.length}>#>^>$> users in chat",
'users' => onlinepeople,
'metadata' => websock.request["query"]["id"],
}.to_json)
end
...# other event handlers
end
Basically I am trying to maintain a list of Person (ActiveRecord Object) and its corresponding WebSocket::Connection Object.
Server code
Migration
Update: Even if I am unable to merge. I should be able to just attach a note to websocket that this belongs to a person named "x"?
I solved it by using a hash.
EventMachine::WebSocket.start(host: '0.0.0.0', port: 8080) do |websock|
websock.onopen do
puts 'New Connection Opened'
cookies = CGI::Cookie::parse( websock.request["cookie"])
person = Person.where(['token = ?', cookies["token"]]).first
unless person
websock.close(code = nil, body = {Error: "Invalid Token"}.to_json) unless person
return
end
puts "#{person.name} authenticated!"
# person=person.attributes.merge(websock.attributes)
# Subscribe the new user to the GrindServer.realtime_channel with the callback function for the push action
new_user = GrindServer.realtime_channel.subscribe { |msg| websock.send msg }
GrindServer.online_people << {:ws_oid => websock.object_id,
:websocket => websock,
:person_name => person.name,
:person_trigram => person.trigram} # this solves it
# Add the new user to the user list
#users[websock.object_id] = new_user
onlinepeople = []
GrindServer.online_people.each do |onli|
onlinepeople << onli.except(:websocket)
end
# Send last 10 messages to the newly connected user
websock.send Message.where({ receiver_id: [0, person.id]}).last(10).to_json
GrindServer.realtime_channel.push ({
'id' => 0,
'sender_id' => 0,
'messagetext' => "#{person.name} joined. <$<^<#<#{#users.length}>#>^>$> users in chat",
'users' => onlinepeople,
'metadata' => person.trigram,
}.to_json)
end
I am using the em-websocket to make communication for clients (may 2 or more users).
In their introduction . https://github.com/igrigorik/em-websocket
I want to modify their simple echo server example to achieve my purpose.
but in their example , the handshake.path output always show "/" .
I cannot know where the client from .
Is there have any solution that can know the client source place and make a broadcast message to all of them ?
I found the answer in their example.
https://github.com/igrigorik/em-websocket/blob/master/examples/multicast.rb
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 8080, :debug => true) do |ws|
ws.onopen {
sid = #channel.subscribe { |msg| ws.send msg }
#channel.push "#{sid} connected!"
ws.onmessage { |msg|
#channel.push "<#{sid}>: #{msg}"
}
ws.onclose {
#channel.unsubscribe(sid)
}
}
end
But i still have problem that: How can I send message to specified clients?
(e.g.) two clients (No.1 and No.2) make their own communication.
My SOAP-Server expects every request to have a valid token in the soap-header to authenticate the soap-client. This token is only valid for a certain period of time, so I have to expect it to be invalid in every call.
I am trying to find a way to force savon to rebuild the SOAP-Header (i.e. use the new auth-token) after I (re)authenticate with the SOAP-Server. I am not sure, if that is either a savon problem or a ruby one. Here is what I have so far.
class Soapservice
extend Savon::Model
# load stored auth-token
##header_data = YAML.load_file "settings.yaml"
client wsdl: 'locally-cached-wsdl.xml',
soap_header: {'verifyingToken' => ##header_data}
operations :get_authentification_token, :get_server_time
# request a new auth-token and store it
def get_authentification_token
response = super(:message => {
'oLogin' => {
'Username' => 'username',
'Userpass' => 'password'
}
})
settings = {
'UserID' => response[:user_id].to_i,
'Token' => response[:token],
}
File.open("settings.yaml", "w") do |file|
file.write settings.to_yaml
end
##header_data = settings
end
def get_server_time
return super()
rescue Savon::SOAPFault => error
fault_code = error.to_hash[:fault][:faultstring]
if fault_code == 'Unauthorized Request - Invalide Token'
get_authentification_token
retry
end
end
end
When I call
webservice = Soapservice.new
webservice.get_server_time
with an invalid Token, it reauthenticates and saves the new Token successfully, but the retry doesn't load the new header (the result is an infinite loop). Any ideas?
I added rubiii's answer from the GitHub-Issue here for future reference:
class Soapservice
# load stored auth-token
##header_data = YAML.load_file "settings.yaml"
def initialize
#client = Savon.client(wsdl: 'locally-cached-wsdl.xml')
end
def call(operation_name, locals = {})
#client.globals[:soap_header] = {'verifyingToken' => ##header_data}
#client.call(operation_name, locals)
end
# request a new auth-token and store it
def get_authentification_token
message = {
'Username' => 'username',
'Userpass' => 'password'
}
response = call(:get_authentification_token, :message => message)
settings = {
'UserID' => response[:user_id].to_i,
'Token' => response[:token],
}
File.open("settings.yaml", "w") do |file|
file.write settings.to_yaml
end
##header_data = settings
end
def get_server_time
call(:get_server_time)
rescue Savon::SOAPFault => error
fault_code = error.to_hash[:fault][:faultstring]
if fault_code == 'Unauthorized Request - Invalide Token'
get_authentification_token
retry
end
end
end
rubiii added:
notice that i removed Savon::Model, as you actually don't need it and i don't know if it supports this workaround.
if you look at the #call method, it accesses and changes the globals before every request.
I'm trying to return a file during an async sinatra request, something like this:
aget "/test" do
if(File.exists?("test.tar"))
send_file("test.tar", :filename => "test.tar", :type => "application/octet-stream")
return
end
EM.defer(proc{
# create test.tar
},
proc{ |r|
send_file("test.tar", :filename => "test.tar", :type => "application/octet-stream")
})
However it seems that when I do that, I get an error:
wrong number of arguments (0 for 1)
file: file.rb
location: call
line: 29
BACKTRACE:
/var/lib/gems/1.9.1/gems/rack-1.4.1/lib/rack/file.rb in call
def call(env)
/var/lib/gems/1.9.1/gems/async_sinatra-1.0.0/lib/sinatra/async.rb in block in body
response.body = Array(async_handle_exception {response.body.call})
/var/lib/gems/1.9.1/gems/async_sinatra-1.0.0/lib/sinatra/async.rb in async_handle_exception
yield
/var/lib/gems/1.9.1/gems/async_sinatra-1.0.0/lib/sinatra/async.rb in body
response.body = Array(async_handle_exception {response.body.call})
/var/lib/gems/1.9.1/gems/sinatra-1.3.2/lib/sinatra/base.rb in invoke
body(res.pop)
/var/lib/gems/1.9.1/gems/async_sinatra-1.0.0/lib/sinatra/async.rb in block in async_catch_execute
invoke { halt h }
/var/lib/gems/1.9.1/gems/async_sinatra-1.0.0/lib/sinatra/async.rb in async_handle_exception
yield
/var/lib/gems/1.9.1/gems/async_sinatra-1.0.0/lib/sinatra/async.rb in async_catch_execute
async_handle_exception do
/var/lib/gems/1.9.1/gems/async_sinatra-1.0.0/lib/sinatra/async.rb in block in async_schedule
native_async_schedule { async_catch_execute(&b) }
/var/lib/gems/1.9.1/gems/eventmachine-1.0.0.beta.4/lib/eventmachine.rb in call
end.each { |j| j.call }
/var/lib/gems/1.9.1/gems/eventmachine-1.0.0.beta.4/lib/eventmachine.rb in block in run_deferred_callbacks
end.each { |j| j.call }
/var/lib/gems/1.9.1/gems/eventmachine-1.0.0.beta.4/lib/eventmachine.rb in each
#next_tick_mutex.synchronize do
/var/lib/gems/1.9.1/gems/eventmachine-1.0.0.beta.4/lib/eventmachine.rb in run_deferred_callbacks
#next_tick_mutex.synchronize do
/var/lib/gems/1.9.1/gems/eventmachine-1.0.0.beta.4/lib/eventmachine.rb in run_machine
run_machine
/var/lib/gems/1.9.1/gems/eventmachine-1.0.0.beta.4/lib/eventmachine.rb in run
run_machine
/var/lib/gems/1.9.1/gems/thin-1.3.1/lib/thin/backends/base.rb in start
EventMachine.run(&starter)
/var/lib/gems/1.9.1/gems/thin-1.3.1/lib/thin/server.rb in start
#backend.start
/var/lib/gems/1.9.1/gems/rack-1.4.1/lib/rack/handler/thin.rb in run
server.start
/var/lib/gems/1.9.1/gems/sinatra-1.3.2/lib/sinatra/base.rb in run!
handler.run self, :Host => bind, :Port => port do |server|
/var/lib/gems/1.9.1/gems/sinatra-1.3.2/lib/sinatra/main.rb in block in <module:Sinatra>
at_exit { Application.run! if $!.nil? && Application.run? }
end
The error you are receiving is cause you are using a function that requires 1 argument and you haven't supplied any. If you can show us the code around line 29 I or somebody can point it out for you.
I have successfully sent email to a remote server using port their port 25 (non-secure) with this script:
require 'rubygems'
require 'mail'
options = { :address => "mail.domain.com",
:port => 25,
:domain => 'mail.domain.com',
:user_name => 'somedude#domain.com',
:password => 'topsecret',
:authentication => 'login',
:enable_starttls_auto => true }
Mail.defaults do
delivery_method :smtp, options
end
mail = Mail.new do
from 'someotherdude#otherdomain.com'
to 'somedude#domain.com'
subject 'This is a test email'
body File.read('body.txt')
end
puts mail.to_s
mail.deliver!
What I need to do now is use their SSL port 466. When I try it, I get the normal output detailing the message, then it pauses for about 2 minutes and coughs up this:
/usr/local/rvm/rubies/ruby-1.8.7-p249/lib/ruby/1.8/timeout.rb:60:in `rbuf_fill': execution expired (Timeout::Error)
from /usr/local/rvm/rubies/ruby-1.8.7-p249/lib/ruby/1.8/net/protocol.rb:134:in `rbuf_fill'
from /usr/local/rvm/rubies/ruby-1.8.7-p249/lib/ruby/1.8/net/protocol.rb:116:in `readuntil'
from /usr/local/rvm/rubies/ruby-1.8.7-p249/lib/ruby/1.8/net/protocol.rb:126:in `readline'
from /usr/local/rvm/rubies/ruby-1.8.7-p249/lib/ruby/1.8/net/smtp.rb:911:in `recv_response'
from /usr/local/rvm/rubies/ruby-1.8.7-p249/lib/ruby/1.8/net/smtp.rb:554:in `do_start'
from /usr/local/rvm/rubies/ruby-1.8.7-p249/lib/ruby/1.8/net/smtp.rb:921:in `critical'
from /usr/local/rvm/rubies/ruby-1.8.7-p249/lib/ruby/1.8/net/smtp.rb:554:in `do_start'
from /usr/local/rvm/rubies/ruby-1.8.7-p249/lib/ruby/1.8/net/smtp.rb:525:in `start'
from /usr/local/rvm/gems/ruby-1.8.7-p249/gems/mail-2.2.10/lib/mail/network/delivery_methods/smtp.rb:127:in `deliver!'
from /usr/local/rvm/gems/ruby-1.8.7-p249/gems/mail-2.2.10/lib/mail/message.rb:243:in `deliver!'
from testmail.rb:30
I think this is because it cannot even begin the SSL authentication process. How do I do it?
Hmm reading the network/delivery_methods/smtp.rb, it doesn't look like it support Direct SSL. TLS isn't them same, as the connection starts out Plain Text and then Switches to SSL on the starttls command. Can you just use starttls on port 587?
pulling my comment up.
see
How to send mail with ruby over smtp with ssl (not with rails, no TLS for gmail)
Which suggests that you can monkey patch Net::SMTP to do it..
Ok kinda found the issue and can patch around it, but so far this solution is yucky.. but it does work :)
#!/usr/bin/env ruby
require 'rubygems'
require "openssl"
require "net/smtp"
require "mail"
Net::SMTP.class_eval do
def self.start( address, port = nil,
helo = 'localhost.localdomain',
user = nil, secret = nil, authtype = nil, use_tls = false,
use_ssl = true, &block) # :yield: smtp
new(address, port).start(helo, user, secret, authtype, use_tls, use_ssl, &block)
end
def start( helo = 'localhost.localdomain',
user = nil, secret = nil, authtype = nil, use_tls = false, use_ssl = true ) # :yield: smtp
start_method = use_tls ? :do_tls_start : use_ssl ? :do_ssl_start : :do_start
if block_given?
begin
send start_method, helo, user, secret, authtype
return yield(self)
ensure
do_finish
end
else
send start_method, helo, user, secret, authtype
return self
end
end
private
def do_tls_start(helodomain, user, secret, authtype)
raise IOError, 'SMTP session already started' if #started
check_auth_args user, secret
sock = timeout(#open_timeout) { TCPSocket.open(#address, #port) }
#socket = Net::InternetMessageIO.new(sock)
#socket.read_timeout = 60 ##read_timeout
#socket.debug_output = STDERR ##debug_output
check_response(critical { recv_response() })
do_helo(helodomain)
raise 'openssl library not installed' unless defined?(OpenSSL)
starttls
ssl = OpenSSL::SSL::SSLSocket.new(sock)
ssl.sync_close = true
ssl.connect
#socket = Net::InternetMessageIO.new(ssl)
#socket.read_timeout = 60 ##read_timeout
#socket.debug_output = STDERR ##debug_output
do_helo(helodomain)
authenticate user, secret, authtype if user
#started = true
ensure
unless #started
# authentication failed, cancel connection.
#socket.close if not #started and #socket and not #socket.closed?
#socket = nil
end
end
def do_ssl_start(helodomain, user, secret, authtype)
raise IOError, 'SMTP session already started' if #started
check_auth_args user, secret
sock = timeout(#open_timeout) { TCPSocket.open(#address, #port) }
raise 'openssl library not installed' unless defined?(OpenSSL)
ssl = OpenSSL::SSL::SSLSocket.new(sock)
ssl.sync_close = true
ssl.connect
#socket = Net::InternetMessageIO.new(ssl)
#socket.read_timeout = 60 ##read_timeout
#socket.debug_output = STDERR ##debug_output
check_response(critical { recv_response() })
do_helo(helodomain)
do_helo(helodomain)
authenticate user, secret, authtype if user
#started = true
ensure
unless #started
# authentication failed, cancel connection.
#socket.close if not #started and #socket and not #socket.closed?
#socket = nil
end
end
def do_helo(helodomain)
begin
if #esmtp
ehlo helodomain
else
helo helodomain
end
rescue Net::ProtocolError
if #esmtp
#esmtp = false
#error_occured = false
retry
end
raise
end
end
def starttls
getok('STARTTLS')
end
def quit
begin
getok('QUIT')
rescue EOFError, OpenSSL::SSL::SSLError
end
end
end
options = {
:address => "mail.domain.net",
:port => 466,
:domain => 'mail.domain.net',
:user_name => 'doon#domain.net',
:password => 'Secret!',
:authentication => 'login',
:use_ssl => true }
Mail.defaults do
delivery_method :smtp, options
end
mail = Mail.new do
from 'doon#domain.net'
to 'doon#someotherdomain.com'
subject 'This is a test email'
body File.read('body.txt')
end
puts mail.to_s
mail.deliver!
for some reason the use_ssl in the orig monkey patch doesn't make it in, and couple that with VERSION being undefined in Net::SMTP. So I changed that out, and forced use_ssl to be true, and was able to send email..
Did you mean to use port 465? That's the standard fallback port, not 466. You're likely timing out connecting to the wrong port.