-
Notifications
You must be signed in to change notification settings - Fork 49
Description
Summary
The runtime currently exposes a single EventPublisher
contract and a single withEventPublisher(...)
builder hook. As a result, engine lifecycle CloudEvents (e.g., io.serverlessworkflow.workflow.*
, io.serverlessworkflow.task.*
) and domain/user CloudEvents produced by workflow logic are delivered to the same publisher list. Downstream integrations often need to route these categories to different sinks (topics, exchanges, observability pipelines) or disable publishing of one category while keeping the other.
This issue proposes adding a first-class separation (or a simple filter mechanism) so SDK users can register publishers specifically for lifecycle vs. domain events without ad‑hoc filtering.
Motivation
- Operational clarity: Lifecycle CEs are primarily observability/monitoring signals; domain CEs represent business integration. Operators typically send them to different systems.
- Config simplicity: Today, frameworks/integrations must implement their own filters (e.g., match
type
prefixio.serverlessworkflow.*
) and duplicate publisher wiring. - Selective publishing: Users may want domain events only (or vice‑versa) without changing engine internals.
- Backwards compatibility: We can keep existing behavior by default and opt‑in to specialization.
Proposed changes
1) Introduce an event category split
Add an enum and utility to standardize detection of lifecycle events:
public enum EventCategory { DOMAIN, LIFECYCLE }
public final class LifecycleEvents {
public static final String PREFIX = "io.serverlessworkflow."; // or more specific if desired
public static boolean isLifecycle(CloudEvent ce) {
String t = ce.getType();
return t != null && t.startsWith(PREFIX);
}
private LifecycleEvents() {}
}
2) New builder hooks (non‑breaking)
Add optional specialized registration methods on WorkflowApplication.Builder
:
public Builder withLifecycleEventPublisher(EventPublisher publisher);
public Builder withDomainEventPublisher(EventPublisher publisher);
Internally, the WorkflowApplication
would maintain two lists: lifecyclePublishers
and domainPublishers
, in addition to the existing eventPublishers
for backwards compatibility.
Routing rules:
-
If
LifecycleEvents.isLifecycle(ce)
istrue
:- publish to
lifecyclePublishers
(if any), otherwise fall back toeventPublishers
(to avoid breaking existing apps)
- publish to
-
Else (domain CE):
- publish to
domainPublishers
(if any), otherwise fall back toeventPublishers
- publish to
This preserves existing behavior while enabling precise targeting.
3) Alternative: filter-aware registration (single list)
Instead of separate lists, allow publishers to declare the categories they accept, or register with a predicate:
public interface EventPublisher extends AutoCloseable {
CompletableFuture<Void> publish(CloudEvent event);
default Set<EventCategory> categories() { return EnumSet.allOf(EventCategory.class); }
void close();
}
// or on the builder:
public Builder withEventPublisher(EventPublisher publisher, Predicate<CloudEvent> filter);
This is slightly more flexible but a little more complex to reason about in common cases.
API sketch (separate lists)
public final class WorkflowApplication {
// ...
private final Collection<EventPublisher> eventPublishers; // legacy
private final Collection<EventPublisher> lifecyclePublishers; // new
private final Collection<EventPublisher> domainPublishers; // new
void publishLifecycle(CloudEvent ce) {
Collection<EventPublisher> targets = lifecyclePublishers.isEmpty() ? eventPublishers : lifecyclePublishers;
targets.forEach(p -> p.publish(ce));
}
void publishDomain(CloudEvent ce) {
Collection<EventPublisher> targets = domainPublishers.isEmpty() ? eventPublishers : domainPublishers;
targets.forEach(p -> p.publish(ce));
}
}
public final class Builder {
public Builder withLifecycleEventPublisher(EventPublisher p) { /* add to lifecyclePublishers */ return this; }
public Builder withDomainEventPublisher(EventPublisher p) { /* add to domainPublishers */ return this; }
}
Call sites:
- Lifecycle emissions (e.g.,
AbstractLifeCyclePublisher.publish(...)
) callapplication.publishLifecycle(ce)
. - Domain task emissions (emit/listen actions) call
application.publishDomain(ce)
. - If refactoring all call sites is not immediately feasible, a temporary internal shim can route based on
LifecycleEvents.isLifecycle(ce)
.
Backwards compatibility
- Existing apps using only
withEventPublisher(...)
continue to receive both lifecycle and domain events. - New methods are additive.
- Optional: expose a flag to completely disable lifecycle CE generation (already provided by
disableLifeCycleCEPublishing()
), which remains orthogonal to publishing.
Configuration / DX (non-normative example)
Downstream integrations can map publishers to different channels/sinks. For example, in a messaging adapter a user might configure:
domain
→ topicflow-out
lifecycle
→ topicflow-lifecycle-out
Without this feature, adapters must filter event types manually.
Alternatives considered
- Rely on type-prefix filtering in adapters — works but repeats logic across integrations and lacks a canonical definition of lifecycle vs. domain in the SDK.
- Single list + predicate only — more flexible but less obvious for common use; still acceptable if you prefer minimal API surface.
Open questions
- Should lifecycle detection be based on a type prefix, or should the SDK mark such events explicitly (e.g., via metadata/extension)?
- Should the SDK expose a well-known extension attribute on lifecycle CEs to avoid relying on
type
naming conventions? - Do we want a hard separation (no fallback to
eventPublishers
) or keep the fallback for compatibility?
Requested outcome
-
Agree on the approach (separate lists vs. predicate-based registration).
-
If agreed, I can contribute a PR with:
- Builder API additions
- Internal routing + fallback
- Minimal refactor of lifecycle emission sites
- Tests proving split delivery & backwards compatibility