Subscriptions

Revision History

Version

Date

Author

Purpose

1

July 2015

Gary Turner

Initial version

2

March 2022

Gary Turner

Added verification and improved user's guide

3

December 2022

Gary Turner

Toggle for silently failing when unsubscribing a disabled model

4

September 2025

Hirad Mirhashemi

Updated verification and user's guide

5

August 2026

Nino Tarantino

Converted to reStructuredText and updated coverage


Introduction

The Subscriptions Model is used as a base for many models; its primary purpose is to facilitate the activation and deactivation of models as they are needed at different phases of a complex simulation.

Consider a simulation that includes two models, A and B, with model A requiring input from model B. For some part of the simulation, model A is required, so its execution must be scheduled in the master simulation sequence. Whenever model A runs, model B must also run, so its execution must be scheduled in the master simulation sequence. But running both models all of the time is highly wasteful, and will slow the simulation unnecessarily. So, for the times that the models are not needed, their scheduled execution should simple return without additional process. Thus, we have the concept of models being active and inactive, and of subscriptions that toggle between the two states.

At some point in the simulation, the simulation subscribes to model A, flagging it as needing to run. In turn, model A subscribes to model B, flagging it as needing to run. As long as a model is subscribed, its scheduled execution will run. If it is unsubscribed, its scheduled execution will return without process. Then, when model A is no longer needed, the simulation can unsubscribe from it, and model A can unsubscribe from model B, potentially deactivating both models and preventing their further execution.

Here, I say potentially deactivating both models because it may not be appropriate to stop them. Suppose a third model, C, also requires input from model B and has been subscribed in the interim time. Stopping model B with the deactivation of model A will break model C. So we need a count of the number of targets depending on a model being executed. If that count is zero, the model does not need to run.

This model handles that subscription count for all models that inherit it.


Requirements

  1. The model shall provide an active flag to inheriting models to identify whether those model should be run.

  2. This model shall control the active flag via a count of the number of demands placed on the inheriting models.

  3. This model shall provide the option to permanently deactivate a model, preventing further demands from re-activating it.


Model Specifications

Architectural Considerations

Existing External Capabilities

Support

No dependencies.

Model Structure

class SubscriptionBase
#include "models/utilities/subscriptions/include/subscriptions.hh"

Provides a subscription baseline for all models

Models can subscribe to anything that inherits from this class. When the first subscription is processed, the model is activated. When subsequent subscriptions are processed, nothing happens. When the last subscription is canceled, the model deactivates.

Subclassed by AbstractTableLookup, AccumulatedAbsoluteDeltas, AeroExecutiveBase, AeroInterfaceBase, ApsidesPredictor, AtmosRelativeState, AtmosphereExecInterface, ConstraintSet, ContactStateOverride, DynamicMassGroup, EGM2008_WGS84, EclipseCalculator, ExtendedPlanetaryDerivedState, GravityFidelityManager, ImpactPoint, LookupAtmosWinds, MassDerivativeDynamics, PistonThruster, PistonThrusterGroup, PlanetPlanetState, PointingRefFrame, RangeComputation, RcsGeneric, RocketMotor_Basic, SeparationState, SimSpeedMonitor, SimpleLookupWind, SimplePlanetRelState, SubsonicWake, TwistSway, Vector3IntegrableObject, VentSet, WakeEffectsBase, WatchValuesBaseCore

Public Functions

SubscriptionBase() = default

Default constructor

SubscriptionBase(const SubscriptionBase&) = delete

Copy constructor deleted

SubscriptionBase &operator=(const SubscriptionBase&) = delete

Copy assignment operator deleted

virtual ~SubscriptionBase() = default

Virtual destructor

virtual void subscribe()

Instructs this model to turn itself on if everything is configured correctly

This may be called before initialization, in which case it is unlikely that everything is configured correctly. In that case, the sub_pending flag gets set so that when initialize gets called, the subscription can be completed.

virtual void unsubscribe()

Tells this model that whoever had previously subscribed to it no longer needs it

If this model had only one subscription, the last customer just left the building and this model can turn itself off.

Note

Takes no action if the model is already disabled.

virtual void initialize()

Configure pending subscriptions

If this class is inherited from, calling this method should be the last step in a child class' initialize() method.

Warning

Calling this function will internally mark the object as initialized. If your child class has input parameters which need to be checked for correctness before marking itself as initialized, make sure to do so before calling this function.

virtual void disable()

Completely disables the model

Sets active to false, preventing the execution of the model, regardless of the current number of active or pending subscriptions.

To re-enable the model requires setting the enabled flag and re-subscribing.

inline bool is_initialized() const
Returns:

true if the model has successfully been initialized

inline bool is_active() const
Returns:

true if the model is active

inline bool is_enabled() const
Returns:

true if the model is enabled

Public Members

std::string subscribe_name = {"unnamed-instance"}

Optional setting, useful for messages and debugging only. Not used for other purposes.

Units:

--

bool initialize_on_failed_activation = {false}

Configuration flag controls the response if the model fails to activate during an initialization operation with subscriptions already pending. If false, retain subscriptions but fail initialization.

Units:

--

bool quiet_unsubscribe_warning = {false}

Optional flag to control whether a warning is printed when attempting to unsubscribe from a model with no active or pending subscriptions.

Units:

--

bool quiet_disabled_warning = {false}

Optional flag to quiet the error meesage that would be posted when subscribing to a disabled model.

Units:

--

Protected Functions

void subscribe_internal()

Internal subscription logic

inline virtual void activate()

Enable execution of the model

Note

Derived classes will typically override this to include a call to the specific update() implementation and potentially subscribe to lower- level components within the class.

inline virtual void deactivate()

Disable execution of the model

Note

Derived classes should remember to unsubscribe from any subscriptions made during activate().

Protected Attributes

bool enabled = {true}

Master flag. Use as a gate on initialize() if desired. If false, will prevent subscription requests.

Units:

--

bool initialized = {false}

The model is ready to be activated

Units:

--

bool active = {false}

The model can be executed. The first line of code in the model inheriting from this should be

if (!active) {
    return;
}

Units:

--

int sub_pending = {0}

Internal subscription count. Incremented if subscriptions are received prior to initialization.

Units:

--

int num_subscriptions = {0}

Number of subscriptions, post-initialization.

Units:

--

Mathematical Formulation

No mathematical formulation.


User's Guide

This model is not intended to be used as a stand-alone model, but rather as the base for other models that need to be activated or deactivated during a complex simulation.

Notes on Disabling a Model

The design decision as to how disabling the model affects the subscription-count and pending-subscription-count is complicated. There are two possible results for each count - being that they are left untouched, or set to zero. Identifying the best default behavior requires consideration of how to process initialize(), YourModel::update(), subscribe() and unsubscribe() calls while the model is disabled.

It is generally desirable that a disabled model cannot be initialized. Calls to initialize() while disabled should be blocked. This is an important features that supports blocking of expensive and time-consuming initialization routines in models inheriting this capability when said models are not necessary for a particular scenario.

Similarly, it is a required feature that calls to YourModel::update() should not be processed while a model is inactive; this is the main purpose of the model. The activity flag can be set by a call to subscribe() if the model is initialized. Therefore, to support cases where a model is disabled post-initialization, calls to subscribe() must not set the active flag when the model is disabled, thereby preventing a disabled model from executing. Any calls to subscribe() while the model is disabled should be interpreted as user-error and therefore flagged with a message; a model should not be simultaneously required (as suggested by the subscribe() call) and not available (as suggested by being disabled).

Because of the inherent design associated with blocking YourModel::update() calls for inactive models (previous paragraph), calls to subscribe() while a model is disabled are immediately flagged as errors and no further action is taken. This decision has implications on how to handle the subscription-count and pending-subscription-count, discussed below.

By symmetry, calls to unsubscribe() while disabled should also result in no further action being taken. This is less significant than calls to subscribe() because the model is not executing anyway while it is disabled; failing to turn it off while it is already off has no executable consequence. However, when an unsubscribe() call is made, the conceptual design calls for the subscriptions-count (or pending-subscriptions-count) to be decremented. This count should never go below zero because unsubscribe() calls should always follow subscribe() calls. Internal sanity checking requires that an error message be posted if the an unsubscribe() is received while the respective count is equal to zero because that implies a misconfiguration. This is where the logical paths start to conflict. If commanding unsubscribe() while disabled results in no operation, then subscriptions posted before the model was disabled could still be included in the count. Conversely, if commanding unsubscribe() while disabled results in decrementing the counts, then subscriptions posted after the model was disabled (or circumvented in sim-configuration in anticipation of the model being disabled) would result in a confusing error message when attempting to decrement the respective counts from zero. The latter option is the more objectionable outcome, so the design decision follows the path of symmetry, that calls to unsubscribe() are not processed (exiting silently) while the model is disabled.

However, this raises a problem of its own in the unlikely scenario that a model is only temporarily disabled - a possibility that is not supported in this core capability, but feasibly implementable in a derived class. If a model is re-enabled, its count of subscriptions is unreliable given that any subscribe() commands and unsubscribe() commands have exited without affecting the count while the model was disabled. This makes the value of the counts at this point largely arbitrary. The two most obvious values would be zero, or the value the model had at the time it was previously disabled. Potential use-cases have been evaluated with neither resolution being universally desirable. So we have a wholly arbitrary decision to make for a situation that is not even supported by this model. The decision was made to leave the counts untouched at the time the model is disabled to better support debugging. This decision could be revisited at some time in the future.

Control Flags

The model provides three optional control flags:

  • initialize_on_failed_activation controls the consequences when the model is initialized with pending subscriptions and the consequential activation fails, while the initialization would otherwise have been successful without pending subscriptions. Note:

    • Initializing with pending subscriptions will automatically lead to an attempt to activate the model. This is a deliberate design decision to make the activation of the model independent of the whether the subscription comes before or after the initialization of the model; both processes are necessary and sufficient for activation to be attempted.

    • Note that while failing activation is not a plausible scenario in this base model (where the activate() method simply sets the active flag to true), it is a very real possibility in a derived model where the activate() method might check availability or compatibility of some data set, or check for null pointers, or other pre-executive verification activities.

    • Initialization and activation are independent processes with independent objectives. The situation of interest here is the one in which the model successfully initializes, but the activation step fails.

    A model that is both initialized and subscribed should be activated. If the activation fails, then one of the two pillars must also be failed. This flag identifies which to fail:

    Specific error messages are produced to alert the user to the outcome of the failed activation.

  • quiet_unsubscribe_warning provides a means of suppressing warnings that would typically be generated when attempting to unsubscribe from a model that has no subscriptions.

    • The underlying design assumption has unsubscribe() only being called from the same unit that previously generated the subscribe() call, so these calls should always occur in pairs and the situation in which unsubscribe() is called without a previous subscribe() should not arise. If an unsubscribed model receives instruction to remove a subscription, this is usually indicative of a problem in the model's architecture and a warning message is generated.

    • This pairing of calls is especially important that when a model can be subscribed from multiple locations. Having one model remove another's subscription fundamentally breaks the purpose of the model.

    • However, when a model has only one dedicated subscriber, this architecture can be onerous on the object making the subscribe()/unsubscribe() calls. There are going to be cases in which a subscription is conditional upon some configuration setting, and for which that setting is no longer testable at the point of decision over whether to call unsubscribe(). For these situations, it would be necessary to do one of:

      • Add an internal flag, effectively confirming that the sub-model has been subscribed and that it can therefore be unsubscribed. This is necessary when the sub-model may be subscribed from multiple locations, but unnecessarily onerous for maintaining the activity of a dedicated sub-model.

      • Check the active status of the sub-model to identify whether it has been subscribed. This is fundamentally risky and should never be used when the sub-model may be subscribed from multiple locations (for reasons that should be apparent); for maintaining the activity of a dedicated sub-model it is an acceptable option and less onerous than maintaining an internal flag, but still requires an additional logic step that may be difficult to inject in some situations.

      • Issue unsubscribe() unconditionally and accept (and ignore) the warning message generated when the sub-model was not previously subscribed. Particularly for large projects, having an architecture in which some warning and error messages are “standard operating procedure” can get very difficult to manage, and easily leads to obfuscation of messages that should be addressed. This is bad practice and not a viable solution.

      • Issue unsubscribe() unconditionally and bypass the generation of the warning message. This is fundamentally risky and should never be used when the sub-model may be subscribed from multiple locations (for reasons that should be apparent); for maintaining the activity of a dedicated sub-model it is an acceptable and simple option.

    The quiet_unsubscribe_warning flag supports the last of these options by allowing the higher-level object to unconditionally unsubscribe from the sub-model. If the subscription had previously been applied, unsubscribe() removes it. If it had not been applied (and this flag is set to true), unsubscribe() has no effect.

    Note that the use of this flag is inherently dangerous and should only be used when there is no possibility that some other model may have subscribed to the sub-model in question.

  • quiet_disabled_warning is similar in intent to the quiet_unsubscribe_warning flag; in this case we consider the error message that is posted when a subscription is made to a model that has been disabled. This sequence is a more serious problem then that of unsubscribing from an inactive model:

    • With quiet_unsubscribe_warning the situation is that some entity is communicating that one of the models on which it depends is no longer required, but that dependency was not active anyway. The effect is typically negligible, the model was inactive before, and it probably should be inactive now. A warning is issued to alert the user to a possible misconfiguration that may have affected data upstream, but the downstream effect of not having a model available that isn't needed anyway is not significant. Blocking this warning message is quite reasonable.

    • The quiet_disabled_warning flag is more significant. With this flag, we are removing an error message that may otherwise be used to alert the user to significant downstream data effects. In this situation, some entity may be communicating that it requires support from some other model, but that model is not available to provide that support. The downstream data is likely to be affected by this configuration, and an error message is the appropriate response - all data beyond this point is suspect - and should be issued in this case.

    This flag is primarily included to support a situation in which the supporting model is not truly required, but simply useful or desirable in some circumstances. In this situation, there may be use-cases where the supporting model is desired, and use-cases where it is not. As an example, consider some simulation-event that triggers the activation of some model:

    • that activated model isn't required by the event, but it is desirable that in at least some cases, the model be activated in response to the simulation-event;

    • to support use-cases where the model is not needed, that model may be disabled but that would result in a subscription to a disabled model and consequential error message even though this was the intended pattern.

    Because blocking this message can have serious consequences, this flag has been implemented as a single-use flag. It can be set to true to block the error message from a specific subscribe() call, but it resets to false and any subsequent subscribe() calls will trigger the error message again (unless the flag is set back to true before each subsequent call).

Extension

The following methods may need redefining in the derived class:

Initialize

This new method should perform all initialization steps required of the new model. The final step should be a call to SubscriptionBase::initialize() where the initialized flag gets set.

Disable

The base implementation deactivates and disables the model, leaving the subscription counts untouched. This should be sufficient for most applications.

Activate

It may be desirable to include an automatic call to the model's regular execution (e.g. YourModel::update()) as a part of the activation process. This is not included in the base implementation.

It may also be necessary to subscribe to additional model dependencies.

The base model rejects subscriptions when activation fails in response to a subscribe() call on a pre-initialized instance. In this case, the model will remain inactive and the number of subscriptions will not be incremented. Any unsubscribe() calls that come after the failed activation will generate a warning (unless quiet_unsubscribe_warning is set to true).

Deactivate

If the activate() call resulted in subscription to other models, these should be unsubscribed at deactivate().


Verification

Code Coverage

------------------------------------------------------------------------------
                        GCC Code Coverage Report
Directory: .
------------------------------------------------------------------------------
File                                       Lines     Exec  Cover   Missing
------------------------------------------------------------------------------
models/utilities/subscriptions/include/subscriptions.hh
                                               7        4    57%   86,158,166
models/utilities/subscriptions/src/subscriptions.cc
                                              56       56   100%
------------------------------------------------------------------------------
TOTAL                                         63       60    95%
------------------------------------------------------------------------------

See detailed coverage information here.

Exceptions

The destructor is marked uncovered because the destruction of a dynamically-allocated isntance of SubscriptionBase is not covered in testing. The compiler emits separate branches for non-dynamically allocated and dynamically allocated objects, and only the non-dynamically allocated case is tested.

86  virtual ~SubscriptionBase() = default;

The SubscriptionBase::activate() and SubscriptionBase::deactivate() functions are not tested.

158  virtual void activate(){ active = true;}
166  virtual void deactivate(){active = false;}

Simulation Configurations

SIM_unit_subs

This verification simulation tests the full model by running different sequences of method calls. It ensures that the model correct tracks pending and active subscriptions before and after initialization, supports activation and deactivation based on those subscriptions, and allows the model to be fully disabled. The simulation also verifies that errors are raised appropriately according to the optional control flags.

Unit-Test Cases

RUN_01_multiple_subscriptions

This run executes a sequence of subscribe and unsubscribe calls:

  • t=0: subscribe() (number of subscriptions = 1, model activates)

  • t=1: subscribe() (number of subscriptions = 2)

  • t=2: unsubscribe() (number of subscriptions = 1)

  • t=3: subscribe() (number of subscriptions = 2)

  • t=4: unsubscribe() (number of subscriptions = 1)

  • t=5: unsubscribe() (number of subscriptions = 0, model deactivates)

  • t=6: unsubscribe() (results in a message about unsubscribing from an unsubscribed model)

    Message: Pre-init unsubscribe error.
    Instruction received to unsubscribe the model (unnamed-instance) but there are no
    pending subscriptions.  Check your configuration.
    Cannot process unsubscriptions in anticipation of incoming
    subscriptions.
    Command failed.
    Model remains unsubscribed.
    
  • t=7: unsubscribe() with quiet_unsubscribe_warning flag set (no effect)

Logged Data

time

enabled

active

active subscription count

pending subscription count

0

1

0

0

1

1

1

0

0

2

2

1

0

0

1

3

1

0

0

2

4

1

0

0

1

5

1

0

0

0

6

1

0

0

0

7

1

0

0

0

RUN_02_not_initialized

This run repeats the same sequence as RUN_01_multiple_subscriptions, but without the model being initialized. This run verifies that subscribe() / unsubscribe() calls affect only the pending subscriptions count, and not the actual active status of the model. Throughout this run, the model remains inactive.

  • t=0: subscribe() (number of pending subscriptions = 1)

  • t=1: subscribe() (number of pending subscriptions = 2)

  • t=2: unsubscribe() (number of pending subscriptions = 1)

  • t=3: subscribe() (number of pending subscriptions = 2)

  • t=4: unsubscribe() (number of pending subscriptions = 1)

  • t=5: unsubscribe() (number of pending subscriptions = 0)

  • t=6: unsubscribe() (results in a message about unsubscribing from a model with no pending subscriptions)

    Message: Pre-init unsubscribe error.
    Instruction received to unsubscribe the model (unnamed-instance) but there are no
    pending subscriptions.  Check your configuration.
    Cannot process unsubscriptions in anticipation of incoming
    subscriptions.
    Command failed.
    Model remains unsubscribed.
    
  • t=7: unsubscribe() with quiet_unsubscribe_warning flag set (no effect)

RUN_03_initialize_with_pending

This run tests the ability of the model to transfer pending subscriptions to active subscriptions at model initialization.

  • t=1: subscribe() (being prior to initialization, this increments the pending subscription count to 1)

  • t=2: subscribe() (being prior to initialization, this increments the pending subscription count to 2)

  • t=3: subscribe() (being prior to initialization, this increments the pending subscription count to 3)

  • t=4: initialize() (model activates; 3 pending subscriptions are moved to active subscriptions)

Logged Data

time

enabled

active

active subscription count

pending subscription count

0

1

0

0

0

1

1

0

0

1

2

1

0

0

2

3

1

0

0

3

4

1

1

3

0

RUN_04_subscribe_disable_init

This run tests the sequence of subscription - disable - initialize. With this sequence:

  • t=0:

    • subscribe() (being prior to initialization, this increments the pending subscription count to 1)

    • subscribe() (being prior to initialization, this increments the pending subscription count to 2)

  • t=1: disable() (sets the enabled flag to false)

  • t=2: initialize() (has no effect on a disabled model)

Note

Disabling the model does not affect the pending subscription count. This is a somewhat arbitrary design decision discussed in the User's Guide.

Logged Data

time

enabled

active

active subscription count

pending subscription count

0

1

0

0

2

1

0

0

0

2

2

0

0

0

2

RUN_05_subscribe_init_disable

This run tests the sequence of subscription - initialize - disable. With this sequence:

  • t=0:

    • subscribe() (being prior to initialization, this increments the pending subscription count to 1)

    • subscribe() (being prior to initialization, this increments the pending subscription count to 2)

  • t=1: initialize() (the pending subscriptions are applied, model activates, subscription count = 2)

  • t=2: disable() (sets the enabled flag and active flag to false, leaving the subscriptions count at 2)

Note

Disabling the model while it is active does not result in changing the subscriptions count, but this value is hereafter unreliable. See discussion in the User's Guide.

Logged Data

time

enabled

active

active subscription count

pending subscription count

0

1

0

0

2

1

1

1

2

0

2

0

0

2

0

RUN_06_disabled

This run tests the effect of issuing subscriptions while disabled:

  • t=0:

    • disable()

    • subscribe() (results in an error message, the model cannot be subscribed while disabled)

    Message: Subscription Error
    Model (unnamed-instance) has been disabled for this scenario.
    Cannot subscribe to a disabled model.
    
  • t=1: unsubscribe() (no effect)

  • t=2:

    Message: Subscription Error
    Model (unnamed-instance) has been disabled for this scenario.
    Cannot subscribe to a disabled model.
    
    Checking on quiet_disabled_warning reset:
    Before subscribe:  1
    No error message during subscribe()
    After  subscribe:  0
    
  • t=3: unsubscribe() (no effect)

Logged Data

time

enabled

active

active subscription count

pending subscription count

0

0

0

0

0

1

0

0

0

0

2

0

0

0

0

3

0

0

0

0

RUN_07_activation_fails

This run tests the consequences of failing to activate the model when initializing with pending subscriptions:

  • t=0:

    • subscribe() (being prior to initialization, this increments the pending subscription count to 1)

    • subscribe() (being prior to initialization, this increments the pending subscription count to 2)

    • subscribe() (being prior to initialization, this increments the pending subscription count to 3)

  • t=1: initialize() called with initialize_on_failed_activation at default false (error message, pending subscriptions retained but initialization failed)

    Message: Failure During Initialization.
    The SubscriptionBase initialization for 'activation-failure test case' failed when the model
    attempted to activate during initialization:
     - activation sequence executed due to having pending subscriptions.
    Model has been neither initialized nor activated
    but pending subscriptions have been retained per setting of
    configuration flag initialize_on_failed_activation.
    Rerun <model>.initialize() to apply pending subscriptions and activate the model.
    
  • t=2: initialize() called with initialize_on_failed_activation set to true (error message, model flagged as initialized but pending subscriptions are stripped)

    Message: Failure During Initialization.
    The SubscriptionBase initialization for 'activation-failure test case' failed when the model
    attempted to activate during initialization:
     - activation sequence executed due to having pending subscriptions.
    Model is marked as having been initialized but not activated;
    pending-subscriptions have been removed per setting of
    configuration flag initialize_on_failed_activation.
    Re-subscribe to the model to activate it.
    
  • t=3: subscribe() called (forwards to activate(), which fails again; model remains inactive and rejects the subscription)

Logged Data

time

initialize_on_failed_activation

initialized

active

pending subscription count

active subscription count

0

0

0

0

3

0

1

0

0

0

3

0

2

1

1

0

0

0

3

1

1

0

0

0

RUN_08_getters

This run tests the model's getter methods:

**********************************************************************
enabled: 1 (1)
initialized: 0 (0)
active: 0 (0)
**********************************************************************
Initialize and Subscribe
**********************************************************************
enabled: 1 (1)
initialized: 1 (1)
active: 1 (1)
***********************************************************************