{"record":{"id":"532b1905619399c0","repo":"apache/beam","slug":"multiple-on-timer-callbacks-registered-for-r","errorCode":null,"errorMessage":"Multiple on_timer callbacks registered for %r.","messagePattern":"Multiple on_timer callbacks registered for %r\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"sdks/python/apache_beam/transforms/userstate.py","lineNumber":239,"sourceCode":"  This decorator allows a user to specify an on_timer processing method\n  in a stateful DoFn.  Sample usage::\n\n    class MyDoFn(DoFn):\n      TIMER_SPEC = TimerSpec('timer', TimeDomain.WATERMARK)\n\n      @on_timer(TIMER_SPEC)\n      def my_timer_expiry_callback(self):\n        logging.info('Timer expired!')\n  \"\"\"\n\n  if not isinstance(timer_spec, TimerSpec):\n    raise ValueError('@on_timer decorator expected TimerSpec.')\n\n  def _inner(method: CallableT) -> CallableT:\n    if not callable(method):\n      raise ValueError('@on_timer decorator expected callable.')\n    if timer_spec._attached_callback:\n      raise ValueError(\n          'Multiple on_timer callbacks registered for %r.' % timer_spec)\n    timer_spec._attached_callback = method\n    return method\n\n  return _inner\n\n\ndef get_dofn_specs(dofn: 'DoFn') -> tuple[set[StateSpec], set[TimerSpec]]:\n  \"\"\"Gets the state and timer specs for a DoFn, if any.\n\n  Args:\n    dofn (apache_beam.transforms.core.DoFn): The DoFn instance to introspect for\n      timer and state specs.\n  \"\"\"\n\n  # Avoid circular import.\n  from apache_beam.runners.common import MethodWrapper\n  from apache_beam.transforms.core import _DoFnParam","sourceCodeStart":221,"sourceCodeEnd":257,"githubUrl":"https://github.com/apache/beam/blob/12126d8942aaf848030c478b4c6a28c6af861c66/sdks/python/apache_beam/transforms/userstate.py#L221-L257","documentation":"Each TimerSpec can have exactly one expiry callback: _inner checks timer_spec._attached_callback and raises ValueError if it is already set. Registering @on_timer(SAME_SPEC) on two methods would leave the runner unable to know which callback to fire, so the second registration fails.","triggerScenarios":"Decorating two methods in the same DoFn with @on_timer(TIMER_SPEC) using the same spec; accidentally duplicating a decorated method (or importing/defining the callback twice); re-running the decorator on the same spec object at module reload in an unusual setup.","commonSituations":"Copy-paste of a timer callback where the new method still references the old spec constant; refactoring that split one callback into two but kept the shared TimerSpec; wanting both watermark and realtime timers but reusing one spec instead of creating two.","solutions":["Create a separate TimerSpec for each callback: TIMER_SPEC_A, TIMER_SPEC_B, each with its own @on_timer decorator","Remove the duplicate @on_timer registration so each spec has exactly one callback","If you need both event-time and processing-time behavior in one DoFn, define two specs with different TimeDomains"],"exampleFix":"// before\n@on_timer(TIMER_SPEC)\ndef cb_a(self): ...\n@on_timer(TIMER_SPEC)\ndef cb_b(self): ...\n// after\nTIMER_SPEC_A = TimerSpec('timer_a', TimeDomain.WATERMARK)\nTIMER_SPEC_B = TimerSpec('timer_b', TimeDomain.REAL_TIME)\n@on_timer(TIMER_SPEC_A)\ndef cb_a(self): ...\n@on_timer(TIMER_SPEC_B)\ndef cb_b(self): ...","handlingStrategy":"validation","validationCode":"specs_seen = set()\nfor m in (cb_a, cb_b):\n    assert getattr(m, '_timer_spec', None) not in specs_seen\n    specs_seen.add(getattr(m, '_timer_spec', None))","typeGuard":null,"tryCatchPattern":"try:\n    _ = MyDoFn()\nexcept ValueError as e:\n    if 'Multiple on_timer callbacks' in str(e):\n        log.error('two methods share the same TimerSpec; split the spec')","preventionTips":["One TimerSpec per expiry callback; create distinct specs for distinct methods","Never copy-paste a @on_timer(SPEC) block without changing the spec constant","Use different TimeDomains intentionally with separate specs"],"tags":["python","apache-beam","timers","decorator","duplicate-registration"],"backgroundTag":"conflicting-config-options","analyzedSha":"12126d8942aaf848030c478b4c6a28c6af861c66","analyzedAt":"2026-09-13T01:50:10.254Z","contentChangedAt":"2026-09-13T01:50:10.254Z","schemaVersion":2},"datasetVersion":"2026-09-20T03:17:13.778Z"}