Skip to content

Instantly share code, notes, and snippets.

@jdegoes
Created March 4, 2020 12:44
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jdegoes/c1d314ae282080d9e3de71ac2328f1c7 to your computer and use it in GitHub Desktop.
Save jdegoes/c1d314ae282080d9e3de71ac2328f1c7 to your computer and use it in GitHub Desktop.
Migration notes for ZIO-1.0.0-RC18

ZIO's last release candidate before 1.0 (RC18) has arrived!

The first question every user needs to ask themselves:

  • Should they upgrade now?
  • Should they wait for ZIO 1.0?

We need some adventurous souls to upgrade now, because the feedback so generated will help convince us we're ready to pull the trigger on ZIO 1.0 in a couple weeks time.

On the other hand, upgrading now is going to be more difficult, because not all ZIO libraries have been updated to RC18 (we are early in that process), and there is very poor documentation.

The conservative choice is to wait for ZIO 1.0, which will be released toward the end of March, when all the ZIO libraries in the ecosystem should be able to quickly deploy 1.0-compatible releases.

That said, let's assume you are an adventurous soul! How can you migrate to RC18 with a minimum of fuss?

Read on to find out!

Structured Concurrency

Many of the breaking changes in RC18 will lead to compile-time errors: you fix them, and the code will work.

One significant change is not like that: a change to the concurrency model of ZIO.

In RC17, when a fiber forked a child fiber, and the parent was interrupted, the child fiber would be interrupted automatically. If the parent fiber ended life normally, however, the child fiber would not be interrupted, but would be relocated from the parent to the grandparent.

This relocation process violated principle of least surprise and could led to edge cases, so in RC18, ZIO has fully embraced a more structured model of concurrency.

In RC18, when a fiber forks a child fiber, then when the parent is ready to exit (either normally or abnormally), the child fiber will be either interrupted or disowned, as determined by the supervise mode specified when the child fiber is forked. The default supervision mode, which is what you get if you call effect.fork, is interruption—which is generally what you want, because in general you don't want child fibers to outlive the parent.

Parent fibers will not exit until after they have performed their supervision: which means they will wait for child fibers to be interrupted or disowned, as appropriate. This means you have a strong guarantee that when a parent is done, it's children are done too, for whatever your definition of "done" might be (interrupted or disowned).

Disowning is a new concept introduced in RC18, powered by the ZIO.disown function: to disown a child fiber is to end supervision of the child fiber by the parent fiber, and to relocate the fiber to a set of "root fibers", which include all ZIO entry-points (every call to the Runtime#unsafeXYZ methods tracks a new root fiber).

Much code can be upgraded without any changes, but there are some exceptions:

  • If you need child fibers to live beyond the life of the parent, then you should fork them using SuperviseMode.Disown (effect.fork(SuperviseMode.Disown)). This means when the parent fiber exits, they will be disowned, and they will continue to live an independent life as new root fibers. Alternately, you can use forkDaemon to fork a fiber and immediately disown it.
  • If your child fibers have a costly wind-down process, or if they might not ever wind-down, then you may not want the parent to wait for them to finish. In this case, instead of calling child.fork, you could call child.disconnect.fork, which would allow the parent to interrupt the child, without waiting for the child to complete.

The safest way to migrate your code to RC18 is to change any code like this:

effect.fork

into this:

effect.forkDaemon

This will fork the fiber and then immediately disown it, which means the life of the child fiber will not be connected (at all) to the life of the parent fiber. This means fiber leaks are possible (that is, child fibers that should be terminated when the parent exits will keep running indefinitely), but will result in more consistent behavior with RC17.

If you are more adventurous, then attempt to identify the child fibers that should outlive the parent, and use forkDaemon with those fibers, but not other fibers.

Finally, if you notice that code which is forking fibers is hanging, then it is likely the parent is waiting on the child to terminate. You can replace effect.fork with effect.disconnect.fork in order to fix this problem—although you may also want to figure out why your child fibers are not shutting down quickly (it's possible they have been made uninterruptible with ZIO.uninterruptible, either directly, or indirectly, as part of a resource-related operation).

ZLayer

If you're like many ZIO users, you have been using Clock, System, Console and other standard ZIO services in your app, and then using ZIO.provide(Clock.Live) (etc.) to give effects what they need.

You may even be using your own custom interfaces in the environment, which are modeled after ZIO's standard services, using the module pattern.

ZIO's standard services have changed from using the module pattern to using ZLayer and Has, which are new data types very important for the future of ZIO Environment.

Roughly speaking:

  • Has[X] with Has[Y] ... represents a collection of services X, Y, etc. For example, the type Has[Clock.Service] with Has[Random.Service] represents a collection of the clock and random services. If you use standard ZIO services, you will now see Has types in the environment (R) parameter of the ZIO effects.
  • ZLayer[A, E, Has[Y] with Has[Y] ...] represents a recipe for constructing a collection of services X, Y, etc., given some input or collection of services A.

The Has type can be thought of as a heterogeneous set, which allows you to add and remove services at will, in a type safe way. The ZLayer type can be thought of as a recipe for building services, given whatever they require.

Although the ZIO data type doesn't require you to use R in any specific way, so you are free to continue using the module pattern, because RC18 migrates all standard ZIO services to use Has / ZLayer, if you wish to continue using standard ZIO services, you need to make a choice:

  • You can convert all your own custom services to ZLayer. Even though this will entail some work, it will radically simplify the process of building complex environments, which can now be built using simple operators on ZLayer. In addition, it gives you new powers, like being able to define effectful, stateful, and resourceful services, which depend on other services in a way that hides implementation details.
  • You can convert ZIO standard services like Clock into something you can continue using with the module pattern.

There is Scaladoc and preliminary documentation on <zio.dev> for ZLayer, as well as a presentation by @adamgfraser on ZLayer. However, documentation is minimal and it will take some time to fully explain the use of ZLayer.

Basic usage is rather straightforward:

val myLayer = ZLayer.succeed(myInterface)

val myLayerDependsOnClock = 
  ZLayer.fromService { (clock: Clock.Service) =>
    new MyService { ... }
  }

See the ZLayer companion object for more simple ways to construct ZLayer values.

Layers compose vertically with the >>> operator, which allows you to feed services produced by one layer into services required by another layer; and horizontally with the ++ operator, which allows you to aggregate layer inputs and outputs. Together, these operators let you build any dependency graph without cycles.

Once you have a layer that builds all services required by your effects, you can give your effects what they need using ZIO#provideLayer, e.g.: myApp.provideLayer(myLayer). You are encouraged to do this a single time, in your application's main function, or at least minimally; as in the general case, layers acquire and release resources, and you don't want to be doing that very often in your application.

The second option is more attractive if your goal is to make your ZIO code work with minimal changes.

In this case, the following helper function may come in handy:

def unsafeExtract[A: Tagged](zlayer: ZLayer[Any, Nothing, Has[A]]): A = 
  Runtime.default.unsafeRun(zlayer.build.use(ZIO.succeed(_))).get 

This helper function can let you turn Clock.live (for example), into a Clock.Service that you can use to provide your effects with, or delegate to if you are creating custom environments that need to "mix in" Clock.Service.

The helper function is useful to get code working with minimal changes, but its usage should be discouraged: the reason is that most layers are effectful and resourceful, and the above bypasses these features of layers.

If you have other questions on migration or want to contribute additional material to this migration guide, then please post below and we'll see what we can do!

Thank you for your feedback and patience as we prepare for the impending release of ZIO 1.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment