Ruby with Opal
Our R&D problems started when we decided to remove a couple letters from the company name. You’d think it would be easy! We used to be called “GetTaxi”, you see – and we provided only on-demand taxis. Then in 2015 we changed the name to “Gett”, and now provide many kinds of on-demand services (salads, flowers, pizzas, etc).
We did a lot of work to make it possible to launch these new services quickly. Part of this work had to do with how each order can be priced, since the pricing business logic has become a lot more complex.
But first, some background. Each service we provide have a Supplier who provides the service and Customer who receives it. The Customer makes an order for a service in the Customer app, the order arrives to the Supplier app and Supplier delivers the goods (in case of a taxi ride the goods are the Customer him or herself :))
How do we determine the price of an order? There is a Pricing Calculator library with complicated business logic which calculates the price of the order based on its current properties at any point in time.
Problem with Connectivity
Normally the Pricing Calculator library runs on the server, and the calculation result gets pushed to or fetched by the clients for display. The Supplier app in particular needs to be able to display the price of the order dynamically – for example, when it acts as a taxi meter during a taxi ride. Now, sometimes it happens that the client does not have network connectivity right at the end of an order.
And if this is an order that needs to be paid with cash, the supplier must immediately collect the money from the customer, so the order must be priced right away, and without network. For this reason, we want to be able to run the Pricing Calculator library locally on the client as a fallback.
Since we support iOS and Android apps, initially we ported this library to native iOS/Android. But the library has complicated business logic which changes often, so this approach was costly, time-consuming and difficult to maintain.
After thinking about this problem for a while, one of our architects came up with an exciting idea: isomorphic code!
Isomorphic code is code that runs on both the server and client.
So, what is our flow for developing a library written in isomorphic Ruby? It ships as a Ruby gem containing both Ruby code and JS code. The workflow for developing code in this library looks like this:
(OK, so ideally the flow looks like this – truth be told, I didn’t have the time to quite get steps 4 and 5 above to work, will update the post when I do.)
Now, this seems like a lot. But here is the good news: steps 2-6 are done for us automatically using the amazing Guard gem! So all we have to do is write Ruby code + Ruby specs, wait for builds and tests to complete, and push to repository.
In production, the Pricing Calculator gem is deployed inside an App Server container (in our case, Rails), which exposes several APIs.
One API simply serves the static Opal Pricing Calculat JS lib file, which is loaded by the client at the beginning of a session, and loaded into a JS Runtime.
The other exposed API is the regular API requesting to perform server-side pricing calculation.
Generally the server-side calculation is the authority, and the client-side calculation is a fallback in case server is not available.
Additionally, during a taxi ride in some cases we want to display a constantly updating taxi meter, and don’t want to make so many calls to the server, so the client-side library is also used for that.
So now we got the same code running on server and on client. Will it produce the same results?
Wait, the capabilities of the execution contexts in which the code runs might be different. For one thing, we assumed that the client execution context might not have any network. What if the Pricing Calculator library needs to make network calls?
To solve this problem, we don’t make any network calls from the library itself. Instead, the library assumes that the runtime context will provide a layer that serves the resources the library needs. In case of server execution context, this layer simply makes a network call to fetch the resources. In case of client execution context, it pre-fetches data from the server so that (in some cases) it is able to “simulate” a network call without any network.
- The described solution is running in production for about a year with no issues.
- Opal JS code is reliable and works well. We have not seen any problems related to gotchas in cross-compilation – awesome job Opal Team!
- After a year of developing new features, our JS lib weighs ~370Kb (73Kb zipped). 67% of it is Opal base runtime, so the overhead for a small library would be very high.
- Isomorphic Ruby you write needs to be “Opal-friendly”:
- Opal’s Ruby stdlib support is still far from complete (e.g., “Time” has only a few methods).
- Your Ruby needs to have no external gem dependencies, since most gems are probably not Opal-friendly. For example, we ended up porting a part of the popular Virtus gem into an Opal-friendly “VirtusLite”.
- Debugging is tough. Production JS exceptions from minified code are unreadable – you will need need to write defensive code and explicitly raise your own exceptions, which should then be caught by the code around the JS Runtime and shipped somewhere for inspection.
Overall this has been a very exciting experiment that also turned out to work really well!