Part 3 (Fiber interruption and Lifecycle hooks) is
A resource is anything that needs to be acquired, and need to released after usage. We cannot keep it open forever, since that would lead to resource leaks. A resource can be anything. Like a database connection, a file handle, a network socket, etc.
Managing Resources in Fibers
Below is a pseudo code we use to simulate a resource in the examples below:
We only simulate acuiring and releasing the resource with some delays. But we don’t care about what the resource actually is for this discussion.
Let’s say we want to use a resource (database connection, websocket connection, file handle, etc.) in a fiber. Where we open the resource, use it, and then close back the resource when fiber completes.
Aquiring already e…
Part 3 (Fiber interruption and Lifecycle hooks) is
A resource is anything that needs to be acquired, and need to released after usage. We cannot keep it open forever, since that would lead to resource leaks. A resource can be anything. Like a database connection, a file handle, a network socket, etc.
Managing Resources in Fibers
Below is a pseudo code we use to simulate a resource in the examples below:
We only simulate acuiring and releasing the resource with some delays. But we don’t care about what the resource actually is for this discussion.
Let’s say we want to use a resource (database connection, websocket connection, file handle, etc.) in a fiber. Where we open the resource, use it, and then close back the resource when fiber completes.
Aquiring already existing resource should throw an error. And closing a non-existing resource should also throw an error.
We want to ensure that the resource is properly released to avoid resource leak. How can we achieve this?
With what we already know, this is one way to do it:
typescript
This Works.
But there are couple of problems here:
- error-prone and cumbersome.
- Even though the resources are meant to be local to the fiber, they are accessible from outside. Which is not ideal.
- For cases where we have multiple resources, we need to manually ensure that each resource is released in all possible code paths (success, failure, interruption). This quickly becomes cumbersome and error-prone.
- Order of cleanup is not guaranteed.
- Many times, we want our resources to be cleaned up in the opposite order of how its created.
While structured concurrency prevents ‘orphaned’ fibers, it does nothing to prevent ‘leaked’ resources. To build truly resilient systems, we need Scope: an abstraction that decouples resource lifetime from execution flow, allowing resources to be composed as easily as functions.
Lets see how Scope helps us achieve this.
Scope
In its simplest form, Scope is an abstraction that gives ability to register finalizers which are just effects. And when the scope is closed, it executes all the registered finalizers.
The flow look like this:
- create scope
- Add finalizers to the scope
- Close the scope
- Closing the scope runs all the finalizers registered with it.
What does this have to do with resources?
Well, we use scope to manage our resources. When we acquire a resource, we add a finalizer to the scope to release the resource. That way, the resource is guaranteed to be released when the scope is closed. Its no more tied to the fiber, but to the scope itself.
About the simulated delays
Few operations that take no time like Scope.make, we explicitly added a sleep to simulate some delay to better visualise in the trace viewer.
typescript
This is much better! But not perfect though!
When Scope is closed, the finalyzers are executed in reverse order! This is important because often times resources depend on each other and need to be released in reverse order of acquisition.
But we are still managing Scope ourselves manually. This is still error-prone and cumbersome. For simpler use cases, we have even better way to manage resources with Scope.
Effect.scoped
For simpler use cases, where we want our resource life cycle to be tied to a fiber itself, we can use Effect.scoped.
typescript
With Effect.scoped, the scope is automatically created for the duration of the fiber execution. Effect.scoped ensures that the scope is closed automatically when the effect completes.
Effect.scoped is the most common way to manage resources in fibers. Its both easy to use and less error-prone.
?
But there is an edge case here!
- We create a resource
- Immediately after that we run addFinaliser to release the resource
But then what happens if the fiber is interrupted just before the addFinaliser is about to run? As we know a Fiber could be interrupted anywhere. Does that cause resource leak?
Answer is yes. It does cause resource leak. Here is an example to illustrate this:
typescript
- Acquiring resource takes 300 millis simulated time.
- Then we sleep for 300 millis simulated time. This is the region where we try to interrupt the fiber.
- Then we add finalizer that takes 300 millis simulated time.
Now, to simulate the interruption before addFinaliser, we interrupt the fiber after 450 millis, when the resource is acquired, but before the finalizer is added.
This causes a resource leak! There is an active resource still present in trace viewer even after the fiber is interrupted.
How can we avoid this?
Remember the uninterruptible region we discussed in ? We can use that to avoid this resource leak!
typescript
Even though the program is interrupted, the resource aquire and release operations are properly handled!
This is already great! But there is even terser, and better way to do this!
Effect.acquireRelease
typescript
Effect.acquireRelease takes care of creating uninterruptible region for us, so we don’t have to manually create it. This is the most concise and least error-prone way to manage resources in fibers 🚀
Manually managing scope
When our entire program is run in Effect, we can create and manage scope automatically using Effect.scoped and Effect.acquireRelease.
But there are cases where we want to manually manage scope. Think about frontend applications, where we want to create a scope for a component, and release all resources when the component is unmounted.
That’s for some other time though.
With this, we come to the end of Fiber series!