Skip to content

Commit 1974819

Browse files
committed
Address comments
1 parent f973a5c commit 1974819

3 files changed

Lines changed: 110 additions & 46 deletions

File tree

lib/action/client.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ class ActionClient extends Entity {
9595

9696
// Enable feedback subscription content filter optimization.
9797
// Only supported on ROS2 Rolling and only effective when the native
98-
// binding provides the required functions.
98+
// binding provides the required functions AND the RMW implementation
99+
// actually supports content filtering on the feedback subscription.
99100
this._enableFeedbackMsgOptimization =
100101
this._options.enableFeedbackMsgOptimization === true &&
101102
DistroUtils.getDistroId() >= DistroUtils.DistroId.ROLLING &&
@@ -115,6 +116,29 @@ class ActionClient extends Entity {
115116
this.qos.statusSubQosProfile
116117
);
117118

119+
// Probe that the RMW actually supports content filtering before enabling
120+
// the optimization. Content filter support is an RMW-level capability,
121+
// so testing any subscription is sufficient. If unsupported, silently
122+
// disable the optimization to avoid a fatal crash in the DDS layer.
123+
if (this._enableFeedbackMsgOptimization) {
124+
try {
125+
const probeSub = node.createSubscription(
126+
this._typeClass.impl.FeedbackMessage,
127+
actionName + '/_action/feedback',
128+
() => {}
129+
);
130+
const supported =
131+
typeof probeSub.isContentFilterSupported === 'function' &&
132+
probeSub.isContentFilterSupported();
133+
probeSub.destroy();
134+
if (!supported) {
135+
this._enableFeedbackMsgOptimization = false;
136+
}
137+
} catch {
138+
this._enableFeedbackMsgOptimization = false;
139+
}
140+
}
141+
118142
node._addActionClient(this);
119143
}
120144

test/test-action-client.js

Lines changed: 85 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ describe('rclnodejs action client', function () {
336336
client.destroy();
337337
});
338338

339-
it('Test send goal with optimization enabled', async function () {
339+
it('Test does not affect normal feedback reception', async function () {
340340
let client = new rclnodejs.ActionClient(node, fibonacci, 'fibonacci', {
341341
enableFeedbackMsgOptimization: true,
342342
});
@@ -360,11 +360,73 @@ describe('rclnodejs action client', function () {
360360

361361
await goalHandle.getResult();
362362
assert.ok(goalHandle.isSucceeded());
363+
assert.ok(feedbackCallback.calledOnce);
363364

364365
client.destroy();
365366
});
366367

367-
it('Test send multiple goals with optimization enabled', async function () {
368+
it('Test handles multiple goals with selective feedback', async function () {
369+
let client = new rclnodejs.ActionClient(node, fibonacci, 'fibonacci', {
370+
enableFeedbackMsgOptimization: true,
371+
});
372+
373+
let goal1Uuid = ActionUuid.randomMessage();
374+
let goal2Uuid = ActionUuid.randomMessage();
375+
376+
let feedback1Callback = sinon.spy();
377+
let feedback2Callback = sinon.spy();
378+
379+
// Only publish feedback for the first goal
380+
publishFeedback = goal1Uuid;
381+
382+
let result = await client.waitForServer(2000);
383+
assert.ok(result);
384+
385+
const [goal1Handle, goal2Handle] = await Promise.all([
386+
client.sendGoal(new Fibonacci.Goal(), feedback1Callback, goal1Uuid),
387+
client.sendGoal(new Fibonacci.Goal(), feedback2Callback, goal2Uuid),
388+
]);
389+
390+
await goal1Handle.getResult();
391+
await goal2Handle.getResult();
392+
393+
// Only first goal should have received feedback
394+
assert.ok(feedback1Callback.calledOnce);
395+
assert.ok(feedback2Callback.notCalled);
396+
397+
client.destroy();
398+
});
399+
400+
it('Test cancel goal then send new goal', async function () {
401+
let client = new rclnodejs.ActionClient(node, fibonacci, 'fibonacci', {
402+
enableFeedbackMsgOptimization: true,
403+
});
404+
405+
let result = await client.waitForServer(2000);
406+
assert.ok(result);
407+
408+
let goalHandle = await client.sendGoal(new Fibonacci.Goal());
409+
assert.ok(goalHandle.isAccepted());
410+
411+
result = await goalHandle.cancelGoal();
412+
assert.ok(result);
413+
414+
assert.strictEqual(
415+
ActionUuid.fromMessage(result.goals_canceling[0].goal_id).toString(),
416+
ActionUuid.fromMessage(goalHandle.goalId).toString()
417+
);
418+
419+
// Send another goal after cancel - should still work
420+
let goalHandle2 = await client.sendGoal(new Fibonacci.Goal());
421+
assert.ok(goalHandle2.isAccepted());
422+
423+
let result2 = await goalHandle2.getResult();
424+
assert.ok(result2);
425+
426+
client.destroy();
427+
});
428+
429+
it('Test send multiple goals (3)', async function () {
368430
let client = new rclnodejs.ActionClient(node, fibonacci, 'fibonacci', {
369431
enableFeedbackMsgOptimization: true,
370432
});
@@ -395,24 +457,35 @@ describe('rclnodejs action client', function () {
395457
client.destroy();
396458
});
397459

398-
it('Test cancel goal with optimization enabled', async function () {
460+
it('Test handles more than 6 goals gracefully', async function () {
461+
// The DDS content filter limit is 6 concurrent goals (100 params / 16 per goal).
462+
// When exceeded, optimization auto-disables but goals should still work.
399463
let client = new rclnodejs.ActionClient(node, fibonacci, 'fibonacci', {
400464
enableFeedbackMsgOptimization: true,
401465
});
402466

467+
let feedbackCallback = sinon.spy();
468+
let goalUuid = ActionUuid.randomMessage();
469+
publishFeedback = goalUuid;
470+
403471
let result = await client.waitForServer(2000);
404472
assert.ok(result);
405473

406-
let goalHandle = await client.sendGoal(new Fibonacci.Goal());
407-
assert.ok(goalHandle.isAccepted());
408-
409-
result = await goalHandle.cancelGoal();
410-
assert.ok(result);
474+
// Send 7 goals sequentially - exceeds the 6 goal content filter limit
475+
let handles = [];
476+
for (let i = 0; i < 7; i++) {
477+
let uuid = i === 0 ? goalUuid : undefined;
478+
let cb = i === 0 ? feedbackCallback : undefined;
479+
let h = await client.sendGoal(new Fibonacci.Goal(), cb, uuid);
480+
assert.ok(h.isAccepted());
481+
handles.push(h);
482+
}
411483

412-
assert.strictEqual(
413-
ActionUuid.fromMessage(result.goals_canceling[0].goal_id).toString(),
414-
ActionUuid.fromMessage(goalHandle.goalId).toString()
415-
);
484+
// Wait for all results
485+
for (const h of handles) {
486+
let r = await h.getResult();
487+
assert.ok(r);
488+
}
416489

417490
client.destroy();
418491
});

test/test-subscription-content-filter.js

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -480,36 +480,3 @@ describe('subscription content-filtering', function () {
480480
done();
481481
});
482482
});
483-
484-
describe('subscription isContentFilterSupported', function () {
485-
this.timeout(30 * 1000);
486-
487-
beforeEach(async function () {
488-
await rclnodejs.init();
489-
this.node = new Node('cft_support_test_node');
490-
});
491-
492-
afterEach(function () {
493-
this.node.destroy();
494-
rclnodejs.shutdown();
495-
});
496-
497-
it('isContentFilterSupported returns boolean matching RMW capability', function (done) {
498-
const typeclass = 'std_msgs/msg/Int16';
499-
const subscription = this.node.createSubscription(
500-
typeclass,
501-
TOPIC,
502-
(msg) => {}
503-
);
504-
505-
const supported = subscription.isContentFilterSupported();
506-
assert.strictEqual(typeof supported, 'boolean');
507-
508-
// isContentFilterSupported requires rolling; on older distros it returns false
509-
const isRolling = DistroUtils.getDistroId() >= DistroUtils.DistroId.ROLLING;
510-
const expectedSupported = isRolling && isContentFilteringSupported();
511-
assert.strictEqual(supported, expectedSupported);
512-
513-
done();
514-
});
515-
});

0 commit comments

Comments
 (0)