Skip to content

Add Modal Serverless Worker integration - #1970

Open
harish-narayanappa-10 wants to merge 6 commits into
mainfrom
modal-sdk-python
Open

harish-narayanappa-10 wants to merge 6 commits into
mainfrom
modal-sdk-python

Conversation

@harish-narayanappa-10

@harish-narayanappa-10 harish-narayanappa-10 commented Oct 15, 2026 •

Copy link
Copy Markdown

Summary

  • Add temporalio.contrib.modal.worker for running a versioned Temporal Worker during one Modal Function invocation
  • Create a fresh client and worker per invocation with Modal-tuned concurrency, polling, identity, Worker Versioning, and eager-activity defaults
  • Derive the worker lifecycle budget from the declared Modal Function timeout and shut down the worker and hooks before the deadline
  • Document a complete Activity, Workflow, and Modal Function setup

Testing

  • uv run pytest tests/contrib/modal/worker/test_modal_worker.py
  • Pyright, mypy, and basedpyright on temporalio/contrib/modal and tests/contrib/modal
  • poe format
  • poe gen-docs

@CLAassistant

CLAassistant commented Oct 15, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@harish-narayanappa-10
harish-narayanappa-10 marked this pull request as ready for review October 15, 2026 16:08
@harish-narayanappa-10
harish-narayanappa-10 requested a review from a team as a code owner October 15, 2026 16:08
"""Run Temporal workers inside Modal Functions.

The integration owns the worker lifecycle within one already-running Modal
Function invocation. Authentication and invocation between a controller and

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can remove "Authentication and invocation ..."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We don't need to call out things we didn't do

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The LLMs are pretty bad at these types of comments - can you go through the comments in the PR in general and make sure it isn't adding any text that a user wouldn't expect to see or that is unrelated to the plugins purpose

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

addressed/reviewed as suggested

config.execution_timeout = WORKER_EXECUTION_TIMEOUT


@app.function(timeout=int(MODAL_FUNCTION_TIMEOUT.total_seconds()))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you add a simple workflow and activity to all the example code?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done!


Call this from a top-level async function decorated with
``@app.function``. A fresh Temporal client and worker are created for every
invocation. The worker polls until its execution budget approaches the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you talk about this behavior in the main README example? As a customer I think I'd want to know very clearly what this is doing - stops polling at time T, then allows X time for finishing activities etc.

@harish-narayanappa-10 harish-narayanappa-10 Oct 19, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

added lifecycle timeline to the README showing setup, when polling stops, Modal deadline etc


WORKER_EXECUTION_TIMEOUT = timedelta(minutes=2)
MODAL_FUNCTION_TIMEOUT = (
WORKER_EXECUTION_TIMEOUT + MIN_FUNCTION_TIMEOUT_HEADROOM

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why expose this to the customer? If I'm a customer I just want to simply state that my functions run for a maximum of 5 minutes. But with this approach they need to do math to figure out how long their functions run.

Can't we just set the @app.function timeout to 5 minutes, set the modal_function_timeout to 5 minutes, and then let the internal run_worker code subtract the MIN_FUNCTION_TIMEOUT_HEADROOM

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Updated as suggested. @app.function(timeout=...) and modal_function_timeout now use the same value

base_connect_config: ClientConnectConfig = {**profile.to_client_connect_config()}

base_worker_config: WorkerConfig = {}
apply_modal_worker_defaults(base_worker_config)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This sets graceful_shutdown_timeout to 5s (DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT) and then when you compute graceful_timeout on line 240 it always uses this 5s value not the user supplied configuration value. So shutdown_deadline_buffer is always 7s

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Polling stops with 7 seconds of budget left no matter what they set for graceful_shutdown_timeout

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. The default shutdown buffer is now calculated after configure from the final graceful_shutdown_timeout, plus two seconds for shutdown hooks

config.worker_config["activities"] = [my_activity]
config.execution_timeout = WORKER_EXECUTION_TIMEOUT


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If you set this:

@app.function(timeout=900) # 15-minute container
async def temporal_worker():
await run_worker(VERSION, configure,
modal_function_timeout=timedelta(minutes=15))

execution_timeout is still 5 minutes if you don't set it in the config

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. execution_timeout now defaults to modal_function_timeout minus the internal 15-second headroom

@tconley1428 tconley1428 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Discussing offline a final location for this code.

This branch has not been deployed

No deployments
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.

4 participants