5858_DEFAULT_EXPECTED_STATUSES : tuple [int , ...] = (200 , 201 , 202 , 204 )
5959_MULTIPLE_BATCH_SIZE = 1000
6060# Concurrent chunk dispatch settings
61- _MAX_WORKERS = 3 # maximum concurrent worker threads; values above this are silently capped
61+ _MAX_WORKERS = 3 # maximum concurrent worker threads; values above this are capped
6262_CHUNK_RETRY_LIMIT = 3 # max retries per chunk on transient errors
6363_CHUNK_RETRY_DEFAULT_WAIT = 60 # seconds to wait when Retry-After header is absent
6464_CHUNK_RETRY_JITTER_MAX = 5 # seconds of random jitter added to Retry-After to desynchronise workers
6767def _dispatch_chunks (fn : Callable , chunks : List , max_workers : int ) -> List :
6868 """Dispatch ``fn(chunk)`` for each chunk, sequentially or concurrently.
6969
70- ``max_workers`` is silently capped to ``_MAX_WORKERS`` (3) so callers
71- that pass a larger value are not penalised with an error .
70+ If ``max_workers`` exceeds ``_MAX_WORKERS`` (3) a :class:`UserWarning` is
71+ issued and the value is capped .
7272
7373 When ``max_workers == 1`` or there is only one chunk, runs sequentially
7474 with no thread overhead. When ``max_workers > 1`` and there are multiple
@@ -85,10 +85,16 @@ def _dispatch_chunks(fn: Callable, chunks: List, max_workers: int) -> List:
8585
8686 :param fn: Callable that accepts a single chunk and returns a result.
8787 :param chunks: List of chunks to process.
88- :param max_workers: Maximum number of concurrent worker threads (capped to ``_MAX_WORKERS``) .
88+ :param max_workers: Maximum number of concurrent worker threads.
8989 :return: List of results in chunk submission order.
9090 """
91- max_workers = min (max_workers , _MAX_WORKERS )
91+ if max_workers > _MAX_WORKERS :
92+ warnings .warn (
93+ f"max_workers={ max_workers } exceeds the maximum of { _MAX_WORKERS } ; capping to { _MAX_WORKERS } ." ,
94+ UserWarning ,
95+ stacklevel = 2 ,
96+ )
97+ max_workers = _MAX_WORKERS
9298
9399 def _execute_with_retry (chunk ):
94100 for attempt in range (_CHUNK_RETRY_LIMIT + 1 ):
@@ -585,18 +591,11 @@ def _upsert_multiple(
585591 When input exceeds ``_MULTIPLE_BATCH_SIZE`` records, the operation is
586592 split into multiple requests and is **not atomic** across batches.
587593 """
588- # Validation uses ValueError (not ValidationError) because this is a
589- # caller-facing precondition check, not a service error. The batch path
590- # (_build_upsert_multiple) raises ValidationError for the same conditions
591- # because batch errors carry structured subcodes.
592594 if len (alternate_keys ) != len (records ):
593595 raise ValueError (
594596 f"alternate_keys and records must have the same length " f"({ len (alternate_keys )} != { len (records )} )"
595597 )
596598 logical_name = table_schema_name .lower ()
597- # Pre-process all targets before chunking so that validation (key
598- # conflicts, label conversion) runs eagerly. This means all records
599- # are held in memory at once, which is acceptable for typical workloads.
600599 targets : List [Dict [str , Any ]] = []
601600 for alt_key , record in zip (alternate_keys , records ):
602601 alt_key_lower = self ._lowercase_keys (alt_key )
@@ -1386,15 +1385,15 @@ def _bulk_fetch_picklists(self, table_schema_name: str) -> None:
13861385 """
13871386 table_key = self ._normalize_cache_key (table_schema_name )
13881387 now = time .time ()
1389- # Fast path — lock -free read for the warm-cache case (common in sequential and
1388+ # Lock -free read for the warm-cache case (common in sequential and
13901389 # subsequent concurrent calls once the cache is populated).
13911390 table_entry = self ._picklist_label_cache .get (table_key )
13921391 if isinstance (table_entry , dict ) and (now - table_entry .get ("ts" , 0 )) < self ._picklist_cache_ttl_seconds :
13931392 return
13941393
1395- # Slow path — serialise concurrent cold-start fetches so only one thread
1396- # makes the metadata HTTP call. Re-check inside the lock (double-checked
1397- # locking) in case another thread populated the cache while we waited.
1394+ # Serialise concurrent cold-start fetches so only one thread makes the
1395+ # metadata HTTP call. Re-check inside the lock (double-checked locking)
1396+ # in case another thread populated the cache while we waited.
13981397 with self ._picklist_cache_lock :
13991398 now = time .time ()
14001399 table_entry = self ._picklist_label_cache .get (table_key )
0 commit comments