Skip to content

Add manual trigger mode for bundle refresh - #112

Open
MohamedFazil1406 wants to merge 11 commits into
open-policy-agent:mainfrom
MohamedFazil1406:feature/manual-bundle-trigger
Open

Add manual trigger mode for bundle refresh#112
MohamedFazil1406 wants to merge 11 commits into
open-policy-agent:mainfrom
MohamedFazil1406:feature/manual-bundle-trigger

Conversation

@MohamedFazil1406

Copy link
Copy Markdown

Summary

This PR adds support for a manual trigger mode for bundle refresh while preserving the existing periodic polling behavior as the default.

##Issues #65

Changes

  • Added Trigger configuration with PERIODIC (default) and MANUAL modes.
  • Extended BundleConfig to support the new trigger option.
  • Passed the configured trigger from BundlePlugin to BundleDownloader.
  • Updated BundleDownloader to skip automatic polling when the trigger is MANUAL.
  • Added a refresh() method to allow bundles to be refreshed on demand.
  • Added validation to reject polling configuration when using the manual trigger.
  • Added unit tests covering manual trigger configuration and validation.

Testing

  • Verified the project compiles successfully with:
    • ./gradlew :opa-services:compileJava
  • Added unit tests for the new manual trigger functionality.
  • Local MTLS integration tests require openssl, which is not available in my Windows environment, resulting in an unrelated test initialization failure.

@sspaink

sspaink commented Jul 13, 2026

Copy link
Copy Markdown
Member

@MohamedFazil1406 thank you for working on this! can you sign your commits please for the DCO check to pass?

@MohamedFazil1406
MohamedFazil1406 force-pushed the feature/manual-bundle-trigger branch 2 times, most recently from ee67282 to d413338 Compare July 13, 2026 16:35
@MohamedFazil1406
MohamedFazil1406 requested a review from a team as a code owner July 13, 2026 16:35
@MohamedFazil1406

Copy link
Copy Markdown
Author

Thanks for the heads-up! I've signed off all the commits and force-pushed the updated branch. The DCO check should pass now.

Signed-off-by: MohamedFazil1406 <mohamedfazil01406@gmail.com>
Signed-off-by: MohamedFazil1406 <mohamedfazil01406@gmail.com>
Signed-off-by: MohamedFazil1406 <mohamedfazil01406@gmail.com>
@sspaink

sspaink commented Jul 16, 2026

Copy link
Copy Markdown
Member

@MohamedFazil1406 looks like there are some real compilation failures, mind taking a look?

@MohamedFazil1406

Copy link
Copy Markdown
Author

ok i will check

Signed-off-by: MohamedFazil1406 <mohamedfazil01406@gmail.com>
@MohamedFazil1406
MohamedFazil1406 force-pushed the feature/manual-bundle-trigger branch from 5208b45 to 1a6e0d2 Compare July 17, 2026 09:24
@MohamedFazil1406

Copy link
Copy Markdown
Author

Hi! I've updated the PR and fixed the issues. Could you please review it when you have time? Thank you!

@sspaink sspaink left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for working on this! Unfortunately the PR isn't mergeable as-is: the manual-trigger feature isn't wired up, the new tests call methods that were never added (so the module won't compile), the validation the summary describes is missing, and the diff also reverts the max_size_bytes feature that's on main. Details inline. Verified against the PR head and main. Recommend rebasing on current main (to drop the max_size_bytes deletion) and adding the actual wiring — BundleConfig.trigger + accessors, Bundle.getTrigger, propagation in initialize, and the validation — before the tests can compile and pass.

Minor: the new Trigger enum and added blocks use 4-space indentation (surrounding code is 2-space), and startPolling gains stray double blank lines.

new Config.BundleConfig()
.setService("test-service")
.setResource("/bundles/test.tar.gz")
.setTrigger(Config.Trigger.MANUAL)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This won't compile — Config.BundleConfig.setTrigger(...) doesn't exist. The diff adds the Config.Trigger enum but no trigger field, setTrigger, or getTrigger on BundleConfig (grep trigger in Config.java finds only the enum). Same problem at line 100 with plugin.getBundle(...).getTrigger()BundleDownloader/Bundle has setTrigger but no getTrigger. Both test methods fail to compile, so the whole opa-services test source set won't build.

servicePlugin == null ? null : servicePlugin.getService(bundleConfig.getService());

Bundle bundle =
plugin.bundles.put(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The configured trigger is never propagated, so MANUAL has no effect. This builder chain sets service/resource/polling but never calls .setTrigger(bundleConfig.getTrigger()), so the downloader keeps its default PERIODIC and the new MANUAL branch in startPolling is unreachable via config. (It also can't, yet — BundleConfig.getTrigger() doesn't exist.) Even once it compiles, initialize_manualTrigger_setsTrigger would fail because the bundle stays PERIODIC.


assertTrue(
errors.stream()
.anyMatch(e -> e.contains("cannot specify polling when trigger is manual")));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The validation this asserts doesn't exist. BundlePlugin.validate() has no check that rejects polling when the trigger is manual (no trigger reference in the method), so no error containing "cannot specify polling when trigger is manual" is ever produced and this assertion fails. The "reject polling configuration when using the manual trigger" change from the PR summary is missing from the diff.

*/
@JsonProperty("max_size_bytes")
private long maxSizeBytes = DEFAULT_MAX_SIZE_BYTES;
public static final long DEFAULT_MAX_SIZE_BYTES = 1024L * 1024L * 1024L;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This reverts the max_size_bytes feature that's on main — likely a stale-branch/bad-merge artifact. The diff deletes BundleConfig.maxSizeBytes (the field, getMaxSizeBytes/setMaxSizeBytes public API, and its Javadoc), drops the bundle.setMaxSizeBytes(...) wiring in BundlePlugin, and switches to the 2-arg new TarballBundleLoader(name, bundleData) (BundlePlugin.java:186). On main this is a configurable DoS safeguard (512 MB default, capping both the compressed download and decompressed contents). Net effect: the configurable limit is gone (falls back to the loader's built-in default) and this line silently changes the default 512 MB → 1 GB. This is unrelated to the trigger feature and shouldn't land; please rebase on current main.

return initialActivation;
}

public CompletableFuture<Void> refresh() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

refresh() is broken for repeated use and blocks the caller. It calls downloadBundle() synchronously on the caller's thread (every other download runs on the scheduler, doing blocking network I/O), and returns initialActivation — a one-shot CompletableFuture whose completion sites are all guarded by if (!initialActivation.isDone()). So the returned future reflects only the first activation: after the first success it's already completed, and later refresh() calls return an already-done future that doesn't track that refresh's outcome (success or failure). Also, in MANUAL mode startPolling returns initialActivation without ever scheduling a download, so getInitialActivation() never completes until a refresh() occurs — which can hang plugin startup if lifecycle code awaits it. Consider returning a fresh future per refresh and dispatching the download on the scheduler.

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.

Thanks for the review! I've addressed the compile issues, trigger propagation, validation, and rebased onto the latest main. For the refresh() redesign, I understand the goal of returning a fresh future per refresh while keeping initialActivation for the first activation only. Before I refactor it, could you clarify the expected behavior for manual mode? Should startPolling() return an already-completed initialActivation in manual mode, or should initialActivation only complete after the first successful refresh()?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I checked how OPA's Go bundle plugin handles this:

  1. Manual mode does no download at startup — the downloader's Start() is a no-op unless the trigger is periodic; a fetch only happens on Trigger().
  2. Readiness = a bundle actually activated — the plugin stays NotReady until every bundle has activated once. But OPA's Start() never blocks on this; readiness is just reported status.

So no, startPolling() shouldn't return a pre-completed initialActivation. Keep it meaningful (first successful activation). The reason a never-completing future hangs us is SDK-specific: build() blocks up to 60s for bundles to reach OK, and BundlePlugin.start() couples that status to allOf(initialActivation). The fix is to decouple them, like OPA:

  • start() shouldn't gate the plugin's OK on a manual bundle's initialActivation — loaders are started and awaiting triggers, so the plugin reaches OK and build() won't hang.
  • initialActivation completes only on the first successful refresh — don't complete it eagerly.
  • refresh() returns a fresh future per call, dispatched on the scheduler (not synchronously on the caller's thread).

A caller wanting to block until the first manual bundle loads awaits refresh(), not getInitialActivation().

Signed-off-by: MohamedFazil1406 <mohamedfazil01406@gmail.com>
Signed-off-by: MohamedFazil1406 <mohamedfazil01406@gmail.com>
Signed-off-by: MohamedFazil1406 <mohamedfazil01406@gmail.com>
Signed-off-by: MohamedFazil1406 <mohamedfazil01406@gmail.com>
Signed-off-by: MohamedFazil1406 <mohamedfazil01406@gmail.com>
Signed-off-by: MohamedFazil1406 <mohamedfazil01406@gmail.com>
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