quarkusio/quarkus · error · IllegalStateException

Function for trigger

Error message

Function for trigger '${trigger}' has multiple matching invokers

What it means

KnativeEventsBindingRecorder.init registers invokers keyed by trigger (ce-type) plus optional attribute filters. If two invokers for the same trigger have overlapping filters so a single incoming event could match more than one function, registration fails with IllegalStateException to prevent ambiguous dispatch.

Solutions

  1. Remove or rename the duplicate function's trigger type so each trigger maps to one invoker.
  2. Differentiate the functions' ce-attribute filters so they are mutually exclusive.
  3. Ensure only one bean exposes a given trigger; delete the stale duplicate class.
  4. Use quarkus.funqy.export to select a single function if multiple must exist in code.

Example fix

// before
@OnTrigger(type = "com.example.created") public void f1(String s) {}
@OnTrigger(type = "com.example.created") public void f2(String s) {}
// after
@OnTrigger(type = "com.example.created") public void f1(String s) {}
@OnTrigger(type = "com.example.updated") public void f2(String s) {}
Defensive patterns

Strategy: validation

Validate before calling

// At startup, verify no two functions share the same trigger type
Map<String, Long> counts = functions.stream()
    .collect(Collectors.groupingBy(f -> f.triggerType(), Collectors.counting()));
List<String> dupes = counts.entrySet().stream().filter(e -> e.getValue() > 1).map(Map.Entry::getKey).toList();
if (!dupes.isEmpty()) throw new IllegalStateException("Duplicate triggers: " + dupes);

Type guard

boolean triggerIsUnique(String trigger, List<FunctionDef> all, FunctionDef self) {
    return all.stream().filter(f -> f.triggerType().equals(trigger)).count() == 1;
}

Try / catch

try { app.start(); } catch (IllegalStateException e) { if (e.getMessage().contains("multiple matching invokers")) { log.error("Duplicate trigger/filters on functions - disambiguate ce-type or filters", e); } throw e; }

Prevention

When it happens

Trigger: Two Funqy functions annotated with the same Knative trigger ce-type and identical/overlapping ce-attribute filters are registered during staticInit; hasSameFilters finds a conflict and init throws.

Common situations: Copy-pasting a @Function with the same @OnTrigger/@OnEvent type and filters; two deployments of the same function class; same ce-type intentionally used with filters that end up identical instead of disjoint.

Related errors


AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05). Data as JSON: /api/errors/e7c64af1a791bf81. Report an issue: GitHub.

Appendix: source

Thrown at extensions/funqy/funqy-knative-events/runtime/src/main/java/io/quarkus/funqy/runtime/bindings/knative/events/KnativeEventsBindingRecorder.java:106

                    trigger = annotation.trigger();
                } else {
                    trigger = invoker.getName();
                }
                filter = filter(invoker.getName(), annotation);
            } else {
                trigger = invoker.getName();
                filter = Collections.emptyList();
            }
            invokersFilters.put(invoker.getName(), filter);
            typeTriggers.compute(trigger, (k, v) -> {
                if (v == null) {
                    v = new ArrayList<>();
                }
                // validate if there are no conflicts for the same type (trigger) and defined filters
                // as resolution based on trigger (ce-type) and optional filters (on ce-attributes) can return only
                // one function invoker
                if (v.stream().anyMatch(i -> hasSameFilters(i.getName(), invokersFilters.get(i.getName()), filter))) {
                    throw new IllegalStateException("Function for trigger '" + trigger + "' has multiple matching invokers");
                }

                v.add(invoker);
                return v;
            });

            if (invoker.hasInput()) {
                Type inputType = invoker.getInputType();

                if (CloudEvent.class.equals(Reflections.getRawType(inputType))) {
                    if (inputType instanceof ParameterizedType) {
                        Type[] params = ((ParameterizedType) inputType).getActualTypeArguments();
                        if (params.length == 1) {
                            inputType = params[0];
                            invoker.getBindingContext().put(INPUT_CE_DATA_TYPE, inputType);
                        }
                    } else {
                        throw new RuntimeException("When using CloudEvent<> generic parameter must be used.");

View on GitHub (pinned to e1c734241f)