Taming WebSocket with Scarlet

Mike Hall

By Zhixuan Lai, Software Engineer, Android Platform

In mobile apps, the data layer is the source of truth for what to display on the screen. Maintaining it, however, became a headache when we integrated WebSocket APIs at Tinder earlier this year. To make WebSocket integration easier on Android, we developed Scarlet, a declarative Kotlin library.

WebSocket is a powerful tool for building real-time applications such as chat, online multiplayer game, and real-time feed. It establishes a bidirectional connection between client and server. While the connection is open, they can send text and binary messages back and forth with low overhead. Here is an example of how one might establish a WebSocket connection in plain JavaScript.

WebSocket with Scarlet

Scarlet is easy to set up and maintain: declaring a WebSocket API client is as simple as declaring methods on an interface. When you pass your interface to Scarlet, it will generate an implementation. For example, GDAX WebSocket Feed API, which offers real-time cryptocurrency prices via WebSocket, takes 9 lines of code to integrate with Scarlet, while other WebSocket libraries require hundreds of lines of code.

Scarlet interprets methods in GdaxService using reflection.

  • A @Send annotated method takes one parameter. When invoked, it sends an outgoing message to the server.
  • A @Receive annotated method, on the other hand, returns a stream of incoming messages or events about the current connection state. You need to subscribe to the stream to observe values.

According to the GDAX API documentation, to begin receiving real-time price Ticker from GDAX, the client must first send a Subscribe message to the server indicating which channels and products it wants to receive.

Messages can be declared as data classes. In this example, we are interested in the Bitcoin price in US dollars. So we want to subscribe to the "ticker" channel of "BTC-USD".

Send a Subscribe message upon connection open and the server will start streaming tickers which contain the latest price.

                                . . .

Besides having a declarative API, Scarlet is also modular, customizable, and easily extensible because it follows a plugin architecture. Its behaviors are encapsulated by useful abstractions that can be easily swapped out like plugins. In addition to choosing from a variety of built-in plugins, you are empowered to provide your own plugins when building a Scarlet instance.

The GDAX example requires three built-in plugins:

  • A WebSocket implementation based on OkHttp.
  • A MoshiMessageAdapter.Factory, which uses Moshi to serialize data. Scarlet also supports Gson and protobuf. If you don’t specify any MessageAdapter. Factory, you may use String or ByteArray.
  • A RxJava2StreamAdapter.Factory used to support RxJava2. Scarlet also supports RxJava1. If you don’t provide any StreamAdapter.Factory, you may use the built-in Stream.

                               . . .

Integration with Android

When the connection is open, WebSocket is straightforward. However, it demands additional efforts on mobile because of its stateful nature. A WebSocket connection may be closed for many reasons:

  • Unstable network
  • Server closure to release resources
  • App enters background to conserve battery

When the connection is closed, the client needs to decide when to retry. While other WebSocket libraries hold the developer liable to keep the connection open, Scarlet manages retries and makes the connection state transparent to the developer. This is achieved with the help of two abstractions: Lifecycle and BackoffStrategy.

A Lifecycle tells Scarlet when to connect to the server and to keep retrying if the server disconnects. As a stream of Lifecycle.State (Started and Stopped), it encapsulates the scope of a WebSocket connection. Lifecycle is particularly useful on Android because it is reusable and composable; e.g.:

  • To keep the connection open only when the app is in the foreground, you can write a Lifecycle for the app foreground scope. (Scarlet comes with an AndroidLifecycle plugin so that you don’t have to write one.)
  • To keep the connection open only when the user is logged in, you can create a Lifecycle for the logged in scope.
  • To satisfy both conditions, you can simply combine their Lifecycles to create an app foreground and user logged in scope.

When the current Lifecycle.State is Started, Scarlet uses a BackoffStrategy to determine how often it should retry after each connection failure. Scarlet comes with three backoff strategies: LinearBackoffStrategy, ExponentialBackoffStrategy, and ExponentialWithJitterBackoffStrategy. For more information on choosing backoff strategies, please see Exponential Backoff And Jitter.

With the desired Lifecycle and BackoffStrategy specified, Scarlet handles connection failures gracefully.

                              . . .

Writing Tests for Scarlet

After a Scarlet instance is created, Lifecycle changes are fed into an internal state machine that manages network calls. To make state transitions declarative and readable, we invented a Kotlin DSL to write the Scarlet state machine in. For more information, please see StateMachine on Github.

Scarlet state machine diagram

The state machine, together with immutable states and events, results in very testable code. In addition, Scarlet comes with declarative testing utilities that make assertions about state transitions readable in unit tests.

What really makes the integration tests readable, however, is the decoupling from OkHttpClient. An integration test asserts the interactions between a Scarlet WebSocket client and a mock WebSocket server. In the beginning, Scarlet was coupled with OkHttpClient and only worked on the client side. Our declarative testing utilities made client states easily assertable. But to assert server states, we had to use a different set of APIs around MockWebServer. The mix of two different assertion styles made integration tests confusing.

To unify the testing APIs used in integration tests, we inverted the dependency on OkHttpClient so that Scarlet also worked on the server side. We made OkHttpClient and MockWebServer plugins of Scarlet. This change not only improved the readability of our integration tests, but also allowed Scarlet to work with any WebSocket libraries in the future.

                              . . .

Thanks for reading! Please try Scarlet out and tell us what you think. Scarlet is one of the tools we made in-house. It is currently used by the Tinder Android app in production, serving millions of users and handling billions of WebSocket messages every day. If being a part of this is interesting to you, please take a look at job openings here at Tinder — we’re always looking for talented engineers to join our team!