3

I've developed an application that allows our customers to create their own membership protected websites. My application then connects to an outside API service (customer specific api_key/api_url) to sync/update/add data to this other service. Well, I've had an API wrapper written for this other service that has worked up to this point. However, I'm now seeing very random drops where the connection is nil. Here is how I'm currently using the connection:

I have a xml/rpc connection class

class ApiConnection
  attr_accessor :api_url, :api_key, :retry_count

  def initialize(url, key)
    @api_url = url
    @api_key = key
    @retry_count = 1
  end

  def api_perform(class_type, method, *args)
    server = XMLRPC::Client.new3({'host' => @api_url, 'path' => "/api/xmlrpc", 'port' => 443, 'use_ssl' => true})
    result = server.call("#{class_type}.#{method}", @api_key, *args)
    return result
  end
end

I also have a module that I can include in my models to access and call the api methods

module ApiService

  # Set account specific ApiConnection obj
  def self.set_account_api_conn(url, key)
    if ac = Thread.current[:api_conn]
      ac.api_url, ac.api_key = url, key
    else
      Thread.current[:api_conn] = ApiConnection.new(url, key)
    end
  end

  ########################
  ###  Email Service   ###
  ########################

  def api_email_optin(email, reason)
    # Enables you to opt contacts in
    Thread.current[:api_conn].api_perform('APIEmailService', 'optIn', email, reason)
  end

  ### more methods here ###

end

Then in the application controller I create a new ApIConnection object on every request using a before filter which sets the Thread.current[:api_conn]. This is because I have hundreds of customers each with their own api_key and api_url, using the application at the same time.

# In before_filter of application controller
def set_api_connection
  Thread.current[:api_conn] = ApiService.set_account_api_conn(url, key)
end

Well my question is that I've read that using Thread.current is not the most ideal way of handling this, and I'm wondering if this is the cause for the ApiConnection to be nil on random requests. So I would like to know how I could better setup this wrapper.

nateleavitt
  • 759
  • 7
  • 9
  • I would try examining what traffic is getting sent to the remote server. Perhaps that would lend a clue into what the issue is, do you know how to use a tool like Wireshark? – Devin M Sep 22 '11 at 05:39
  • The traffic never gets sent because the ApiConnection object is randomly nil. Which I don't understand because I'm setting it in my before_filter on the ApplicationController. I'm guessing it's because of the way Rails is handling the Thread.current on each request??? – nateleavitt Sep 22 '11 at 06:22

1 Answers1

6

Answer 1

I'd expect that the problem is the next request coming before the connection has finished, and then the before_filter overwrites the connection for the still ongoing connection. I'd try to stay away from threads. It's easier to fork_off, but there's certain caveats to that as well, especially regarding performance.

I try to move logic like this over to a background job of some sort. A common solution is delayed job https://github.com/collectiveidea/delayed_job that way you don't have to mess with threads and it's more robust and easy to debug. You can then start background jobs to asynchronously sync the service whenever somebody logs in.

@account.delay.optin_via_email(email,user)

This will serialize the account, save it to the job queue, where it will be picked up by delayed job unserialized and the method after delay will be called. You can have any number of background jobs, and even some job queues dedicated to certain types of actions (via using job priorities - let's say two bj for high prio jobs and one dedicated to low prio jobs)

Answer 2

Just make it as an object instead

def before_filter
  @api_connection =  ApiConnection.new(url, key)
end

then you can use that connection in your controller methods

def show
   #just use it straight off
   @api_connection.api_perform('APIEmailService', 'optIn', email, reason)
   # or send the connection as a parameter to some other class
   ApiService.do_stuff(@api_connection)
end

Answer 3

The easiest solution might just be to create the api connection whenever you need it

class User < ActiveRecord::Base
  def api_connection 
    # added caching of the the connection in object
    # doing this makes taking a block a little pointless but making methods take blocks
    # makes the scope of incoming variables more explicit and looks better imho
    # might be just as good to not keep @conn as an instance variable
    @conn = ApiConnection.new(url, key) unless @conn 
    if block_given?
      yield(@conn)
    else
      @conn
    end
  end
end

that way you can easily just forget about the creation of the connection and have a fresh one handy. There might be performance penalities with this but I suspect that they are insignificant unless there's an extra login request

@user.api_connection do { |conn| conn.optin_via_email(email,user) }
sunkencity
  • 3,382
  • 1
  • 18
  • 18
  • Thanks for the answer.. and I actually do use DelayedJob for jobs that I can put in a queue. But some I need to run instantly during the request. The optin_via_email was a bad one to put. Essentially, I'm trying to figure out that instead of using Thread.current, what would be a better way to structure my API wrapper? – nateleavitt Sep 22 '11 at 07:21
  • I'd just create a new apiconnection for every request in that case, i suspect it's cheaper than managing a conneciton pool in terms of complexity and perhaps also not that far away in terms of speed. Just be sure to set a timeout for the request and handle that so that the whole app doesn't go down for all customers if some other party goes offline. – sunkencity Sep 22 '11 at 08:53
  • Thanks for your answers. I think I'll probably use something similar to Answer 3. – nateleavitt Sep 22 '11 at 18:50
  • 2
    @sunkencity Very good answer, nicely formatted and easy to read. Well done! – Devin M Sep 23 '11 at 18:58