Repository navigation
feat!(extstore): add new concurrency controls for external storage. - #1917
cconstable wants to merge 8 commits into
Conversation
f88df78 to
cf8a627
Compare
cf8a627 to
8d3eb8f
Compare
| skip_search_attributes: bool = False, | ||
| skip_headers: bool = False, | ||
| concurrency_limit: int = 1, | ||
| concurrency_limit: int | None = 1, |
There was a problem hiding this comment.
Discussed elsewhere. I think visiting should not be unbounded and should pass in ExternalStorageConcurrency.max_operations_per_message
| ) | ||
| """Selector context derived from :attr:`_store_context`.""" | ||
|
|
||
| _per_instance_semaphores: MutableMapping[ |
There was a problem hiding this comment.
I think this cache is per ExternalStorage instance. And each ExternalStorage instance gets a fresh new copy every time a new DataConverter gets created. So it's likely that while this works for a given instance of ExternalStorage / DataConverter, it is probably not worker-wide or client-wide; each time with_context is called, a new DataConverter is created.
| This API is experimental. | ||
| """ | ||
|
|
||
| _driver_operation_semaphore: asyncio.Semaphore | None = dataclasses.field( |
There was a problem hiding this comment.
This is tied to the event loop that the DataConverter is on and forces DataConverter initialization to be on an event loop; unsure the latter was the case already.
| self._init_from_config(config) | ||
|
|
||
| def _init_from_config(self, config: ClientConfig): | ||
| converter = config["data_converter"] |
There was a problem hiding this comment.
I was not expecting a semaphore to be shared out from DataConverter. I think it might be easier to have a client constructor one itself directly from the DataConverter.external_storage.concurrency values and hold onto it. It might need to be created per asyncio event-loop if clients can be reused across event loops.
…e concurrency limits. Removed dead code.
What was changed
ExternalStoragenow has aconcurrencyfield with two limits:max_driver_operationscaps requests in flight for a single client or worker. Each client and each worker gets its own limit.max_operations_per_messagecaps requests for a single message (workflow activation, etc). Each message gets its own budget.Both limits are cooperative. Drivers receive a limiter on the
storeandretrievecontexts and must wrap each request inpermit. Requests made outside apermitare not counted, and a warning is logged when a driver completes without taking one.Removed the
max_workflow_task_external_storage_concurrencyWorkerargument. The payload visitor now walks a message's payload sitesmax_operations_per_messageat a time. The bundled S3 driver takes permits.Why?
The previous control bounded payload visits rather than driver calls, so setting it to 3 did not limit external storage requests to 3. It also only applied to workflow tasks, leaving client, activity, and Nexus paths unbounded.
max_driver_operationsis scoped to a client or worker so thatExternalStoragestays configuration. Holding the counter there meant two clients built from one config silently shared a limit.Checklist
Added tests