regarmukesh3g opened a new pull request, #72576:
URL: https://github.com/apache/airflow/pull/72576

   `Timer` in `shared/observability/.../metrics/protocols.py` declares two
   attributes but never creates them:
   
   ```python
   class Timer(TimerProtocol):
       _start_time: float | None    # bare annotation — creates no attribute
       duration: float | None       # bare annotation — creates no attribute
   
       def __init__(self, real_timer=None):
           self.real_timer = real_timer   # the only attribute actually set
   ```
   
   A bare annotation at class level records a type in `__annotations__` for type
   checkers; it does not create an attribute. So after `Timer()` the instance 
has
   only `real_timer` — `_start_time` and `duration` do not exist until `start()`
   and `stop()` assign them.
   
   ### Why this is a bug rather than a "call start() first" contract
   
   `stop()` already guards its duration calculation:
   
   ```python
   def stop(self, send: bool = True) -> None:
       if self._start_time is not None:      # reads a possibly-nonexistent 
attribute
           self.duration = 1000.0 * (time.perf_counter() - self._start_time)
   ```
   
   That `is not None` check only makes sense if `_start_time` is expected to
   sometimes *be* `None` — i.e. "the timer was never started, skip the math". 
The
   intent is clearly to tolerate that state. But `_start_time` can never be 
`None`:
   it is either missing entirely (before `start()`) or a float (after). The 
guard
   can therefore never do what it was written to do, and raises instead.
   
   The sibling `_DualTimer` in the same package already initializes both 
attributes
   in its constructor (`self.duration: float | None = None`), so this also 
brings
   `Timer` in line with it.
   
   ### Consequences
   
   - `stop()` on a timer that was never started raises
     `AttributeError: 'Timer' object has no attribute '_start_time'`.
   - Reading `t.duration` on a timer that never ran raises `AttributeError`, 
even
     though the class docstring documents reading `duration` as normal usage.
   
   This matters on error paths that stop a timer they may not have started. The
   scheduler stops its timers inside `except OperationalError` blocks
   (`scheduler_job_runner.py:851` and `:2081`):
   
   ```python
   try:
       ...
       timer.stop(send=True)
   except OperationalError as e:
       timer.stop(send=False)   # AttributeError here masks the real DB error
       raise e
   ```
   
   If the failure happens before the timer starts, the `AttributeError` raised 
from
   inside the handler replaces the `OperationalError` actually being handled.
   
   ### The change
   
   Initialize both attributes in `__init__`, so the existing guard can work:
   
   ```python
   self._start_time = None
   self.duration = None
   ```
   
   Normal start/stop usage is unaffected.
   
   ### Testing
   
   Adds `TestTimer` covering the un-started states. All four tests fail with
   `AttributeError` without the change and pass with it. The rest of
   `test_stats.py` (101 tests) is unaffected.
   
   Note: 
`test_otel_logger.py::TestOtelMetrics::test_reinit_after_fork_exports_metrics`
   fails locally, but it fails identically on unmodified `main` and is 
unrelated to
   this change.
   
   ---
   
   ##### Was generative AI tooling used to co-author this PR?
   
   - [X] Yes — Claude Code (Opus 5)
   
   Generated-by: Claude Code (Opus 5) following [the 
guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to