From 3c89b2737ac20843ad3d5cc54d3ead12746f7215 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Wed, 12 Aug 2026 17:48:38 +0200 Subject: [PATCH 1/8] queueing: unindent the branch-creation path in DynamicClassifier Invert the class lookup into an early return, so that the block creating the branch of a first-seen class sits at function level instead of inside the conditional. Whitespace-only except for the inverted condition and the hoisted return -- review with a whitespace-ignoring diff. No change in behavior. This puts the block in position for the next commit to move it out verbatim. --- .../queueing/classifier/DynamicClassifier.cc | 51 +++++++++---------- .../queueing/classifier/DynamicClassifier.h | 1 - 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/src/inet/queueing/classifier/DynamicClassifier.cc b/src/inet/queueing/classifier/DynamicClassifier.cc index 43d6fe71c02..78456cf9347 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.cc +++ b/src/inet/queueing/classifier/DynamicClassifier.cc @@ -30,35 +30,32 @@ int DynamicClassifier::classifyPacket(Packet *packet) { int index = PacketClassifier::classifyPacket(packet); auto it = classIndexToGateItMap.find(index); - if (it == classIndexToGateItMap.end()) { - auto parentModule = getParentModule(); - int submoduleIndex = gateSize("out"); - int origVectorSize = parentModule->getSubmoduleVectorSize(submoduleName); - parentModule->setSubmoduleVectorSize(submoduleName, std::max(origVectorSize, submoduleIndex + 1)); - auto module = moduleType->create(submoduleName, parentModule, submoduleIndex); - auto moduleInputGate = module->gate("in"); - auto moduleOutputGate = module->gate("out"); - auto multiplexer = parentModule->getSubmodule("multiplexer"); - multiplexer->setGateSize("in", multiplexer->gateSize("in") + 1); - auto multiplexerInputGate = multiplexer->gate("in", multiplexer->gateSize("in") - 1); - setGateSize("out", submoduleIndex + 1); - auto classifierOutputGate = gate("out", gateSize("out") - 1); - classifierOutputGate->connectTo(moduleInputGate); - outputGates.push_back(classifierOutputGate); - PassivePacketSinkRef consumer; - consumer.reference(classifierOutputGate, false); - consumers.push_back(consumer); - moduleOutputGate->connectTo(multiplexerInputGate); - module->finalizeParameters(); - module->buildInside(); - module->callInitialize(); - classIndexToGateItMap[index] = submoduleIndex; - return submoduleIndex; - } - else + if (it != classIndexToGateItMap.end()) return it->second; + auto parentModule = getParentModule(); + int submoduleIndex = gateSize("out"); + int origVectorSize = parentModule->getSubmoduleVectorSize(submoduleName); + parentModule->setSubmoduleVectorSize(submoduleName, std::max(origVectorSize, submoduleIndex + 1)); + auto module = moduleType->create(submoduleName, parentModule, submoduleIndex); + auto moduleInputGate = module->gate("in"); + auto moduleOutputGate = module->gate("out"); + auto multiplexer = parentModule->getSubmodule("multiplexer"); + multiplexer->setGateSize("in", multiplexer->gateSize("in") + 1); + auto multiplexerInputGate = multiplexer->gate("in", multiplexer->gateSize("in") - 1); + setGateSize("out", submoduleIndex + 1); + auto classifierOutputGate = gate("out", gateSize("out") - 1); + classifierOutputGate->connectTo(moduleInputGate); + outputGates.push_back(classifierOutputGate); + PassivePacketSinkRef consumer; + consumer.reference(classifierOutputGate, false); + consumers.push_back(consumer); + moduleOutputGate->connectTo(multiplexerInputGate); + module->finalizeParameters(); + module->buildInside(); + module->callInitialize(); + classIndexToGateItMap[index] = submoduleIndex; + return submoduleIndex; } } // namespace queueing } // namespace inet - diff --git a/src/inet/queueing/classifier/DynamicClassifier.h b/src/inet/queueing/classifier/DynamicClassifier.h index e1907fe1e55..f052601d718 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.h +++ b/src/inet/queueing/classifier/DynamicClassifier.h @@ -31,4 +31,3 @@ class INET_API DynamicClassifier : public PacketClassifier } // namespace inet #endif - From d86066b6b53ebf539c5fcd9f232d114bb994a5a3 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Wed, 12 Aug 2026 17:49:19 +0200 Subject: [PATCH 2/8] queueing: move branch creation out of DynamicClassifier::classifyPacket Extract-function move: the block that builds a branch -- grows the submodule vector, creates the module, wires it between the classifier and the multiplexer, and initializes it -- becomes createBranch(), the lines byte-identical (review with --color-moved). The class-to-branch map entry stays at the call site, fed by the return value: the map is classification bookkeeping, and createBranch() is topology only. No change in behavior. classifyPacket() reads as what it is: look the class up, create its branch on first sight. --- src/inet/queueing/classifier/DynamicClassifier.cc | 8 +++++++- src/inet/queueing/classifier/DynamicClassifier.h | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/inet/queueing/classifier/DynamicClassifier.cc b/src/inet/queueing/classifier/DynamicClassifier.cc index 78456cf9347..bac32535354 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.cc +++ b/src/inet/queueing/classifier/DynamicClassifier.cc @@ -32,6 +32,13 @@ int DynamicClassifier::classifyPacket(Packet *packet) auto it = classIndexToGateItMap.find(index); if (it != classIndexToGateItMap.end()) return it->second; + int branchIndex = createBranch(); + classIndexToGateItMap[index] = branchIndex; + return branchIndex; +} + +int DynamicClassifier::createBranch() +{ auto parentModule = getParentModule(); int submoduleIndex = gateSize("out"); int origVectorSize = parentModule->getSubmoduleVectorSize(submoduleName); @@ -53,7 +60,6 @@ int DynamicClassifier::classifyPacket(Packet *packet) module->finalizeParameters(); module->buildInside(); module->callInitialize(); - classIndexToGateItMap[index] = submoduleIndex; return submoduleIndex; } diff --git a/src/inet/queueing/classifier/DynamicClassifier.h b/src/inet/queueing/classifier/DynamicClassifier.h index f052601d718..91c187fdfeb 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.h +++ b/src/inet/queueing/classifier/DynamicClassifier.h @@ -25,6 +25,8 @@ class INET_API DynamicClassifier : public PacketClassifier protected: virtual void initialize(int stage) override; virtual int classifyPacket(Packet *packet) override; + + virtual int createBranch(); }; } // namespace queueing From 71f745b505a0d2cc9601aaea84f79cb2badd6cd9 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Wed, 12 Aug 2026 17:14:01 +0200 Subject: [PATCH 3/8] queueing: let DynamicClassifier wire branches into any aggregator ~DynamicClassifier could only wire a branch into a submodule literally named "multiplexer". The downstream aggregator is now named by the aggregatorSubmoduleName parameter (still "multiplexer" by default), and it may be a pull scheduler instead of a push multiplexer: an aggregator that has to take notice of an input appearing at runtime learns about it from the POST_MODEL_CHANGE notification of the connection being made (cPostPathCreateNotification), so no contract is needed between the classifier and the aggregator beyond wiring the gate. For the pull side the classifier now also takes a collector reference per branch, the way it already took a consumer reference for the push side. The missing-submodule-vector and missing-aggregator cases fail with a clear error naming the module instead of a null dereference. --- .../queueing/classifier/DynamicClassifier.cc | 22 ++++++++++++++----- .../queueing/classifier/DynamicClassifier.h | 10 +++++++-- .../queueing/classifier/DynamicClassifier.ned | 20 +++++++++++++++-- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/inet/queueing/classifier/DynamicClassifier.cc b/src/inet/queueing/classifier/DynamicClassifier.cc index bac32535354..aae4142374e 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.cc +++ b/src/inet/queueing/classifier/DynamicClassifier.cc @@ -21,9 +21,12 @@ void DynamicClassifier::initialize(int stage) if (stage == INITSTAGE_LOCAL) { submoduleName = par("submoduleName"); moduleType = cModuleType::get(par("moduleType")); + aggregatorSubmoduleName = par("aggregatorSubmoduleName"); if (!getParentModule()->hasSubmoduleVector(submoduleName)) - throw cRuntimeError("The submodule vector '%s' missing from %s", submoduleName, getParentModule()->getFullPath().c_str()); - } + throw cRuntimeError("The submodule vector '%s' is missing from %s", submoduleName, getParentModule()->getFullPath().c_str()); + if (getParentModule()->getSubmodule(aggregatorSubmoduleName) == nullptr) + throw cRuntimeError("The aggregator submodule '%s' is missing from %s", aggregatorSubmoduleName, getParentModule()->getFullPath().c_str()); + } } int DynamicClassifier::classifyPacket(Packet *packet) @@ -46,9 +49,13 @@ int DynamicClassifier::createBranch() auto module = moduleType->create(submoduleName, parentModule, submoduleIndex); auto moduleInputGate = module->gate("in"); auto moduleOutputGate = module->gate("out"); - auto multiplexer = parentModule->getSubmodule("multiplexer"); - multiplexer->setGateSize("in", multiplexer->gateSize("in") + 1); - auto multiplexerInputGate = multiplexer->gate("in", multiplexer->gateSize("in") - 1); + // Wire the branch output into the aggregator's next input gate. An aggregator that has + // to take notice of a runtime-added input (a pull scheduler, for example) learns about + // it from the model change notification of this very connection, so nothing here needs + // to know what kind of aggregator it is. + auto aggregator = parentModule->getSubmodule(aggregatorSubmoduleName); + aggregator->setGateSize("in", aggregator->gateSize("in") + 1); + auto aggregatorInputGate = aggregator->gate("in", aggregator->gateSize("in") - 1); setGateSize("out", submoduleIndex + 1); auto classifierOutputGate = gate("out", gateSize("out") - 1); classifierOutputGate->connectTo(moduleInputGate); @@ -56,7 +63,10 @@ int DynamicClassifier::createBranch() PassivePacketSinkRef consumer; consumer.reference(classifierOutputGate, false); consumers.push_back(consumer); - moduleOutputGate->connectTo(multiplexerInputGate); + ActivePacketSinkRef collector; + collector.reference(classifierOutputGate, false); + collectors.push_back(collector); + moduleOutputGate->connectTo(aggregatorInputGate); module->finalizeParameters(); module->buildInside(); module->callInitialize(); diff --git a/src/inet/queueing/classifier/DynamicClassifier.h b/src/inet/queueing/classifier/DynamicClassifier.h index 91c187fdfeb..dfddccd0187 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.h +++ b/src/inet/queueing/classifier/DynamicClassifier.h @@ -15,11 +15,17 @@ namespace queueing { using namespace inet::queueing; +/** + * Creates the branch of each traffic class on demand, the first time a packet of + * that class is seen. See the NED file for what the branches are built from and + * how they are wired. + */ class INET_API DynamicClassifier : public PacketClassifier { protected: - const char *submoduleName = nullptr; - cModuleType *moduleType = nullptr; + const char *submoduleName = nullptr; // submodule vector that holds the branches + cModuleType *moduleType = nullptr; // type of the per-class branch module (may be a compound) + const char *aggregatorSubmoduleName = nullptr; // downstream aggregator submodule (multiplexer or scheduler) std::map classIndexToGateItMap; protected: diff --git a/src/inet/queueing/classifier/DynamicClassifier.ned b/src/inet/queueing/classifier/DynamicClassifier.ned index 9a679e097ce..e9bd8772480 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.ned +++ b/src/inet/queueing/classifier/DynamicClassifier.ned @@ -7,10 +7,26 @@ package inet.queueing.classifier; +// +// Creates the branch of each traffic class on demand. Each branch is one element of +// the `submoduleName` submodule vector, of type `moduleType` (which may be a +// compound module), wired between this classifier's output and a downstream aggregator +// submodule (`aggregatorSubmoduleName`, a push multiplexer by default). The aggregator may also +// be a pull scheduler; one that has to take notice of an input appearing at runtime learns +// about it from the model change notification of the connection being made, so no extra +// contract is needed between the two. +// +// The submodule vector must be declared in the enclosing compound module, where it may be +// empty; the classifier extends it as branches are created. A branch is created with its final +// name and index, so parameter assignments (both from the enclosing NED declaration and from +// the ini file), display string configuration and result recording all address it as +// `[k]`. +// simple DynamicClassifier extends PacketClassifier { parameters: - string submoduleName; - string moduleType; + string moduleType; // NED type of the per-class branch module, may be a compound module + string submoduleName; // Name of the submodule vector that holds the branches + string aggregatorSubmoduleName = default("multiplexer"); // Name of the downstream aggregator submodule the branches are wired into @class(DynamicClassifier); } From d81ec1d4cb65d534785aa63a88322de8a298861d Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Wed, 12 Aug 2026 17:15:02 +0200 Subject: [PATCH 4/8] queueing: initialize a DynamicClassifier branch only once it is wired Branch modules were initialized right after being built, before the branch was connected to the aggregator, and the classifier took its sink references on its new out gate while the far end of the path was still incomplete. Both are traps for a compound branch: a module that resolves its downstream peer in initialize() would see a dangling gate, and ModuleRefByGate::reference() resolves the peer eagerly by walking the connection -- with mandatory=false it silently stores a nullptr that nothing ever re-resolves, leaving a permanently null consumer whose canPushPacket() throws and whose pushPacket() quietly degrades to send(), bypassing back-pressure. Wire first, resolve and initialize after: createBranchModule() builds the branch module (with its final name and index, so its parameters, display string and result recording are all resolved for the module path it keeps) and leaves it uninitialized; createBranch() connects the chain up to and including the aggregator, then takes the references and initializes the branch. The complete path is also the earliest point at which the packet operations of the branch can be checked, so the new gate now gets the checkPacketOperationSupport() that the base class gives every gate wired in NED. A branch type that does not support pushing is refused with the usual message instead of failing later on the first packet. No change in behavior for the existing simple-branch users, where the old order happened to be safe. --- .../queueing/classifier/DynamicClassifier.cc | 48 +++++++++++++------ .../queueing/classifier/DynamicClassifier.h | 1 + 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/inet/queueing/classifier/DynamicClassifier.cc b/src/inet/queueing/classifier/DynamicClassifier.cc index aae4142374e..743c024b732 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.cc +++ b/src/inet/queueing/classifier/DynamicClassifier.cc @@ -42,23 +42,24 @@ int DynamicClassifier::classifyPacket(Packet *packet) int DynamicClassifier::createBranch() { - auto parentModule = getParentModule(); - int submoduleIndex = gateSize("out"); - int origVectorSize = parentModule->getSubmoduleVectorSize(submoduleName); - parentModule->setSubmoduleVectorSize(submoduleName, std::max(origVectorSize, submoduleIndex + 1)); - auto module = moduleType->create(submoduleName, parentModule, submoduleIndex); - auto moduleInputGate = module->gate("in"); - auto moduleOutputGate = module->gate("out"); + cModule *parent = getParentModule(); + int index = gateSize("out"); + // grow this classifier's output gate vector + setGateSize("out", index + 1); + cGate *classifierOutputGate = gate("out", index); + // the branch module is built but not initialized yet: its initialization is deferred until + // the whole chain, including the aggregator connection, is wired + cModule *branch = createBranchModule(index, classifierOutputGate); // Wire the branch output into the aggregator's next input gate. An aggregator that has // to take notice of a runtime-added input (a pull scheduler, for example) learns about // it from the model change notification of this very connection, so nothing here needs // to know what kind of aggregator it is. - auto aggregator = parentModule->getSubmodule(aggregatorSubmoduleName); + cModule *aggregator = parent->getSubmodule(aggregatorSubmoduleName); aggregator->setGateSize("in", aggregator->gateSize("in") + 1); - auto aggregatorInputGate = aggregator->gate("in", aggregator->gateSize("in") - 1); - setGateSize("out", submoduleIndex + 1); - auto classifierOutputGate = gate("out", gateSize("out") - 1); - classifierOutputGate->connectTo(moduleInputGate); + cGate *aggregatorInputGate = aggregator->gate("in", aggregator->gateSize("in") - 1); + branch->gate("out")->connectTo(aggregatorInputGate); + // the sink references resolve the far end of the path eagerly, so they can only be taken + // now that the whole branch, up to and including the aggregator, is connected outputGates.push_back(classifierOutputGate); PassivePacketSinkRef consumer; consumer.reference(classifierOutputGate, false); @@ -66,11 +67,28 @@ int DynamicClassifier::createBranch() ActivePacketSinkRef collector; collector.reference(classifierOutputGate, false); collectors.push_back(collector); - moduleOutputGate->connectTo(aggregatorInputGate); + branch->callInitialize(); + // the branch is a complete path only now, so this is the earliest point where the packet + // operations of its modules can be checked, the way the base class checks the gates that + // are wired in NED + checkPacketOperationSupport(classifierOutputGate); + return index; +} + +cModule *DynamicClassifier::createBranchModule(int index, cGate *classifierOutputGate) +{ + cModule *parent = getParentModule(); + // the vector is only ever extended: it may have been declared larger in NED, and shrinking + // one that still holds submodules is an error + parent->setSubmoduleVectorSize(submoduleName, std::max(parent->getSubmoduleVectorSize(submoduleName), index + 1)); + // the branch is created with its final name and index, so that its parameters (from the + // enclosing NED declaration and from the ini file), its display string and its result + // recording are all resolved for the module path it keeps + cModule *module = moduleType->create(submoduleName, parent, index); + classifierOutputGate->connectTo(module->gate("in")); module->finalizeParameters(); module->buildInside(); - module->callInitialize(); - return submoduleIndex; + return module; } } // namespace queueing diff --git a/src/inet/queueing/classifier/DynamicClassifier.h b/src/inet/queueing/classifier/DynamicClassifier.h index dfddccd0187..3e7139145f3 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.h +++ b/src/inet/queueing/classifier/DynamicClassifier.h @@ -33,6 +33,7 @@ class INET_API DynamicClassifier : public PacketClassifier virtual int classifyPacket(Packet *packet) override; virtual int createBranch(); + virtual cModule *createBranchModule(int index, cGate *classifierOutputGate); }; } // namespace queueing From 4de1f01ca556ce51c3527d23725de87f2ceff5d7 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Wed, 12 Aug 2026 17:15:39 +0200 Subject: [PATCH 5/8] queueing: fix DynamicClassifier keying its classes on gate indices The class-to-branch map was keyed on the result of PacketClassifier::classifyPacket(), which maps the classifier function's index through getOutputGateIndex(). With reverseOrder that mapping is relative to the current number of output gates -- which grows with each branch created -- so the same class would be looked up under a different key later, miss, and get a second branch. Key the map on the classifier function's index directly, taken through the new getClassIndex(), which classifies without the branch-creating side effect of classifyPacket(). The map is renamed after what it now holds. Bypassing getOutputGateIndex() leaves reverseOrder with nothing to act on, so it is refused in initialize() instead of being silently ignored. Nothing is lost: the order of the output gates is the order in which the classes first appear, and no configuration that sets it works today -- that is the bug this commit fixes. --- .../queueing/classifier/DynamicClassifier.cc | 20 +++++++++++++++---- .../queueing/classifier/DynamicClassifier.h | 3 ++- .../queueing/classifier/DynamicClassifier.ned | 3 +++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/inet/queueing/classifier/DynamicClassifier.cc b/src/inet/queueing/classifier/DynamicClassifier.cc index 743c024b732..cb46061eade 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.cc +++ b/src/inet/queueing/classifier/DynamicClassifier.cc @@ -26,17 +26,29 @@ void DynamicClassifier::initialize(int stage) throw cRuntimeError("The submodule vector '%s' is missing from %s", submoduleName, getParentModule()->getFullPath().c_str()); if (getParentModule()->getSubmodule(aggregatorSubmoduleName) == nullptr) throw cRuntimeError("The aggregator submodule '%s' is missing from %s", aggregatorSubmoduleName, getParentModule()->getFullPath().c_str()); + if (reverseOrder) + throw cRuntimeError("The reverseOrder parameter is not supported: branches are created in the order the classes of the packets first appear"); } } +int DynamicClassifier::getClassIndex(Packet *packet) const +{ + // the class of the packet, with no side effect -- unlike classifyPacket() below, which + // creates the branch of a class that is seen for the first time. Note that the class index + // is taken as it is, and not mapped through getOutputGateIndex(): that mapping depends on + // the number of output gates, which grows with each branch, so the same class would end up + // under a different key over time, and get a second branch. + return packetClassifierFunction->classifyPacket(packet); +} + int DynamicClassifier::classifyPacket(Packet *packet) { - int index = PacketClassifier::classifyPacket(packet); - auto it = classIndexToGateItMap.find(index); - if (it != classIndexToGateItMap.end()) + int index = getClassIndex(packet); + auto it = classIndexToBranchIndex.find(index); + if (it != classIndexToBranchIndex.end()) return it->second; int branchIndex = createBranch(); - classIndexToGateItMap[index] = branchIndex; + classIndexToBranchIndex[index] = branchIndex; return branchIndex; } diff --git a/src/inet/queueing/classifier/DynamicClassifier.h b/src/inet/queueing/classifier/DynamicClassifier.h index 3e7139145f3..6c7be6894ec 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.h +++ b/src/inet/queueing/classifier/DynamicClassifier.h @@ -26,10 +26,11 @@ class INET_API DynamicClassifier : public PacketClassifier const char *submoduleName = nullptr; // submodule vector that holds the branches cModuleType *moduleType = nullptr; // type of the per-class branch module (may be a compound) const char *aggregatorSubmoduleName = nullptr; // downstream aggregator submodule (multiplexer or scheduler) - std::map classIndexToGateItMap; + std::map classIndexToBranchIndex; // the branch of a class, keyed by the index the classifier function returns protected: virtual void initialize(int stage) override; + virtual int getClassIndex(Packet *packet) const; virtual int classifyPacket(Packet *packet) override; virtual int createBranch(); diff --git a/src/inet/queueing/classifier/DynamicClassifier.ned b/src/inet/queueing/classifier/DynamicClassifier.ned index e9bd8772480..4c7158f9891 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.ned +++ b/src/inet/queueing/classifier/DynamicClassifier.ned @@ -16,6 +16,9 @@ package inet.queueing.classifier; // about it from the model change notification of the connection being made, so no extra // contract is needed between the two. // +// The inherited `reverseOrder` parameter is not supported and is refused, because the order of +// the output gates is the order in which the classes of the packets first appear. +// // The submodule vector must be declared in the enclosing compound module, where it may be // empty; the classifier extends it as branches are created. A branch is created with its final // name and index, so parameter assignments (both from the enclosing NED declaration and from From f9c334731f26c190ecbddc92ff6da11eec1edf93 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Tue, 1 Sep 2026 18:04:45 +0200 Subject: [PATCH 6/8] queueing: make DynamicClassifier classification free of side effects classifyPacket() built the branch of a class it had not seen before, so classifying a packet changed the model. Every path classifies: the capacity checks classify the packet they are asked about, the pull path classifies on every peek, and the delivery path classifies again -- so merely asking this classifier whether it could take a packet grew a gate vector, created a submodule, wired connections and initialized modules. classifyPacket() is a plain map lookup now, and the branch is created where the fate of a packet is actually decided: pushPacket() and startPacketStreaming() create it before delegating to the base class. They are the two doors of the delivery path -- the three streaming push operations all classify through startPacketStreaming(). canPushPacket() creates it and asks it. The answer has to hold for the very packet it is asked about, because a source that asks may push exactly that packet next, and a branch that does not exist yet cannot promise to take it: a branch whose first module is a closed gate refuses, and the packet is then pushed through a gate that is not open. This is the one query whose answer decides the fate of a packet, so it is the one query that may build what decides it. The pull side keeps classifying without creating, and a class that has no branch fails there with the base class's out-of-range error. It is not a supported configuration: a puller cannot ask an output gate that does not exist yet for a class that has never been seen. --- .../queueing/classifier/DynamicClassifier.cc | 54 ++++++++++++++----- .../queueing/classifier/DynamicClassifier.h | 6 +++ 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/src/inet/queueing/classifier/DynamicClassifier.cc b/src/inet/queueing/classifier/DynamicClassifier.cc index cb46061eade..3c9492976e2 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.cc +++ b/src/inet/queueing/classifier/DynamicClassifier.cc @@ -33,23 +33,53 @@ void DynamicClassifier::initialize(int stage) int DynamicClassifier::getClassIndex(Packet *packet) const { - // the class of the packet, with no side effect -- unlike classifyPacket() below, which - // creates the branch of a class that is seen for the first time. Note that the class index - // is taken as it is, and not mapped through getOutputGateIndex(): that mapping depends on - // the number of output gates, which grows with each branch, so the same class would end up - // under a different key over time, and get a second branch. + // The class of the packet, taken as the classifier function returns it, and not mapped + // through getOutputGateIndex(): that mapping depends on the number of output gates, which + // grows with each branch, so the same class would end up under a different key over time, + // and get a second branch. return packetClassifierFunction->classifyPacket(packet); } int DynamicClassifier::classifyPacket(Packet *packet) { - int index = getClassIndex(packet); - auto it = classIndexToBranchIndex.find(index); - if (it != classIndexToBranchIndex.end()) - return it->second; - int branchIndex = createBranch(); - classIndexToBranchIndex[index] = branchIndex; - return branchIndex; + // a plain lookup, free of side effects: the branch of a class that is seen for the first + // time is created before the base class classifies, on the delivery path only + auto it = classIndexToBranchIndex.find(getClassIndex(packet)); + return it != classIndexToBranchIndex.end() ? it->second : -1; +} + +void DynamicClassifier::pushPacket(Packet *packet, const cGate *gate) +{ + Enter_Method("pushPacket"); + // usually a no-op: a source that asked canPushPacket() first already had it created + createBranchIfAbsent(packet); + PacketClassifier::pushPacket(packet, gate); +} + +void DynamicClassifier::startPacketStreaming(Packet *packet) +{ + // the one place all three streaming push operations classify through + createBranchIfAbsent(packet); + PacketClassifier::startPacketStreaming(packet); +} + +bool DynamicClassifier::canPushPacket(Packet *packet, const cGate *gate) const +{ + // The answer has to hold for this very packet, because a source that asks may push exactly + // it next. Answering yes for a class that has no branch would promise on behalf of a branch + // that does not exist and may refuse its first packet, so the branch is created here and + // asked. This is the one query whose answer decides the fate of a packet, and it therefore + // has to build the thing that decides it; classification itself stays a pure lookup. + // KLUDGE the query is const, the model change it needs is not + const_cast(this)->createBranchIfAbsent(packet); + return consumers[classIndexToBranchIndex.at(getClassIndex(packet))].canPushPacket(packet); +} + +void DynamicClassifier::createBranchIfAbsent(Packet *packet) +{ + int classIndex = getClassIndex(packet); + if (classIndexToBranchIndex.find(classIndex) == classIndexToBranchIndex.end()) + classIndexToBranchIndex[classIndex] = createBranch(); } int DynamicClassifier::createBranch() diff --git a/src/inet/queueing/classifier/DynamicClassifier.h b/src/inet/queueing/classifier/DynamicClassifier.h index 6c7be6894ec..ed7d20520e5 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.h +++ b/src/inet/queueing/classifier/DynamicClassifier.h @@ -32,9 +32,15 @@ class INET_API DynamicClassifier : public PacketClassifier virtual void initialize(int stage) override; virtual int getClassIndex(Packet *packet) const; virtual int classifyPacket(Packet *packet) override; + virtual void startPacketStreaming(Packet *packet) override; + virtual void createBranchIfAbsent(Packet *packet); virtual int createBranch(); virtual cModule *createBranchModule(int index, cGate *classifierOutputGate); + + public: + virtual bool canPushPacket(Packet *packet, const cGate *gate) const override; + virtual void pushPacket(Packet *packet, const cGate *gate) override; }; } // namespace queueing From 10d9744fc8ef52827129062d4bd78d7fa1a7eb5d Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Tue, 1 Sep 2026 18:05:09 +0200 Subject: [PATCH 7/8] queueing: let DynamicClassifier accept a packet before it has any branch canPushSomePacket() is inherited as "one of the existing branches can take a packet", which is false for a classifier that has not built any branch yet. An active source in front of such a classifier stops, waits for the notification that would tell it packets can be pushed again, and never gets it, because nothing else creates the first branch. Answer true while there is no branch: a packet of a class that has not been seen yet is taken by the branch created for it, and the range of the classifier function is not known here, so there may always be such a class. Only while there is no branch. This query has no packet, so it cannot create a branch and ask it the way canPushPacket() does, and an active source takes the answer as the licence to produce a packet and push it; answering true once the branches exist would push into a full branch, and a queue that has no packet dropper refuses that and fails the run. The inherited answer is the right one from the first branch on. It costs a full branch stopping a source that would have opened a new class, but stopping is the safe error, and it is what a statically wired classifier does in the same situation. Three queueing tests cover the module, which had none. The first builds two branches on demand -- its producer is connected to the classifier directly, so without this commit it never produces and no branch is built -- and covers the rest of the contract: an ini file assignment addressing a submodule of a branch takes effect, and the statistics of the branch submodules are recorded under the branch path. The second fills a branch and requires the producer to stop instead of overloading it. The third wires the branches into an aggregator that is not the one named by default. --- .../queueing/classifier/DynamicClassifier.cc | 17 +++ .../queueing/classifier/DynamicClassifier.h | 1 + tests/queueing/DynamicClassifier_1.test | 130 ++++++++++++++++++ tests/queueing/DynamicClassifier_2.test | 113 +++++++++++++++ tests/queueing/DynamicClassifier_3.test | 77 +++++++++++ 5 files changed, 338 insertions(+) create mode 100644 tests/queueing/DynamicClassifier_1.test create mode 100644 tests/queueing/DynamicClassifier_2.test create mode 100644 tests/queueing/DynamicClassifier_3.test diff --git a/src/inet/queueing/classifier/DynamicClassifier.cc b/src/inet/queueing/classifier/DynamicClassifier.cc index 3c9492976e2..1265ac0fc23 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.cc +++ b/src/inet/queueing/classifier/DynamicClassifier.cc @@ -75,6 +75,23 @@ bool DynamicClassifier::canPushPacket(Packet *packet, const cGate *gate) const return consumers[classIndexToBranchIndex.at(getClassIndex(packet))].canPushPacket(packet); } +bool DynamicClassifier::canPushSomePacket(const cGate *gate) const +{ + // The inherited answer is "one of the existing branches can take a packet", which is false + // for a classifier that has not built any branch yet: an active source in front of such a + // classifier stops and waits for the notification that packets can be pushed again, and + // never gets it, because nothing else creates the first branch. Answer true instead, the + // branch of the class of the first packet being created for it. + // + // Once a branch exists the inherited answer is used, and deliberately so. An active source + // takes this as the licence to produce a packet and push it -- it cannot ask + // canPushPacket() about a packet it has not created yet -- so answering true while every + // branch is full pushes into a full queue, which a queue without a packet dropper refuses. + // The price is that a full branch also stops a source that would have opened a new class, + // but stopping is the safe error, and it is what a statically wired classifier does too. + return outputGates.empty() || PacketClassifierBase::canPushSomePacket(gate); +} + void DynamicClassifier::createBranchIfAbsent(Packet *packet) { int classIndex = getClassIndex(packet); diff --git a/src/inet/queueing/classifier/DynamicClassifier.h b/src/inet/queueing/classifier/DynamicClassifier.h index ed7d20520e5..dca16ed905a 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.h +++ b/src/inet/queueing/classifier/DynamicClassifier.h @@ -39,6 +39,7 @@ class INET_API DynamicClassifier : public PacketClassifier virtual cModule *createBranchModule(int index, cGate *classifierOutputGate); public: + virtual bool canPushSomePacket(const cGate *gate) const override; virtual bool canPushPacket(Packet *packet, const cGate *gate) const override; virtual void pushPacket(Packet *packet, const cGate *gate) override; }; diff --git a/tests/queueing/DynamicClassifier_1.test b/tests/queueing/DynamicClassifier_1.test new file mode 100644 index 00000000000..cb0ef14bedd --- /dev/null +++ b/tests/queueing/DynamicClassifier_1.test @@ -0,0 +1,130 @@ +%description: + +In this test, packets are produced periodically by an active packet source (ActivePacketSource) +and are classified into two classes by a dynamic classifier (DynamicClassifier). The classifier +creates the branch of a class when the first packet of that class arrives, as one element of the +branch submodule vector, and wires it into the packet multiplexer that aggregates the branches. + +The producer is connected to the classifier directly, so the test also covers that a classifier +which has no branch yet accepts a packet, rather than stopping the producer before the first +branch is created. + +The branch is created with its final name and index, so the test checks that an ini file +assignment addressing a submodule of a branch (the delay of the packet delayer in it) takes +effect, and that the statistics of the branch submodules are recorded under the branch path. +Empty output vectors are turned off, so a vector appears in the result file only if data was +recorded into it. + +%file: test.ned + +import inet.queueing.classifier.DynamicClassifier; +import inet.queueing.common.BackPressureBarrier; +import inet.queueing.common.PacketDelayer; +import inet.queueing.common.PacketMultiplexer; +import inet.queueing.sink.PassivePacketSink; +import inet.queueing.source.ActivePacketSource; + +module TestBranch +{ + gates: + input in; + output out; + submodules: + first: BackPressureBarrier { + @display("p=100,100"); + } + second: PacketDelayer { + delay = default(0s); + @display("p=200,100"); + } + connections: + in --> first.in; + first.out --> second.in; + second.out --> out; +} + +module TestDemultiplexer +{ + gates: + input in; + output out; + submodules: + classifier: DynamicClassifier { + moduleType = "TestBranch"; + submoduleName = "branch"; + @display("p=100,100"); + } + branch[0]: TestBranch { // grown on demand, one branch per class + @display("p=250,100,column,80"); + } + multiplexer: PacketMultiplexer { + @display("p=400,100"); + } + connections allowunconnected: + in --> classifier.in; + multiplexer.out --> out; +} + +network TestDynamicClassifier +{ + submodules: + producer: ActivePacketSource { + @display("p=100,100"); + } + demultiplexer: TestDemultiplexer { + @display("p=200,100"); + } + consumer: PassivePacketSink { + @display("p=300,100"); + } + connections: + producer.out --> demultiplexer.in; + demultiplexer.out --> consumer.in; +} + +%file: Test.cc +#include "inet/queueing/function/PacketClassifierFunction.h" +#include "inet/common/packet/Packet.h" + +using namespace inet; + +static int testClassify(Packet *packet) +{ + return packet->getId() % 2; +} + +Register_Packet_Classifier_Function(TestClassifier, testClassify); + +%inifile: omnetpp.ini + +[General] +network = TestDynamicClassifier +sim-time-limit = 10s +cmdenv-event-banners = false +cmdenv-log-prefix = "At %ts %N: " +**.vector-record-empty = false +*.producer.packetLength = 1B +*.producer.productionInterval = 1s +*.demultiplexer.classifier.classifierClass = "TestClassifier" +*.demultiplexer.branch[*].second.delay = 2s + +%# remove formatting +%subst: /\x1B\[[0-9;]*m// +%# remove method call lines added in OMNeT++ 6.4 +%subst: /^At \S+ \S+: Method call [^\n]*\n//m +%#-------------------------------------------------------------------------------------------------------------- +%# the delay assigned to the branch submodule from the ini file must be applied +%contains-regex: stdout +At 0s producer: Producing packet, .*?producer-0.*? +At 1s producer: Producing packet, .*?producer-1.*? +At 2s consumer: Consuming packet, .*?producer-0.*? +At 3s consumer: Consuming packet, .*?producer-1.*? +%#-------------------------------------------------------------------------------------------------------------- +%# the modules that have data in an output vector, and the branch submodules among them +%postrun-command: grep "^vector " results/*.vec | cut -d ' ' -f 3 | sort -u > modules.out +%postrun-command: grep -E "\.branch\[" modules.out > branchmodules.out || true +%#-------------------------------------------------------------------------------------------------------------- +%contains: branchmodules.out +TestDynamicClassifier.demultiplexer.branch[0].first +TestDynamicClassifier.demultiplexer.branch[1].first +%#-------------------------------------------------------------------------------------------------------------- diff --git a/tests/queueing/DynamicClassifier_2.test b/tests/queueing/DynamicClassifier_2.test new file mode 100644 index 00000000000..13330c4e85e --- /dev/null +++ b/tests/queueing/DynamicClassifier_2.test @@ -0,0 +1,113 @@ +%description: + +In this test, packets are produced periodically by an active packet source (ActivePacketSource) +and are classified by a dynamic classifier (DynamicClassifier) into a branch that holds a queue +of a limited capacity, drained slowly by a packet server (PacketServer). + +The test covers the back-pressure of an already created branch. An active source asks only +whether some packet can be pushed, and takes the answer as the licence to produce one and push +it, so a classifier that answers yes while the branch of the packet is full pushes into a full +queue, and the queue fails the run. The producer must stop instead. + +It stays stopped for the rest of the run, and this is not what the classifier decides: a queue +notifies its producer that it has room again only at initialization, so a source in front of a +queue that once filled up never restarts, with a dynamic classifier or without one. + +%file: test.ned + +import inet.queueing.classifier.DynamicClassifier; +import inet.queueing.common.PacketMultiplexer; +import inet.queueing.queue.PacketQueue; +import inet.queueing.server.PacketServer; +import inet.queueing.sink.PassivePacketSink; +import inet.queueing.source.ActivePacketSource; + +module TestSlowBranch +{ + gates: + input in; + output out; + submodules: + queue: PacketQueue { + packetCapacity = 2; + @display("p=100,100"); + } + server: PacketServer { + processingTime = 10s; + @display("p=200,100"); + } + connections: + in --> queue.in; + queue.out --> server.in; + server.out --> out; +} + +network TestDynamicClassifierBackPressure +{ + submodules: + producer: ActivePacketSource { + packetLength = 1B; + productionInterval = 1s; + @display("p=100,100"); + } + classifier: DynamicClassifier { + moduleType = "TestSlowBranch"; + submoduleName = "branch"; + @display("p=200,100"); + } + branch[0]: TestSlowBranch { // grown on demand, one branch per class + @display("p=300,100,column,80"); + } + multiplexer: PacketMultiplexer { + @display("p=400,100"); + } + consumer: PassivePacketSink { + @display("p=500,100"); + } + connections allowunconnected: + producer.out --> classifier.in; + multiplexer.out --> consumer.in; +} + +%file: Test.cc +#include "inet/common/packet/Packet.h" +#include "inet/queueing/function/PacketClassifierFunction.h" + +using namespace inet; + +static int testClassifyToOneClass(Packet *packet) +{ + return 0; +} + +Register_Packet_Classifier_Function(TestOneClassClassifier, testClassifyToOneClass); + +%inifile: omnetpp.ini + +[General] +network = TestDynamicClassifierBackPressure +sim-time-limit = 30s +cmdenv-event-banners = false +cmdenv-log-prefix = "At %ts %N: " +*.classifier.classifierClass = "TestOneClassClassifier" + +%# remove formatting +%subst: /\x1B\[[0-9;]*m// +%# remove method call lines added in OMNeT++ 6.4 +%subst: /^At \S+ \S+: Method call [^\n]*\n//m +%#-------------------------------------------------------------------------------------------------------------- +%# three packets fill the branch: one in the server, two in the queue +%contains-regex: stdout +At 0s producer: Producing packet, .*?producer-0.*? +At 1s producer: Producing packet, .*?producer-1.*? +At 2s producer: Producing packet, .*?producer-2.*? +At 10s server: Processing packet ended, .*?producer-0.*? +%#-------------------------------------------------------------------------------------------------------------- +%# the load-bearing assertion: the fourth packet is never produced, because the classifier +%# answers that the full branch cannot take one -- without it the queue fails the run instead +%not-contains: stdout +producer-3 +%#-------------------------------------------------------------------------------------------------------------- +%# and the run ends normally rather than on the overloaded queue +%contains: stdout + Simulation time limit reached diff --git a/tests/queueing/DynamicClassifier_3.test b/tests/queueing/DynamicClassifier_3.test new file mode 100644 index 00000000000..1f679290ce4 --- /dev/null +++ b/tests/queueing/DynamicClassifier_3.test @@ -0,0 +1,77 @@ +%description: + +In this test, the aggregator that the dynamic classifier (DynamicClassifier) wires its branches +into is not the submodule named "multiplexer" that the classifier defaults to, but one named +"remultiplexer", selected with the aggregatorSubmoduleName parameter. The packets of both +classes must reach the sink through it. + +%file: test.ned + +import inet.queueing.classifier.DynamicClassifier; +import inet.queueing.common.PacketMultiplexer; +import inet.queueing.common.PacketDelayer; +import inet.queueing.sink.PassivePacketSink; +import inet.queueing.source.ActivePacketSource; + +network TestNamedAggregator +{ + submodules: + producer: ActivePacketSource { + packetLength = 1B; + productionInterval = 1s; + @display("p=100,100"); + } + classifier: DynamicClassifier { + moduleType = "inet.queueing.common.PacketDelayer"; + submoduleName = "branch"; + aggregatorSubmoduleName = "remultiplexer"; + @display("p=200,100"); + } + branch[0]: PacketDelayer { // grown on demand, one branch per class + @display("p=300,100,column,80"); + } + remultiplexer: PacketMultiplexer { + @display("p=400,100"); + } + consumer: PassivePacketSink { + @display("p=500,100"); + } + connections allowunconnected: + producer.out --> classifier.in; + remultiplexer.out --> consumer.in; +} + +%file: Test.cc +#include "inet/common/packet/Packet.h" +#include "inet/queueing/function/PacketClassifierFunction.h" + +using namespace inet; + +static int testClassify(Packet *packet) +{ + return packet->getId() % 2; +} + +Register_Packet_Classifier_Function(TestClassifier, testClassify); + +%inifile: omnetpp.ini + +[General] +network = TestNamedAggregator +sim-time-limit = 4s +cmdenv-event-banners = false +cmdenv-log-prefix = "At %ts %N: " +*.classifier.classifierClass = "TestClassifier" +*.branch[*].delay = 1s + +%# remove formatting +%subst: /\x1B\[[0-9;]*m// +%# remove method call lines added in OMNeT++ 6.4 +%subst: /^At \S+ \S+: Method call [^\n]*\n//m +%#-------------------------------------------------------------------------------------------------------------- +%# both branches are wired into the aggregator selected by name, so both classes reach the sink +%contains-regex: stdout +At 0s producer: Producing packet, .*?producer-0.*? +At 1s producer: Producing packet, .*?producer-1.*? +At 1s consumer: Consuming packet, .*?producer-0.*? +At 2s consumer: Consuming packet, .*?producer-1.*? From 55d4626d62c2fe40ca4a50d5a32d1af472a9f29b Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Tue, 1 Sep 2026 18:05:09 +0200 Subject: [PATCH 8/8] doc: note the dynamic classifier branches in WHATSNEW --- WHATSNEW | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/WHATSNEW b/WHATSNEW index 2acaff561e1..5b0e15ff0d6 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -56,6 +56,22 @@ Notable backward compatible changes are the following: owns a MAC address, by the host's path relative to the network, and falls back to the MAC address string when no host owns the address. +4. Dynamic packet classifier branches + + DynamicClassifier creates the branch of each traffic class the first time a + packet of that class arrives. It creates it on the delivery path now, instead + of as a side effect of classifying a packet, which also happens whenever the + classifier is merely asked whether it can take one. + + It also gained an aggregatorSubmoduleName parameter that names the submodule + its branches are wired into, which was previously a submodule named + "multiplexer"; the aggregator may now be a pull scheduler as well as a push + multiplexer. The inherited reverseOrder parameter is refused, because the + classifier no longer maps class indices through the output gate order. + + Existing simulation models are unaffected: the defaults reproduce the previous + behavior. + INET-4.7 (July 2026) — feature release --------------------------------------