-
Notifications
You must be signed in to change notification settings - Fork 602
MQ-1202 Include metrics metadata in queue() handler message batch #6339
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -116,6 +116,22 @@ class WorkerQueue: public jsg::Object { | |
|
|
||
| // Event handler types | ||
|
|
||
| // Metadata delivered with a message batch in the queue() handler | ||
|
|
||
| struct MessageBatchMetrics { | ||
| double backlogCount; | ||
| double backlogBytes; | ||
| double oldestMessageTimestamp; | ||
jasnell marked this conversation as resolved.
Show resolved
Hide resolved
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you buy my argument on #6246 (comment), then this should be a Although I'm not totally sure why we're using a different struct here than on the "metrics" endpoint in the first place. Are we worried that eventually we'll end up exposing different metrics from each place?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed with the comment! All of these changes are behind the experimental flag right now. Would it be alright if I modified all of them to use That was my thought process, although I imagined they would likely stay in sync, I didn't want to permanently tie all of the metrics types together.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's totally fine, thanks!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here is the follow-up PR: #6445 |
||
| JSG_STRUCT(backlogCount, backlogBytes, oldestMessageTimestamp); | ||
| JSG_STRUCT_TS_OVERRIDE(MessageBatchMetrics); | ||
| }; | ||
|
|
||
| struct MessageBatchMetadata { | ||
| MessageBatchMetrics metrics; | ||
| JSG_STRUCT(metrics); | ||
| JSG_STRUCT_TS_OVERRIDE(MessageBatchMetadata); | ||
| }; | ||
|
|
||
| // Types for other workers passing messages into and responses out of a queue handler. | ||
|
|
||
| struct IncomingQueueMessage { | ||
|
|
@@ -235,6 +251,7 @@ class QueueEvent final: public ExtendableEvent { | |
| struct Params { | ||
| kj::String queueName; | ||
| kj::Array<IncomingQueueMessage> messages; | ||
| MessageBatchMetadata metadata; | ||
| }; | ||
|
|
||
| explicit QueueEvent(jsg::Lock& js, | ||
|
|
@@ -250,30 +267,45 @@ class QueueEvent final: public ExtendableEvent { | |
| kj::StringPtr getQueueName() { | ||
| return queueName; | ||
| } | ||
| MessageBatchMetadata getMetadata() { | ||
jasnell marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return metadata; | ||
| } | ||
|
|
||
| void retryAll(jsg::Optional<QueueRetryOptions> options); | ||
| void ackAll(); | ||
|
|
||
| JSG_RESOURCE_TYPE(QueueEvent) { | ||
| JSG_RESOURCE_TYPE(QueueEvent, CompatibilityFlags::Reader flags) { | ||
| JSG_INHERIT(ExtendableEvent); | ||
|
|
||
| JSG_LAZY_READONLY_INSTANCE_PROPERTY(messages, getMessages); | ||
| JSG_READONLY_INSTANCE_PROPERTY(queue, getQueueName); | ||
|
|
||
| if (flags.getWorkerdExperimental()) { | ||
| JSG_READONLY_INSTANCE_PROPERTY(metadata, getMetadata); | ||
jasnell marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| JSG_METHOD(retryAll); | ||
| JSG_METHOD(ackAll); | ||
|
|
||
| JSG_TS_ROOT(); | ||
| JSG_TS_OVERRIDE(QueueEvent<Body = unknown> { | ||
| readonly messages: readonly Message<Body>[]; | ||
| }); | ||
| if (flags.getWorkerdExperimental()) { | ||
| JSG_TS_OVERRIDE(QueueEvent<Body = unknown> { | ||
| readonly messages: readonly Message<Body>[]; | ||
| readonly metadata: MessageBatchMetadata; | ||
| }); | ||
| } else { | ||
| JSG_TS_OVERRIDE(QueueEvent<Body = unknown> { | ||
| readonly messages: readonly Message<Body>[]; | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| void visitForMemoryInfo(jsg::MemoryTracker& tracker) const { | ||
| for (auto& message: messages) { | ||
| tracker.trackField("message", message); | ||
| } | ||
| tracker.trackField("queueName", queueName); | ||
| tracker.trackFieldWithSize("metadata", sizeof(MessageBatchMetadata)); | ||
| tracker.trackFieldWithSize("IoPtr<QueueEventResult>", sizeof(IoPtr<QueueEventResult>)); | ||
| } | ||
|
|
||
|
|
@@ -297,6 +329,7 @@ class QueueEvent final: public ExtendableEvent { | |
| // array to avoid one intermediate copy? | ||
| kj::Array<jsg::Ref<QueueMessage>> messages; | ||
| kj::String queueName; | ||
| MessageBatchMetadata metadata; | ||
| IoPtr<QueueEventResult> result; | ||
| CompletionStatus completionStatus = Incomplete{}; | ||
|
|
||
|
|
@@ -316,24 +349,38 @@ class QueueController final: public jsg::Object { | |
| kj::StringPtr getQueueName() { | ||
| return event->getQueueName(); | ||
| } | ||
| MessageBatchMetadata getMetadata() { | ||
| return event->getMetadata(); | ||
| } | ||
| void retryAll(jsg::Optional<QueueRetryOptions> options) { | ||
| event->retryAll(options); | ||
| } | ||
| void ackAll() { | ||
| event->ackAll(); | ||
| } | ||
|
|
||
| JSG_RESOURCE_TYPE(QueueController) { | ||
| JSG_RESOURCE_TYPE(QueueController, CompatibilityFlags::Reader flags) { | ||
| JSG_READONLY_INSTANCE_PROPERTY(messages, getMessages); | ||
| JSG_READONLY_INSTANCE_PROPERTY(queue, getQueueName); | ||
|
|
||
| if (flags.getWorkerdExperimental()) { | ||
| JSG_READONLY_INSTANCE_PROPERTY(metadata, getMetadata); | ||
| } | ||
|
|
||
| JSG_METHOD(retryAll); | ||
| JSG_METHOD(ackAll); | ||
|
|
||
| JSG_TS_ROOT(); | ||
| JSG_TS_OVERRIDE(MessageBatch<Body = unknown> { | ||
| readonly messages: readonly Message<Body>[]; | ||
| }); | ||
| if (flags.getWorkerdExperimental()) { | ||
| JSG_TS_OVERRIDE(MessageBatch<Body = unknown> { | ||
| readonly messages: readonly Message<Body>[]; | ||
| readonly metadata: MessageBatchMetadata; | ||
| }); | ||
| } else { | ||
| JSG_TS_OVERRIDE(MessageBatch<Body = unknown> { | ||
| readonly messages: readonly Message<Body>[]; | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| void visitForMemoryInfo(jsg::MemoryTracker& tracker) const { | ||
|
|
@@ -400,8 +447,9 @@ class QueueCustomEvent final: public WorkerInterface::CustomEvent, public kj::Re | |
|
|
||
| #define EW_QUEUE_ISOLATE_TYPES \ | ||
| api::WorkerQueue, api::WorkerQueue::SendOptions, api::WorkerQueue::SendBatchOptions, \ | ||
| api::WorkerQueue::MessageSendRequest, api::WorkerQueue::Metrics, api::IncomingQueueMessage, \ | ||
| api::QueueRetryBatch, api::QueueRetryMessage, api::QueueResponse, api::QueueRetryOptions, \ | ||
| api::QueueMessage, api::QueueEvent, api::QueueController, api::QueueExportedHandler | ||
| api::WorkerQueue::MessageSendRequest, api::WorkerQueue::Metrics, api::MessageBatchMetrics, \ | ||
| api::MessageBatchMetadata, api::IncomingQueueMessage, api::QueueRetryBatch, \ | ||
| api::QueueRetryMessage, api::QueueResponse, api::QueueRetryOptions, api::QueueMessage, \ | ||
| api::QueueEvent, api::QueueController, api::QueueExportedHandler | ||
|
|
||
| } // namespace workerd::api | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| // Copyright (c) 2026 Cloudflare, Inc. | ||
| // Licensed under the Apache 2.0 license found in the LICENSE file or at: | ||
| // https://opensource.org/licenses/Apache-2.0 | ||
|
|
||
| import assert from 'node:assert'; | ||
|
|
||
| export default { | ||
| async queue(batch, env, ctx) { | ||
| const flagEnabled = env.METADATA_FLAG; | ||
|
|
||
| if (!flagEnabled) { | ||
| // Flag disabled → metadata property should not exist | ||
| assert.strictEqual(batch.metadata, undefined); | ||
| batch.ackAll(); | ||
| return; | ||
| } | ||
|
|
||
| // Flag enabled → metadata should always be present | ||
| assert.ok(batch.metadata, 'Expected batch.metadata to be defined'); | ||
| assert.ok( | ||
| batch.metadata.metrics, | ||
| 'Expected batch.metadata.metrics to be defined' | ||
| ); | ||
|
|
||
| if ( | ||
| batch.metadata.metrics.backlogCount === 0 && | ||
| batch.metadata.metrics.backlogBytes === 0 && | ||
| batch.metadata.metrics.oldestMessageTimestamp === 0 | ||
| ) { | ||
| // If metadata is omitted → all values default to zero | ||
| batch.ackAll(); | ||
| return; | ||
| } | ||
|
|
||
| // Explicit metadata path | ||
| assert.strictEqual(batch.metadata.metrics.backlogCount, 100); | ||
| assert.strictEqual(batch.metadata.metrics.backlogBytes, 2048); | ||
| assert.strictEqual(batch.metadata.metrics.oldestMessageTimestamp, 1000000); | ||
| batch.ackAll(); | ||
| }, | ||
|
|
||
| async test(ctrl, env, ctx) { | ||
| const flagEnabled = env.METADATA_FLAG; | ||
| const timestamp = new Date(); | ||
|
|
||
| if (flagEnabled) { | ||
| const response1 = await env.SERVICE.queue( | ||
| 'test-queue', | ||
| [{ id: '0', timestamp, body: 'hello', attempts: 1 }], | ||
| { | ||
| metrics: { | ||
| backlogCount: 100, | ||
| backlogBytes: 2048, | ||
| oldestMessageTimestamp: 1000000, | ||
| }, | ||
| } | ||
| ); | ||
| assert.strictEqual(response1.outcome, 'ok'); | ||
| assert(response1.ackAll); | ||
|
|
||
| // Test with omitted metadata | ||
| const response2 = await env.SERVICE.queue('test-queue', [ | ||
| { id: '1', timestamp, body: 'world', attempts: 1 }, | ||
| ]); | ||
| assert.strictEqual(response2.outcome, 'ok'); | ||
| assert(response2.ackAll); | ||
| } else { | ||
| // Flag disabled → handler still works, metadata not visible | ||
| const response = await env.SERVICE.queue('test-queue', [ | ||
| { id: '0', timestamp, body: 'foobar', attempts: 1 }, | ||
| ]); | ||
| assert.strictEqual(response.outcome, 'ok'); | ||
| assert(response.ackAll); | ||
| } | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| using Workerd = import "/workerd/workerd.capnp"; | ||
|
|
||
| const unitTests :Workerd.Config = ( | ||
| services = [ | ||
| ( name = "queue-metadata-test", | ||
| worker = ( | ||
| modules = [ | ||
| ( name = "worker", esModule = embed "queue-metadata-test.js" ) | ||
| ], | ||
| bindings = [ | ||
| ( name = "SERVICE", service = "queue-metadata-test" ), | ||
| ( name = "METADATA_FLAG", json = "true" ), | ||
| ], | ||
| compatibilityFlags = ["nodejs_compat", "service_binding_extra_handlers", "experimental"], | ||
| ) | ||
| ), | ||
| ( name = "queue-metadata-disabled-test", | ||
jasnell marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| worker = ( | ||
| modules = [ | ||
| ( name = "worker-disabled", esModule = embed "queue-metadata-test.js" ) | ||
| ], | ||
| bindings = [ | ||
| ( name = "SERVICE", service = "queue-metadata-disabled-test" ), | ||
| ( name = "METADATA_FLAG", json = "false" ), | ||
| ], | ||
| compatibilityFlags = ["nodejs_compat", "service_binding_extra_handlers"], | ||
| ) | ||
| ), | ||
| ], | ||
| ); | ||
Uh oh!
There was an error while loading. Please reload this page.