From afec8adce82e1c27a3cf496006d68618d580b1f8 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::classifyPacket 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 dc7af743f0e9df9b59ed16190d9e610c3f591e47 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 7263f0b49772728ef6155e74e8790b7583372044 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 | 18 +++++++++++++-- .../queueing/classifier/DynamicClassifier.ned | 20 +++++++++++++++-- 3 files changed, 50 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..3151479fa7d 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.h +++ b/src/inet/queueing/classifier/DynamicClassifier.h @@ -15,11 +15,25 @@ namespace queueing { using namespace inet::queueing; +/** + * A packet classifier that creates the branch for each traffic class on demand, the first + * time a packet of that class is seen. Each branch is one element of a submodule vector + * (submoduleName) of a configurable type (moduleType), wired between this classifier's output + * and a downstream aggregator submodule (aggregatorSubmoduleName). + * + * The aggregator may be either a push ~PacketMultiplexer (the traditional use) or a pull + * scheduler. An aggregator that needs to take notice of an input appearing at runtime picks + * it up from the POST_MODEL_CHANGE notification of the connection itself (see + * cPostPathCreateNotification), so no extra contract is needed between the two. This lets the + * same classifier build both push demux/remux chains and pull per-class queue/scheduler + * structures. + */ 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..7cbf88dd536 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.ned +++ b/src/inet/queueing/classifier/DynamicClassifier.ned @@ -7,10 +7,26 @@ package inet.queueing.classifier; +// +// A packet classifier that creates the branch for 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) + string submoduleName; // the submodule vector that holds the branches + string aggregatorSubmoduleName = default("multiplexer"); // downstream aggregator submodule to wire branches into @class(DynamicClassifier); } From 1b0ed6121c99cc91c6a0700f586cb188b71aac0e 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 fully 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: createModuleBranch() 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 defers its initialization; createBranch() connects the chain up to and including the aggregator, then takes the references and runs the deferred initializations. No change in behavior for the existing simple-branch users, where the old order happened to be safe. --- .../queueing/classifier/DynamicClassifier.cc | 47 +++++++++++++------ .../queueing/classifier/DynamicClassifier.h | 3 ++ 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/inet/queueing/classifier/DynamicClassifier.cc b/src/inet/queueing/classifier/DynamicClassifier.cc index aae4142374e..11c7993dec3 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.cc +++ b/src/inet/queueing/classifier/DynamicClassifier.cc @@ -42,23 +42,25 @@ 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); + // build the branch and collect the modules whose initialization is deferred until the + // whole chain (including the aggregator connection) is wired + std::vector modulesToInitialize; + cGate *branchOutputGate = createModuleBranch(index, classifierOutputGate, modulesToInitialize); // 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); + branchOutputGate->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 +68,26 @@ int DynamicClassifier::createBranch() ActivePacketSinkRef collector; collector.reference(classifierOutputGate, false); collectors.push_back(collector); - moduleOutputGate->connectTo(aggregatorInputGate); + for (auto module : modulesToInitialize) + module->callInitialize(); + return index; +} + +cGate *DynamicClassifier::createModuleBranch(int index, cGate *classifierOutputGate, std::vector& modulesToInitialize) +{ + 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; + modulesToInitialize.push_back(module); + return module->gate("out"); } } // namespace queueing diff --git a/src/inet/queueing/classifier/DynamicClassifier.h b/src/inet/queueing/classifier/DynamicClassifier.h index 3151479fa7d..78b036e28fc 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.h +++ b/src/inet/queueing/classifier/DynamicClassifier.h @@ -8,6 +8,8 @@ #ifndef __INET_DYNAMICCLASSIFIER_H #define __INET_DYNAMICCLASSIFIER_H +#include + #include "inet/queueing/classifier/PacketClassifier.h" namespace inet { @@ -41,6 +43,7 @@ class INET_API DynamicClassifier : public PacketClassifier virtual int classifyPacket(Packet *packet) override; virtual int createBranch(); + virtual cGate *createModuleBranch(int index, cGate *classifierOutputGate, std::vector& modulesToInitialize); }; } // namespace queueing From 060cdbf3d69553ef3c896b1c00515d84539c4f04 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 class map on shifting 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(). --- src/inet/queueing/classifier/DynamicClassifier.cc | 12 +++++++++++- src/inet/queueing/classifier/DynamicClassifier.h | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/inet/queueing/classifier/DynamicClassifier.cc b/src/inet/queueing/classifier/DynamicClassifier.cc index 11c7993dec3..5fd39e43a1f 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.cc +++ b/src/inet/queueing/classifier/DynamicClassifier.cc @@ -29,9 +29,19 @@ 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. + return packetClassifierFunction->classifyPacket(packet); +} + int DynamicClassifier::classifyPacket(Packet *packet) { - int index = PacketClassifier::classifyPacket(packet); + int index = getClassIndex(packet); auto it = classIndexToGateItMap.find(index); if (it != classIndexToGateItMap.end()) return it->second; diff --git a/src/inet/queueing/classifier/DynamicClassifier.h b/src/inet/queueing/classifier/DynamicClassifier.h index 78b036e28fc..039a7d88ffa 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.h +++ b/src/inet/queueing/classifier/DynamicClassifier.h @@ -40,6 +40,7 @@ class INET_API DynamicClassifier : public PacketClassifier protected: virtual void initialize(int stage) override; + virtual int getClassIndex(Packet *packet) const; virtual int classifyPacket(Packet *packet) override; virtual int createBranch(); From ad7b15b0a9b2892dd93179e701736515051c4215 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Wed, 12 Aug 2026 17:16:36 +0200 Subject: [PATCH 6/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 instead: 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. canPushPacket() is worse than useless in its inherited form here: it classifies the packet, and for this classifier classifying creates the branch of a new class, so a query that is supposed to be a query builds submodules, grows gate vectors, wires connections and initializes the new modules. Look the class up instead, and only delegate to the branch that already exists. (The pull side classifies in canPullPacket() too, and is left alone: there the query is what drives branch creation, and this classifier has no pull user.) The module test covers this -- the producer is connected to the classifier directly, so without the fix it never produces and no branch is built -- along with the rest of the contract: two branches built on demand, an ini file assignment addressing a submodule of a branch taking effect, and the statistics of the branch submodules being recorded under the branch path. --- .../queueing/classifier/DynamicClassifier.cc | 19 +++ .../queueing/classifier/DynamicClassifier.h | 4 + tests/queueing/DynamicClassifier_1.test | 130 ++++++++++++++++++ 3 files changed, 153 insertions(+) create mode 100644 tests/queueing/DynamicClassifier_1.test diff --git a/src/inet/queueing/classifier/DynamicClassifier.cc b/src/inet/queueing/classifier/DynamicClassifier.cc index 5fd39e43a1f..c005b7296d9 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.cc +++ b/src/inet/queueing/classifier/DynamicClassifier.cc @@ -50,6 +50,25 @@ int DynamicClassifier::classifyPacket(Packet *packet) return branchIndex; } +bool DynamicClassifier::canPushSomePacket(const cGate *gate) const +{ + // Not the inherited "one of the existing branches can take a packet": a packet of a class + // that has not been seen yet is taken by the branch created for it, and there may always be + // such a class, the range of the classifier function not being known here. Without this, a + // classifier that has no branch yet answers that it cannot accept anything, and an active + // source in front of it stops before the first branch is ever created. Whether a particular + // packet can be pushed is answered by canPushPacket() below. + return true; +} + +bool DynamicClassifier::canPushPacket(Packet *packet, const cGate *gate) const +{ + // deliberately not the inherited implementation: that one classifies the packet, which + // creates the branch of a new class as a side effect of what is supposed to be a query + auto it = classIndexToGateItMap.find(getClassIndex(packet)); + return it == classIndexToGateItMap.end() || consumers[it->second].canPushPacket(packet); +} + int DynamicClassifier::createBranch() { cModule *parent = getParentModule(); diff --git a/src/inet/queueing/classifier/DynamicClassifier.h b/src/inet/queueing/classifier/DynamicClassifier.h index 039a7d88ffa..79c6bc0d448 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.h +++ b/src/inet/queueing/classifier/DynamicClassifier.h @@ -45,6 +45,10 @@ class INET_API DynamicClassifier : public PacketClassifier virtual int createBranch(); virtual cGate *createModuleBranch(int index, cGate *classifierOutputGate, std::vector& modulesToInitialize); + + public: + virtual bool canPushSomePacket(const cGate *gate) const override; + virtual bool canPushPacket(Packet *packet, const cGate *gate) const override; }; } // namespace queueing 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 +%#-------------------------------------------------------------------------------------------------------------- From d427993de88b69139454b918d4919bafbc377f3a Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Thu, 13 Aug 2026 12:01:55 +0200 Subject: [PATCH 7/8] queueing: let a classifier grow an output gate for a packet, outside classification classifyPacket() is a query: the capacity checks (canPushPacket(), canPullPacket()) classify the very packet whose delivery classifies it again, and the pull path classifies it on every peek, so classification must be free of side effects. The contract is now spelled out, together with the convention -- already followed by PriorityClassifier, WrrClassifier and the diffserv classifiers -- that -1 means "no existing output gate suits this packet" rather than an out-of-range error. What happens to such a packet is decided on the delivery path only: pushPacket() and startPacketStreaming() ask the new createGateForPacket() hook, whose default refuses the packet just as the range check did, and canPushPacket() asks its side-effect-free query pair, canCreateGateForPacket(). A classifier that extends itself on demand creates its new output gate in the hook, never from a query. The pull-side callers pass -1 through unchanged: canPullPacket() already answers "not for this gate", the pullPacket() family already refuses, and handleCanPullPacketChanged() now skips the notification instead of indexing collectors[-1]. The stateful classifyPacket() implementations that remain (WrrClassifier, TokenBucketClassifier, MultiTokenBucketClassifier) are out of scope here; they are why callClassifyPacket() keeps its const_cast kludge. --- .../queueing/base/PacketClassifierBase.cc | 20 +++++++++++++++- src/inet/queueing/base/PacketClassifierBase.h | 24 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/inet/queueing/base/PacketClassifierBase.cc b/src/inet/queueing/base/PacketClassifierBase.cc index c221362e2c5..39a64b584f8 100644 --- a/src/inet/queueing/base/PacketClassifierBase.cc +++ b/src/inet/queueing/base/PacketClassifierBase.cc @@ -60,11 +60,21 @@ int PacketClassifierBase::callClassifyPacket(Packet *packet) const { // KLUDGE int index = const_cast(this)->classifyPacket(packet); - if (index < 0 || static_cast(index) >= outputGates.size()) + if (index < -1 || index >= (int)outputGates.size()) throw cRuntimeError("Packet is classified to invalid output gate: %d", index); return index; } +int PacketClassifierBase::createGateForPacket(Packet *packet) +{ + throw cRuntimeError("Packet cannot be classified to any output gate"); +} + +bool PacketClassifierBase::canCreateGateForPacket(Packet *packet) const +{ + return false; +} + void PacketClassifierBase::checkPacketStreaming(Packet *packet) { if (inProgressStreamId != -1 && (packet == nullptr || packet->getTreeId() != inProgressStreamId)) @@ -76,6 +86,8 @@ void PacketClassifierBase::startPacketStreaming(Packet *packet) EV_INFO << "Classifying packet" << EV_FIELD(packet) << EV_ENDL; inProgressStreamId = packet->getTreeId(); inProgressGateIndex = callClassifyPacket(packet); + if (inProgressGateIndex == -1) + inProgressGateIndex = createGateForPacket(packet); } void PacketClassifierBase::endPacketStreaming(Packet *packet) @@ -97,6 +109,8 @@ bool PacketClassifierBase::canPushSomePacket(const cGate *gate) const bool PacketClassifierBase::canPushPacket(Packet *packet, const cGate *gate) const { int index = callClassifyPacket(packet); + if (index == -1) + return canCreateGateForPacket(packet); return consumers[index].canPushPacket(packet); } @@ -107,6 +121,8 @@ void PacketClassifierBase::pushPacket(Packet *packet, const cGate *gate) checkPacketStreaming(nullptr); EV_INFO << "Classifying packet" << EV_FIELD(packet) << EV_ENDL; int index = callClassifyPacket(packet); + if (index == -1) + index = createGateForPacket(packet); handlePacketProcessed(packet); emit(packetPushedSignal, packet); pushOrSendPacket(packet, outputGates[index], consumers[index]); @@ -219,6 +235,8 @@ void PacketClassifierBase::handleCanPullPacketChanged(const cGate *gate) auto packet = provider.canPullPacket(); if (packet != nullptr) { int index = callClassifyPacket(packet); + if (index == -1) + return; // the packet routes to no existing gate, so there is no collector to notify auto collector = collectors[index]; if (collector != nullptr) collector.handleCanPullPacketChanged(); diff --git a/src/inet/queueing/base/PacketClassifierBase.h b/src/inet/queueing/base/PacketClassifierBase.h index 7556ec6b89e..1732771f38a 100644 --- a/src/inet/queueing/base/PacketClassifierBase.h +++ b/src/inet/queueing/base/PacketClassifierBase.h @@ -43,9 +43,33 @@ class INET_API PacketClassifierBase : public PacketProcessorBase, public Transpa virtual void mapRegistrationForwardingGates(cGate *gate, std::function f) override; virtual size_t getOutputGateIndex(size_t i) const { return reverseOrder ? outputGates.size() - i - 1 : i; } + + /** + * Returns the index of the output gate the packet is classified to, or -1 + * if no existing output gate suits the packet. Classification is a query + * and must be free of side effects: the capacity checks (canPushPacket(), + * canPullPacket()) classify the same packet as its eventual delivery, and + * the pull path classifies it more than once. + */ virtual int classifyPacket(Packet *packet) = 0; virtual int callClassifyPacket(Packet *packet) const; + /** + * Called when a packet being pushed is classified to no existing output + * gate. This is where side effects of taking such a packet belong: a + * classifier that extends itself on demand creates the new output gate + * here and returns its index. Called from packet delivery only, never + * from a query. The default refuses the packet with an error. + */ + virtual int createGateForPacket(Packet *packet); + + /** + * Returns true if createGateForPacket() would provide an output gate for + * the packet: the query pair of createGateForPacket(), consulted by + * canPushPacket() when classifyPacket() finds no gate. + */ + virtual bool canCreateGateForPacket(Packet *packet) const; + virtual bool isStreamingPacket() const { return inProgressStreamId != -1; } virtual void startPacketStreaming(Packet *packet); virtual void endPacketStreaming(Packet *packet); From 4a91aeb1f360f5418fa6346428df058819ebd8b5 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Thu, 13 Aug 2026 12:02:40 +0200 Subject: [PATCH 8/8] queueing: make DynamicClassifier classification free of side effects classifyPacket() now only answers where a packet of an already-seen class goes, and -1 for a class that has no branch yet; the branch is created in createGateForPacket(), which the base class calls on packet delivery only. The canPushPacket() override disappears: the inherited implementation, with canCreateGateForPacket() answering that a branch can be created for every class, gives the same answers -- an unseen class is always welcome, a seen one asks its branch. --- .../queueing/classifier/DynamicClassifier.cc | 40 ++++++++++--------- .../queueing/classifier/DynamicClassifier.h | 3 +- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/inet/queueing/classifier/DynamicClassifier.cc b/src/inet/queueing/classifier/DynamicClassifier.cc index c005b7296d9..644f0416edf 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.cc +++ b/src/inet/queueing/classifier/DynamicClassifier.cc @@ -31,25 +31,34 @@ 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 = classIndexToGateItMap.find(index); - if (it != classIndexToGateItMap.end()) - return it->second; + // a class seen for the first time has no gate yet; its branch is created by + // createGateForPacket(), which the base class calls on packet delivery only + auto it = classIndexToGateItMap.find(getClassIndex(packet)); + return it != classIndexToGateItMap.end() ? it->second : -1; +} + +int DynamicClassifier::createGateForPacket(Packet *packet) +{ int branchIndex = createBranch(); - classIndexToGateItMap[index] = branchIndex; + classIndexToGateItMap[getClassIndex(packet)] = branchIndex; return branchIndex; } +bool DynamicClassifier::canCreateGateForPacket(Packet *packet) const +{ + // a branch can be created for every class, so every packet gets an output gate + return true; +} + bool DynamicClassifier::canPushSomePacket(const cGate *gate) const { // Not the inherited "one of the existing branches can take a packet": a packet of a class @@ -57,18 +66,11 @@ bool DynamicClassifier::canPushSomePacket(const cGate *gate) const // such a class, the range of the classifier function not being known here. Without this, a // classifier that has no branch yet answers that it cannot accept anything, and an active // source in front of it stops before the first branch is ever created. Whether a particular - // packet can be pushed is answered by canPushPacket() below. + // packet can be pushed is answered by the inherited canPushPacket(), through + // canCreateGateForPacket() above. return true; } -bool DynamicClassifier::canPushPacket(Packet *packet, const cGate *gate) const -{ - // deliberately not the inherited implementation: that one classifies the packet, which - // creates the branch of a new class as a side effect of what is supposed to be a query - auto it = classIndexToGateItMap.find(getClassIndex(packet)); - return it == classIndexToGateItMap.end() || consumers[it->second].canPushPacket(packet); -} - int DynamicClassifier::createBranch() { cModule *parent = getParentModule(); diff --git a/src/inet/queueing/classifier/DynamicClassifier.h b/src/inet/queueing/classifier/DynamicClassifier.h index 79c6bc0d448..66dc3f59998 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.h +++ b/src/inet/queueing/classifier/DynamicClassifier.h @@ -42,13 +42,14 @@ 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 int createGateForPacket(Packet *packet) override; + virtual bool canCreateGateForPacket(Packet *packet) const override; virtual int createBranch(); virtual cGate *createModuleBranch(int index, cGate *classifierOutputGate, std::vector& modulesToInitialize); public: virtual bool canPushSomePacket(const cGate *gate) const override; - virtual bool canPushPacket(Packet *packet, const cGate *gate) const override; }; } // namespace queueing