<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Queues on Brian Faust</title><link>https://wimdy.works/tags/queues/</link><description>Recent content in Queues on Brian Faust</description><generator>Hugo</generator><language>en-US</language><lastBuildDate>Mon, 01 Jan 0001 00:00:00 +0000</lastBuildDate><atom:link href="https://wimdy.works/tags/queues/rss.xml" rel="self" type="application/rss+xml"/><item><title>A Worker Count Is Not a Throughput Plan</title><link>https://wimdy.works/thoughts/a-worker-count-is-not-a-throughput-plan/</link><pubDate>Wed, 01 Jan 2025 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/a-worker-count-is-not-a-throughput-plan/</guid><description>&lt;p&gt;Four Laravel queue workers complete 114 waiting jobs per second where one
worker completes 37. Keep the worker count at four, make every job update the
same database row, and the rate falls to 32. Make each job fail twice before
succeeding, and those four workers perform 540 attempts to produce 180 effects.&lt;/p&gt;
&lt;p&gt;Nothing is wrong with the worker setting. It is answering three different
workloads.&lt;/p&gt;
&lt;p&gt;A worker count describes available executors. It says nothing about whether a
job can run in parallel, which dependency must absorb the concurrency, or how
many attempts are needed for one business effect. A throughput plan needs those
parts too.&lt;/p&gt;</description></item><item><title>A Queued Command Changes the Contract</title><link>https://wimdy.works/thoughts/a-queued-command-changes-the-contract/</link><pubDate>Sat, 01 Apr 2023 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/a-queued-command-changes-the-contract/</guid><description>&lt;p&gt;Send the same payment-capture request through two Laravel routes. The
synchronous route returns 204 after the provider has created charge
&lt;code&gt;ch_001&lt;/code&gt; and the order records that reference. When the provider returns 503,
the HTTP request returns 500 and the order remains unpaid.&lt;/p&gt;
&lt;p&gt;The queued route returns 202 while the order is still unpaid and the provider
ledger is empty. Its response contains an operation URL. Cancel the order
before a worker starts and that operation later becomes &lt;code&gt;refused&lt;/code&gt;; the
provider is never called.&lt;/p&gt;</description></item><item><title>Framework Convenience Has a Boundary</title><link>https://wimdy.works/thoughts/framework-convenience-has-a-boundary/</link><pubDate>Wed, 01 Jul 2020 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/framework-convenience-has-a-boundary/</guid><description>&lt;p&gt;A model was saved inside a database transaction. Its observer dispatched a
queue job. The worker started quickly, looked for the record which described
its work, and found nothing.&lt;/p&gt;
&lt;p&gt;The record existed in the transaction which dispatched the job, but it was not
yet visible to the worker&amp;rsquo;s database connection. Retained history connects that
race to successful wallet payments without a corresponding wallet transaction.
The job had been published; the state it needed had not. The framework did
exactly what the code asked. The shortcut failed because its lifecycle was
smaller than the business operation.&lt;/p&gt;</description></item><item><title>Deprecation Is a Migration, Not a Warning</title><link>https://wimdy.works/thoughts/deprecation-is-a-migration-not-a-warning/</link><pubDate>Tue, 01 Oct 2019 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/deprecation-is-a-migration-not-a-warning/</guid><description>&lt;p&gt;A billing rewrite introduced durable export records before removing several
legacy queue jobs. Each new worker carried a record identifier. The export
type, payload snapshot, attempts, failure details, and retry state lived in the
database. A registry selected the current handler.&lt;/p&gt;
&lt;p&gt;That order matters. A queued object is not merely code waiting to run. Its
serialized class name and properties are data, even if nobody thinks of them as
a schema. Delete the class while an old payload remains, and PHP can deserialize
it as an incomplete object which no longer has the method the worker expects.&lt;/p&gt;</description></item><item><title>Partial Work Needs a Child Ledger</title><link>https://wimdy.works/thoughts/partial-work-needs-a-child-ledger/</link><pubDate>Sat, 01 Sep 2018 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/partial-work-needs-a-child-ledger/</guid><description>&lt;p&gt;A queue job receives 1,000 events in one remote response. It dispatches 600
child jobs, then times out.&lt;/p&gt;
&lt;p&gt;The parent row says processing failed. Some children have already finished.
Others are waiting in the queue. The remaining 400 were never dispatched. A
counter can report 573 completed events, but it cannot say which 573 they were.&lt;/p&gt;
&lt;p&gt;Running the parent again may duplicate the first 600. Leaving it alone loses
the last 400. Neither choice is defensible because the system retained a total
and discarded the membership behind it.&lt;/p&gt;</description></item><item><title>Retry Budgets Multiply Across Layers</title><link>https://wimdy.works/thoughts/retry-budgets-multiply-across-layers/</link><pubDate>Wed, 01 Aug 2018 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/retry-budgets-multiply-across-layers/</guid><description>&lt;p&gt;An HTTP connector allows three attempts. The queue job calling it also allows
three attempts.&lt;/p&gt;
&lt;p&gt;When the remote service keeps failing, the job does not make three calls. It
makes nine:&lt;/p&gt;
&lt;figure class="code-figure"&gt;
 &lt;div class="code-block" data-copy-code&gt;
 &lt;button class="copy-code" type="button" aria-label="Copy code"&gt;copy&lt;/button&gt;&lt;span class="sr-only" data-copy-status role="status" aria-live="polite"&gt;&lt;/span&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;job attempt 1 -&amp;gt; connector calls 1, 2, 3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;job attempt 2 -&amp;gt; connector calls 4, 5, 6
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;job attempt 3 -&amp;gt; connector calls 7, 8, 9&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;Both settings look modest in isolation. The remote system still receives their
product. Three attempts multiplied by three attempts is nine calls, regardless
of how harmless each configuration file looks.&lt;/p&gt;</description></item><item><title>A Rate Limit Belongs to All the Workers Sharing It</title><link>https://wimdy.works/thoughts/a-rate-limit-belongs-to-all-the-workers-sharing-it/</link><pubDate>Sun, 01 Jul 2018 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/a-rate-limit-belongs-to-all-the-workers-sharing-it/</guid><description>&lt;p&gt;One polling job receives a 429 response saying to try again in fifteen seconds,
so it releases itself for fifteen seconds. During that pause, twenty other
workers poll the same provider. They have not seen the response, and each sends
another request into the closed window.&lt;/p&gt;
&lt;p&gt;The first job obeyed the limit. The application did not.&lt;/p&gt;
&lt;h2 id="find-the-scope-before-choosing-the-delay"&gt;Find the scope before choosing the delay&lt;/h2&gt;
&lt;p&gt;A rate limit applies to some shared identity: an API credential, account,
tenant, endpoint family, source address, or a combination of them. Every worker
using that identity spends the same remote capacity.&lt;/p&gt;</description></item><item><title>Data Outlives the Code That Created It</title><link>https://wimdy.works/thoughts/data-outlives-the-code-that-created-it/</link><pubDate>Fri, 01 Dec 2017 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/data-outlives-the-code-that-created-it/</guid><description>&lt;p&gt;A failed queue record kept returning. Its payload named a mail class the
running application could not resolve correctly, so retrying it could only
repeat the same failure.&lt;/p&gt;
&lt;p&gt;The workaround was blunt: once a minute, delete failed records whose
payload matched that job. It stopped an unusable delivery attempt from
disturbing the queue, but it obviously did not make the payload readable.&lt;/p&gt;
&lt;p&gt;That small incident exposes an awkward storage contract. Serializing an
executable object does not merely store its arguments. It preserves facts about
the code which created it: class name, namespace, property layout, and sometimes
framework behavior. The payload can live much longer than those facts.&lt;/p&gt;</description></item><item><title>Retries Can Multiply the Original Failure</title><link>https://wimdy.works/thoughts/retries-can-multiply-the-original-failure/</link><pubDate>Mon, 01 Aug 2016 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/retries-can-multiply-the-original-failure/</guid><description>&lt;p&gt;We once had roughly 50,000 failed jobs sitting in queues and trying again every
60 seconds. Some represented a provider outage. Others contained malformed
input which could wait for three months and still be malformed. The queue made
no distinction and had no terminal condition.&lt;/p&gt;
&lt;p&gt;During a long outage, workers spent their time reproducing failures we already
understood. Redis filled with delayed work, fresh operations had to compete
with old ones, and a dependency problem became sustained load on our own
application. The retry loop was meant to make work reliable. What it actually
made durable was failure.&lt;/p&gt;</description></item><item><title>Runtime Configuration Is Process State</title><link>https://wimdy.works/thoughts/runtime-configuration-is-process-state/</link><pubDate>Sun, 01 May 2016 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/runtime-configuration-is-process-state/</guid><description>&lt;p&gt;A queued job selected the settings for one business context by overwriting
Laravel&amp;rsquo;s global configuration. The values were correct for that job. The
problem was what happened afterward.&lt;/p&gt;
&lt;p&gt;The worker stayed alive.&lt;/p&gt;
&lt;p&gt;Only a small fraction of the queued job classes selected their context this
way. A later job could read a key without first replacing it and inherit
whatever the previous job had left behind. Nothing was wrong with the
configuration file, environment variables, or deployment. The process
contained valid values for the wrong work.&lt;/p&gt;</description></item></channel></rss>