I am attempting to write a chat server with EventMachine. How do I pass a message from one EventMachine connection, to another, in a thread-safe manner?
I see a messaging protocol (Stomp) being supported but I can't figure out how to use it. Any help is appreciated.
Stomp in EventMachine - http://eventmachine.rubyforge.org/EventMachine/Protocols/Stomp.html
See http://eventmachine.rubyforge.org/EventMachine/Channel.html
you can try something in these lines:
require 'eventmachine'
class Chat < EventMachine::Connection
def initialize channel
#channel = channel
end
def post_init
send_data 'Hello'
#sid = #channel.subscribe do |msg|
send_data msg
end
end
def receive_data(msg)
#channel.push msg
end
def unbind
#channel.unsubscribe #sid
end
end
EM.run do
#channel = EventMachine::Channel.new
EventMachine.start_server '127.0.0.1', 8081, Chat, #channel
end
EDIT: also check out https://github.com/eventmachine/eventmachine/tree/master/examples/guides/getting_started - there is a nice chatroom example
Try starting out with an in memory message dispatcher.
require 'thread'
class Room
def initialize
#users = []
end
def join(user)
#users << user
end
def leave(user)
#user.delete(user)
end
def broadcast(message)
#users.each do |user|
user.enqueue(message)
end
end
end
class User
def initialize
#mutex = Mutex.new
#queued_messages = []
end
def enqueue(message)
#mutex.synchronize do
#queued_message << message
end
end
def get_new_messages
#mutex.synchronize do
output = #queued_messages
#queued_messages = []
end
return output
end
end
UPDATE
ROOM = Room.new
class Connection
def user_logged_in
# #user = ...
ROOM.join(#user)
end
def received_message(message)
ROOM.broadcast(message)
end
def receive_send_more_messages_request(req)
messages = #user.get_new_messages
# write messages
end
def connection_closed
ROOM.leave(#user)
end
end
Related
Given something like:
class MyClass
def subscribe
$redis.subscribe('channel') do |on|
on.message do |channel, msg|
Something.create(msg)
end
end
end
end
How can I test that when MyClass executes subscribe, it will run Something.create for each message it receives on the channel?
This code you have, it's not very testable. First of all, absolutely get rid of this global $redis variable. Instead, accept an instance of redis in the constructor.
class MyClass
attr_reader :redis
def initialize(redis)
#redis = redis
end
def subscribe
redis.subscribe('channel') do |on|
on.message do |channel, msg|
Something.create(msg)
end
end
end
end
Then in tests you can make a dummy redis that you can totally control but which conforms to the api you're using. Something along these lines:
class DummyRedis
def subscribe(&block)
#block = block
end
def trigger_on
#block.call make_on_message
end
end
fake_redis = DummyRedis.new
expect {
mc = MyClass.new(fake_redis)
mc.subscribe
fake_redis.trigger_on
}.to change{Something.count}.from(0).to(1)
This cool technique is called Dependency Injection (or, as some people put it, "passing parameters to constructors").
Well, it could be as easy as
describe MyClass do
it 'should create something' do
expect(Something).to receive(:create)
subject.subscribe
subject.trigger_message # you should trigger a message somehow
end
end
Although this approach is not using actual tests, i would do the following and check the logs.
class MyClass
def subscribe
$redis.subscribe('channel') do |on|
on.message do |channel, msg|
event = Something.create(msg)
p event.persisted? ? "success" : "fail"
p event
end
end
end
end
There has to be a better way to do this:
require 'log4r'
class PaddedLogger
attr_accessor :logger, :padding
def initialize(args)
#logger = Log4r::Logger.new args[:name]
#padding = args[:padding]
end
def debug(system, message)
system = "[#{system}]"
#logger.debug "#{system.ljust(#padding)}#{message}"
end
def info(system, message)
system = "[#{system}]"
#logger.info "#{system.ljust(#padding)}#{message}"
end
def warn(system, message)
system = "[#{system}]"
#logger.warn "#{system.ljust(#padding)}#{message}"
end
def error(system, message)
system = "[#{system}]"
#logger.error "#{system.ljust(#padding + 3)}#{message}"
end
def fatal(system, message)
system = "[#{system}]"
#logger.fatal "#{system.ljust(#padding + 3)}#{message}"
end
end
It's just a wrapping class to help me get my log4r logs appearing in a specific format, and it works exactly as I need it too, but surely Ruby's meta-programming magic can simplify this so I don't have to constantly repeat myself.
Something like this maybe?
require 'log4r'
class PaddedLogger
attr_accessor :logger, :padding
def initialize(args)
#logger = Log4r::Logger.new args[:name]
#padding = args[:padding]
end
[:debug, :info, :warn, :error, :fatal].each do |reason|
define_method reason do |system, message|
system = "[#{system}]"
#logger.send reason, "#{system.ljust(#padding)}#{message}"
end
end
end
And while you're at it, why not collapse the two lines in the define_method block to one?
#logger.send reason, "[#{system}]".ljust(#padding) + message
Here is and alternative solution using method_missing:
class TestClass
##methods = [:upcase, :other_methods_to_catch]
def initialize
#message = 'hello'
end
def method_missing name, *args
super unless ##methods.include? name
#message.send(name, *args)
end
end
x = TestClass.new
p x.upcase #=> HELLO
So witness and observe the following code, my questions is why do i never make it to the on_connect after starting the cool.io loop in send_to_server, the l.run should fire off the request as per the documented example on the github, and how the code handles incoming connections in module Server #socket.attach(l)
l.run
which does work and accepts the incoming data and sends it to my parser, which does work and fires off all the way up until the aforementioned send_to_server. So what is going on here?
require 'cool.io'
require 'http/parser'
require 'uri'
class Hash
def downcase_key
keys.each do |k|
store(k.downcase, Array === (v = delete(k)) ? v.map(&:downcase_key) : v)
end
self
end
end
module ShadyProxy
extend self
module ClientParserCallbacks
extend self
def on_message_complete(conn)
lambda do
puts "on_message_complete"
PluginHooks.before_request_to_server(conn)
end
end
def on_headers_complete(conn)
lambda do |headers|
conn.headers = headers
end
end
def on_body(conn)
lambda do |chunk|
conn.body << chunk
end
end
end
module PluginHooks
extend self
def before_request_to_server(conn)
# modify request here
conn.parser.headers.delete "Proxy-Connection"
conn.parser.headers.downcase_key
send_to_server(conn)
end
def send_to_server(conn)
parser = conn.parser
uri = URI::parse(parser.request_url)
l = Coolio::Loop.default
puts uri.scheme + "://" + uri.host
c = ShadyHttpClient.connect(uri.scheme + "://" + uri.host,uri.port).attach(l)
c.connection_reference = conn
c.request(parser.http_method,uri.request_uri)
l.run
end
def before_reply_to_client(conn)
end
end
class ShadyHttpClient < Coolio::HttpClient
def connection_reference=(conn)
puts "haz conneciton ref"
#connection_reference = conn
end
def connection_reference
#connection_reference
end
def on_connect
super
#never gets here
#headers = nil
#body = ''
#buffer = ''
end
def on_connect_failed
super
# never gets here either
end
def on_response_header(header)
#headers = header
end
def on_body_data(data)
puts "on data?"
#body << data
STDOUT.write data
end
def on_request_complete
puts "Headers"
puts #headers
puts "Body"
puts #body
end
def on_error(reason)
STDERR.puts "Error: #{reason}"
end
end
class ShadyProxyConnection < Cool.io::TCPSocket
attr_accessor :headers, :body, :buffer, :parser
def on_connect
#headers = nil
#body = ''
#buffer = ''
#parser = Http::Parser.new
#parser.on_message_complete = ClientParserCallbacks.on_message_complete(self)
#parser.on_headers_complete = ClientParserCallbacks.on_headers_complete(self)
#parser.on_body = ClientParserCallbacks.on_body(self)
end
def on_close
puts "huh?"
end
def on_read(data)
#buffer << data
#parser << data
end
end
module Server
def run(opts)
begin
# Start our server to handle connections (will raise things on errors)
l = Coolio::Loop.new
#socket = Cool.io::TCPServer.new(opts[:host],opts[:port], ShadyProxy::ShadyProxyConnection)
#socket.attach(l)
l.run
# Handle every request in another thread
loop do
Thread.new s = #socket.accept
end
# CTRL-C
rescue Interrupt
puts 'Got Interrupt..'
# Ensure that we release the socket on errors
ensure
if #socket
#socket.close
puts 'Socked closed..'
end
puts 'Quitting.'
end
end
module_function :run
end
end
ShadyProxy::Server.run(:host => '0.0.0.0',:port => 1234)
I created small API library, everything worked fine, until I realized, that I need multiple configurations.
It looks like this:
module Store
class Api
class << self
attr_accessor :configuration
def configure
self.configuration ||= Configuration.new
yield configuration
end
def get options = {}
url = "#{configuration.domain}/#{options[:resource]}"
# ResClient url ...
end
end
end
class Configuration
attr_accessor :domain
def initialize options = {}
#domain = options[:domain]
end
end
class Product
def self.get
sleep 5
Api.get resource: 'products'
end
end
end
When I run it simultaneously, it override module configuration.
Thread.new do
10.times do
Store::Api.configure do |c|
c.domain = "apple2.com"
end
p Store::Product.get
end
end
10.times do
Store::Api.configure do |c|
c.domain = "apple.com"
end
p Store::Product.get
end
I can't figure out, how make this module better. Thanks for your advise
Well, if you don't want multiple threads to compete for one resource, you shouldn't have made it a singleton. Try moving object configuration from class to its instances, then instantiate and configure them separately.
There is more to refactor here, but this solves your problem:
module Store
class API
attr_reader :domain
def initialize(options = {})
#domain = options[:domain]
end
def products
sleep 5
get resource: 'products'
end
private
def get(options = {})
url = "#{configuration.domain}/#{options[:resource]}"
# ResClient url ...
end
end
end
Thread.new do
10.times do
api = Store::API.new(domain: 'apple2.com')
p api.products
end
end
10.times do
api = Store::API.new(domain: 'apple.com')
p api.products
end
I'm trying to set up a basic routing system in Rack, however.. I can't understand why the first route ('/') works, and the second ('/help') doesn't. What gets returned in the case of '/help' is a NoMethodError. Why is that, and how can I fix it? Thank you.
require 'rack'
class MyApp
def self.call(env)
new.call(env)
end
def self.get(path, &block)
##routes ||= {}
##routes[path] = block
end
def call(env)
dup.call!(env)
end
def call!(env)
#req = Rack::Request.new(env)
#res = Rack::Response.new
#res.write route(#req.path)
#res.finish
end
def route(path)
if ##routes[path]
##routes[path].call
else
'Not Found'
end
end
def to_do(arg)
arg
end
end
MyApp.get '/' do
'index'
end
MyApp.get '/help' do
to_do 'help'
end
run MyApp