Testing background process with ZIO

ZIO is an awesome library to deal with the complicated part of effects, most notably concurrency and asynchronicity.

Sometimes, you want to create a “batch” like process, i.e. a fiber running in background and never terminating, which does some effect based on a trigger, like a period of time or reception of an external event. And of course you want to test its semantic. Fortunately, ZIO provides a test environment with a test clock that you can adjust for your needs. Here, it becomes apparent why the clock effect is explicit in ZIO: you can control it!

Note for the whole example, imports are:
The whole code is available here:
So, let’s build a trivial Batch system:
So now, we want to test it. For that, we are going to use ZIO test environment:
Often, when you want to test effects that normally spread upon some time, you build a record of what happens. A record is a mutable effect, so you learn to put it in a Ref:

OK, so we have a batch that will record its triggering time in a Ref at each of its executions

Perfect, let’s test that by adjusting the test clock after two iterations:

Great! In a couple of lines, we are ready to test our batch by injecting a test clock and adjusting time to what we need.

The only remaining part is to run the program and check for recorded values:
Run it, and… TADA!

You got an error.
So, what’s going on? We see both prints, but the list is empty.

Actually, what happens is a race condition. Because concurrency is hard, and that’s why you let that to others.

The race condition is between the time it takes for adjust to tell fibers « hey, you need to do what you should when time is passing » and our batch fiber to actually do what it should on the one hand, and the r.get on the other hand. r.get is really fast. More than fiber business.

So you can unset the race condition with something like that:
This time, if you run prog2, you get the expected result:

Success! Really, success?
No, because you introduced non determinism in your test. It may happen that sometime, due to execution constraints like load, background fiber takes more time to adjust. Or, like in that case, you massively overestimated the sleep duration and so your tests will take much more time than needed, most of that time spent sleeping.

So, what’s the correct solution? As always with concurrency problems, the correct answer is to encode your protocol by forcing a synchronisation point for each execution of your batch. This is tedious, since it means that you will need to clearly count each of them, but it’s the only way toward determinism.
To force synchronization, the simplest way is to use a Queue and call one take for each offer.

So, for the batch, you will have:
And your program becomes:
That time, you have a deterministic test that runs in the minimum possible time. And remember: concurrency is hard, and in the same spirit as date or crypto algorithms, just don’t try to build your concurrency primitives by yourself: use a lib that has the corresponding batteries.
Francois ARMAND

ZIO is an awesome library to deal with the complicated part of effects, most notably concurrency and asynchronicity.

Sometimes, you want to create a “batch” like process, i.e. a fiber running in background and never terminating, which does some effect based on a trigger, like a period of time or reception of an external event. And of course you want to test its semantic. Fortunately, ZIO provides a test environment with a test clock that you can adjust for your needs. Here, it becomes apparent why the clock effect is explicit in ZIO: you can control it!

Note for the whole example, imports are:
The whole code is available here:
So, let’s build a trivial Batch system:
So now, we want to test it. For that, we are going to use ZIO test environment:
Often, when you want to test effects that normally spread upon some time, you build a record of what happens. A record is a mutable effect, so you learn to put it in a Ref:

OK, so we have a batch that will record its triggering time in a Ref at each of its executions

Perfect, let’s test that by adjusting the test clock after two iterations:

Great! In a couple of lines, we are ready to test our batch by injecting a test clock and adjusting time to what we need.

The only remaining part is to run the program and check for recorded values:
Run it, and… TADA!

You got an error.
So, what’s going on? We see both prints, but the list is empty.

Actually, what happens is a race condition. Because concurrency is hard, and that’s why you let that to others.

The race condition is between the time it takes for adjust to tell fibers « hey, you need to do what you should when time is passing » and our batch fiber to actually do what it should on the one hand, and the r.get on the other hand. r.get is really fast. More than fiber business.

So you can unset the race condition with something like that:
This time, if you run prog2, you get the expected result:

Success! Really, success?
No, because you introduced non determinism in your test. It may happen that sometime, due to execution constraints like load, background fiber takes more time to adjust. Or, like in that case, you massively overestimated the sleep duration and so your tests will take much more time than needed, most of that time spent sleeping.

So, what’s the correct solution? As always with concurrency problems, the correct answer is to encode your protocol by forcing a synchronisation point for each execution of your batch. This is tedious, since it means that you will need to clearly count each of them, but it’s the only way toward determinism.
To force synchronization, the simplest way is to use a Queue and call one take for each offer.

So, for the batch, you will have:
And your program becomes:
That time, you have a deterministic test that runs in the minimum possible time. And remember: concurrency is hard, and in the same spirit as date or crypto algorithms, just don’t try to build your concurrency primitives by yourself: use a lib that has the corresponding batteries.
Francois ARMAND

Partager ce post

Retour en haut
Rudder robot named Ruddy makes an announcement.

Rudder 8.2 : simplifiez la conformité de la sécurité IT avec la solution Policy and benchmark

Détails du module Security management

Ce module a pour objectif de garantir une sécurité et une conformité optimales pour la gestion de votre infrastructure, avec des fonctionnalités pour les entreprises telles que :

Pour en savoir plus sur ce module, consultez la page gestion de la sécurité.

Détails du module configuration & patch management

Ce module vise une performance et une fiabilité optimales pour la gestion de votre infrastructure et de vos patchs, avec des fonctionnalités pour les entreprises telles que :

Pour en savoir plus sur ce module, consultez la page gestion des configurations et des patchs.