Skip to content

Commit 2f53dab

Browse files
ovrclaude
andcommitted
feat(cubestore): bound the priority inversion of ADD_AND_RETRIEVE
Priority ordering is enforced by the selection step, not by the claim: QUEUE PENDING returns items highest priority first and reconcile takes toProcessLimit off the top of that list. QUEUE RETRIEVE is priority blind and is safe only because the path it gets comes from that sorted list. ADD_AND_RETRIEVE selects itself, so it had nothing to compensate for being priority blind. It now claims the item only while there is almost nothing to jump over: | condition | meaning | |------------------------------------|------------------------------------| | active < concurrency | a slot is free, as before | | pending * 2 < concurrency | the backlog is shallow | A claimed item goes straight to active and never becomes pending, so a burst onto an idle queue still fast tracks every query. Items start to accumulate in pending only when the concurrency budget is exhausted, which is exactly when the fast track should step aside and let reconcile pick by priority. The condition is written as a multiplication, because concurrency / 2 truncates to 0 for a queue with concurrency 1 (the pre-aggregation build queue), which would disable the fast track there entirely. An item which is already pending is not counted as its own backlog, otherwise claiming an existing item would never happen for the small concurrency queues. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 7a54bee commit 2f53dab

2 files changed

Lines changed: 89 additions & 11 deletions

File tree

packages/cubejs-query-orchestrator/DEVELOPMENT.md

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -230,17 +230,24 @@ active and returns the payload. Between those two calls another node can take th
230230
concurrency slot, so the enqueueing node often pays for the second round-trip and gets
231231
nothing back.
232232

233-
`QUEUE ADD_AND_RETRIEVE` inserts **and** claims the item in one atomic operation when the
234-
prefix has a free concurrency slot, so the enqueueing request can go straight to executing:
233+
`QUEUE ADD_AND_RETRIEVE` inserts **and** claims the item in one atomic operation, so the
234+
enqueueing request can go straight to executing:
235235

236236
```
237237
QUEUE ADD_AND_RETRIEVE [EXCLUSIVE] [PRIORITY ?n] [ORPHANED ?ttl] [EXTERNAL_ID ?id]
238238
?path ?payload ?concurrency
239239
```
240240

241-
`payload IS NULL` in the response means the item was not claimed — the budget was full, the
242-
item was already active, or it belongs to another process — and the caller falls back to
243-
the normal path with nothing lost, because the item is enqueued either way.
241+
The item is claimed when both hold for its prefix:
242+
243+
- a concurrency slot is free — `active < concurrency`, the same budget
244+
`QUEUE RETRIEVE CONCURRENCY` uses;
245+
- the backlog is shallow — `pending < concurrency / 2`, not counting the item itself.
246+
247+
`payload IS NULL` in the response means the item was not claimed — one of the two
248+
conditions failed, the item was already active, or it belongs to another process — and the
249+
caller falls back to the normal path with nothing lost, because the item is enqueued either
250+
way.
244251

245252
```mermaid
246253
sequenceDiagram
@@ -287,10 +294,18 @@ Everything after the claim is unchanged: `MERGE_EXTRA`, `HEARTBEAT`, `ACK` and
287294
`RESULT_BLOCKING` behave exactly as in the normal path, and a fast-tracked item is a
288295
regular active item — `TO_CANCEL` will reclaim it if the heartbeat stops.
289296

290-
The concurrency budget is the same one `QUEUE RETRIEVE CONCURRENCY` uses: prefix scoped,
291-
and blind to both exclusivity and priority. Being priority blind is the trade-off of the
292-
fast track — it claims the item being added even when a higher priority item is pending in
293-
the same prefix, so it should not be used for queues that rely on priority ordering.
297+
Priority ordering is enforced by the *selection* step, not by the claim: `QUEUE PENDING`
298+
returns items highest priority first (oldest first within a priority) and reconcile takes
299+
`toProcessLimit` off the top of that list. `QUEUE RETRIEVE <path>` itself is priority blind
300+
— it is safe only because the path it is given came from that sorted list.
301+
302+
The fast track selects itself, so it is priority blind with nothing to compensate. That is
303+
what the `pending < concurrency / 2` condition bounds: with a shallow backlog there is
304+
almost nothing to jump over, and with a deep one the fast track steps aside and lets
305+
reconcile pick by priority. Note that a claimed item goes straight to active and never
306+
becomes pending, so a burst onto an idle queue still fast-tracks every query — items only
307+
start accumulating in pending once the concurrency budget is exhausted, which is exactly
308+
when the condition should stop firing.
294309

295310
> **Status:** the `QUEUE ADD_AND_RETRIEVE` command exists in Cube Store. The driver still
296311
> emits `QUEUE ADD`; wiring the fast track into `CubeStoreQueueDriver.addToQueue` needs a

rust/cubestore/cubestore/src/cachestore/cache_rocksstore.rs

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1467,12 +1467,24 @@ impl CacheStore for RocksCacheStore {
14671467
let queue_schema = QueueItemRocksTable::new(db_ref.clone());
14681468
let (pending, mut active) = Self::queue_prefix_counters(&queue_schema, &payload.path)?;
14691469

1470-
let claim = active.len() < (payload.concurrency as usize);
1471-
14721470
let index_key = QueueItemIndexKey::ByPath(payload.path.clone());
14731471
let id_row_opt = queue_schema
14741472
.get_single_opt_row_by_index(&index_key, &QueueItemRocksIndex::ByPath)?;
14751473

1474+
// An item which is already pending is not a part of its own backlog
1475+
let backlog = match &id_row_opt {
1476+
Some(id_row) if id_row.get_row().get_status() == &QueueItemStatus::Pending => {
1477+
pending.saturating_sub(1)
1478+
}
1479+
_ => pending,
1480+
};
1481+
1482+
// A deep backlog is left to QUEUE PENDING + reconcile, which pick by priority.
1483+
// Claiming the item being added ignores priority, it's acceptable only while
1484+
// there is almost nothing to jump over.
1485+
let claim = active.len() < (payload.concurrency as usize)
1486+
&& backlog * 2 < (payload.concurrency as u64);
1487+
14761488
if let Some(id_row) = id_row_opt {
14771489
let id = id_row.get_id();
14781490
let claim_result = if claim {
@@ -2945,6 +2957,57 @@ mod tests {
29452957
Ok(())
29462958
}
29472959

2960+
#[tokio::test]
2961+
async fn test_queue_add_and_retrieve_backlog() -> Result<(), CubeError> {
2962+
init_test_logger().await;
2963+
2964+
let (_, cachestore) = RocksCacheStore::prepare_test_cachestore(
2965+
"test_queue_add_and_retrieve_backlog",
2966+
Config::test("test_queue_add_and_retrieve_backlog"),
2967+
);
2968+
2969+
for path in ["prefix:path1", "prefix:path2"] {
2970+
cachestore
2971+
.queue_add(QueueAddPayload {
2972+
path: path.to_string(),
2973+
value: "v".to_string(),
2974+
priority: 0,
2975+
orphaned: None,
2976+
process_id: None,
2977+
exclusive: false,
2978+
external_id: None,
2979+
})
2980+
.await?;
2981+
}
2982+
2983+
// 2 pending items is not less than a half of 4, the backlog is left to reconcile
2984+
// even though all the concurrency slots are free
2985+
let res = cachestore
2986+
.queue_add_and_retrieve(queue_add_and_retrieve_payload("prefix:path3", "v3", 4))
2987+
.await?;
2988+
assert!(res.added);
2989+
assert_eq!(res.payload, None);
2990+
assert_eq!(res.active, Vec::<String>::new());
2991+
assert_eq!(res.pending, 3);
2992+
2993+
assert_queue_item_status(&cachestore, "path3", QueueItemStatus::Pending, false).await?;
2994+
2995+
// 3 pending items is less than a half of 7
2996+
let res = cachestore
2997+
.queue_add_and_retrieve(queue_add_and_retrieve_payload("prefix:path4", "v4", 7))
2998+
.await?;
2999+
assert!(res.added);
3000+
assert_eq!(res.payload, Some("v4".to_string()));
3001+
assert_eq!(res.active, vec!["path4".to_string()]);
3002+
assert_eq!(res.pending, 3);
3003+
3004+
assert_queue_item_status(&cachestore, "path4", QueueItemStatus::Active, true).await?;
3005+
3006+
RocksCacheStore::cleanup_test_cachestore("test_queue_add_and_retrieve_backlog");
3007+
3008+
Ok(())
3009+
}
3010+
29483011
#[tokio::test]
29493012
async fn test_queue_add_and_retrieve_exclusive() -> Result<(), CubeError> {
29503013
init_test_logger().await;

0 commit comments

Comments
 (0)