loading...

Queues and Stateful Services

cr0wst profile image Steve Crow Originally published at smcrow.net on ・5 min read

Edit: I originally thought that Laravel’s service container defaulted to binding as singletons. This doesn’t appear to be the case. I have made updates where appropriate. I also added a section on statics!

Edit Edit: When I say singleton I specifically mean a class that has been instantiated once, and then reused by the service container. I’m not referring to the singleton pattern created by using statics.

Laravel offers Queues to defer time-consuming tasks such as sending emails and batch processing. To learn about using Queues you can reference the Laravel Documentation: Queues.

Queue Worker

Laravel provides a Queue Worker that can be used to process new jobs as they are added to the Queues.

What you might not realize, is that by using stateful services within your queued jobs you may run into issues. From the Laravel Documentation: Queues:

Remember, queue workers are long-lived processes and store the booted application state in memory. As a result, they will not notice changes in your code base after they have been started.

While that does a nice job informing the user that they should restart their queue worker should they make any changes to the application, it doesn’t point out that keeping the application in a booted state means that the same state is going to be used for each job. This can cause issues if your services have any sort of state and you’re binding them as singletons.

First, let’s look into what causes the application’s booted state to be stored in memory.

The Queue Worker Command

The php artisan queue:work command starts the Queue Worker and will continue to run until you manually cancel the process or close your terminal.

The WorkCommand has an injected Worker object which is used to process the jobs. At this point the application has already been booted.

The Worker

The worker itself can be told to run either once, where it will process a single job, or in daemon mode where it will continue to run until cancelled barring the queue being paused, or memory limits have been reached.

Daemon mode is defined as the daemon function in Illuminate\Queue\Worker. In this daemon function we can see:

public function daemon($connectionName, $queue, WorkerOptions $options)
{
    $this->listenForSignals();

    $lastRestart = $this->getTimestampOfLastRestart();

    while (true) {
    // ... stuff
    }
}

The while loop is exactly what keeps the application booted into memory. It will keep the thread processing until it is told to cancel programmatically, cancelled manually by the user, or encounters some sort of scenario that requires it to cancel.

Why do I need to worry about this?

Another thing that Laravel provides for us is its service container which allows dependencies to be injected instead of hard-coded into classes that need them. One of the other big benefits of dependency injection, is that the dependency can be instantiated once and then shared among each consumer. You can find out more about the service container at Laravel Documentation: Service Container.

Whether or not your services have been bound to the container, or they have been discovered via reflection, the container keeps track of the dependencies that have already been instantiated. And, if you’re binding them as singletons, it will provide you the same instance to each of your jobs.

Normally, in the process of handling requests in your web app, the application container is booted for each request that passes through. However, we’ve already seen that the application stays in memory for the full duration of the queue worker running. That means that for every job that has dependencies, the same object will be injected into each of them. This is why stateless services are so important, when working with singletons.

Stateless vs Stateful Services

I’ll go into more detail in a later article but let’s look at a small example. Don’t worry about what the services are trying to accomplish.

Stateful Service

class ServiceA()
{
    private $someDependency;

    private $listOfNames;

    public function __construct(SomeDependency $someDependency)
    {
        $this->someDependency = $someDependency;
    }

    public function getListOfNames()
    {
        // Lazy Loading Technique
        if (empty($this->listOfNames)) {
            $this->listOfNames = SomeHelper::buildListOfNames();
        }

        return $this->listOfNames;
    }
}

This service has a single dependency on SomeDependency and has a method for returning the value in the $listOfNames array. Notice that the array is lazy loaded. The value of the array is set by calling SomeHelper::buildListOfNames() and is only done when the method is first invoked.

Having the $listOfNames variable gives ServiceA state or statefulness.

Stateless Service

class ServiceB()
{
    private $someDependency;

    public function __construct(SomeDependency $someDependency)
    {
        $this->someDependency = $someDependency;
    }

    public function getListOfNames()
    {
        return SomeHelper::buildListOfNames();
    }
}

This service has had its state removed. It will always return a fresh list of names provided by another class.

Note that the dependency on $someDependency does not provide state as long as SomeDependency is also stateless.

The Consequences of Stateful Services

Consider a job with the following handle method:

public function handle(ServiceA $service)
{
    $listOfNames = $service->getListOfNames();
    doSomething($listOfNames);
}

When this job is executed the ServiceA class will be injected. The getListOfNames() method will be invoked. The first time that this job is executed, maybe when the queue is first starting to be worked, the $listOfNames array on ServiceA will be initialized.

However, for every job that runs the $listOfNames array will always remain the same because the instance of ServiceA is only going to be created when the queue worker first starts, because we’re binding as a singleton.

If something changed in the database where SomeHelper::buildListOfNames() gets its data, the new list won’t be built until the worker has been restarted.

Keep this in mind the next time you start to write a stateful service, especially when working with queues.

But wait, I’m not using Singletons, am I safe?

By default Laravel will issue a new instance from the container unless you explicitely ask for singleton binding. However, that doesn’t mean that you’re safe! There is another way for your services to have state, consider the following modification to ServiceA:

class ServiceA()
{
    private $someDependency;

    public static $listOfNames = [];

    public function __construct(SomeDependency $someDependency)
    {
        $this->someDependency = $someDependency;
    }

    public static function getListOfNames()
    {
        // Lazy Loading Technique
        if (empty(self::listOfNames)) {
            self::listOfNames = SomeHelper::buildListOfNames();
        }

        return self::listOfNames;
    }
}

Notice how how the $listOfNames and getListOfNames are static. This means that both the method and the variable don’t live on the class instance, they’re not instance variables. Instead, PHP (based on an assumption I’m making from Java) scopes the variable to the class instead of the instance. That means that any code referencing the variable will share the same variable reference. Some people refer to statics as global-scoped variables.

In this example, the first time that getListOfNames is called, the static variable $listOfNames will be populated. Any subsequent calls to this method will reference the same variable, and it won’t allow it to be loaded again. Since the workers are keeping the PHP thread open, PHP is unable to clear out the memory as it would when working with HTTP requests. That means that the variable is persisted to each and every job that runs, until the thread is closed and the memory can be cleared out.

This is a similar problem to singletons, because that’s really what statics are. Single references that are persisted for the duration of execution.

Another thing to keep in mind! :)

Posted on Mar 22 '19 by:

cr0wst profile

Steve Crow

@cr0wst

Steve is a lover of Greyhounds, twisty puzzles, and European Board Games. When not talking math to non-math people, and Java to non-Java people, he can be found sipping coffee and hacking on code.

Discussion

markdown guide