feat(triggers): implement event-based conditional data delivery (#203)#293
Open
feat(triggers): implement event-based conditional data delivery (#203)#293
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds an event-based “Triggers” subsystem to ros2_medkit_gateway, enabling condition-driven notifications (primarily via SSE) when observed resources change, with optional persistence via SQLite and plugin-extensible condition evaluators.
Changes:
- Introduces core trigger infrastructure:
ResourceChangeNotifier, condition evaluators/registry,TriggerManager, and SQLite-backed persistence. - Adds REST endpoints for trigger CRUD + SSE event streaming and wires triggers into discovery/capabilities and gateway lifecycle.
- Expands unit/integration test coverage and updates docs/config to describe trigger behavior and parameters.
Reviewed changes
Copilot reviewed 64 out of 64 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/ros2_medkit_gateway/src/gateway_node.cpp | Creates/wires trigger subsystem, notifier, store, subscribers; adds params and shutdown sequencing. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/gateway_node.hpp | Exposes TriggerManager/ConditionRegistry/ResourceChangeNotifier accessors and members. |
| src/ros2_medkit_gateway/src/http/rest_server.cpp | Registers trigger routes for entities; adds handler wiring hook. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/http/rest_server.hpp | Adds set_trigger_handlers() and stores TriggerHandlers instance. |
| src/ros2_medkit_gateway/src/http/handlers/trigger_handlers.cpp | Implements trigger CRUD + SSE stream and resource URI parsing/validation. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/http/handlers/trigger_handlers.hpp | Declares TriggerHandlers public API and helpers. |
| src/ros2_medkit_gateway/src/resource_change_notifier.cpp | Implements async fan-out hub for resource change events (worker thread + filters). |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/resource_change_notifier.hpp | Declares notifier types (ResourceChange, filters, subscribe/notify/shutdown). |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/condition_evaluator.hpp | Adds built-in condition evaluators + thread-safe registry for plugin extensibility. |
| src/ros2_medkit_gateway/src/trigger_manager.cpp | Implements trigger lifecycle, condition evaluation, indexing, SSE wakeups, persistence integration. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/trigger_manager.hpp | Declares TriggerManager API, state model, and configuration. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/trigger_store.hpp | Introduces TriggerStore abstraction and TriggerInfo persisted model. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/sqlite_trigger_store.hpp | Declares SQLite implementation for trigger persistence/state. |
| src/ros2_medkit_gateway/src/trigger_topic_subscriber.cpp | Adds generic topic subscriber with retry timer for late type resolution. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/trigger_topic_subscriber.hpp | Declares topic subscriber API and retry/pending behavior. |
| src/ros2_medkit_gateway/src/trigger_fault_subscriber.cpp | Bridges /fault_manager/events into notifier events for trigger evaluation. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/trigger_fault_subscriber.hpp | Declares fault-event subscriber bridge. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/trigger_transport_provider.hpp | Adds plugin interface for alternative trigger event delivery protocols. |
| src/ros2_medkit_gateway/src/updates/update_manager.cpp | Emits update status changes to notifier for triggers. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/updates/update_manager.hpp | Adds notifier integration points for update status notifications. |
| src/ros2_medkit_gateway/src/operation_manager.cpp | Emits action status changes to notifier; threads entity_id through goal tracking. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/operation_manager.hpp | Adds notifier support and stores entity_id for operation trigger notifications. |
| src/ros2_medkit_gateway/src/http/handlers/operation_handlers.cpp | Passes entity_id into OperationManager action goal creation for trigger notifications. |
| src/ros2_medkit_gateway/src/log_manager.cpp | Adds programmatic add_log_entry() and emits log CREATED notifications. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/log_manager.hpp | Declares add_log_entry() + notifier wiring for log triggers. |
| src/ros2_medkit_gateway/src/plugins/plugin_context.cpp | Exposes notifier + condition registry to plugins via PluginContext. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_context.hpp | Adds trigger-related accessors (notifier/registry) to plugin API. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_types.hpp | Bumps PLUGIN_API_VERSION for ABI change. |
| src/ros2_medkit_gateway/src/http/handlers/health_handlers.cpp | Advertises trigger availability in root capabilities. |
| src/ros2_medkit_gateway/src/http/handlers/discovery_handlers.cpp | Adds trigger links and TRIGGERS capability for entities. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/http/handlers/capability_builder.hpp | Adds TRIGGERS capability enum value. |
| src/ros2_medkit_gateway/src/http/handlers/capability_builder.cpp | Maps TRIGGERS capability to "triggers" string. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/http/handlers/handlers.hpp | Adds TriggerHandlers include to handler aggregation header. |
| src/ros2_medkit_gateway/package.xml | Adds SQLite system dependency. |
| src/ros2_medkit_gateway/CMakeLists.txt | Finds/links SQLite3; adds trigger sources to gateway_lib; adds new gtests. |
| src/ros2_medkit_gateway/test/test_condition_evaluator.cpp | Unit tests for built-in evaluators + registry + plugin extension path. |
| src/ros2_medkit_gateway/test/test_resource_change_notifier.cpp | Unit tests for notifier filtering, async behavior, shutdown safety. |
| src/ros2_medkit_gateway/test/test_trigger_store.cpp | Unit tests for SQLite trigger store CRUD + state persistence. |
| src/ros2_medkit_gateway/test/test_trigger_handlers.cpp | Unit tests for resource URI parsing, JSON shape, SSE tracker, error schema. |
| src/ros2_medkit_gateway/test/test_log_manager.cpp | Adds tests for add_log_entry() behavior and severity normalization. |
| src/ros2_medkit_gateway/test/test_graph_provider_plugin.cpp | Updates FakePluginContext for new PluginContext virtuals. |
| src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/test/test_topic_beacon_plugin.cpp | Updates MockPluginContext for new PluginContext virtuals. |
| src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/test/test_param_beacon_plugin.cpp | Updates MockPluginContext for new PluginContext virtuals. |
| src/ros2_medkit_gateway/config/gateway_params.yaml | Documents trigger parameters in default YAML config. |
| src/ros2_medkit_gateway/design/index.rst | Documents trigger subsystem architecture and component diagram. |
| docs/config/server.rst | Documents trigger config options and SSE limits including trigger streams. |
| docs/requirements/specs/subscriptions.rst | Marks trigger requirements verified and adds REQ_INTEROP_096/097. |
| docs/requirements/specs/discovery.rst | Marks REQ_INTEROP_002 verified. |
| docs/tutorials/index.rst | Adds triggers tutorial entry. |
| README.md | Adds “Triggers” to feature matrix. |
| src/ros2_medkit_integration_tests/test/features/test_triggers_updates.test.py | Trigger CRUD integration tests for “updates” resources. |
| src/ros2_medkit_integration_tests/test/features/test_triggers_faults.test.py | Trigger CRUD + SSE connectivity tests for faults resources. |
| src/ros2_medkit_integration_tests/test/features/test_triggers_logs.test.py | Trigger CRUD + SSE connectivity tests for logs resources. |
| src/ros2_medkit_integration_tests/test/features/test_triggers_hierarchy.test.py | Integration tests for hierarchy-scoped triggers (area/component/function). |
| src/ros2_medkit_integration_tests/test/features/test_triggers_late_publisher.test.py | Integration test intended to cover late publisher retry subscription. |
| src/ros2_medkit_integration_tests/test/features/test_triggers_persistent.test.py | Integration tests for persistent trigger restore using shared SQLite DB. |
| src/ros2_medkit_integration_tests/test/scenarios/test_scenario_ota_monitoring.test.py | Scenario test for multi-trigger CRUD + SSE connectivity for OTA monitoring workflow. |
Comment on lines
+344
to
+368
| std::string path = "/tmp/test_trigger_store_persist.db"; | ||
| std::remove(path.c_str()); | ||
|
|
||
| // Scope 1: create and save | ||
| { | ||
| SqliteTriggerStore store(path); | ||
| ASSERT_TRUE(store.save(make_trigger("persist_001")).has_value()); | ||
| ASSERT_TRUE(store.save_state("persist_001", json{{"val", 99}}).has_value()); | ||
| } | ||
|
|
||
| // Scope 2: reopen and verify | ||
| { | ||
| SqliteTriggerStore store(path); | ||
| auto loaded = store.load_all(); | ||
| ASSERT_TRUE(loaded.has_value()); | ||
| ASSERT_EQ(loaded->size(), 1u); | ||
| EXPECT_EQ(loaded->at(0).id, "persist_001"); | ||
|
|
||
| auto state = store.load_state("persist_001"); | ||
| ASSERT_TRUE(state.has_value()); | ||
| ASSERT_TRUE(state->has_value()); | ||
| EXPECT_EQ(state->value(), json({{"val", 99}})); | ||
| } | ||
|
|
||
| std::remove(path.c_str()); |
Comment on lines
+55
to
+60
| void UpdateManager::notify_status_change(const std::string & id, const UpdateStatusInfo & status) { | ||
| if (!notifier_) { | ||
| return; | ||
| } | ||
| notifier_->notify("updates", id, id, update_status_to_json(status)); | ||
| } |
Comment on lines
+37
to
+51
| // Derive entity_id from reporting_sources (first source, if available) | ||
| std::string entity_id; | ||
| if (!msg->fault.reporting_sources.empty()) { | ||
| entity_id = msg->fault.reporting_sources[0]; | ||
| } | ||
|
|
||
| // Map event_type to ChangeType | ||
| ChangeType change_type = ChangeType::UPDATED; | ||
| if (msg->event_type == "fault_confirmed") { | ||
| change_type = ChangeType::CREATED; | ||
| } else if (msg->event_type == "fault_cleared") { | ||
| change_type = ChangeType::DELETED; | ||
| } | ||
|
|
||
| notifier_.notify("faults", entity_id, msg->fault.fault_code, fault_json, change_type); |
Comment on lines
+217
to
+227
| auto result = trigger_mgr_.create(create_req); | ||
| if (!result) { | ||
| // Distinguish between validation errors (400) and capacity errors (503). | ||
| // NOTE: String matching on TriggerManager error messages - coupled to the exact | ||
| // error text in trigger_manager.cpp::create(). If those strings change, update here. | ||
| const auto & err = result.error(); | ||
| if (err.find("Maximum trigger capacity") != std::string::npos) { | ||
| HandlerContext::send_error(res, 503, ERR_SERVICE_UNAVAILABLE, err); | ||
| } else { | ||
| HandlerContext::send_error(res, 400, ERR_INVALID_PARAMETER, err, {{"parameter", "trigger_condition"}}); | ||
| } |
Comment on lines
+257
to
+260
| // Subscribe to topic for data triggers | ||
| if (topic_subscriber_ && req.collection == "data" && !req.resolved_topic_name.empty()) { | ||
| topic_subscriber_->subscribe(req.resolved_topic_name, req.resource_path, req.entity_id); | ||
| } |
…ators Header-only ConditionEvaluator abstract base class with evaluate() and validate_params() methods. Four SOVD-standard condition types: OnChange, OnChangeTo, EnterRange, LeaveRange. Thread-safe ConditionRegistry for built-in and plugin-registered evaluators. 40 unit tests covering all evaluators, registry, and plugin extension.
Range evaluators now return false for non-numeric values instead of throwing. Also document validate_params() precondition on base class.
Central async notification hub for resource changes. Producers call notify() to report changes (non-blocking, pushes to internal queue). Observers register callbacks with collection/entity/resource filters. Dedicated worker thread processes the queue and dispatches to matching subscribers with exception isolation.
- Remove spurious mutable on sub_mutex_ (no const methods lock it) - Fix fragile multi-subscriber test with atomic counter pattern - Fix copyright to match existing convention (bburda not selfpatch)
- Guard condition_params parse against corrupt JSON (is_discarded check) - Add column allowlist to update() preventing arbitrary column modification - Add lifetime_sec assertion to UpdateFields test - Add UpdateDisallowedColumn test
…rarchy matching TriggerManager is the central coordinator for the triggers feature. It integrates ConditionRegistry, ResourceChangeNotifier, and TriggerStore to provide full trigger lifecycle management: - CRUD operations with capacity enforcement and condition validation - Async condition evaluation on resource change events via dispatch index - Entity hierarchy matching (components/areas scope down to apps) - SSE synchronization (wait_for_event/consume_pending_event pattern) - Lifetime expiry with lazy termination - Persistent trigger loading on restart - EventEnvelope generation with ISO 8601 timestamps Also relaxes ResourceChangeNotifier filter matching: empty collection now acts as a catch-all (matching all collections), consistent with the existing empty entity_id and resource_path behavior. 29 unit tests covering CRUD, event evaluation, hierarchy matching, SSE synchronization, lifetime expiry, and condition-specific behavior.
- Move store_.save() outside triggers_mutex_ in create() to avoid blocking dispatch during SQLite I/O. Persist before inserting into memory so failures leave no in-memory state. - Clean up expired triggers from dispatch index and triggers_ map via new cleanup_expired_trigger() helper, preventing unbounded accumulation. - Fix on_removed_ data race by copying callback under triggers_mutex_ in remove() and guarding set_on_removed() with the same mutex. - Replace sleep_for(100ms) with wait_for_event() + consume_pending_event() in tests that expect events; keep short sleeps only for negative tests (verifying no event fires) with explanatory comments. - Reduce LifetimeExpiry test from 2s sleep to 1.1s. - Reject non-positive lifetime in update() with validation error. - Revert premature PLUGIN_API_VERSION bump (3->4); will bump in Task 11 when PluginContext vtable actually changes. - Add threading safety comments on TriggerState immutable fields.
Thin REST handler layer over TriggerManager following the CyclicSubscriptionHandlers pattern. Implements CRUD (create, list, get, update, delete) and SSE event streaming for triggers. Includes a local parse_resource_uri that supports areas in addition to apps, components, and functions. 22 unit tests cover resource URI parsing (including areas, path traversal rejection), JSON serialization, error response format, and SSE client tracker limits.
- Add explicit lifetime <= 0 validation in handle_update - Document string-matching coupling for capacity error detection - Add Cache-Control and X-Accel-Buffering SSE headers - Document trigger_mgr_ lifetime requirement in SSE lambda - Remove invalid @verifies tags (will be added in Task 16)
Add trigger route registration to RESTServer and integrate trigger capability into the entity discovery system. Routes are registered for all 4 entity types (areas, components, apps, functions) with runtime guards that return 501 until TriggerManager is wired up by GatewayNode (Task 7). Adds set_trigger_handlers() method for deferred handler initialization.
…atewayNode Initialize trigger infrastructure in GatewayNode: ResourceChangeNotifier, ConditionRegistry (4 built-in evaluators), SqliteTriggerStore, and TriggerManager. Add triggers configuration section to gateway_params.yaml with enable flag, max triggers, restart behavior, and storage path. Wire trigger handlers into REST server and add proper shutdown ordering.
…pdateManager, OperationManager
…text - Add TriggerTransportProvider abstract interface (trigger_transport_provider.hpp) with TriggerEventDelivery struct for delivering trigger events over custom protocols - Extend PluginContext with four new pure virtual methods: get_resource_change_notifier(), get_condition_registry(), set_trigger_store(), register_trigger_transport() - Implement new methods in GatewayPluginContext: getters delegate to GatewayNode, setters store override store and transport providers as members - Update FakePluginContext in tests to implement the new interface methods - Bump PLUGIN_API_VERSION from 3 to 4
…, and hierarchy triggers Five new integration test files covering the full trigger lifecycle: - test_triggers_data: CRUD + SSE event delivery for data topics (20 tests) - test_triggers_faults: CRUD + SSE connectivity for fault triggers (8 tests) - test_triggers_updates: CRUD + error handling for update triggers (12 tests) - test_triggers_logs: CRUD + SSE connectivity for log triggers (9 tests) - test_triggers_hierarchy: entity scoping across components, areas, functions (7 tests) Also fixes beacon plugin MockPluginContext to implement new PluginContext pure virtual methods from Task 11 (get_resource_change_notifier, get_condition_registry, set_trigger_store, register_trigger_transport).
Add requirements traceability for trigger endpoints: - Add REQ_INTEROP_096 (GET single trigger) and REQ_INTEROP_097 (GET trigger events SSE) to subscriptions.rst as new verified requirements - Update REQ_INTEROP_029 through REQ_INTEROP_032 from open to verified - Add @verifies tags to test_trigger_handlers.cpp unit tests - Add @verifies tags to all 5 trigger integration test files - REQ_INTEROP_002 auto-updated to verified by generate_verification.py
…subscription Data triggers never fire end-to-end because parse_resource_uri() captures resource_path as a URI segment (e.g. "/temperature") while TriggerTopicSubscriber emits the full ROS 2 topic name (e.g. "/sensor/temperature") as resource_path in notifications. The comparison in on_resource_change() never matches. Fix by resolving the resource_path to a full ROS 2 topic name at trigger creation time in trigger_handlers.cpp using the entity's cached topic data. TriggerTopicSubscriber now uses the original resource_path (not the topic name) in notifications, aligning both sides of the match. Also fix multi-entity topic subscription: TriggerTopicSubscriber now stores a set of entity_ids per topic instead of a single entity_id, and emits one notification per entity. unsubscribe() removes individual entities and only destroys the ROS 2 subscription when the set is empty.
- Remap 16 @verifies tags from undefined REQ_TRIGGER_001/002/003 to existing requirement IDs (REQ_INTEROP_029-032, 096, 097) - Add Triggers section to docs/config/server.rst documenting all trigger configuration parameters - Update SSE max_clients description to mention trigger event streams - Add Triggers row to README.md features table - Fix log_settings docs in rest.rst to match implementation (severity and marker fields, not severity_filter and max_entries)
…ix multishot default - Validate JSON Pointer syntax and length (max 1024) for the path field (I19) - Reject unsupported protocols; only 'sse' is accepted (I20) - Reject unknown collections; allow x-* vendor extensions (I21) - Fix TriggerInfo::multishot default from true to false to match API contract (I3) - Add unit tests: InvalidJsonPointer_Returns400, PathTooLong_Returns400, UnsupportedProtocol_Returns400, UnknownCollection_Returns400, VendorExtensionCollection_Accepted
…g_settings in response
- I24: Add SSE id: field to every event frame using per-trigger event_counter (atomic uint64_t in TriggerState), enabling client reconnection with Last-Event-ID
- I25: Add millisecond precision to to_iso8601() - formats as .NNN before trailing Z
- I26: Replace hardcoded {"triggers", true} with dynamic check via get_trigger_manager() != nullptr, consistent with updates capability
- I27: Include log_settings in trigger_to_json() when set, so clients can inspect what was configured
…ent load, log entry - I14: EnterRangeEvaluator/LeaveRangeEvaluator non-numeric guard tests (string, boolean previous/current values must return false without exception) - I11: load_persistent_triggers() tests using SqliteTriggerStore + plain TriggerManager construction (restore/reset behavior, expired trigger TERMINATED in store, ID collision avoidance after high-ID restore) - I12: LogManager::add_log_entry() tests using existing LogManagerBufferTest fixture (entry retrievable, invalid severity fallback to INFO, metadata suffix appended, empty metadata produces clean message) - I13: TriggerFaultSubscriber event mapping requires rclcpp node and live ROS 2 topic subscriptions; mapping logic is covered indirectly by integration tests
…ial prerequisites - Add @verifies REQ_INTEROP_029 to condition validator and trigger store tests - Add @verifies REQ_INTEROP_097 to resource change notifier tests - Replace C++ constant names (ERR_RESOURCE_NOT_FOUND) with wire-format codes (resource-not-found) in trigger section of rest.rst - Add prerequisite note for Scenario 2 explaining how to find the engine component - Replace /path/to/demo_nodes_manifest.yaml placeholder with actual ros2 pkg path - Add SOVD compliance note about script executions and locks not yet supported as observable trigger resources
When a trigger is created on a data topic whose publisher hasn't started yet, get_topic_names_and_types() returns empty and the subscription was silently skipped. Now unresolved topics are queued as pending and retried every 5 seconds via a wall timer, with a 60-second timeout. - Add PendingSubscription struct to track topics awaiting type resolution - Extract create_subscription_internal() for shared use by subscribe() and retry_pending_subscriptions() - Update subscribe() to queue pending instead of silently skipping - Update unsubscribe() to also check and clean up pending entries - Update shutdown() to cancel retry timer and clear pending map - Add integration test for late publisher scenario
When a ROS 2 node disappears, its triggers remained active in TriggerManager, wasting capacity slots and never receiving events. Add EntityExistsFn and sweep_orphaned_triggers() to TriggerManager with a two-phase locking approach to avoid deadlock. Wire into GatewayNode's periodic refresh timer so orphaned triggers are cleaned up after each discovery cache refresh.
e7ae8fd to
5d74077
Compare
- Fix ROS_DOMAIN_ID=70 collision between test_graph_provider_plugin and test_lock_handlers (moved lock handlers to 77) - Increase persistent trigger test timeouts to 60s for CI robustness - Switch hierarchy diagnostics test from manifest_only to hybrid mode (manifest_only doesn't link runtime data topics) - Increase hierarchy data topic wait from 15s to 30s
…start The dual-gateway test was failing because both gateways started simultaneously, causing SQLite WAL lock contention and the secondary loading from an empty DB. Fixed by: - Starting primary first, demo nodes after 2s - Delaying secondary by 25s so tests 01-02 create triggers first - Secondary then starts and loads persistent triggers from shared DB - Removed cross-gateway delete verification (no real-time sync)
- Reformat trigger_topic_subscriber.cpp with ament_clang_format - Fix MultishotRapidEvents test: wait+consume each event sequentially instead of firing all 3 and hoping the worker processes them all before the first wait_for_event (fails on Humble with slower GCC 11)
…geNotifier ResourceChangeNotifier is pure C++ (no rclcpp dependency) but was using std::cerr for exception logging. Replace with an optional ErrorLoggerFn callback, wired to RCLCPP_WARN in GatewayNode. Follows the same pattern as set_on_removed, set_entity_children_fn, set_entity_exists_fn.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pull Request
Summary
Implement SOVD Triggers - event-based notifications that fire when a condition is met on an observed resource. Unlike cyclic subscriptions (fixed-interval push), triggers only fire when something specific happens - a value changes, reaches a target, enters or leaves a range.
New components: ResourceChangeNotifier (async push hub), TriggerManager (lifecycle + condition evaluation), ConditionRegistry (pluggable evaluators), SQLiteTriggerStore (persistence), TriggerHandlers (6 REST endpoints + SSE), TriggerFaultSubscriber, TriggerTopicSubscriber, TriggerTransportProvider (plugin interface).
Issue
Type
Testing
Checklist