Isomorphic Server/Client Ruby with Opal
Max Rozenoer
November, 2015
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 we changed the name to Gett,
and now provide many kinds of on-demand services
(salads, flowers, pizzas, etc).
Intro
To make it possible to launch these new services
quickly, we did a lot of work in the last year. 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. This is what this talk is about.
Intro
Intro
Supplier App
iOS / Android
Customer App
iOS / Android
Server
So, customer has our Customer app installed,
Supplier has our supplier app installed.
Customer makes an order for something, it arrives
to the Supplier app, Supplier delivers the goods (in
case of a taxi ride the goods are the Customer
him/herself :))
Intro
So, how do we determine the price of an order?
Well, there is a Pricing Calculator library with
complicated business logic which dynamically
calculates the price of the order based on its current
properties.
Intro
Intro
Server
Pricing
Calculator
Supplier App
iOS / Android
Price: $XX
Normally this code runs on the server, and gets
pushed to or fetched by the client. However,
sometimes the client loses network connection right
at the end of an order which needs to be paid with
cash. Since in this case the supplier must
immediately collect the money from the client, the
ride must be priced right away, and without
network.
Problem
Problem
Server
Pricing
Calculator
Supplier App
iOS / Android
Price: ???
For this reason, we want to be able to run the
Pricing Calculator library locally on the client as a
fallback.
Problem
Problem
Server
Pricing
Calculator
Supplier App
iOS / Android
Pricing
Calculator
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.
Problem
Solution
Isomorphic CodeServer
Client
Isomorphic code is code that runs on both the server
and client.
You probably heard of isomorphic code in the
context of Node.js web development (if not, see
isomorphic.net for more details).
Well, it’s easy there because in the context of
Node.js Javascript runs both on server and on the
client (browser).
Solution
But we love Ruby and want to develop and
maintain code in Ruby!
Alas, currently nothing beats Javascript when
executing on clients.
What to do? Opal: Ruby to Javascript compiler.
Solution
Solution: Opal!
Isomorphic CodeServer
Client
Ruby JavascriptOpal
opalrb.org
The Opal Open-source Project is a source-to-source
Ruby-to-Javascript compiler. It is similar to
Coffeescript in that it allows a web developer to
write her client code in something nicer than JS,
which then compiles to JS.
Unlike Coffeescript, Opal code needs a runtime to
run.
Opal
So, what is our flow for developing isomorphic
Ruby? It ships as a gem containing both Ruby code
and JS code.
Development Flow
Development Flow: Pricing Calculator Gem
1. Write Ruby Code
2. Run
+ Ruby Specs .
JS Code
3. Opal-Compile
5. Run
Minified JS Code
6. Minify
JS Specs
4. Opal -Compile
Truth be told, I didn’t have the time to quite get
steps 4 and 5 above to work, but in theory it should
be like this :)
Now, this seems like a lot. But 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.
Development Flow
Development Flow: Pricing Calculator Gem
1. Write Ruby Code
2. Run
+ Ruby Specs .
JS Code
3. Compile
5. Run
Minified JS Code
6. Minify
Guard!JS Specs
4. Compile
7. Push!
In production, the Pricing Calculator gem is
deployed inside an App Server container (in our
case, Rails), which exposes several APIs.
Runtime Flow
Runtime Flow
App Server Container (Rails)
Pricing Calculator Gem
JS
Code
Ruby
Code
One API simply serves the static Opal Pricing
Calculator lib file, which is loaded by the client at
the beginning of a session, and loaded in a JS
Runtime.
On Android, the JS runtime we use is J2V8, on iOS
it’s an SDK Framework called JavascriptCore.
Runtime Flow
Supplier App
iOS / Android
Runtime Flow
App Server Container (Rails)
Pricing Calculator Gem
JS
Code
Ruby
Code
JS Runtime
JS Pricing
Calculator
Load JS
Pricing Calculator
The other exposed API is the regular API requesting
to perform server-side pricing calculation.
Runtime Flow
Supplier App
iOS / Android
Runtime Flow
App Server Container (Rails)
Pricing Calculator Gem
JS
Code
Ruby
Code
JS Runtime
JS Pricing
Calculator
Load JS
Pricing Calculator
Server-side
Calculation API
Generally the server-side calculation is the authority,
and the client-side calculation is a fallback in case
server is not available.
But also, 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.
Runtime Flow
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
needs to make network calls?
Execution Context
Different Capabilities of Execution Context
Supplier App
Pricing
Calculator
(JS)
Pricing
Calculator
(Ruby)
Server
Network Call
Network Call
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 produces the resources the
library needs.
Execution Context
In case of server execution context, it 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’s able to
“simulate” a network call without any network.
Execution Context
Runtime Context Layer
Pricing
Calculator
(Ruby)
Server
Runtime
Context
Layer
Supplier App
Pricing
Calculator
(JS)
Runtime
Context
Layer
Simulated
network call
via pre-
loaded
data
Network Call
Lessons Learned
● Solution is running in production for several
months
● Opal JS code is reliable and works well
Downsides:
○ Our JS lib weighs ~310Kb (60Kb zipped)
(80% is Opal base runtime)
Downsides (continued):
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.
Downsides (continued):
● Debugging is tough. Production JS exceptions
from minified code are unreadable - need to
write defensive code and raise your own
exceptions!
That’s all, Folks!
Max Rozenoer
we’re hiring!maxr@gett.com
This presentation on SlideShare: bit.ly/gett_isomorphic

Isomorphic Server/Client Ruby with Opal

  • 1.
    Isomorphic Server/Client Rubywith Opal Max Rozenoer November, 2015
  • 2.
    Our R&D problemsstarted 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 we changed the name to Gett, and now provide many kinds of on-demand services (salads, flowers, pizzas, etc). Intro
  • 3.
    To make itpossible to launch these new services quickly, we did a lot of work in the last year. 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. This is what this talk is about. Intro
  • 4.
    Intro Supplier App iOS /Android Customer App iOS / Android Server
  • 5.
    So, customer hasour Customer app installed, Supplier has our supplier app installed. Customer makes an order for something, it arrives to the Supplier app, Supplier delivers the goods (in case of a taxi ride the goods are the Customer him/herself :)) Intro
  • 6.
    So, how dowe determine the price of an order? Well, there is a Pricing Calculator library with complicated business logic which dynamically calculates the price of the order based on its current properties. Intro
  • 7.
  • 8.
    Normally this coderuns on the server, and gets pushed to or fetched by the client. However, sometimes the client loses network connection right at the end of an order which needs to be paid with cash. Since in this case the supplier must immediately collect the money from the client, the ride must be priced right away, and without network. Problem
  • 9.
  • 10.
    For this reason,we want to be able to run the Pricing Calculator library locally on the client as a fallback. Problem
  • 11.
  • 12.
    Initially, we portedthis 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. Problem
  • 13.
    Solution Isomorphic CodeServer Client Isomorphic codeis code that runs on both the server and client.
  • 14.
    You probably heardof isomorphic code in the context of Node.js web development (if not, see isomorphic.net for more details). Well, it’s easy there because in the context of Node.js Javascript runs both on server and on the client (browser). Solution
  • 15.
    But we loveRuby and want to develop and maintain code in Ruby! Alas, currently nothing beats Javascript when executing on clients. What to do? Opal: Ruby to Javascript compiler. Solution
  • 16.
  • 17.
    The Opal Open-sourceProject is a source-to-source Ruby-to-Javascript compiler. It is similar to Coffeescript in that it allows a web developer to write her client code in something nicer than JS, which then compiles to JS. Unlike Coffeescript, Opal code needs a runtime to run. Opal
  • 18.
    So, what isour flow for developing isomorphic Ruby? It ships as a gem containing both Ruby code and JS code. Development Flow
  • 19.
    Development Flow: PricingCalculator Gem 1. Write Ruby Code 2. Run + Ruby Specs . JS Code 3. Opal-Compile 5. Run Minified JS Code 6. Minify JS Specs 4. Opal -Compile
  • 20.
    Truth be told,I didn’t have the time to quite get steps 4 and 5 above to work, but in theory it should be like this :) Now, this seems like a lot. But 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. Development Flow
  • 21.
    Development Flow: PricingCalculator Gem 1. Write Ruby Code 2. Run + Ruby Specs . JS Code 3. Compile 5. Run Minified JS Code 6. Minify Guard!JS Specs 4. Compile 7. Push!
  • 22.
    In production, thePricing Calculator gem is deployed inside an App Server container (in our case, Rails), which exposes several APIs. Runtime Flow
  • 23.
    Runtime Flow App ServerContainer (Rails) Pricing Calculator Gem JS Code Ruby Code
  • 24.
    One API simplyserves the static Opal Pricing Calculator lib file, which is loaded by the client at the beginning of a session, and loaded in a JS Runtime. On Android, the JS runtime we use is J2V8, on iOS it’s an SDK Framework called JavascriptCore. Runtime Flow
  • 25.
    Supplier App iOS /Android Runtime Flow App Server Container (Rails) Pricing Calculator Gem JS Code Ruby Code JS Runtime JS Pricing Calculator Load JS Pricing Calculator
  • 26.
    The other exposedAPI is the regular API requesting to perform server-side pricing calculation. Runtime Flow
  • 27.
    Supplier App iOS /Android Runtime Flow App Server Container (Rails) Pricing Calculator Gem JS Code Ruby Code JS Runtime JS Pricing Calculator Load JS Pricing Calculator Server-side Calculation API
  • 28.
    Generally the server-sidecalculation is the authority, and the client-side calculation is a fallback in case server is not available. But also, 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. Runtime Flow
  • 29.
    So now wegot 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 needs to make network calls? Execution Context
  • 30.
    Different Capabilities ofExecution Context Supplier App Pricing Calculator (JS) Pricing Calculator (Ruby) Server Network Call Network Call
  • 31.
    To solve thisproblem, we don’t make any network calls from the library itself. Instead, the library assumes that the runtime context will provide a layer that produces the resources the library needs. Execution Context
  • 32.
    In case ofserver execution context, it 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’s able to “simulate” a network call without any network. Execution Context
  • 33.
    Runtime Context Layer Pricing Calculator (Ruby) Server Runtime Context Layer SupplierApp Pricing Calculator (JS) Runtime Context Layer Simulated network call via pre- loaded data Network Call
  • 34.
    Lessons Learned ● Solutionis running in production for several months ● Opal JS code is reliable and works well Downsides: ○ Our JS lib weighs ~310Kb (60Kb zipped) (80% is Opal base runtime)
  • 35.
    Downsides (continued): Isomorphic Rubyyou 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.
  • 36.
    Downsides (continued): ● Debuggingis tough. Production JS exceptions from minified code are unreadable - need to write defensive code and raise your own exceptions!
  • 37.
    That’s all, Folks! MaxRozenoer we’re hiring!maxr@gett.com This presentation on SlideShare: bit.ly/gett_isomorphic

Editor's Notes

  • #2  The problems started when we decided to remove a couple letters from the company name. We used to be called GetTaxi, you see - and we provided only on-demand taxis. Then we changed the name to Gett, and now provide many kinds of on-demand services (salads, flowers, pizzas, etc). To make it possible to launch these new services quickly, we did a lot of work in the last year, and part of this work had to do with how each order can be priced, which is what this talk is about.
  • #20 We package the Pricing Calculator as a gem that ships both Ruby and JS code. Development flow is driven by https://github.com/guard/guard .
  • #24  Generally, the server-side calculation is the authority, and the client-side calculation is a fallback in case server is not available. However, 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. On Android, the JS runtime we use is J2V8, on iOS it’s an SDK Framework called JavascriptCore.
  • #26  Generally, the server-side calculation is the authority, and the client-side calculation is a fallback in case server is not available. However, 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.