How to keep persistent socket connections?

Hello ruby experts!

I've been working in Ruby on Rails for awhile now (like 3 months), I and have *thoroughly* enjoyed it thus far.

But my team and I have come up against a problem that I don't think Rails can address. I know Ruby itself can implement a multithreaded socket server, but I don't know how to make that happen within the context of rails.

We have 2 distinct (and similar problems) that center around the need to keep persistent socket connections open (as a socket server, not client).

For the first, we need to be able to open a socket to a 3rd party server and keep it open. Today I'm going to try opening the socket and then storing the socket in the Rails cache, in order to see if that socket remains open and useable even while just sitting in the cache (I'm kind of thinking it won't).

For the second, we need to build a game server with built-in chat. The clients need to be able to open a socket and then send and receive game-related messages on that socket. I have done some digging and found TCPServer class that Ruby provides, which could definitely fit the bill. I've also seen Gserver and the "basic scrappy little chat server" example provided at http://www.rubyinside.com/advent2006/10-gserver.html. I'm thinking -- is there any way I could run a Gserver or TCPServer on top of rails? Have it listen on a different port, but still have all the code inside be able to access my rails classes/models/etc? Then I could have a multithreaded server, but with all the convenience and power of RoR.

Let me know what you folks think. I'm really looking forward to having my mind blown by your solutions. :slight_smile:

-Steve

Today I'm going to try opening the socket and then storing the socket in the Rails cache, in order to see if that socket remains open and useable even while just sitting in the cache (I'm kind of thinking it won't).

Yeah, that didn't work because caching the connection makes it immutable, which means you can no longer write data to it. But you know what I think *will* work? Storing the connection(s) as class vars. So I'm trying that next. I will report back here afterward for anyone who might be interested.

If I was you I'd ask how to do this over on comp.lang.ruby. My guess is you'll have a better time doing this entirely outside of rails.

BTW--the first appendix of the pickaxe book covers ruby's socket library. If you haven't already looked at that, there could be some useful stuff in there...

FWIW--you can use AR in a straight ruby program. Frx--something like this should work I think:

  require "rubygems"   require "activerecord"   require "sqlite3"

  ActiveRecord::Base.establish_connection(:adapter => 'sqlite', :database => '~/my_db.db')

  c = ::ActiveRecord::Base.connection

  unless c.tables.member?("features")     c.create_table(:features) do |t|       t.column :feature, :string       t.column :category, :string       t.column :count, :integer     end     c.add_index(:features, [:category, :feature], :unique => true)   end

  class Feature < ActiveRecord::Base   end

  f = Feature.new(:feature => "bibbity", :category => "bobbity", :count => 1)   f.save!   puts("finished!")

Roy --

Thank you for that example!! I guess I'll give that a shot :slight_smile:

It still hurts my heart that this doesn't appear to be possible with Rails. :frowning:

One followup question, if I may: Why is it that class variables don't behave as I expect them to? So since it appears that adding class attributes (inheritable or not) to a descendent of ActiveRecord is unsupported, I added a new Class (inheriting from Object) and put it in /lib:

class ConnectionManager   HOST = "the.host.com"   PATH = '/'   PORT = 2195

  PASSPHRASE = "foobar"   CACERT = File.expand_path(File.dirname(__FILE__) + "certs/ca.the.host.com.crt")   USERAGENT = 'Mozilla/5.0 (ConnectionManager Ruby on Rails 0.1)'   cattr_accessor :connection, :cert_name, :cert   self.cert_name = "my_pem_file.pem"

  def initialize     super     cert = File.read("config/#{cert_name}") if File.exists?("config/#{cert_name}")     puts "cert = #{cert.inspect}"     if connection.nil?       puts "Connecting now!!"       ctx = OpenSSL::SSL::SSLContext.new       ctx.key = OpenSSL::PKey::RSA.new(cert, PASSPHRASE)       ctx.cert = OpenSSL::X509::Certificate.new(cert)

      s = TCPSocket.new(HOST, PORT)       connection = OpenSSL::SSL::SSLSocket.new(s, ctx)       connection.sync = true       connection.connect     end   end end

But when I try to use this in my class that inherits from ActiveRecord, like this:

  def send     ssl = cm.class.connection     logger.info "ssl = #{ssl.inspect}"     ssl.write(self.message_for_sending)

  rescue SocketError => error     raise "Error while sending: #{error}"   end

First I got an error and I realized that my ConnectionManager's initialize method wasn't being called. So I added a line to get an instance of ConnectionManager, just so that initialize would be called:

  def send_notification     cm = ConnectionManager.new     ssl = cm.class.connection     logger.info "ssl = #{ssl.inspect}"     ssl.write(self.message_for_sending)

  rescue SocketError => error     raise "Error while sending notifications: #{error}"   end

I could tell from my log ("Connecting Now!!" appeared) that initialize had been called. But I'm still getting an error:

You have a nil object when you didn't expect it! The error occurred while evaluating nil.write

Clearly ConnectionManager.connection is returning null. In Java, once you've used a class, its static vars are instantiated and hold their values. It seems that in Ruby, the class's static vars are instantiated/sandboxed in each .rb file. Am I correct here, or no?

Thanks again for the example code Roy!

-Steve

Heh--you're welcome. But please don't let me bully you into dropping rails--I don't understand your goals as well as you do. :wink: I'm just trying to explain options. I frankly don't understand rails well enough to know when/how objects live beyond individual http requesst/response cycles. My guess is that your socket server thingy needs to be independent of that in order to handle incoming connections reliably.

I can say that it is possible to add class *methods* to a descendant of AR::Base--frx, in the below find_by_fragment is a class method.

  class SpecificTherapeuticClass < ActiveRecord::Base     has_many :ingredients     belongs_to :general_therapeutic_class     def self.find_by_fragment(frag)       frag += "%"       self.find(:all, :conditions => ["description like :frag", {:frag => frag}])     end   end

I'm not totally sure I follow your code below, but one thing jumps out--this line:

  cattr_accessor :connection, :cert_name, :cert

Defines *class* methods & vars for those attributes. So you'd say, e.g. ConnectionManager.cert = "/bibbity/bobbity/boo.pem" and that bit of data is entirely independent of any particular instance of ConnectionManager. But initialize is an *instance* method--it gets called in the context of a new instance of ConnectionManager. So the line "cert = File.read..." in your initialize method is just setting a local var 'cert' which will die as soon as the initialize call is over. If you want to define instance methods connection, cert_name & cert, you'd use the (native ruby) attr_accessor method (and you may want to call it as self.cert, just to make it totally clear to the interpreter that you intend to call a method rather than set a local var).

Maybe that helps?

Cheers,

-Roy

Roy,

thanks!! That did help. I also found that a solution to my problem: I used a singleton connection manager. This avoided all my problems. :slight_smile:

I have a question regarding thread safety, but I think I'll post a new thread for it. The general gist is: if I have a singleton class (by using the Singleton mixin), I understand it's thread-safe in that it ensures only a single instance of this class will be created. BUT (in theory), the instance methods of my singleton instance could be accessed concurrently by various threads, thus causing a concurrency issue. Right?

BUT Rails is essentially single-threaded (only a single request processed at a time), so if I have only a single call to my singleton instance per http request, then it should be impossible to get concurrency problems with it. Right?

Steve Hull wrote:

Yeah, that didn't work because caching the connection makes it immutable, which means you can no longer write data to it. But you know what I think *will* work? Storing the connection(s) as class vars. So I'm trying that next. I will report back here afterward for anyone who might be interested.

I'd use a singleton object (ruby Singleton module lets you do this easily, or it's easy enough to do yourself) that encapsulates logic and state for your 'connection pool', rather than class variables. But you're on the right track.

Note though that you've got to make sure all your logic (in this case in that hypothetical Singleton object), is concurrency-safe, if your rails app can possibly be deployed in any kind of a concurrent-request environment (or if you plan to create Threads yourself in your rails app, but hardly anyone ever does that).

You could look at the newish Rails 2.2 ActiveRecord ConnectionPool for a model, since it's doing basically what you want, but specifically for db connections. The ConnectionPool code might just end up being too confusing and complicated though, since it has to deal with Rails backwards compatibility issues and stuff. I haven't actually looked at the ConnectionPool code myself much yet.