Skip to content

queueing: generalize DynamicClassifier for pull-based per-class structures - #1122

Open
adamgeorge309 wants to merge 2 commits into
masterfrom
topic/gy/queueing-dynamic-classifier
Open

queueing: generalize DynamicClassifier for pull-based per-class structures#1122
adamgeorge309 wants to merge 2 commits into
masterfrom
topic/gy/queueing-dynamic-classifier

Conversation

@adamgeorge309

@adamgeorge309 adamgeorge309 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

DynamicClassifier could only build one shape: create a submodule of the configured type in a preexisting submodule vector, and wire it to a submodule literally named multiplexer. This generalizes it along three axes so it can also build pull-based per-class structures.

  • Parametric aggregator. The downstream aggregator is named by aggregatorSubmoduleName (still multiplexer by default). If it implements the new IDynamicInputScheduler contract, the runtime-created input gate is registered with it — so the aggregator can be a pull scheduler instead of a push multiplexer.
  • Branch splicing. With spliceBranchSubmodules, a compound moduleType forming a linear in -> a -> b -> ... -> out chain is flattened: its inner submodules are reparented into the classifier's parent as vector elements named after them (a[k], b[k]). A downstream matched-pair scheduler can then address them directly, which a compound boundary would prevent.
  • Parameter forwarding. forwardMatchingParams() copies same-named parameters from the enclosing module into the created branch before it is finalized.

Branch module initialization is deferred until the whole chain — including the aggregator connection — is wired, since a module that resolves its downstream peer in initialize() would otherwise see a dangling gate. submoduleName becomes optional (unused when splicing), and the missing-vector / missing-aggregator cases now fail with a clear error instead of a null dereference.

Behaviour of existing configurations is unchanged: the defaults reproduce the previous push-multiplexer shape.

This is the enabling change for the per-station airtime-fair 802.11 transmit queue, which is proposed separately on top of this branch.

Test

Builds at every commit. Existing DynamicClassifier users are unaffected by construction (defaults unchanged).

🤖 Generated with Claude Code


Open in Devin Review

Schedulers that get their inputs wired up at runtime -- rather than from
the NED topology -- need a way to be told about a newly connected input
gate, so that they start considering it. ~DynamicClassifier creates and
connects the gate; addInput() is how the scheduler learns about it.

This makes it possible for a dynamic classifier to build pull-based
per-class structures (queue + scheduler), not just push-based demux/remux
chains terminating in a ~PacketMultiplexer.
~DynamicClassifier could only build one shape: create a submodule of the
configured type in a preexisting submodule vector, and wire it to a
submodule literally named "multiplexer". Three generalizations:

- The downstream aggregator is now named by the aggregatorSubmoduleName
  parameter (still "multiplexer" by default). If it implements
  ~IDynamicInputScheduler, the newly created input gate is registered
  with it, so the aggregator can be a pull scheduler instead of a push
  multiplexer.

- With spliceBranchSubmodules, a compound moduleType forming a linear
  in -> a -> b -> ... -> out chain is flattened: its inner submodules are
  reparented into the classifier's parent as vector elements named after
  them (a[k], b[k]), rather than staying behind the compound's boundary.
  A downstream matched-pair scheduler can then address them directly,
  which a compound boundary would prevent.

- forwardMatchingParams() copies same-named parameters from the enclosing
  module into the created branch before it is finalized, so a per-class
  compound picks up the enclosing queue's configuration.

Branch module initialization is deferred until the whole chain, including
the aggregator connection, is wired -- a module that resolves its
downstream peer in initialize() would otherwise see a dangling gate.

submoduleName is now optional (it is unused when splicing), and the
missing-submodule-vector and missing-aggregator cases fail with a clear
error instead of a null dereference.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines +54 to +60
outputGates.push_back(classifierOutputGate);
PassivePacketSinkRef consumer;
consumer.reference(classifierOutputGate, false);
consumers.push_back(consumer);
ActivePacketSinkRef collector;
collector.reference(classifierOutputGate, false);
collectors.push_back(collector);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Newly created traffic-class branches are never linked to the classifier, so packets of a new class break delivery

The downstream target of a newly added output is looked up (consumer.reference(...) at src/inet/queueing/classifier/DynamicClassifier.cc:56) before the branch is actually attached to that output, so the classifier ends up with no known destination for that class.
Impact: The first packet of every new traffic class either aborts the simulation with a null-reference error or silently falls back to a different delivery path, breaking normal flow control.

Reference resolution happens on a still-unconnected gate

ModuleRefByGate::reference(gate, false) (src/inet/common/ModuleRefByGate.h:80-89) resolves the peer immediately by walking gate->getNextGate() (src/inet/common/ModuleAccess.h:126-134). At DynamicClassifier.cc:55-60 the new out[index] gate has just been created by setGateSize() and is not connected yet -- the connection is only made later inside createModuleBranch() (src/inet/queueing/classifier/DynamicClassifier.cc:98) or spliceBranch() (src/inet/queueing/classifier/DynamicClassifier.cc:140). With mandatory == false the lookup silently yields nullptr, and the references are never re-resolved.

Consequences in PacketClassifierBase:

  • canPushSomePacket() / canPushPacket() (src/inet/queueing/base/PacketClassifierBase.cc:86-98) call into consumers[i], whose checkReference() throws "Dereferencing nullptr...".
  • pushPacket() uses pushOrSendPacket() (src/inet/queueing/base/PacketProcessorBase.cc:118-126), which falls back to send() when the consumer is null, bypassing the synchronous push API and back-pressure.

The previous implementation connected the gate first and only then created the reference, so it resolved correctly. The fix is to move the outputGates/consumers/collectors bookkeeping after the branch (and aggregator) wiring is done.

Prompt for agents
In DynamicClassifier::createBranch() (src/inet/queueing/classifier/DynamicClassifier.cc), the PassivePacketSinkRef and ActivePacketSinkRef for the new out[index] gate are resolved via reference(gate, false) immediately after setGateSize(), while the gate is still unconnected. ModuleRefByGate::reference() resolves the peer eagerly by following the connection, so with mandatory==false both refs become nullptr permanently. The gate is only connected later, in createModuleBranch()/spliceBranch(). This makes consumers[index] null, which makes PacketClassifierBase::canPushPacket()/canPushSomePacket() throw and makes pushPacket() fall back to send() instead of the push API. Restructure createBranch() so that outputGates/consumers/collectors are populated only after the branch has been created and classifierOutputGate has been connected to the branch input (the previous implementation connected first, then referenced). Note outputGates must stay index-aligned with consumers/collectors.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

cGate *DynamicClassifier::createModuleBranch(int index, cGate *classifierOutputGate, std::vector<cModule *>& modulesToInitialize)
{
cModule *parent = getParentModule();
parent->setSubmoduleVectorSize(submoduleName, index + 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Creating the first per-class branch can delete pre-existing branch modules declared in the network description

The branch container is resized to exactly the new branch position (setSubmoduleVectorSize(submoduleName, index + 1) at src/inet/queueing/classifier/DynamicClassifier.cc:96) instead of only ever growing it, so any pre-existing branches beyond that position are destroyed.
Impact: Statically configured per-class branches can silently disappear at runtime, so traffic that should flow through them is lost or the run aborts.

Removal of the std::max() guard

The previous code deliberately used parentModule->setSubmoduleVectorSize(submoduleName, std::max(origVectorSize, submoduleIndex + 1)) so the vector was never shrunk. The new code passes index + 1 unconditionally. index is the classifier's current out gate count, which is not necessarily >= the NED-declared vector size (e.g. a parent declaring defragmenter[numDefragmenter] whose classifier out gate vector was sized independently). Shrinking an existing submodule vector deletes the elements above the new size. The same unguarded resize is repeated in the splice path at src/inet/queueing/classifier/DynamicClassifier.cc:133.

Suggested change
parent->setSubmoduleVectorSize(submoduleName, index + 1);
parent->setSubmoduleVectorSize(submoduleName, std::max(parent->getSubmoduleVectorSize(submoduleName), index + 1));
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@levy

levy commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

The IDynamicInputScheduler interface has no implementors, how does this work? What's the point of having this interface?

Why doesn't the module use the signals emitted when a gate gets connected?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants