Skip to content

Commit 4460ef9

Browse files
authored
Merge pull request #31 from franccesco/dream/2026-07-23.1/finding-011
dream: drop redundant gather re-sort in async bulk
2 parents 04de20e + 7023fa7 commit 4460ef9

2 files changed

Lines changed: 55 additions & 12 deletions

File tree

src/bloomy/utils/async_base_operations.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,29 +93,25 @@ async def _process_bulk_async[T](
9393

9494
async def create_single(
9595
index: int, item_data: dict[str, Any]
96-
) -> tuple[int, T | BulkCreateError]:
96+
) -> T | BulkCreateError:
9797
async with semaphore:
9898
try:
9999
self._validate_bulk_item(item_data, required_fields)
100-
created = await create_func(item_data)
101-
return (index, created)
100+
return await create_func(item_data)
102101
except Exception as e:
103-
error = BulkCreateError(
102+
return BulkCreateError(
104103
index=index, input_data=item_data, error=str(e)
105104
)
106-
return (index, error)
107105

108-
tasks = [
109-
create_single(index, item_data) for index, item_data in enumerate(items)
110-
]
111-
results = await asyncio.gather(*tasks)
112-
results_list = list(results)
113-
results_list.sort(key=lambda x: x[0])
106+
# asyncio.gather preserves input order, so no post-sort is needed.
107+
results = await asyncio.gather(
108+
*(create_single(index, item_data) for index, item_data in enumerate(items))
109+
)
114110

115111
successful: list[T] = []
116112
failed: list[BulkCreateError] = []
117113

118-
for _, result in results_list:
114+
for result in results:
119115
if isinstance(result, BulkCreateError):
120116
failed.append(result)
121117
else:

tests/test_async_base_operations.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,50 @@ async def test_get_default_user_id(self) -> None:
8181

8282
# Verify API call
8383
client.get.assert_called_once_with("users/mine")
84+
85+
@pytest.mark.asyncio
86+
async def test_process_bulk_async_preserves_input_order(self) -> None:
87+
"""Gather results stay in input order even when later items finish first."""
88+
import asyncio
89+
90+
client = MockAsyncHTTPClient()
91+
ops = AsyncBaseOperations(client)
92+
93+
# Delays inverted so item 0 finishes last, item 2 finishes first.
94+
delays = {0: 0.05, 1: 0.02, 2: 0.01}
95+
96+
async def create_func(item_data: dict) -> str:
97+
await asyncio.sleep(delays[item_data["index"]])
98+
return f"created-{item_data['index']}"
99+
100+
items = [{"index": i, "title": f"item-{i}"} for i in range(3)]
101+
result = await ops._process_bulk_async(
102+
items, create_func, required_fields=["title"], max_concurrent=3
103+
)
104+
105+
assert result.failed == []
106+
assert result.successful == ["created-0", "created-1", "created-2"]
107+
108+
@pytest.mark.asyncio
109+
async def test_process_bulk_async_failure_index_matches_input(self) -> None:
110+
"""Failed items keep the original input index without post-sort."""
111+
import asyncio
112+
113+
client = MockAsyncHTTPClient()
114+
ops = AsyncBaseOperations(client)
115+
116+
async def create_func(item_data: dict) -> str:
117+
await asyncio.sleep(0.01 if item_data["index"] != 0 else 0.03)
118+
if item_data["index"] == 1:
119+
raise ValueError("boom")
120+
return f"created-{item_data['index']}"
121+
122+
items = [{"index": i, "title": f"item-{i}"} for i in range(3)]
123+
result = await ops._process_bulk_async(
124+
items, create_func, required_fields=["title"], max_concurrent=3
125+
)
126+
127+
assert result.successful == ["created-0", "created-2"]
128+
assert len(result.failed) == 1
129+
assert result.failed[0].index == 1
130+
assert "boom" in result.failed[0].error

0 commit comments

Comments
 (0)