You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: website/docs/implementation_guides/pact_plugins/docs/proposals/proposal_007_driver_plugin_callback_model.md
@@ -21,51 +21,246 @@ framework, or they cannot reuse functionality across plugins at all.
21
21
22
22
This is especially relevant for richer matcher/generator use cases and for any future in-process plugin runtime.
23
23
24
+
### The dependency-cycle constraint
25
+
26
+
The driver crate deliberately depends only on `pact_models` (data types), not on `pact_matching` or any other crate
27
+
that implements actual Pact matching/generation behaviour. Those higher-level crates depend on the driver (so they can
28
+
call out to plugins), so the reverse dependency is not available: the driver cannot link against the code that knows
29
+
how to compare an XML body, apply a `DateGenerator`, or run the standard matching rule set. The same constraint holds
30
+
for the JVM driver, which depends on `au.com.dius.pact.core:model` but not on the Pact-JVM matching engine.
31
+
32
+
This means the driver can advertise that a capability exists (it already does this via the catalogue and
33
+
`hostCapabilities` in `InitPlugin`, see [005](/implementation_guides/pact_plugins/docs/proposals/proposal_005_plugin_capability_negotiation_and_versioning)), but it cannot
34
+
itself execute that capability. Something registered at runtime by the embedding framework has to do the work, and the
35
+
driver has to be able to invoke it without knowing its concrete type.
36
+
24
37
## Recommended direction
25
38
26
39
### Logical model
27
40
28
-
Callbacks are modelled as specific typed host capabilities, not a generic message envelope. Each capability is a well-defined operation with a typed request and response. The driver advertises which capabilities it supports during the `InitPlugin` handshake (via [005](/implementation_guides/pact_plugins/docs/proposals/proposal_005_plugin_capability_negotiation_and_versioning)), so a plugin knows before making any call whether a given capability is available.
41
+
Callbacks are modelled as specific typed host capabilities, not a generic message envelope. Each capability is a
42
+
well-defined operation with a typed request and response. The driver advertises which capabilities it supports during
43
+
the `InitPlugin` handshake (via [005](/implementation_guides/pact_plugins/docs/proposals/proposal_005_plugin_capability_negotiation_and_versioning)), so a plugin knows before making any call whether a given capability is available.
44
+
45
+
### Breaking the dependency cycle: registered handlers, not linked implementations
46
+
47
+
This proposal does not introduce a new pattern — it generalises one already shipped in
48
+
[008](/implementation_guides/pact_plugins/docs/proposals/proposal_008_plugin_observability_and_logging). The `PluginLogSink` trait in
49
+
`drivers/rust/driver/src/plugin_log_sink.rs` is the working example: the driver defines the trait, holds a global
50
+
replaceable instance, and exposes a `set_plugin_log_sink()` registration function. The embedding framework implements
51
+
the trait and registers itself once at startup. The driver's compiled code never references the concrete
52
+
implementation. The JVM driver's `PluginHostServer` object (instance registry keyed by plugin instance ID) is the same
53
+
shape for a simpler case.
54
+
55
+
The callback model reuses this exactly, generalised from "one sink" to "one handler per capability":
56
+
57
+
- The driver defines one narrow trait per **capability shape** — not one generic `dyn Any` handler. A capability shape
58
+
corresponds to an operation already defined for plugins (e.g. `CompareContents`, `GenerateContent`) or one that
59
+
[006](/implementation_guides/pact_plugins/docs/proposals/proposal_006_field_level_matchers_and_generators) adds for field-level operations. Reusing the existing typed
60
+
messages (`CompareContentsRequest`/`Response`, `GenerateContentRequest`/`Response`, `ContentMismatch`, etc.) means the
61
+
driver→host interface and the driver→plugin interface share a data model — a capability looks the same regardless of
No gRPC is involved here: the driver and the registered handler are in the same process.
113
+
114
+
**Direction B — a plugin calling back.** This is the actual subject of this proposal. A plugin (external gRPC process
115
+
or, per [003](/implementation_guides/pact_plugins/docs/proposals/proposal_003_support_wasm_plugins), an in-process WASM module) needs a capability it doesn't implement
116
+
itself — for example a field-level plugin from 006 wants the host's standard `type` matcher for one field of a larger
117
+
document it otherwise owns. It calls back with a catalogue entry key. The driver resolves that key exactly the way
118
+
Direction A does:
119
+
120
+
-**`CORE`** → call the registered handler in-process, same as Direction A.
121
+
-**`PLUGIN`**, owned by a *different* plugin than the caller → forward the call over gRPC to that plugin, using the
122
+
existing `lookup_plugin` mechanism already used for driver→plugin calls. This makes cross-plugin capability calls
123
+
possible: plugin A can transparently use a capability plugin B registered, mediated entirely by the driver. Neither
124
+
plugin needs to know about the other directly.
125
+
-**Not found** → fail the callback with a clear error; the plugin surfaces this as a failure in the parent request.
29
126
30
127
### gRPC transport
31
128
32
-
The driver implements a `PactPluginHost` gRPC service on a local listener and passes its address to the plugin in `InitPlugin`. The plugin creates a standard gRPC client channel to that address. Each callback is a normal blocking unary RPC call from plugin to driver — no bi-directional streaming is required.
129
+
The driver implements the callback RPCs as an extension of the `PluginHost` gRPC service introduced in
130
+
[008](/implementation_guides/pact_plugins/docs/proposals/proposal_008_plugin_observability_and_logging) (currently just `Log`) on the same local listener, whose address is
131
+
already passed to the plugin via the `PACT_PLUGIN_HOST` environment variable. Each callback is a normal blocking unary
132
+
RPC from plugin to driver — no bi-directional streaming.
Each callback completes before the plugin continues. The call stack is synchronous and nested, which makes error propagation and deadline tracking straightforward.
172
+
Each callback completes before the plugin continues. The call stack is synchronous and nested, which makes error
173
+
propagation and deadline tracking straightforward.
174
+
175
+
**Baseline vs. optional:** the `PluginHost` service itself, and the ability to resolve a catalogue key via it, is a
176
+
**baseline V2 capability** per [005](/implementation_guides/pact_plugins/docs/proposals/proposal_005_plugin_capability_negotiation_and_versioning)'s classification rule —
177
+
its absence would make the protocol structurally incomplete, not just degrade one feature. Any V2 driver must expose
178
+
it. Individual entries behind it (e.g. whether `content-matcher/xml` specifically is registered) remain **optional** —
179
+
a plugin that needs a specific capability checks for it in `hostCapabilities` at `InitPlugin` time, same as today.
180
+
181
+
### Cycle detection and deadlines (gRPC only)
182
+
183
+
**Cycle detection.** A call-chain ID is generated by the driver at the root of any call that may trigger callbacks
and sent as gRPC request metadata (`pact-call-chain-id`). A plugin forwards the same chain ID as metadata on any
186
+
callback it makes. The driver keeps an in-memory stack per chain ID (`chain_id -> Vec<entry_key>`) in a new
187
+
`call_chain` module:
188
+
189
+
- Before dispatching a call for `entry_key` under `chain_id`, push `entry_key` onto that chain's stack; if it's already
190
+
present, reject immediately with a cycle error instead of forwarding.
191
+
- Pop it when the call completes (success or failure).
46
192
47
-
**Cycle detection is required for gRPC.** A call chain ID must be threaded through gRPC request metadata for any request that may trigger callbacks. If the driver receives a callback whose chain ID matches an in-flight request it is currently processing, the call is a cycle and must be rejected with a clear error. The plugin surfaces this as a failure in the parent request.
193
+
This applies identically to Direction B forwarding to another plugin: the driver pushes the target entry key before
194
+
forwarding, so a cycle across two or more plugins is caught the same way a self-cycle is.
48
195
49
-
**Deadlines:** a callback's deadline must be bounded by the remaining deadline of the parent request that triggered it. The driver enforces this when it receives the callback.
196
+
**Deadlines.** The driver sets an absolute deadline (`pact-deadline-ms`, Unix epoch milliseconds) as metadata on the
197
+
root call. Every hop — the plugin's outbound callback, and any forwarding the driver does on the plugin's behalf —
198
+
reads it, fails fast if it has already passed, and uses the remaining budget (`deadline_ms - now`) as the timeout for
199
+
its own call (`tonic`'s `.timeout()` on the client). A callback can never outlive the request that triggered it.
50
200
51
-
**Unavailable target:** if the driver's `PactPluginHost` service is unreachable, the plugin must fail the parent request with a clear error rather than hanging.
201
+
**Unavailable target:** if the driver's `PluginHost` service is unreachable, the plugin must fail the parent request
202
+
with a clear error rather than hanging.
52
203
53
204
### WASM transport
54
205
55
-
Host capabilities are exposed as host-exported functions that the WASM module imports at load time. Calls resolve via the native call stack — there is no network hop and no blocking concern. A true cycle would manifest as a stack overflow, which the WASM runtime handles. No explicit cycle detection is needed for WASM.
206
+
Host capabilities are exposed as host-exported functions that the WASM module imports at load time. The exported
207
+
function signatures correspond directly to the capability traits above (the same `CoreContentMatcher`/
208
+
`CoreContentGenerator` etc. are called directly — no gRPC serialisation, though the request/response types can still be
209
+
passed as serialised protobuf bytes across the WASM linear-memory boundary, since WASM has no native way to pass Rust
210
+
structs by reference). Calls resolve via the native call stack — there is no network hop and no blocking concern. A
211
+
true cycle would manifest as a stack overflow, which the WASM runtime handles. No explicit cycle detection or
212
+
call-chain metadata is needed for WASM.
56
213
57
-
##Non-goals for this proposal
214
+
### Lua transport (in-process, shipped ahead of WASM)
58
215
59
-
- Defining the detailed payload model for verification.
60
-
- Solving observability/logging by itself.
61
-
- Redesigning plugin discovery or packaging.
216
+
The Lua plugin runtime (`drivers/rust/driver/src/lua_plugin.rs`) already runs in-process and already registers host
217
+
functions into the script's global table (`logger`, `rsa_sign`, etc., see `register_host_functions`). The callback
218
+
model extends this the same way as WASM: a `host_compare_contents(entry_key, table)` Lua global that runs the same
219
+
resolver as Direction A/B and converts through the conversion helpers `lua_plugin.rs` already has
220
+
(`compare_request_to_lua`, `lua_to_compare_response`). Like WASM, no chain ID or cycle detection is needed — it's a
221
+
direct, synchronous Rust function call from the Lua VM's perspective.
222
+
223
+
The logical capability interface — what can be called, what parameters it takes, what it returns — is identical
224
+
between gRPC, WASM, and Lua. Only the transport differs.
225
+
226
+
### Sequencing
62
227
63
-
## WASM compatibility
228
+
This proposal ships as one vertical slice through the mechanism, not the full capability surface:
64
229
65
-
For WASM plugins, the callback model maps directly to host-exported functions imported by the module at load time. This is the established model for WASM host integration and works well: calls are synchronous, resolve via the call stack, and require no connection management or cycle detection logic in the plugin or driver.
230
+
1. The registry/trait pattern (`core_capabilities` module, generalising `PluginLogSink`).
231
+
2. The extended `PluginHost` gRPC service with cycle detection and deadline propagation.
232
+
3. Wiring the two existing `is_core()` branches in `content.rs` to call through instead of panicking.
233
+
4. The Lua and WASM host-function equivalents.
66
234
67
-
The logical capability interface — what can be called, what parameters it takes, what it returns — is identical between gRPC and WASM. Only the transport differs.
235
+
This is deliberately the smallest slice that proves the mechanism end-to-end, because `is_core()` already exists as an
236
+
unfinished seam. [006](/implementation_guides/pact_plugins/docs/proposals/proposal_006_field_level_matchers_and_generators) then adds new capability trait shapes
237
+
(field-level matching/generation) on top of the same registry and the same `PluginHost` extension pattern — no new
238
+
plumbing is needed for it. [009](/implementation_guides/pact_plugins/docs/proposals/proposal_009_host_provided_core_matching_and_generation) is, in turn, just "register the
239
+
standard Pact matcher/generator set as `CoreContentMatcher`/field-level handlers using this mechanism" — see that
240
+
proposal for details.
241
+
242
+
## Non-goals for this proposal
243
+
244
+
- Defining the detailed payload model for verification.
245
+
- Solving observability/logging by itself (see [008](/implementation_guides/pact_plugins/docs/proposals/proposal_008_plugin_observability_and_logging), already implemented).
246
+
- Redesigning plugin discovery or packaging.
247
+
- Defining the field-level operation shapes themselves (see [006](/implementation_guides/pact_plugins/docs/proposals/proposal_006_field_level_matchers_and_generators)) —
248
+
this proposal defines the mechanism they will be registered and invoked through.
68
249
69
-
## Open questions
250
+
## Resolved questions
70
251
71
-
- Which specific host capabilities should be exposed first? This will be driven by the needs of [006](/implementation_guides/pact_plugins/docs/proposals/proposal_006_field_level_matchers_and_generators) and [009](/implementation_guides/pact_plugins/docs/proposals/proposal_009_host_provided_core_matching_and_generation).
252
+
-**Which specific host capabilities should be exposed first?** Content-level `CompareContents`/`GenerateContent`,
253
+
because the `is_core()` seam for these already exists and is the smallest slice that exercises the full mechanism
254
+
(registry, cycle detection, deadlines, all three transports). Field-level capabilities follow once
255
+
[006](/implementation_guides/pact_plugins/docs/proposals/proposal_006_field_level_matchers_and_generators) defines their shape.
256
+
-**Generic envelope vs. typed capabilities?** Typed, one trait/RPC pair per capability shape, reusing existing
257
+
message types (`CompareContentsRequest`, `ContentMismatch`, etc.) rather than introducing a parallel generic
258
+
request/response model.
259
+
-**How is the dependency cycle avoided?** By generalising the `PluginLogSink` registration pattern from
260
+
[008](/implementation_guides/pact_plugins/docs/proposals/proposal_008_plugin_observability_and_logging): the driver defines traits and a registry, the embedding framework
261
+
implements and registers handlers at startup, and the driver never has a compile-time dependency on the
262
+
implementation.
263
+
-**Are cross-plugin calls (plugin A using a capability plugin B provides) in scope?** Yes. The resolver that answers
264
+
"who provides this catalogue entry" doesn't distinguish CORE-forwarding from PLUGIN-forwarding as separate
265
+
mechanisms — supporting one means supporting both, and cycle detection is required as soon as any callback exists
0 commit comments