apache/beam · error · RuntimeException

Unable to construct @OnTimer invoker for ${className}

Error message

Unable to construct @OnTimer invoker for ${className}

What it means

ByteBuddyOnTimerInvokerFactory.forTimer generates and loads an invoker class for a DoFn's @OnTimer method via ByteBuddy, then reflectively instantiates it. Any failure during generation, classloading, constructor invocation, or the invoked method's static init (InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, SecurityException, ExecutionException) is rethrown as this RuntimeException with the DoFn class name. It means Beam could not produce the timer callback machinery for that specific DoFn.

Source

Thrown at sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/ByteBuddyOnTimerInvokerFactory.java:79

  @Override
  public <InputT, OutputT> OnTimerInvoker<InputT, OutputT> forTimer(
      DoFn<InputT, OutputT> fn, String timerId) {

    @SuppressWarnings("unchecked")
    Class<? extends DoFn<?, ?>> fnClass = (Class<? extends DoFn<?, ?>>) fn.getClass();
    try {
      OnTimerMethodSpecifier onTimerMethodSpecifier =
          OnTimerMethodSpecifier.forClassAndTimerId(fnClass, timerId);
      Constructor<?> constructor = constructorCache.get(onTimerMethodSpecifier);

      return (OnTimerInvoker<InputT, OutputT>) constructor.newInstance(fn);
    } catch (InstantiationException
        | IllegalAccessException
        | IllegalArgumentException
        | InvocationTargetException
        | SecurityException
        | ExecutionException e) {
      throw new RuntimeException(
          String.format(
              "Unable to construct @%s invoker for %s",
              OnTimer.class.getSimpleName(), fn.getClass().getName()),
          e);
    }
  }

  public <InputT, OutputT> OnTimerInvoker<InputT, OutputT> forTimerFamily(
      DoFn<InputT, OutputT> fn, String timerId) {

    @SuppressWarnings("unchecked")
    Class<? extends DoFn<?, ?>> fnClass = (Class<? extends DoFn<?, ?>>) fn.getClass();
    try {
      OnTimerMethodSpecifier onTimerMethodSpecifier =
          OnTimerMethodSpecifier.forClassAndTimerId(fnClass, timerId);
      Constructor<?> constructor = constructorTimerFamilyCache.get(onTimerMethodSpecifier);

      return (OnTimerInvoker<InputT, OutputT>) constructor.newInstance(fn);

View on GitHub (pinned to 12126d8942)

Solutions

  1. Make the DoFn a public static top-level class (not an inner/anonymous class) so the generated invoker can instantiate it.
  2. Read the cause chain (getCause) to identify whether it's generation, classloading, or constructor failure, and fix that specific layer.
  3. Ensure the JVM/ByteBuddy versions are compatible; set -Dnet.bytebuddy.experimental=true if running on a very new JDK.
  4. Check classloader setup in the runner so Beam and the user jar can see each other's classes (e.g. child-first loading config).

Example fix

// before: anonymous inner DoFn
pipeline.apply(..., new DoFn<String, String>() { @OnTimer("t") public void onTimer(OnTimerContext ctx) {...} });
// after: public static class
public class MyDoFn extends DoFn<String, String> {
  @OnTimer("t")
  public void onTimer(OnTimerContext ctx) { ... }
}
pipeline.apply(..., ParDo.of(new MyDoFn()));
Defensive patterns

Strategy: try-catch

Validate before calling

if (!Modifier.isPublic(myDoFn.getClass().getModifiers())
    || myDoFn.getClass().isAnonymousClass()
    || (myDoFn.getClass().isLocalClass() && !Modifier.isStatic(myDoFn.getClass().getModifiers()))) {
  throw new IllegalStateException("DoFn must be a public top-level or static nested class");
}

Try / catch

try {
  invoker = ByteBuddyOnTimerInvokerFactory.forTimer(fn, timerId);
} catch (RuntimeException e) {
  Throwable cause = e.getCause();
  log.error("Failed to build @OnTimer invoker for " + fn.getClass().getName()
      + "; cause=" + cause, e);
  throw e;
}

Prevention

When it happens

Trigger: Calling forTimer (directly or through timer expiry dispatch) for a DoFn class whose generated invoker cannot be constructed: the DoFn is an inner/non-static class, has no accessible no-arg context, the generated class fails verification, or ByteBuddy generation fails for the class's visibility/package.

Common situations: Anonymous or non-static inner DoFn classes that cannot be instantiated reflectively; DoFn classes in restricted classloader contexts (Flink/Spark/Dataflow user-jar classloaders); JDK compatibility issues where ByteBuddy can't subclass/proxy under a newer JVM without the right ByteBuddy experimental flag.

Related errors


AI-assisted analysis of apache/beam@12126d8942 (2026-09-13). Data as JSON: /api/errors/754c9597251c14cc. Report an issue: GitHub.