SLA-Based Timeout Handling for Review Queues

Every item in a redaction review queue needs a deadline: a claimed item that a reviewer abandons must return to the pool, work approaching its deadline must be prioritised, and a breached SLA must trigger a conservative hold — never a blind auto-release. This page implements per-item deadlines, a stale-claim sweeper, priority bumping, and safe breach handling.

Deadlines matter here because redaction review sits on the critical path of statutory response clocks. A claimed item stuck in an abandoned session is invisible work that quietly consumes the response window, so the queue must reclaim it and escalate rather than let it rot. This extends the claim/lock mechanics introduced in Reviewer UI Patterns for Redaction Review Queues and shares the audit and safety posture of the parent Human-in-the-Loop Override Sync guide.

Prerequisites & Version Matrix Permalink to this section

Component Version Role
Python 3.11+ Runtime
Redis 7.2+ Claim + deadline store
APScheduler 3.10+ Periodic sweeper
pytest 8.2+ Verification

Items reach the queue with a priority derived from confidence and matter urgency (see Confidence Threshold Configuration). Each claim carries a claim_expires timestamp, as established by the reviewer surface. All timestamps are UTC epoch seconds from a single trusted clock source.

Step 1 — Compute a per-item deadline Permalink to this section

Assign a deadline at enqueue time from a class-based SLA, so priority bumping later has a fixed target to measure against.

import time

SLA_SECONDS = {"urgent": 4 * 3600, "standard": 24 * 3600}

def assign_deadline(item_id: str, sla_class: str = "standard") -> float:
    deadline = time.time() + SLA_SECONDS[sla_class]
    r.hset(f"review:{item_id}", mapping={
        "deadline": deadline,
        "sla_class": sla_class,
    })
    return deadline

Step 2 — Bump priority as the deadline nears Permalink to this section

A sweeper pass promotes items whose remaining time drops below a threshold, so reviewers pull the most urgent work first without any manual triage.

def bump_if_urgent(item_id: str, warn_window: int = 3600) -> bool:
    remaining = float(r.hget(f"review:{item_id}", "deadline")) - time.time()
    if 0 < remaining <= warn_window:
        r.zadd("review:pending", {item_id: 0})   # 0 = highest priority
        emit_audit("PRIORITY_BUMPED", item_id, actor="system",
                   extra={"remaining_s": int(remaining)})
        return True
    return False

Step 3 — Reclaim stale claims exactly once Permalink to this section

The sweeper returns expired claims to the pending pool. Reclaim is guarded by a compare-and-set on the claim owner so a reviewer who submits at the last second is not double-processed, and so two sweeper runs cannot both reclaim the same item.

def reclaim_expired(item_id: str) -> bool:
    row = r.hgetall(f"review:{item_id}")
    claimed_by = row.get(b"claimed_by")
    expires = float(row.get(b"claim_expires", 0))
    # Only reclaim if still claimed AND genuinely expired.
    if not claimed_by or expires > time.time():
        return False
    # Atomic clear: if claimed_by changed underneath us, abort (no double-process).
    with r.pipeline() as pipe:
        pipe.watch(f"review:{item_id}")
        if pipe.hget(f"review:{item_id}", "claimed_by") != claimed_by:
            pipe.unwatch()
            return False
        pipe.multi()
        pipe.hdel(f"review:{item_id}", "claimed_by", "claim_expires")
        pipe.zadd("review:pending", {item_id: 1})
        pipe.execute()
    emit_audit("CLAIM_RECLAIMED", item_id, actor="system",
               extra={"prev_owner": claimed_by.decode()})
    return True

Step 4 — Hold, never auto-redact, on SLA breach Permalink to this section

When the deadline itself passes, the conservative default is to place the item on hold and escalate — never to auto-apply or auto-release a redaction decision a human never made.

def on_breach(item_id: str) -> None:
    r.hset(f"review:{item_id}", "state", "SLA_HELD")     # conservative hold
    assign_to_queue("compliance_review", item_id)         # escalate, don't guess
    emit_audit("SLA_BREACHED", item_id, actor="system")

Compliance Checkpoint Permalink to this section

SLAs on this queue exist to keep the pipeline inside statutory response windows. GDPR Article 12(3) requires a controller to respond to a data-subject request — including erasure under Article 17 — without undue delay and within one month of receipt. Review latency is part of that clock, so per-item deadlines and stale-claim reclamation are what keep a manual review step from silently blowing the statutory window. The conservative-hold default matters too: when an SLA is breached, holding and escalating preserves the accuracy the regulation also demands, rather than trading correctness for speed by auto-releasing an unreviewed span.

Gotchas Permalink to this section

  • Clock skew. Deadlines computed on one host and evaluated on another drift if clocks disagree. Use a single authoritative time source (NTP-synced) and store UTC epoch seconds only; never mix local time zones into deadline math.
  • Reclaim must not double-process. A naive “delete claim and requeue” races a last-second submission and a second sweeper pass. The compare-and-set on claimed_by above ensures an expired claim is reclaimed exactly once.
  • Breach must escalate, not auto-redact. Treating a missed deadline as license to blindly apply the automated suggestion defeats the purpose of human review. Breaches route to Dead-Letter Queue Escalation for Failed Reviews or a compliance queue.
  • Sweeper cadence vs. hold duration. Run the sweeper well inside the shortest claim hold (for example every 60s against a 15-minute hold) so an abandoned item does not sit idle for most of its SLA before anyone notices.

Verification Permalink to this section

This test asserts an expired claim is reclaimed exactly once — a second sweeper pass is a no-op.

def test_expired_claim_reclaimed_exactly_once():
    item_id = "item-7"
    r.hset(f"review:{item_id}", mapping={
        "claimed_by": "rev-1",
        "claim_expires": time.time() - 5,   # already expired
    })
    r.delete("review:pending")
    first = reclaim_expired(item_id)
    second = reclaim_expired(item_id)       # claim already cleared
    assert first is True                    # reclaimed on the first pass
    assert second is False                  # no double-process
    assert r.zscore("review:pending", item_id) == 1  # requeued once

Running pytest -q prints 1 passed: the first pass reclaims and requeues the item, and the second finds nothing to do.

The claims these deadlines govern are created at the Reviewer UI Patterns for Redaction Review Queues surface, breaches escalate through Dead-Letter Queue Escalation for Failed Reviews, and the priorities that order the queue trace back to Confidence Threshold Configuration. Part of the Human-in-the-Loop Override Sync section of the PII Detection & Automated Redaction Patterns field guide.