Skip to content

Commit b53c49e

Browse files
fix(service-bus): Read max-message-batch-size vendor property for batch sizing (#38049)
## Problem On Premium large-message Service Bus entities, the AMQP link's `max-message-size` can be up to 100 MB, but the broker enforces a 1 MB limit for batch sends. The JS SDK reads `max-message-size` (via `this.link.maxMessageSize`) for batch sizing, so `createMessageBatch()` accepts batches up to 100 MB that the broker then rejects. Related: [azure-service-bus#708](Azure/azure-service-bus#708) ## Fix Read the `com.microsoft:max-message-batch-size` vendor property from the AMQP sender link attach frame, which correctly reports 256 KB (Standard) / 1 MB (Premium) independent of entity-level `max-message-size`. When the vendor property is absent (older service versions), fall back to `Math.min(maxMessageSize, defaultMaxBatchSize)` where `defaultMaxBatchSize` is 1 MB — this caps the batch size to prevent using the raw `max-message-size` (which can be up to 100 MB on Premium large-message entities). ### Changes - **`messageSender.ts`**: Add `defaultMaxBatchSize` constant (1 MB); add `getMaxBatchSizeFromLink()` that reads the vendor property from `this.link.properties`, falling back to `Math.min(maxMessageSize, defaultMaxBatchSize)`; update `createBatch()` to use the new method - **`messageSender.spec.ts`**: Unit tests for vendor property present, absent (with 1 MB cap), undefined properties, wrong type, zero value, user override, rejection of oversized `maxSizeInBytes`, Standard tier ### Cross-SDK alignment | SDK | Status | |-----|--------| | .NET | [PR #57941](Azure/azure-sdk-for-net#57941) | | Java | [PR #48214](Azure/azure-sdk-for-java#48214) | | Go | [PR #26530](Azure/azure-sdk-for-go#26530) | | Python | [PR #46197](Azure/azure-sdk-for-python#46197) | --------- Co-authored-by: Eldert Grootenboer (from Dev Box) <egrootenboer@microsoft.com>
1 parent c825cf9 commit b53c49e

3 files changed

Lines changed: 257 additions & 5 deletions

File tree

sdk/servicebus/service-bus/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
### Bugs Fixed
1010

11+
- Read `com.microsoft:max-message-batch-size` vendor property from the AMQP sender link to correctly limit batch size on Premium large-message entities, where `max-message-size` can be up to 100 MB but the batch limit is 1 MB.
1112
- Fixed `TimeoutNegativeWarning` on Node.js v24+ when timeout budget is exceeded during CBS authentication by clamping remaining-time computations to a minimum of 0. [#38166](https://github.com/Azure/azure-sdk-for-js/pull/38166)
1213

1314
### Other Changes

sdk/servicebus/service-bus/src/core/messageSender.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ import { ServiceBusError, translateServiceBusError } from "../serviceBusError.js
3232
import { isDefined } from "@azure/core-util";
3333
import { defaultDataTransformer } from "../dataTransformer.js";
3434

35+
/**
36+
* Default maximum batch size (1 MB). Used when the service does not
37+
* advertise a batch size limit on the AMQP link.
38+
* @internal
39+
*/
40+
const defaultMaxBatchSize = 1048576;
41+
3542
/**
3643
* @internal
3744
* Describes the MessageSender that will send messages to ServiceBus.
@@ -395,22 +402,48 @@ export class MessageSender extends LinkEntity<AwaitableSender> {
395402
return retry(config);
396403
}
397404

405+
/**
406+
* Returns the maximum batch size allowed by the service, reading the
407+
* vendor-specific batch size property from the AMQP link if available.
408+
* Falls back to `Math.min(maxMessageSize, defaultMaxBatchSize)` when
409+
* the property is absent or invalid.
410+
*/
411+
private getMaxBatchSizeFromLink(): number {
412+
if (this.link) {
413+
const vendorBatchSize = this.link.properties?.["com.microsoft:max-message-batch-size"];
414+
if (typeof vendorBatchSize === "number" && vendorBatchSize > 0) {
415+
return vendorBatchSize;
416+
}
417+
}
418+
// Fallback: cap at defaultMaxBatchSize to avoid using the raw
419+
// max-message-size (which can be 100 MB on Premium large-message entities)
420+
// as the batch limit. Matches the .NET SDK pattern.
421+
const maxMessageSize = this.link?.maxMessageSize ?? 0;
422+
return maxMessageSize > 0 ? Math.min(maxMessageSize, defaultMaxBatchSize) : 0;
423+
}
424+
398425
async createBatch(options?: CreateMessageBatchOptions): Promise<ServiceBusMessageBatch> {
399426
throwErrorIfConnectionClosed(this._context);
400-
let maxMessageSize = await this.getMaxMessageSize({
427+
// Ensure the link is open so we can read link properties.
428+
const maxMessageSize = await this.getMaxMessageSize({
401429
retryOptions: this._retryOptions,
402430
abortSignal: options?.abortSignal,
403431
});
432+
// Use the vendor batch size if available; fall back to
433+
// min(maxMessageSize, defaultMaxBatchSize) to prevent using the raw
434+
// max-message-size as the batch limit on large-message entities.
435+
let maxBatchSize =
436+
this.getMaxBatchSizeFromLink() || Math.min(maxMessageSize, defaultMaxBatchSize);
404437
if (options?.maxSizeInBytes) {
405-
if (options.maxSizeInBytes > maxMessageSize!) {
438+
if (options.maxSizeInBytes > maxBatchSize) {
406439
const error = new Error(
407-
`Max message size (${options.maxSizeInBytes} bytes) is greater than maximum message size (${maxMessageSize} bytes) on the AMQP sender link.`,
440+
`Requested max batch size (${options.maxSizeInBytes} bytes) exceeds the maximum batch size (${maxBatchSize} bytes) on the AMQP sender link.`,
408441
);
409442
throw error;
410443
}
411-
maxMessageSize = options.maxSizeInBytes;
444+
maxBatchSize = options.maxSizeInBytes;
412445
}
413-
return new ServiceBusMessageBatchImpl(this._context, maxMessageSize!);
446+
return new ServiceBusMessageBatchImpl(this._context, maxBatchSize);
414447
}
415448

416449
async sendBatch(

sdk/servicebus/service-bus/test/internal/unit/messageSender.spec.ts

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,222 @@ describe("MessageSender unit tests", () => {
183183

184184
assert.equal(openCalled, retryOptions.maxRetries + 1);
185185
});
186+
187+
describe("createBatch uses vendor property for batch sizing", () => {
188+
function createSender(): MessageSender {
189+
return new MessageSender(
190+
"serviceBusClientId",
191+
createConnectionContextForTests(),
192+
"entityPath",
193+
{ maxRetries: 0, retryDelayInMs: 0, timeoutInMs: 1000 },
194+
);
195+
}
196+
197+
it("prefers com.microsoft:max-message-batch-size over maxMessageSize", async () => {
198+
const sender = createSender();
199+
sender["open"] = async () => {
200+
sender["_link"] = {
201+
maxMessageSize: 100 * 1024 * 1024, // 100 MB (Premium large-message)
202+
properties: {
203+
"com.microsoft:max-message-batch-size": 1048576, // 1 MB
204+
},
205+
isOpen: () => true,
206+
} as any;
207+
};
208+
209+
const batch = await sender.createBatch();
210+
assert.equal(
211+
batch.maxSizeInBytes,
212+
1048576,
213+
"Batch size should use vendor property (1 MB), not maxMessageSize (100 MB)",
214+
);
215+
});
216+
217+
it("falls back to Math.min(maxMessageSize, defaultMaxBatchSize) when vendor property is absent", async () => {
218+
const sender = createSender();
219+
sender["open"] = async () => {
220+
sender["_link"] = {
221+
maxMessageSize: 262144, // 256 KB (Standard tier)
222+
properties: {},
223+
isOpen: () => true,
224+
} as any;
225+
};
226+
227+
const batch = await sender.createBatch();
228+
assert.equal(
229+
batch.maxSizeInBytes,
230+
262144,
231+
"Batch size should be Math.min(maxMessageSize, defaultMaxBatchSize) = Math.min(256KB, 1MB) = 256KB",
232+
);
233+
});
234+
235+
it("falls back to Math.min(maxMessageSize, defaultMaxBatchSize) when properties dict is undefined", async () => {
236+
const sender = createSender();
237+
sender["open"] = async () => {
238+
sender["_link"] = {
239+
maxMessageSize: 262144,
240+
isOpen: () => true,
241+
} as any;
242+
};
243+
244+
const batch = await sender.createBatch();
245+
assert.equal(
246+
batch.maxSizeInBytes,
247+
262144,
248+
"Batch size should be Math.min(maxMessageSize, defaultMaxBatchSize) when properties is undefined",
249+
);
250+
});
251+
252+
it("falls back to capped size when vendor property has wrong type", async () => {
253+
const sender = createSender();
254+
sender["open"] = async () => {
255+
sender["_link"] = {
256+
maxMessageSize: 262144,
257+
properties: {
258+
"com.microsoft:max-message-batch-size": "not-a-number",
259+
},
260+
isOpen: () => true,
261+
} as any;
262+
};
263+
264+
const batch = await sender.createBatch();
265+
assert.equal(
266+
batch.maxSizeInBytes,
267+
262144,
268+
"Batch size should fall back to Math.min(maxMessageSize, defaultMaxBatchSize) when vendor property is not a number",
269+
);
270+
});
271+
272+
it("falls back to capped size when vendor property is zero", async () => {
273+
const sender = createSender();
274+
sender["open"] = async () => {
275+
sender["_link"] = {
276+
maxMessageSize: 262144,
277+
properties: {
278+
"com.microsoft:max-message-batch-size": 0,
279+
},
280+
isOpen: () => true,
281+
} as any;
282+
};
283+
284+
const batch = await sender.createBatch();
285+
assert.equal(
286+
batch.maxSizeInBytes,
287+
262144,
288+
"Batch size should fall back to Math.min(maxMessageSize, defaultMaxBatchSize) when vendor property is zero",
289+
);
290+
});
291+
292+
it("user-specified maxSizeInBytes still takes precedence over vendor property", async () => {
293+
const sender = createSender();
294+
sender["open"] = async () => {
295+
sender["_link"] = {
296+
maxMessageSize: 100 * 1024 * 1024,
297+
properties: {
298+
"com.microsoft:max-message-batch-size": 1048576,
299+
},
300+
isOpen: () => true,
301+
} as any;
302+
};
303+
304+
const batch = await sender.createBatch({ maxSizeInBytes: 512 });
305+
assert.equal(
306+
batch.maxSizeInBytes,
307+
512,
308+
"User-specified maxSizeInBytes should override vendor property",
309+
);
310+
});
311+
312+
it("rejects user-specified maxSizeInBytes above vendor batch limit", async () => {
313+
const sender = createSender();
314+
sender["open"] = async () => {
315+
sender["_link"] = {
316+
maxMessageSize: 100 * 1024 * 1024,
317+
properties: {
318+
"com.microsoft:max-message-batch-size": 1048576,
319+
},
320+
isOpen: () => true,
321+
} as any;
322+
};
323+
324+
try {
325+
await sender.createBatch({ maxSizeInBytes: 2 * 1024 * 1024 });
326+
assert.fail("Should have thrown for maxSizeInBytes > batch limit");
327+
} catch (e: any) {
328+
assert.include(e.message, "Requested max batch size");
329+
}
330+
});
331+
332+
it("Standard tier uses 256 KB from vendor property", async () => {
333+
const sender = createSender();
334+
sender["open"] = async () => {
335+
sender["_link"] = {
336+
maxMessageSize: 262144, // 256 KB (Standard)
337+
properties: {
338+
"com.microsoft:max-message-batch-size": 262144,
339+
},
340+
isOpen: () => true,
341+
} as any;
342+
};
343+
344+
const batch = await sender.createBatch();
345+
assert.equal(batch.maxSizeInBytes, 262144, "Standard tier should use 256 KB batch size");
346+
});
347+
348+
it("caps batch size at 1 MB when vendor property is absent and maxMessageSize is 100 MB", async () => {
349+
const sender = createSender();
350+
sender["open"] = async () => {
351+
sender["_link"] = {
352+
maxMessageSize: 100 * 1024 * 1024, // 100 MB (Premium large-message)
353+
properties: {},
354+
isOpen: () => true,
355+
} as any;
356+
};
357+
358+
const batch = await sender.createBatch();
359+
assert.equal(
360+
batch.maxSizeInBytes,
361+
1048576,
362+
"Batch size should be capped at 1 MB (defaultMaxBatchSize) when vendor property is absent, even if maxMessageSize is 100 MB",
363+
);
364+
});
365+
366+
it("caps batch size at 1 MB when vendor property is absent and maxMessageSize is 2 MB", async () => {
367+
const sender = createSender();
368+
sender["open"] = async () => {
369+
sender["_link"] = {
370+
maxMessageSize: 2 * 1024 * 1024, // 2 MB
371+
properties: {},
372+
isOpen: () => true,
373+
} as any;
374+
};
375+
376+
const batch = await sender.createBatch();
377+
assert.equal(
378+
batch.maxSizeInBytes,
379+
1048576,
380+
"Batch size should be capped at 1 MB even when maxMessageSize is only slightly larger",
381+
);
382+
});
383+
384+
it("rejects user-specified maxSizeInBytes above capped batch limit (no vendor property)", async () => {
385+
const sender = createSender();
386+
sender["open"] = async () => {
387+
sender["_link"] = {
388+
maxMessageSize: 100 * 1024 * 1024, // 100 MB
389+
properties: {},
390+
isOpen: () => true,
391+
} as any;
392+
};
393+
394+
try {
395+
// Without the cap, this would succeed (2 MB < 100 MB).
396+
// With the cap, it should fail (2 MB > 1 MB cap).
397+
await sender.createBatch({ maxSizeInBytes: 2 * 1024 * 1024 });
398+
assert.fail("Should have thrown for maxSizeInBytes > capped batch limit");
399+
} catch (e: any) {
400+
assert.include(e.message, "Requested max batch size");
401+
}
402+
});
403+
});
186404
});

0 commit comments

Comments
 (0)