Skip to content

fix(plugin): register executor in pluginMap and enable gRPC in Serve#1946

Open
pameican wants to merge 1 commit intodkron-io:mainfrom
pameican:fix/executor-plugin-grpc-serve
Open

fix(plugin): register executor in pluginMap and enable gRPC in Serve#1946
pameican wants to merge 1 commit intodkron-io:mainfrom
pameican:fix/executor-plugin-grpc-serve

Conversation

@pameican
Copy link
Copy Markdown

@pameican pameican commented Mar 13, 2026

Problem

External executor plugins always crash on startup with:

level=fatal msg="unsupported plugin protocol \"netrpc\". Supported: [grpc]"

This makes it impossible to use any custom executor plugin with dkron v4.

Root cause

Two bugs in plugin/serve.go:

1. Missing GRPCServer in Serve()

plugin.Serve() was called without GRPCServer: plugin.DefaultGRPCServer. The go-plugin library defaults to netrpc when no gRPC server is configured. Since dkron v4 only accepts gRPC, every external plugin is rejected at handshake time.

2. pluginMap() never registers the executor

pluginMap() only registers "processor" and ignores "executor", despite ServeOpts.Executor and ExecutorPluginName being defined. Any plugin calling Serve() with an Executor is silently ignored — the plugin map is empty for its type.

Fix

func Serve(opts *ServeOpts) {
    plugin.Serve(&plugin.ServeConfig{
        HandshakeConfig: Handshake,
        Plugins:         pluginMap(opts),
        GRPCServer:      plugin.DefaultGRPCServer, // added
    })
}

func pluginMap(opts *ServeOpts) map[string]plugin.Plugin {
    plugins := map[string]plugin.Plugin{}

    if opts.Processor != nil {
        plugins[ProcessorPluginName] = &ProcessorPlugin{Processor: opts.Processor}
    }

    if opts.Executor != nil {
        plugins[ExecutorPluginName] = &ExecutorPlugin{Executor: opts.Executor} // added
    }

    return plugins
}

Tested

Verified with a custom executor plugin (dkron-executor-secrethttp) running on dkron v4.0.9 on Kubernetes (AKS).

  • Before: all pods in CrashLoopBackOff with unsupported plugin protocol "netrpc"
  • After: all pods Running, jobs executing correctly

Summary by CodeRabbit

  • Refactor
    • Improved plugin system configuration to selectively load plugins based on enabled settings.
    • Enhanced plugin server integration for better communication and efficiency.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 13, 2026

📝 Walkthrough

Walkthrough

The plugin/serve.go file now conditionally registers processor and executor plugins based on configuration options and passes a DefaultGRPCServer configuration to the plugin server, replacing unconditional plugin registration with conditional logic.

Changes

Cohort / File(s) Summary
Plugin Server Configuration
plugin/serve.go
Added conditional plugin map generation where Processor and Executor plugins are registered only when their corresponding options are non-nil. The plugin server now receives a DefaultGRPCServer configuration parameter.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 With hoppy code and conditional care,
Our plugins dance when config's there,
No null pointers left to dare,
Just GRPC serving with flair! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main changes: registering the executor plugin and enabling gRPC in the Serve function.
Description check ✅ Passed The description comprehensively covers the problem, root causes, fix, and testing, but lacks the structured template sections (Proposed changes and Types of changes checkboxes).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can disable the changed files summary in the walkthrough.

Disable the reviews.changed_files_summary setting to disable the changed files summary in the walkthrough.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@plugin/serve.go`:
- Around line 40-50: Add defensive validation at the start of the Serve
function: check if the incoming ServeOpts pointer is nil and return a clear
error (or panic with an explicit message) rather than dereferencing it; after
building the plugins map (using ProcessorPluginName/ExecutorPluginName and types
ProcessorPlugin/ExecutorPlugin) validate that at least one plugin was registered
and return a descriptive error if the map is empty to fail fast on all-nil
configuration. Ensure the checks reference ServeOpts, ProcessorPluginName,
ExecutorPluginName, ProcessorPlugin and ExecutorPlugin so callers get an
immediate, explicit failure instead of a later panic or unclear error.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 870e3418-e6f4-4212-ab63-db1217f02829

📥 Commits

Reviewing files that changed from the base of the PR and between 000e622 and 718c787.

📒 Files selected for processing (1)
  • plugin/serve.go

Comment on lines +40 to +50
plugins := map[string]plugin.Plugin{}

if opts.Processor != nil {
plugins[ProcessorPluginName] = &ProcessorPlugin{Processor: opts.Processor}
}

if opts.Executor != nil {
plugins[ExecutorPluginName] = &ExecutorPlugin{Executor: opts.Executor}
}

return plugins
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add defensive validation for ServeOpts to avoid panic/misconfiguration paths.

At Line 42 and Line 46, opts is dereferenced without a nil check. Serve(nil) will panic, and all-nil ServeOpts creates an empty plugin map that can fail later with less-clear errors. Consider failing fast with an explicit message.

Suggested hardening
 func Serve(opts *ServeOpts) {
+	if opts == nil {
+		panic("plugin: ServeOpts must not be nil")
+	}
+	plugins := pluginMap(opts)
+	if len(plugins) == 0 {
+		panic("plugin: at least one plugin (processor or executor) must be configured")
+	}
 	plugin.Serve(&plugin.ServeConfig{
 		HandshakeConfig: Handshake,
-		Plugins:         pluginMap(opts),
+		Plugins:         plugins,
 		GRPCServer:      plugin.DefaultGRPCServer,
 	})
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/serve.go` around lines 40 - 50, Add defensive validation at the start
of the Serve function: check if the incoming ServeOpts pointer is nil and return
a clear error (or panic with an explicit message) rather than dereferencing it;
after building the plugins map (using ProcessorPluginName/ExecutorPluginName and
types ProcessorPlugin/ExecutorPlugin) validate that at least one plugin was
registered and return a descriptive error if the map is empty to fail fast on
all-nil configuration. Ensure the checks reference ServeOpts,
ProcessorPluginName, ExecutorPluginName, ProcessorPlugin and ExecutorPlugin so
callers get an immediate, explicit failure instead of a later panic or unclear
error.

@vcastellm
Copy link
Copy Markdown
Member

@pameican Dkron already has many external plugins and they work out of the box, verify your plugin's code, use the ones in the project as starting point and your plugin should work.

Or do you mean something else?

@pabloacan
Copy link
Copy Markdown

pabloacan commented Mar 14, 2026

Hi @vcastellm, you're right, sorry for the noise.

After checking the builtin plugins (like dkron-executor-kafka) I realized the correct pattern is to call go-plugin's plugin.Serve() directly with GRPCServer and the executor registered manually — not using dkplugin.Serve().

My plugin was incorrectly using dkplugin.Serve() which doesn't register the executor in pluginMap and doesn't set GRPCServer, so it was defaulting to netrpc.

Once I aligned my main.go with the builtin plugin pattern, everything works fine.

That said, plugin.Serve() in plugin/serve.go still silently ignores ServeOpts.Executor — it might be worth adding a note in the docs or a warning log for future plugin developers, but that's up to you.

Thanks for the quick response!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants