Skip to content

The worker treats a pull of queue items as belonging to one provider #76

Description

@dpage

Summary

The worker still treats a pull of queue items as belonging to a single provider, which stopped being true in #74. Two consequences follow from that one assumption, and they are cheaper to fix together than apart, because most of what either needs is the same.

One provider's rate limit stalls every other provider. provider_cooldown_until is a single file-static deadline (src/worker.c:94), set by provider_begin_cooldown() after a 429 and checked at the top of process_queue_batch(), which returns immediately if it has not passed. So a hosted provider on a free tier holds up a local Ollama that has no rate limit at all. Latency rather than correctness: the items stay pending and are picked up when the cooldown expires, and #74 already stopped the deferral being charged to the wrong provider's items.

One provider that cannot be resolved stalls every other provider. A vectorizer naming a provider that does not exist, or one whose init() fails, raises:

provider = get_embedding_provider(providers[batch_start]);
if (provider == NULL)
    elog(ERROR, "embedding provider \"%s\" is not available",
         providers[batch_start]);

The raise aborts the transaction, so the whole pull returns to pending, including items whose provider is perfectly fine. The next pull selects the same oldest items, reaches the same bad group and aborts again. The batch backoff keeps this from spinning, growing the wait to BATCH_RETRY_MAX, but nothing drains and nothing fails. Before #74 this could not discriminate, since a bad provider meant nothing could proceed anyway; now the failure of one table's configuration is borne by every other table.

Why one issue

Both need the same two things: per-provider state keyed on the provider name, in place of the single file-static, and the ability to skip one request group whilst the rest of the pull proceeds, returning its items to pending uncharged rather than aborting. #74 already put a seed of this in the rate-limit deferral loop, which compares providers[idx] against providers[batch_start] so that one provider's 429 does not charge another's items; both halves of this issue are about generalising that comparison into something the whole function understands.

Doing them separately means building a narrow version of that state twice.

Where they differ

Each half has one part the other does not give it for free.

The cooldown needs the claim filtered. The pull selects by age, so unless the SELECT itself excludes items whose resolved provider is cooling down, every poll claims them, marks them processing and puts them straight back: churn on the queue table for the length of the cooldown. The resolved provider is already available in that query, since inheritance resolves there, so this is a filter rather than new plumbing.

The resolution failure needs a backoff that is not an exception. Whether a provider name resolves is only knowable in C, after the rows are fetched, so it cannot be filtered the same way. The batch backoff is today reachable only by throwing: queue_item_record_failure() returning false is what tells the main loop to grow batch_retry_interval. Skipping a group without throwing means a pull in which every group is unresolvable would spin at the poll interval, so process_queue_batch() needs a way to report a batch-level fault back to its caller without an exception.

What must not change

Items must not be charged for a provider that cannot be resolved. test/t/005_batch_failure_backoff.pl exists to prevent exactly that and injects exactly this fault, describing it as the most likely way a real deployment lands here:

A failure belonging to the batch has none of that. Nothing is charged, deliberately, since billing a blameless item for a misconfigured provider would work through the queue retiring one innocent row per max_attempts cycles.

A single mistyped pgedge_vectorizer.provider would otherwise mark the whole queue failed, recoverable only with retry_failed() after fixing the setting. The item is blameless; the configuration is wrong. Any fix has to skip the group and leave its items uncharged and still reach the backoff when nothing in a pull could be attempted.

Notes

Raised whilst implementing #27 and during review of #77. Supersedes the separate rate-limit issue this one began as, and #78, which is closed in favour of it.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions