DEV Community

RemielBarrett8283
RemielBarrett8283

Posted on

Media Batch Operations UI Polling and Cancellation Across Five Terminal States

Short answer: model a batch tagging run as a durable state machine, poll with a bounded backoff, make cancellation an intent rather than an instant result, and stop polling only after the server reports a terminal state.

An e-commerce media library makes this surprisingly concrete. A single catalog import can enqueue thousands of product images, alternate crops, and short clips for automatic labels such as front-view, material, or 360-spin. The console has to remain useful while work is distributed across workers, retried, and sometimes cancelled by an editor. The expensive mistake is treating the screen as the source of truth. It is not. The operation record is.

This matters to a backend engineer who has spent too much time reconciling ledgers: a UI that displays “done” before all child records are durable creates the same class of audit problem as a payment posted before its journal entry. The design below keeps the batch observable, idempotent, and explainable without tying it to a particular vendor or SDK.

Start with a state contract, not a spinner

Use a small, explicit set of states. queued, running, cancelling, succeeded, failed, and cancelled are enough for the console; the last three are terminal. A separate partial state can be useful when the business wants to publish successful labels while retaining failed items, but it must be a deliberate contract, not a UI guess.

Every transition belongs to the server and carries a monotonically increasing revision. The client stores the last revision it saw and ignores older responses that arrive late. This protects the display when two polling requests overlap or a browser tab wakes from sleep with stale data.

The operation should expose counts, timestamps, and a reason for a terminal outcome. For example, items_total=2400, items_completed=2397, and items_failed=3 tells an operator much more than a green progress bar. Keep the raw event trail as well: who requested cancellation, when workers acknowledged it, and which child items were already committed. That trail is useful for support and for replaying a disputed catalog change.

Here is a deliberately generic Go shape for the record and its legal transitions. It is an in-process example; production code would persist the compare-and-swap in a database.

package batch

import "fmt"

type State string

const (
    Queued     State = "queued"
    Running    State = "running"
    Cancelling State = "cancelling"
    Succeeded  State = "succeeded"
    Failed     State = "failed"
    Cancelled  State = "cancelled"
)

type Operation struct {
    ID             string
    State          State
    Revision       int64
    ItemsTotal     int
    ItemsCompleted int
    ItemsFailed    int
    CancelRequested bool
}

func (o Operation) Transition(next State) error {
    valid := map[State]map[State]bool{
        Queued:     {Running: true, Cancelling: true},
        Running:    {Cancelling: true, Succeeded: true, Failed: true},
        Cancelling: {Cancelled: true, Succeeded: true, Failed: true},
    }
    if valid[o.State][next] == false {
        return fmt.Errorf("illegal transition %s -> %s", o.State, next)
    }
    o.State = next
    o.Revision++
    return nil
}
Enter fullscreen mode Exit fullscreen mode

The example intentionally has no transition out of a terminal state. Retrying a network request must not resurrect a cancelled operation. If an operator wants another attempt, create a new operation with a new idempotency key and a link to the original record.

How should polling, cancellation, and terminal states work together?

Polling is a read protocol with operational limits. Start with a short delay after submission, then use bounded exponential backoff with jitter, for example 500 ms, 1 s, 2 s, 4 s, and a cap of 10 s. Reset the delay when the revision changes. Add a maximum wall-clock age for the browser session, but do not reinterpret that timeout as a job failure; show “status unavailable” and allow the user to resume observation.

The request should carry the last revision, or use an HTTP cache validator such as ETag, so an unchanged operation does not repeatedly transfer a large item summary. A 304 response is still a successful observation. The client should also pause polling when the tab is hidden and resume with an immediate refresh on visibility change. That keeps a catalog team's laptop from becoming a fleet of needless timers, and it gives the cache a predictable shape: short-lived status responses can be shared while item-level details remain behind an authorization check. In a large catalog, this distinction prevents a harmless refresh from pulling thousands of labels into every browser, while still letting an operator see current counts when they actually ask for them.

Cancellation has two phases. A user click records intent, and the service acknowledges it by moving the operation to cancelling if work has started. Workers stop taking new items, finish or roll back the item they hold according to the transaction boundary, and then publish cancelled or succeeded. A cancellation response therefore means “request accepted,” not “all side effects have vanished.” The UI should say that plainly.

I once assumed a disabled Cancel button was enough protection against duplicate requests. It was not. A double click and a retrying mobile connection produced two cancellation commands. The fix was an idempotency key tied to the operation and action, plus a server-side event record. The second command became a harmless replay. Your mileage may vary on the exact storage primitive, but the invariant is stable: one user intent must have one auditable effect.

When a terminal state arrives, stop the timer before rendering the final summary. Do not keep polling "just in case"; a terminal revision is the contract. If the summary is eventually consistent, the server should expose a summary_revision and only mark the operation terminal after that revision is durable. Otherwise the UI can show succeeded while a search index still lacks the final labels.

Stop early.

That boundary matters.

How should a batch operations UI handle polling, cancellation, and failures?

There are at least three distinct failure classes: the batch cannot start, individual media items fail, and the observer cannot reach the status endpoint. They deserve different messages and retry rules. A transient observer error should retry with backoff and retain the last known state. A rejected batch should stop immediately and provide an actionable reason. Item-level failures should remain attached to item ids so an operator can requeue only the affected subset.

For a media library, validate format and metadata before enqueueing expensive work. The Media Formats Guide from MDN is a useful baseline for explaining why a browser preview and a worker decoder may accept different inputs; the service should record the detected type rather than trust a filename extension. Never silently turn an unsupported clip into a successful batch with zero labels.

Telemetry should answer four questions: how long until the first worker starts, how often polling observes a revision, how long cancellation takes to reach terminal state, and how many items require retry. Emit an operation id and revision in logs, but avoid putting product names or customer media URLs in a shared dashboard. Auditability includes restraint about sensitive payloads.

A five-state rollout that protects storage and cache budgets

The storage and cache cost axis changes the rollout order. First ship the operation record and terminal-state reader with a feature flag; it can observe existing workers without changing labels. Next add revision-aware polling and cancellation intent. Only then enable automatic retries and partial-result publishing. Each phase can be tested with deterministic fixtures: a zero-item batch, a batch that fails before its first worker, a cancellation during the last item, a duplicated command, and a late response with an old revision.

Use contract tests for every transition and property tests for idempotency. A useful property is that applying the same cancellation command one, two, or ten times leaves the same state, revision sequence, and audit event identity. Load tests should vary the number of open consoles rather than only the number of media items; a cheap worker queue can still be overwhelmed by thousands of eager pollers.

The catch is that this model is not suitable when users need per-frame, sub-second progress for live editing. In that case, a push channel or a job-specific streaming protocol may be justified, with polling retained as recovery. Stick with a simpler periodic status endpoint when batches are short, the console is internal, or the cost of maintaining another connection outweighs the freshness requirement.

There is no universal polling interval. Measure worker latency, browser visibility patterns, and cache hit rates in your own workload, then adjust the cap without changing the state contract. I am not sure any team can predict the right value from a design document alone; a week of revision and cancellation telemetry usually resolves that uncertainty.

References

Top comments (0)