Skip to content

feat(event): opt-in dual-sink for native queue and event plugin#6878

Open
perfch wants to merge 1 commit into
tronprotocol:developfrom
perfch:feat/native-queue-and-plugin-dual-sink
Open

feat(event): opt-in dual-sink for native queue and event plugin#6878
perfch wants to merge 1 commit into
tronprotocol:developfrom
perfch:feat/native-queue-and-plugin-dual-sink

Conversation

@perfch

@perfch perfch commented Jul 15, 2026

Copy link
Copy Markdown

What does this PR do?

Lets the native ZMQ queue and the event plugin (e.g. MongoDB) run at the same time, so a node can have both low-latency ZMQ delivery and durable plugin persistence from a single configuration. Activation is made explicit instead of being inferred from the plugin path.

  • New opt-in flag event.subscribe.runPluginWithNativeQueue (default false). With the native queue enabled, the plugin only joins when this is true. When the native queue is disabled, behavior is unchanged (the plugin remains the only sink).
  • New event.subscribe.pluginLoadFailurePolicy (fail | ignore, default fail) to define what happens when the plugin cannot load.
  • Args resolves activation via EventPluginConfig.resolveUseEventPlugin(...); the loader no longer infers activation from a raw path.
  • In dual-sink mode every trigger is delivered to both sinks. Each trigger is serialized once and shared between sinks, and plugin delivery is wrapped so a plugin fault cannot tear down the node's event thread.
  • Plugin back-pressure (isBusy) is honored whenever the plugin is active, including dual-sink mode, both at runtime and during historical event sync (HistoryEventService).

This builds on the idea from #6852 and is an alternative that addresses the backward-compatibility and isolation concerns raised there. Related: #6862.

Why are these changes required?

Previously the native queue and the plugin were mutually exclusive: with useNativeQueue = true the plugin config was ignored entirely, forcing operators to choose between low latency and durable storage.

The main risk with enabling both is an upgrade regression: a node that still carries a stale path in its config must not suddenly start loading a plugin after an upgrade. Making activation an explicit opt-in keeps every existing native-queue deployment on its current behavior by default, while giving operators who want dual-sink a clear switch. The failure policy and the try/catch isolation keep a plugin problem from taking the node down.

Review concern → how it is addressed:

Concern raised in review Resolution
Stale path in an upgraded native-queue config would silently activate the plugin (backward-compat regression) Explicit runPluginWithNativeQueue opt-in (default false); a path alone never activates the plugin when the native queue is on
Behavior on plugin load failure undefined pluginLoadFailurePolicy = fail (default, abort startup) or ignore (log and continue native-queue-only)
Plugin failure coupled into core node operation Plugin trigger delivery is isolated in try/catch so it cannot break the node's event thread
Per-trigger serialization overhead on the hot path Each trigger is serialized once and shared between sinks; the default native-only path keeps its original cost
Missing regression tests Added tests for the upgrade scenario, dual-sink routing, plugin-failure isolation, load-failure policy, and back-pressure semantics

This PR has been tested by:

  • Unit Tests
    • EventPluginLoaderTest: testResolveUseEventPlugin (upgrade regression), testIsBusy (back-pressure incl. dual-sink), testStartRequiresASink, testPostRoutesToPluginOnlyWhenActive, testPluginFailureIsIsolated, testLoadFailurePolicyIgnoreDisablesPlugin
    • Existing ArgsTest and ParameterTest pass (config binding of the two new keys is backward compatible)
    • checkstyleMain / checkstyleTest pass
  • Manual Testing

Follow up

Documentation for combined mode is covered inline in config.conf. A separate docs page update can follow if desired.

Extra details

Default behavior is unchanged: with runPluginWithNativeQueue = false (the default), a native-queue node behaves exactly as before, even if a path is still present in its config.

@github-actions
github-actions Bot requested review from 0xbigapple and xxo1shine July 15, 2026 12:15
@perfch
perfch force-pushed the feat/native-queue-and-plugin-dual-sink branch from 20c3c5b to 0e13b2f Compare July 15, 2026 12:32
@perfch
perfch marked this pull request as ready for review July 15, 2026 13:08
Copilot AI review requested due to automatic review settings July 15, 2026 13:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces an explicit, backward-compatible “dual-sink” event delivery mode where the native ZMQ queue and the event plugin (e.g., MongoDB) can run simultaneously, without inferring plugin activation from a stale path.

Changes:

  • Adds explicit plugin activation resolution (runPluginWithNativeQueue) and plugin load failure policy (pluginLoadFailurePolicy = fail|ignore).
  • Updates EventPluginLoader to support dual-sink routing, shared per-trigger serialization, plugin fault isolation, and back-pressure semantics in dual-sink mode.
  • Updates config templates and adds/extends unit tests for upgrade regression, routing, isolation, and load-failure behavior.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
framework/src/test/java/org/tron/core/event/EventPluginLoaderTest.java Adds regression/routing/isolation/load-failure tests for explicit dual-sink/plugin activation behavior.
framework/src/main/resources/config.conf Documents opt-in dual-sink flag and plugin load failure policy in the sample config.
framework/src/main/java/org/tron/core/services/event/HistoryEventService.java Ensures plugin back-pressure is respected before native queue pacing (incl. dual-sink mode).
framework/src/main/java/org/tron/core/config/args/Args.java Resolves plugin activation explicitly via EventPluginConfig.resolveUseEventPlugin(...) and wires failure policy.
framework/src/main/java/org/tron/common/logsfilter/EventPluginLoader.java Implements dual-sink routing, plugin activation flag, failure policy behavior, and shared serialization.
common/src/main/resources/reference.conf Adds new config keys with defaults to the reference configuration.
common/src/main/java/org/tron/core/config/args/EventConfig.java Adds bean fields for runPluginWithNativeQueue and pluginLoadFailurePolicy.
common/src/main/java/org/tron/common/logsfilter/EventPluginConfig.java Adds useEventPlugin, pluginLoadFailurePolicy, and centralized activation resolution helper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +414 to +423
private void postToPlugin(Consumer<IPluginEventListener> handler) {
if (Objects.isNull(eventListeners)) {
return;
}
try {
eventListeners.forEach(handler);
} catch (Exception e) {
logger.error("event plugin failed to handle trigger", e);
}
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in 3c03c6d. postToPlugin now isolates per listener (each handler.accept(listener) in its own try/catch), so one failing plugin no longer blocks delivery to the others or the event thread. I intentionally catch Exception rather than Throwable: OutOfMemoryError/linkage Errors should propagate and fail the process, not be silently swallowed. Added a two-listener test (testPluginFailureIsIsolated) asserting the healthy listener still receives the trigger.

Comment on lines +259 to +263
useNativeQueue = config.isUseNativeQueue();
useEventPlugin = config.isUseEventPlugin();
if (StringUtils.hasText(config.getPluginLoadFailurePolicy())) {
pluginLoadFailurePolicy = config.getPluginLoadFailurePolicy().trim();
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in 3c03c6d. Added normalizeFailurePolicy(): only an explicit "ignore" enables ignore behavior; any unknown/typo value normalizes to the safe "fail" default (with a warning log). The policy is also re-resolved on every start(), so no policy state leaks across runs. Covered by testLoadFailurePolicyIgnoreDisablesPlugin, which asserts an unknown value normalizes to "fail".

Comment on lines 286 to 288
if (useNativeQueue) {
return launchNativeQueue(config);
}

@perfch perfch Jul 15, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch, addressed in 3c03c6d. When launchNativeQueue(config) fails after the plugin has already started, start() now calls stopPlugin() before returning false, so a failed startup no longer leaks the plugin threads/resources.

Comment on lines +373 to +384
// Plugin connection settings are always copied when a path is configured, so the
// plugin can run either on its own (native queue off) or alongside the native queue.
boolean hasPluginPath = StringUtils.isNotEmpty(ec.getPath());
if (hasPluginPath) {
epc.setPluginPath(ec.getPath().trim());
}
if (StringUtils.isNotEmpty(ec.getServer())) {
epc.setServerAddress(ec.getServer().trim());
}
if (StringUtils.isNotEmpty(ec.getDbconfig())) {
epc.setDbConfig(ec.getDbconfig().trim());
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in 3c03c6d. server/dbconfig are now copied only under hasPluginPath, matching the comment. Without a plugin path the plugin cannot be activated, so these connection settings are left unset rather than lingering on EventPluginConfig.

Comment on lines 16 to +24
public class EventPluginLoaderTest {

private static IPluginEventListener mockListener(EventPluginLoader loader) {
IPluginEventListener listener = mock(IPluginEventListener.class);
List<IPluginEventListener> list = new ArrayList<>();
list.add(listener);
ReflectUtils.setFieldValue(loader, "eventListeners", list);
return listener;
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in 3c03c6d. Added an @After resetLoader() that resets the singletons useNativeQueue/useEventPlugin/eventListeners/pluginLoadFailurePolicy after each test, preventing state from leaking into other test classes that use EventPluginLoader.getInstance().

Allow the native ZMQ queue and the event plugin to run at the same time,
with activation made explicit rather than inferred from a path so that
existing native-queue deployments keep their current behavior after an
upgrade.

- Plugin activation is resolved explicitly in Args via
  EventPluginConfig.resolveUseEventPlugin(): with the native queue on it
  only joins when the operator opts in (runPluginWithNativeQueue = true),
  so a stale "path" in an upgraded config can never silently load a plugin.
- Add pluginLoadFailurePolicy ("fail" | "ignore") to define behavior when
  the plugin cannot load; default "fail" preserves data integrity.
- Isolate plugin trigger posting in try/catch so a plugin fault cannot tear
  down the node event thread; serialize each trigger once and share it with
  both sinks so the default path keeps its current cost.
- Apply plugin back-pressure (isBusy) whenever the plugin is active,
  including dual-sink mode, in EventPluginLoader and HistoryEventService.
- Tests: upgrade regression, dual-sink routing, plugin-failure isolation,
  load-failure policy, and updated back-pressure semantics.
@perfch
perfch force-pushed the feat/native-queue-and-plugin-dual-sink branch from 0e13b2f to 3c03c6d Compare July 15, 2026 13:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants