Skip to content

Conversation

@desertaxle
Copy link
Collaborator

@desertaxle desertaxle commented Oct 31, 2025

Summary

Adds comprehensive execution state tracking and progress monitoring capabilities to Docket, enabling real-time observability of task execution through Redis pub/sub.

  • Implements ExecutionProgress class with instance attributes for tracking task progress
  • Adds execution state machine: SCHEDULED → QUEUED → RUNNING → COMPLETED/FAILED
  • Provides pub/sub event publishing for state transitions and progress updates
  • Moves scheduling logic from Docket to Execution class for better encapsulation
  • Add docket watch CLI to view progress for a single task

Demo

Screen.Recording.2025-11-03.at.10.17.22.AM.mov

Key Changes

Execution State Management

  • Renamed PENDING state to QUEUED throughout codebase
  • Immediate tasks now transition directly to QUEUED state
  • Future tasks start as SCHEDULED, then move to QUEUED when due
  • Worker scheduler now publishes pub/sub events when moving tasks from scheduled to queued

ExecutionProgress Class

  • Added instance attributes: current, total, message, updated_at
  • All Redis-modifying methods now update both Redis and instance attributes atomically
  • Added create() classmethod for initialization
  • Progress data automatically deleted on task completion/failure

Scheduling Refactor

  • Moved scheduling logic from Docket._schedule() to Execution.schedule()
  • Atomic operation combining stream insertion and state record writing via Lua script
  • Removed redundant set_scheduled() and set_queued() methods
  • State transitions now handled automatically by scheduling logic

Pub/Sub Event Publishing

  • State changes publish to {docket}:state:{key} channel
  • Progress updates publish to {docket}:progress:{key} channel
  • Both Execution.subscribe() and ExecutionProgress.subscribe() methods available
  • Worker scheduler Lua script now publishes events for scheduled→queued transitions

Closes #88

🤖 Generated with Claude Code

desertaxle and others added 2 commits October 31, 2025 14:20
Ensures the worker scheduler emits pub/sub events when moving tasks from
scheduled to queued state, providing consistent event monitoring across
all state transitions. Removes the redundant set_queued() method since
state transitions are now automated in the scheduling Lua scripts.

Also improves type annotations to achieve 100% type completeness score
in pyright verification by adding explicit return types to subscribe()
methods and list type annotations in tests.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@claude
Copy link
Contributor

claude bot commented Oct 31, 2025

Claude finished @desertaxle's task —— View job


PR Review: Add execution state tracking and progress monitoring

I'm reviewing this comprehensive PR that adds execution state tracking and progress monitoring capabilities to Docket.

Review Checklist

  • Review ExecutionProgress implementation
  • Examine state transition logic
  • Check pub/sub event system
  • Review scheduling refactor (Docket → Execution)
  • Analyze test coverage quality
  • Performance considerations
  • Security review
  • Review existing feedback and suggestions

Summary

This is an excellent, well-architected implementation that adds comprehensive execution state tracking and real-time progress monitoring to Docket. The code quality is high, test coverage remains at 100%, and the design patterns are solid.

Strengths ✅

Architecture & Design

  • Clean separation of concerns: Moving scheduling logic from Docket to Execution class improves encapsulation
  • State machine implementation: Clear transitions through SCHEDULED → QUEUED → RUNNING → COMPLETED/FAILED
  • Atomic operations: Excellent use of Lua scripts for atomic Redis operations
  • Progress tracking: Well-designed ExecutionProgress class with intuitive API

Code Quality

  • Type safety: Comprehensive type hints throughout
  • Documentation: Excellent docstrings and inline comments
  • Error handling: Robust validation (e.g., total >= 1 check)
  • Test coverage: Maintained at 100% with thorough test scenarios

Performance

  • Atomic Lua scripts: Efficient Redis operations minimize round trips
  • TTL management: Smart automatic cleanup of execution state data
  • Pub/sub efficiency: Events only published when needed

Review Comments & Suggestions 🔍

1. Default Progress Total Value

In ExecutionProgress.__init__ (execution.py:127), the default total is 1, but in other places it's 100. Consider standardizing:

# execution.py:127 - Currently 1
self.total: int = 1

# execution.py:236, 249 - Uses 100
self.total = 100

# worker.py:596 - Sets to 100
redis.call('HSET', progress_key, 'current', '0', 'total', '100')

Recommendation: Set default to 1 consistently as suggested by reviewer, since it's more intuitive for simple use cases.

2. Progress Total Validation

The validation in set_total() (execution.py:152) is good. Consider adding similar validation in the constructor to prevent direct attribute manipulation:

def __init__(self, docket: "Docket", key: str, initial_total: int = 1) -> None:
    if initial_total < 1:
        raise ValueError("Total must be at least 1")
    self.total = initial_total

3. Context Manager Support

The reviewer asked about context managers. While not implemented, this could be a nice future enhancement for automatic progress cleanup:

@asynccontextmanager
async def track_progress(execution: Execution):
    """Context manager for automatic progress tracking."""
    try:
        yield execution.progress
    finally:
        # Progress is already auto-deleted on completion/failure
        pass

4. Event Type Definitions

Good use of TypedDict for ProgressEvent and StateEvent. Consider adding a discriminated union type for better type narrowing:

ExecutionEvent = Union[StateEvent, ProgressEvent]

5. Legacy Key Cleanup

Excellent handling of backwards compatibility with TODO comments marking legacy code for removal. The migration path is clear and safe.

Minor Issues 🔬

1. Redundant Comments

Some AI-generated breadcrumb comments should be removed (docket.py:439, as noted by reviewer).

2. Progress Subscribe Coverage

The subscribe() methods have pragma: no cover on the async iteration loops. While understandable for testing async generators, consider adding integration tests for these paths.

3. TTL Configuration

Good addition of configurable execution_ttl. Consider documenting recommended values based on use cases in the docstring.

Security Considerations 🔒

  • ✅ No credential exposure in logs or events
  • ✅ Proper input validation for progress values
  • ✅ Safe Redis key construction preventing injection
  • ✅ Atomic operations prevent race conditions
  • ✅ TTL ensures automatic cleanup of sensitive data

Performance Considerations ⚡

  • ✅ Lua scripts minimize Redis round trips
  • ✅ Efficient pub/sub with targeted channels
  • ✅ Smart TTL management prevents memory bloat
  • ✅ Atomic state transitions avoid race conditions
  • ⚠️ Consider adding a maximum message size check for progress messages to prevent memory issues with very large status messages

Test Coverage Analysis 🧪

Test coverage is excellent at 100%. Tests comprehensively cover:

  • State transitions through full lifecycle
  • Progress tracking with increments and messages
  • Edge cases (invalid values, missing data)
  • TTL verification
  • Pub/sub event publishing
  • CLI integration with watch command

Recommendations 📝

  1. Immediate: Fix the default total value inconsistency (use 1 throughout)
  2. Immediate: Add total validation as suggested by reviewer
  3. Consider: Add maximum length validation for progress messages
  4. Future: Consider adding context manager support for progress tracking
  5. Future: Add metrics for tracking average task duration by state

Conclusion

This is a well-executed feature addition that significantly enhances Docket's observability capabilities. The code is production-ready with only minor adjustments needed. The architecture is solid, the implementation is clean, and the test coverage is comprehensive.

Verdict: ✅ Approved with minor suggestions

Excellent work on this significant enhancement to Docket! The execution state tracking and progress monitoring features are well-designed and thoroughly tested.

@github-actions
Copy link

github-actions bot commented Oct 31, 2025

📚 Documentation has been built for this PR!

You can download the documentation directly here:
https://github.com/chrisguidry/docket/actions/runs/19083904930/artifacts/4467514519

Copy link
Owner

@chrisguidry chrisguidry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love where this is heading!!!

initiating_context = initiating_span.get_span_context()
return [trace.Link(initiating_context)] if initiating_context.is_valid else []

async def schedule(self, replace: bool = False) -> None:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wow this is a cool refactoring, didn't think of this!

desertaxle and others added 10 commits November 3, 2025 10:20
Adds module-level docstring explaining progress tracking concepts, detailed
function docstrings for long_task and main, and inline comments throughout
to help users understand and adapt the example to their own use cases.

Documentation covers:
- Progress dependency injection pattern
- ExecutionProgress API (increment, set_message)
- Real-time monitoring with docket watch command
- Task scheduling and state lifecycle
- Complete setup from Redis to Worker to execution

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Reduces Redis key count from 4 to 2-3 per task by storing known marker and
stream_id as fields in the execution state hash instead of separate keys.

Changes:
- Immediate tasks: 4→2 keys (50% reduction)
- Future tasks: 4→3 keys (25% reduction)

Implementation:
- Store 'known' field in {docket}:runs:{key} hash for duplicate prevention
- Store 'stream_id' field in {docket}:runs:{key} hash for cancellation
- Delete known/stream_id fields from runs hash when task completes to allow rescheduling
- Update retry logic to use replace=True to handle existing runs hash

Backwards compatibility:
- Check both new (HGET runs hash) and legacy (GET separate key) locations
- Clean up legacy keys during replacement and cancellation
- All backwards compat code marked with TODO for removal in v0.14.0

Benefits:
- Fewer Redis keys to manage and clean up
- Reduced memory overhead (~200-300 bytes per task)
- Simpler cleanup logic (fewer DEL operations)
- Better data locality (related fields in same hash)

Testing:
- All 349 tests passing including legacy compatibility test
- Fixed test_redis_key_cleanup_cancelled_task to use correct assertion method

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Consolidates worker operations when claiming a task into a single atomic
Lua script, reducing Redis round trips from 3-4 to 1 operation.

Changes:
- Add claim_and_run() method that atomically:
  - Sets state to RUNNING with worker name and timestamp
  - Initializes progress tracking (current=0, total=100)
  - Deletes known/stream_id fields to allow task rescheduling
  - Cleans up legacy keys for backwards compatibility
- Remove redundant set_running() method
- Update worker to use claim_and_run() instead of set_running()
- Update all tests to use claim_and_run()

Benefits:
- Single Redis round trip (75% reduction)
- Atomic operation prevents partial state updates
- Consistent with schedule() Lua script pattern
- Better performance and reliability

Testing:
- All 349 tests passing
- All state transitions verified
- Progress initialization working correctly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Renames state transition methods to be more explicit and self-documenting:
- set_completed() → mark_as_completed()
- set_failed() → mark_as_failed()

This improves API clarity and consistency with the new claim_and_run() method.

Changes:
- Rename method definitions in Execution class
- Update 2 call sites in Worker
- Update 2 call sites in tests

All 349 tests passing with 98% coverage.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Renames the method to better reflect its actual behavior - it claims the
task and transitions to RUNNING state, but does not execute the function.

Changes:
- claim_and_run() → claim() in Execution class
- Update Worker call site
- Update 6 test call sites

The new name is more accurate since the method only claims the task for
execution; the actual function execution happens later in Worker._execute().

All 349 tests passing with 98% coverage.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@codecov-commenter
Copy link

codecov-commenter commented Nov 4, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (eb8269f) to head (8d0029f).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##              main      #181    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files           34        36     +2     
  Lines         5216      6100   +884     
  Branches       265       295    +30     
==========================================
+ Hits          5216      6100   +884     
Flag Coverage Δ
python-3.10 100.00% <100.00%> (ø)
python-3.11 98.96% <99.89%> (+0.17%) ⬆️
python-3.12 100.00% <100.00%> (ø)
python-3.13 100.00% <100.00%> (ø)
python-3.14 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/docket/__init__.py 100.00% <100.00%> (ø)
src/docket/agenda.py 100.00% <ø> (ø)
src/docket/cli.py 100.00% <100.00%> (ø)
src/docket/dependencies.py 100.00% <100.00%> (ø)
src/docket/docket.py 100.00% <100.00%> (ø)
src/docket/execution.py 100.00% <100.00%> (ø)
src/docket/worker.py 100.00% <100.00%> (ø)
tests/cli/test_watch.py 100.00% <100.00%> (ø)
tests/test_dependencies.py 100.00% <ø> (ø)
tests/test_docket.py 100.00% <100.00%> (ø)
... and 5 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

desertaxle and others added 3 commits November 4, 2025 11:22
The watch command was failing in CI when tasks completed quickly because:
- subscribe() would sync() and find task already completed
- Progress data would be deleted on completion
- Watch would never see progress events

Solution: subscribe() now emits initial progress event along with initial
state event, ensuring subscribers capture progress state before deletion.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
The conditional check was preventing progress events from being emitted
when tasks hadn't set progress (current=None, total=100 defaults).

Now always emits initial progress event to ensure watch command and
other subscribers can display progress state consistently.

Also fixes test expecting 3 progress events (initial + 2 updates).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@desertaxle desertaxle marked this pull request as ready for review November 4, 2025 17:39
desertaxle and others added 3 commits November 4, 2025 11:42
The watch command was checking worker_name to determine if a task had
started, but worker_name timing varied across Python versions in CI.

Now checks execution.started_at which is set atomically when task starts,
providing a more reliable indicator that's available in initial state event.

Fixes Python 3.12-specific test failures in CI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
desertaxle and others added 7 commits November 4, 2025 12:02
The redis context was incorrectly dedented outside the Worker context.
This caused branch coverage issues in CI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Copy link
Owner

@chrisguidry chrisguidry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a beaut!

from rich.console import Console
from rich.layout import Layout
from rich.live import Live
from rich.progress import (
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️ what a great toolkit

TaskFunction,
)

# Run class has been consolidated into Execution
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably AI breadcrumb comments

# ARGV: task_key
"""
local stream_key = KEYS[1]
-- TODO: Remove in next breaking release (v0.14.0) - legacy key locations
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

self.key = key
self._redis_key = f"{docket.name}:progress:{key}"
self.current: int | None = None
self.total: int = 100
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make the default for total 1 instead.

await instance.sync()
return instance

async def set_total(self, total: int) -> None:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's validate that total >= 1

}
await redis.publish(channel, json.dumps(payload))

async def subscribe(self) -> AsyncGenerator[dict[str, Any], None]:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could define a ProgressEvent here if we wanted to

@desertaxle desertaxle merged commit c5dc2c1 into main Nov 4, 2025
42 of 44 checks passed
@desertaxle desertaxle deleted the exectuion-expansion branch November 4, 2025 22:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Task progress meters

4 participants