An introduction to OTP, concurrency and, testing strategies for sync and async server requests in Elixir. As presented in Montreal Elixir, April 12th, 2017. Code examples are available on GH. https://github.com/xymbol/otp-examples
7. • Erlang interpreter and compiler
• Erlang standard library
• Dialyzer, a static analysis tool
• Mnesia, a distributed database
• ETS, and in-memory database
• A debugger
• An event tracer
• A release-management tool
74. • Distribute processing across cores 1:n
• Coordinate work one message at a time n:1
• Keep long-running concurrent state
• Need processes supervised
• Want state upgraded in place
Concurrency is at the heart and soul of Erlang systems.
A surprisingly simple set of concurrency primitives.
Recommended read: guides section on concurrency.
All you need to write OTP-compliant servers.
What makes a server process?
Example: a counter server, written using primitives only.
Boilerplate code for the most simple server process call.
GenServer: a module to write servers.
Think behaviors as design patterns for processes.
Example: a counter server, now rewritten to use GenServer.
Within the same module, some functions will execute in the client process, some functions in the server.
Brief mention of supervisors and supervision trees.
Some conventions like `start_link` are imposed by supervisors, responsible for starting, restarting and stopping server processes.
A basic principle for fault tolerance and self-healing in Erlang systems.
How processes are organized in OTP.
Example: the supervisor spec generated in Phoenix apps. Here, with the counter server added to the supervisor.
Example: a server can provide coordinated access to state or resources. Here, the counter getting used between concurrent requests.
Why TDD is particularly valuable when writing servers.
Servers are long-lived processes, only restarted on update or failure.
Edge cases and errors can be harder to notice in servers as most won’t be directly exposed in the system UI. Also, exception messages can be omitted or, not fully reported in logs.
Some practical lessons learned from a real-world app.
Make sure to `start_link` server processes or have them supervised. If not linked in tests, servers will continue running until all tests are run.
Example: a tick server module with a `start` function.
In the tests, we start two servers, one for each. Then, add a third test to slee for one second.
In the output, we can see both servers running after each test completes.
Example: the same server, now with a `start_link` function.
Now tests and servers are linked. In the output, we see that each server exits once the test is run.
Data hiding is not good friends with functional programming. Make server state accessible to test updates and edge cases.
Example: a client state function to access state.
Test initial server state.
Provide a client function to stop the server and ensure is tested. For example, when persisting state between restarts, stopping a server can be as important as any feature.
Example: a client stop function.
Test server process exits on stop.
Common mistake: can run only one server because of name registration or, not sure which server gets called or is under test.
Example: a server that registers itself under a hardcoded name.
Example: a server that registers itself under a name by default but, also accepts options to be overridden.
Test can now override options and avoid name registration. Each test gets a new, unregistered server.
Common mistake: client functions don’t allow a server argument to be passed in.
Example: a client function without a server argument.
Example: a client function that optionally accepts a server.
Test your client interface first. Usually this will improve your server design.
Example: start with minimal client function tests.
Writing tests for server calls should be straightforward.
Example: an adder server that computes a total. Here the `add` and `total` client functions are implemented using synchronous calls to the server.
State setup is easy and concurrency is not a problem.
But, writing test for server asynchronous casts can be tricky.
Example: same adder server with `add` implemented using an asynchronous cast. Will this break the existing test?
No, as the synchronous call to `total` guarantees execution order.
Is this a safe assumption or just good luck?
Recommended read: Erlang documentation on message order.
Example: a writer server to coordinate file writes and reads. Writes are non-blocking, reads are blocking.
Tests for writes break because execution is not guaranteed.
Fix: add a server `wait` call to sync cast calls.
Make sure to call the server before any assertions.
And now, the tests passes.
Callbacks are functions and can be tested too.
Example: a stack calculator server. Sometimes, recreating a particular server state or edge case can be long and tedious. Here, we test an addition after a few values are pushed.
Callback functions can be tested with known initial states.
How to test servers that interact with external services.
Recommended solution: resort to behaviors as a way to have different modules that implement a same set of function signatures.
Recommended read: guides section on behaviors.
Recommended read: José Valim on mocks.
When to use and not to use servers in apps.
Recommended read: servers as OOP instances.
That said, servers are not objects.
Recommended read: Saša Jurić on when to use processes.
Use servers for runtime concerns. Model domain with modules and functions.