Skip to content

Commit dd41083

Browse files
authored
Merge pull request #165 from weaviate/jose/fix-parallel-mt-return-collection
Fix parallel MT ingestion returning base collection without tenant context
2 parents 3be57a2 + 9560928 commit dd41083

5 files changed

Lines changed: 260 additions & 5 deletions

File tree

.github/workflows/main.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ jobs:
8686
- name: Run integration tests with pytest
8787
run: |
8888
pip install pytest-html
89-
pytest test/integration/test_integration.py --html=test-report-${{ matrix.version }}.html --self-contained-html
89+
pytest test/integration/test_integration.py test/integration/test_data_integration.py test/integration/test_create_data_return_collection.py --html=test-report-${{ matrix.version }}.html --self-contained-html
9090
integration-auth-tests:
9191
needs: [unit-tests, get-latest-weaviate-version]
9292
env:
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
"""Integration tests verifying that DataManager.create_data returns a usable
2+
Collection object for both single-tenant and multi-tenant collections.
3+
4+
Regression guard for a bug where parallel multi-tenant ingestion returned the
5+
base collection (without tenant context), causing ``len(collection)`` and
6+
``batch.wait_for_vector_indexing()`` to fail with:
7+
8+
"class X has multi-tenancy enabled, but request was without tenant"
9+
"""
10+
11+
import pytest
12+
import weaviate
13+
from weaviate_cli.managers.collection_manager import CollectionManager
14+
from weaviate_cli.managers.config_manager import ConfigManager
15+
from weaviate_cli.managers.data_manager import DataManager
16+
from weaviate_cli.managers.tenant_manager import TenantManager
17+
18+
NUM_OBJECTS = 20
19+
NUM_TENANTS = 3
20+
21+
22+
@pytest.fixture
23+
def client() -> weaviate.WeaviateClient:
24+
config = ConfigManager()
25+
return config.get_client()
26+
27+
28+
@pytest.fixture
29+
def collection_manager(client: weaviate.WeaviateClient) -> CollectionManager:
30+
return CollectionManager(client)
31+
32+
33+
@pytest.fixture
34+
def data_manager(client: weaviate.WeaviateClient) -> DataManager:
35+
return DataManager(client)
36+
37+
38+
@pytest.fixture
39+
def tenant_manager(client: weaviate.WeaviateClient) -> TenantManager:
40+
return TenantManager(client)
41+
42+
43+
def test_create_data_returns_usable_collection_single_tenant(
44+
client: weaviate.WeaviateClient,
45+
collection_manager: CollectionManager,
46+
data_manager: DataManager,
47+
):
48+
"""For a non-MT collection, the returned collection must support len() and
49+
batch.wait_for_vector_indexing() without errors."""
50+
collection_name = "TestReturnCollSingleTenant"
51+
52+
try:
53+
if client.collections.exists(collection_name):
54+
client.collections.delete(collection_name)
55+
56+
collection_manager.create_collection(
57+
collection=collection_name,
58+
vectorizer="none",
59+
replication_factor=1,
60+
async_enabled=True,
61+
)
62+
63+
returned_col = data_manager.create_data(
64+
collection=collection_name,
65+
limit=NUM_OBJECTS,
66+
consistency_level="one",
67+
randomize=True,
68+
skip_seed=True,
69+
vector_dimensions=128,
70+
)
71+
72+
# The returned collection must be usable for these operations
73+
returned_col.batch.wait_for_vector_indexing()
74+
assert (
75+
len(returned_col) == NUM_OBJECTS
76+
), f"Expected {NUM_OBJECTS} objects via returned collection, got {len(returned_col)}"
77+
finally:
78+
if client.collections.exists(collection_name):
79+
client.collections.delete(collection_name)
80+
81+
82+
def test_create_data_returns_usable_collection_multitenant_sequential(
83+
client: weaviate.WeaviateClient,
84+
collection_manager: CollectionManager,
85+
data_manager: DataManager,
86+
tenant_manager: TenantManager,
87+
):
88+
"""For a multi-tenant collection with sequential ingestion (parallel_workers=1),
89+
the returned collection must carry tenant context so len() works."""
90+
collection_name = "TestReturnCollMTSeq"
91+
92+
try:
93+
if client.collections.exists(collection_name):
94+
client.collections.delete(collection_name)
95+
96+
collection_manager.create_collection(
97+
collection=collection_name,
98+
vectorizer="none",
99+
replication_factor=1,
100+
async_enabled=True,
101+
multitenant=True,
102+
)
103+
104+
tenant_manager.create_tenants(
105+
collection=collection_name,
106+
number_tenants=NUM_TENANTS,
107+
)
108+
109+
returned_col = data_manager.create_data(
110+
collection=collection_name,
111+
limit=NUM_OBJECTS,
112+
consistency_level="one",
113+
randomize=True,
114+
skip_seed=True,
115+
vector_dimensions=128,
116+
parallel_workers=1,
117+
)
118+
119+
# Must not raise "multi-tenancy enabled, but request was without tenant"
120+
returned_col.batch.wait_for_vector_indexing()
121+
count = len(returned_col)
122+
assert (
123+
count == NUM_OBJECTS
124+
), f"Expected {NUM_OBJECTS} objects via returned collection, got {count}"
125+
finally:
126+
if client.collections.exists(collection_name):
127+
client.collections.delete(collection_name)
128+
129+
130+
def test_create_data_returns_usable_collection_multitenant_parallel(
131+
client: weaviate.WeaviateClient,
132+
collection_manager: CollectionManager,
133+
data_manager: DataManager,
134+
tenant_manager: TenantManager,
135+
):
136+
"""For a multi-tenant collection with parallel ingestion (parallel_workers>1),
137+
the returned collection must carry tenant context so len() works.
138+
139+
This is the exact scenario that was broken: parallel mode never assigned the
140+
tenant-scoped collection back to the return variable."""
141+
collection_name = "TestReturnCollMTParallel"
142+
143+
try:
144+
if client.collections.exists(collection_name):
145+
client.collections.delete(collection_name)
146+
147+
collection_manager.create_collection(
148+
collection=collection_name,
149+
vectorizer="none",
150+
replication_factor=1,
151+
async_enabled=True,
152+
multitenant=True,
153+
)
154+
155+
tenant_manager.create_tenants(
156+
collection=collection_name,
157+
number_tenants=NUM_TENANTS,
158+
)
159+
160+
returned_col = data_manager.create_data(
161+
collection=collection_name,
162+
limit=NUM_OBJECTS,
163+
consistency_level="one",
164+
randomize=True,
165+
skip_seed=True,
166+
vector_dimensions=128,
167+
parallel_workers=NUM_TENANTS,
168+
)
169+
170+
# Must not raise "multi-tenancy enabled, but request was without tenant"
171+
returned_col.batch.wait_for_vector_indexing()
172+
count = len(returned_col)
173+
assert (
174+
count == NUM_OBJECTS
175+
), f"Expected {NUM_OBJECTS} objects via returned collection, got {count}"
176+
finally:
177+
if client.collections.exists(collection_name):
178+
client.collections.delete(collection_name)

test/integration/test_data_integration.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def test_data_creation_with_different_configs(
9393
assert obj.vector is not None
9494

9595
vector_dimensions = {
96-
"transformers": 384,
96+
"transformers": 256,
9797
"contextionary": 300,
9898
}
9999

@@ -193,11 +193,12 @@ def test_data_creation_with_named_vectors(
193193
named_vector = obj.vector[named_vector_name]
194194
assert named_vector is not None
195195

196-
# Check vector dimensions (should be 768 for transformers)
197-
assert len(named_vector) == 384
196+
# Check vector dimensions (256 for model2vec transformers, 384 for none/custom)
197+
expected_dim = 256 if vectorizer == "transformers" else 384
198+
assert len(named_vector) == expected_dim
198199

199200
# Verify vector is not all zeros
200-
assert not np.allclose(named_vector, np.zeros(384))
201+
assert not np.allclose(named_vector, np.zeros(expected_dim))
201202

202203
# Verify vector has finite values
203204
assert np.all(np.isfinite(named_vector))

test/unittests/test_managers/test_data_manager.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,81 @@ def fake_ingest(collection, **kwargs):
779779
# Single "None" pseudo-tenant processed
780780
assert len(processed) == 1
781781

782+
def test_parallel_returns_tenant_scoped_collection(self, mock_client):
783+
"""Parallel mode must return a tenant-scoped collection, not the base one.
784+
785+
Regression test: parallel ingestion discarded the tenant-scoped collection
786+
returned by __ingest_data, causing callers to get back the base collection
787+
which fails with 'multi-tenancy enabled, but request was without tenant'.
788+
"""
789+
manager = DataManager(mock_client)
790+
tenants = ["Tenant-0", "Tenant-1", "Tenant-2"]
791+
col = self._make_col(tenants)
792+
_setup_mock_client_with_col(mock_client, col)
793+
794+
def fake_ingest(collection, **kwargs):
795+
return collection
796+
797+
with patch.object(
798+
manager, "_DataManager__ingest_data", side_effect=fake_ingest
799+
):
800+
result = manager.create_data(
801+
collection="TestCollection",
802+
limit=5,
803+
parallel_workers=4,
804+
)
805+
806+
# The returned collection must NOT be the base col (which has no tenant)
807+
assert (
808+
result is not col
809+
), "Parallel mode returned the base collection instead of a tenant-scoped one"
810+
# It should be one of the tenant-scoped collections
811+
col.with_tenant.assert_called()
812+
813+
def test_sequential_returns_tenant_scoped_collection(self, mock_client):
814+
"""Sequential mode must also return a tenant-scoped collection."""
815+
manager = DataManager(mock_client)
816+
tenants = ["Tenant-0", "Tenant-1"]
817+
col = self._make_col(tenants)
818+
_setup_mock_client_with_col(mock_client, col)
819+
820+
def fake_ingest(collection, **kwargs):
821+
return collection
822+
823+
with patch.object(
824+
manager, "_DataManager__ingest_data", side_effect=fake_ingest
825+
):
826+
result = manager.create_data(
827+
collection="TestCollection",
828+
limit=5,
829+
parallel_workers=1,
830+
)
831+
832+
assert (
833+
result is not col
834+
), "Sequential mode returned the base collection instead of a tenant-scoped one"
835+
836+
def test_non_mt_returns_base_collection(self, mock_client):
837+
"""Non-MT collections should return the base collection (no tenant context)."""
838+
manager = DataManager(mock_client)
839+
col = _make_non_mt_col()
840+
_setup_mock_client_with_col(mock_client, col)
841+
842+
def fake_ingest(collection, **kwargs):
843+
return collection
844+
845+
with patch.object(
846+
manager, "_DataManager__ingest_data", side_effect=fake_ingest
847+
):
848+
result = manager.create_data(
849+
collection="TestCollection",
850+
limit=5,
851+
parallel_workers=4,
852+
)
853+
854+
# For non-MT, the base collection IS the correct return value
855+
assert result is col
856+
782857

783858
# ---------------------------------------------------------------------------
784859
# create_data – concurrent_requests scaling with parallel_workers

weaviate_cli/managers/data_manager.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,7 @@ def _ingest_one_tenant(tenant: str):
961961
inserted, _coll = future.result()
962962
with _lock:
963963
total_inserted += inserted
964+
collection = _coll
964965
except Exception as exc:
965966
_errors.append(f"Tenant '{t}': {exc}")
966967
if _errors:

0 commit comments

Comments
 (0)