Gambling development has its own rules and if you have chance to face them, you may know how hard it can be to meet all the gambling regulatory authority requirements. In one of our recent projects (horses racing betting website) we had to follow a lot of rules from the ARJEL - regulatory Authority for online gambling in France. The ARJEL provides french gambling operators with license, but only if they prove that they can protect players, prevent fraud, and, the most important, prove integrity of gambling activity. Which means for developers that each bet, and most of the users’ actions, must be tracked in secured locations in append-only mode.
In practice, we need to follow this workflow:

User sends a secure (https) request to the Collector, the request is intercepted before hitting the Gaming Platform (aka “GP”);
The Collector will hit the GP, but all params are saved from the initial request (so data can’t be altered by the GP);
The GP is answering to the Collector: if status is OK, then the process continues, if there’s error - the Collector respond to the user with the corresponding message and the process is stopped);
The Collector is sending data to the secure Vault; the Vault is answering to the Collector, if status is OK, then the process continues, otherwise GP (which is important) must rollback the action;
the Collector sends the answer back from the GP to the User (if 1-5 went with OK status).
Such process is quite simple on the paper. But really hard to put in place in the real life. This article explains how we resolved this using only a few lines of Ruby.
The technical specs can be found (in French only) here and especially in this doc
Parameters sent by the player must be saved locally in the Collector, to ensure their consistency when events are sent to the vault. They can’t be altered in any way by the Gaming Platform, so the best solution is to never trust it.
That was solved by using ruby closures, that make up most of collector’s logging. Closures will allow us to write code that can be passed like an object (to be called later) with all the variables in context saved.
A very simplified version of this code could be:
class Handler
attr_reader :queue
def initialize
@queue = []
end
def enqueue(trace_type, options={}, &block)
@queue << block
end
end
which can be called like this:
collector = Collector.new.enqueue do
Bet.create(params[:bet])
end
Next major problem was the performance - we need to take into account that Collector->Vault requests had to be as fast as possible because :
Unfortunately, by default, only one request can be sent to the Vault at once. And here comes Typhoeus, a Curl wrapper that can send send multiple requests in parallel, just what we need to improve performance (the vault has a simple HTTP API). Typhoeus gathers all the requests to send in a queue. This queue is called “hydra” and once fired, it sends all the requests with a nice concurrency. That way, we could hit the vault with dozens of requests (ie: bets) at once, in parallel.
As one of the ARJEL requirements, on the step 4 of the workflow it’s necessary to track all the events in the requests, so the Collector creates XML document with all the info about the request (some kind of a log entry) and sends it to the Vault in the request. The main issue here is that only one XML document can be sent per request.
Why is it an issue? Just imagine a typical signup request, which contains 2 XML documents (or they are also called “traces”) for 2 events: one straight for signup and one for accepting Terms of Services, or “TOS” (in France you must accept TOS to register). Those 2 traces must be sent together, but as mentioned above the Vault allows only one trace per request.
So, if the signup trace was not be successfully recorded (vaulted), the “Accept TOS” trace must not be sent to the Vault. That’s why we introduced a DFS algorythm (Deep first Search) in our CollectorTransaction model, where traces are stacked and can even be in a tree within one request: canceling a node will also cancel the following sub-nodes.
def cancel_dependent_transactions
@stack = []
dfs(self.on_complete)
@stack.each(&:cancel)
true
end
def dfs(ctransactions)
ctransactions.each do |ct|
@stack << ct
dfs(ct.on_complete)
end
end
(Surprisingly, it’s pretty rare to see recursive functions in Ruby.)
Remember that events are sent in parallel to the vault (using Typhoeus). If we send the 2 events in parallel, there’s a (small) chance that one of them fails: that’s why we force some events to be sent after others. This DFS implementation will help to cancel and clean dependent traces waiting to be sent.
In addition, it’s really important to be aware of requests’ state (e.g. in case of server down). That’s the role of the CollectorTransaction model, where state is maintained with State Machine, giving 2 advantages :
The state machine was quite simple:
state_machine :status, :initial => :pending do
event :send_to_vault do
transition :pending => :sent
end
event :cancel do
transition :pending => :cancelled
end
event :rollback do
transition :pending => :rollbacked
end
before_transition :on => :rollback, :do => :restore_model
after_transition :on => :rollback, :do => :cancel_dependent_transactions
before_transition :on => :send_to_vault, :do => :save_model_id
after_transition :on => :send_to_vault, :do => :fire_model_callbacks
state :sent do
validates :response_code, :inclusion => {:in => [200]}
end
state :rollbacked do
validates :response_code, :exclusion => {:in => [200]} # 0 = could not connect, 409 = no IDE header returned by vault
end
end
So, here’s typical example of enqueued collector’s request :
bet = Bet.new(:combination => combination, :bet_type => bet_type, :stake => stake.to_i, :order_id => @order.id, :race_id => @race.id)
collector.enqueue(:PAHIMISE,
:model => bet,
:extra_params => {
:race_name => [@race.to_s, @race.name, @race.place.name].join(' - '),
:balance_before_checkout => current_user.balance,
:balance_after_checkout => current_user.balance - bet.stake}) do
bet.save!
end
See how the closure is acting? We don’t actually save the bet in the controller, but we save the context of the bet inside a Collector::Handler instance. If the vaulting went ok, then we can execute that code, and the bet will be saved. Ruby has some magic inside sometimes, because the variable “bet” only live in this piece of code, and the closure block context. As long as the collector object is alive, the context is.
Note, that the model (ie: the bet) in the transaction is set before being changed, this is required because existing model will be used for the rollback in case of failure. Remember we should vault only when the GP returned a valid response. Literally, this means that the bet was committed to the DB, so we can’t use DB transactions to rollback in case of vaulting error (it would be too simple ;°) ).
New request from the the Collector queue calls enqueue method:
def enqueue(trace_type, options={}, &block)
ct = CollectorTransaction.new do |ctransaction|
ctransaction.trace_type = trace_type.to_s
ctransaction.model = options[:model] if options[:model]
ctransaction.extra_params = options[:extra_params].nil? ? {} : options[:extra_params]
end
ct.block = block
@queue << ct
if options[:after]
options[:after].on_complete << ct
ct.dont_enqueue = true
end
ct
end
block is executed when the collector will try to vault the whole transaction. Request parameters from the player are also saved in this model.
Then, finally, vaulting is fired, which means “dequeuing” the handler:
ActiveRecord::Base.transaction do
@queue.each do |ctransaction|
...
# Transaction is created
ctransaction.request = create_request(ctransaction)
# And the process responses
ctransaction.request.on_complete do |response|
if response.success? and response.headers['IDE']
...
Rails.logger.info "Vaulting successfull for transaction ##{ctransaction.id} (#{ctransaction.trace_type})"
@queue.delete ctransaction if ctransaction.sent? # transactions left in the queue are failures
else
if ctransaction.max_attempts_tripped? # we have tried enough, it's time to give up
Rails.logger.info "Vaulting failed for transaction ##{ctransaction.id} (#{ctransaction.trace_type}) with code=#{ctransaction.response_code}"
ctransaction.rollback # Rollback the transaction, and cancel dependent ones (cf. CollectorTransaction model)
else # let's retry!
ctransaction.attempt += 1 # one more try
@hydra.queue ctransaction.request
end
end
end
end
end
Thanks to ruby magic closures, we could face and solve a very complex problem in a short coding time. This system was audited and validated by the ARJEL in 2011. We did our best to ensure the maximum of features, but keeping simplicity in mind. The whole vaulting system can be seen as a queueing system, with specific transaction system for rollbacking. Less code means less bugs, and easy maintenance, always keep that in mind! The complete collector code is available is in this gist.