Skip to content

Commit 72b9440

Browse files
authored
fix: order resource classes by GPU, CPU, RAM, storage (#420)
* fix: order resource classes by GPU, CPU, RAM, storage * fix: order resource classes by GPU, CPU, RAM, storage * fix tests
1 parent 63b22b2 commit 72b9440

5 files changed

Lines changed: 106 additions & 12 deletions

File tree

components/renku_data_services/crc/db.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ async def filter_resource_pools(
191191
max_storage: int = 0,
192192
gpu: int = 0,
193193
) -> list[models.ResourcePool]:
194-
"""Get resource pools from database with indication of which resource class matches the specified crtieria."""
194+
"""Get resource pools from database with indication of which resource class matches the specified criteria."""
195195
async with self.session_maker() as session:
196196
criteria = models.ResourceClass(
197197
name="criteria",
@@ -205,18 +205,17 @@ async def filter_resource_pools(
205205
)
206206
stmt = (
207207
select(schemas.ResourcePoolORM)
208-
.join(schemas.ResourcePoolORM.classes)
208+
.distinct()
209+
.options(selectinload(schemas.ResourcePoolORM.classes))
209210
.order_by(
210211
schemas.ResourcePoolORM.id,
211212
schemas.ResourcePoolORM.name,
212-
schemas.ResourceClassORM.id,
213-
schemas.ResourceClassORM.name,
214213
)
215214
)
216215
# NOTE: The line below ensures that the right users can access the right resources, do not remove.
217216
stmt = _resource_pool_access_control(api_user, stmt)
218217
res = await session.execute(stmt)
219-
return [i.dump(self.quotas_repo.get_quota(i.quota), criteria) for i in res.unique().scalars().all()]
218+
return [i.dump(self.quotas_repo.get_quota(i.quota), criteria) for i in res.scalars().all()]
220219

221220
@_only_admins
222221
async def insert_resource_pool(

components/renku_data_services/crc/models.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -207,10 +207,23 @@ def __post_init__(self) -> None:
207207
if len(default_classes) != 1:
208208
raise ValidationError(message="One default class is required in each resource pool.")
209209

210-
# We need to sort classes to make '__eq__' reliable
211-
object.__setattr__(
212-
self, "classes", sorted(self.classes, key=lambda x: (x.default, x.cpu, x.memory, x.default_storage, x.name))
213-
)
210+
def __eq__(self, other: Any) -> bool:
211+
"""Check two resource pools for equality."""
212+
if not isinstance(other, ResourcePool):
213+
return False
214+
215+
if self.id != other.id:
216+
return False
217+
if self.name != other.name:
218+
return False
219+
if self.default != other.default or self.public != other.public:
220+
return False
221+
if self.idle_threshold != other.idle_threshold or self.hibernation_threshold != other.hibernation_threshold:
222+
return False
223+
224+
this_classes = sorted(self.classes, key=lambda x: (x.default, x.cpu, x.memory, x.default_storage, x.name))
225+
other_classes = sorted(other.classes, key=lambda x: (x.default, x.cpu, x.memory, x.default_storage, x.name))
226+
return this_classes == other_classes
214227

215228
def set_quota(self, val: Quota) -> "ResourcePool":
216229
"""Set the quota for a resource pool."""

components/renku_data_services/crc/orm.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ class ResourcePoolORM(BaseORM):
144144
default_factory=list,
145145
cascade="save-update, merge, delete",
146146
lazy="selectin",
147+
order_by=(
148+
"[ResourceClassORM.gpu,ResourceClassORM.cpu,ResourceClassORM.memory,ResourceClassORM.max_storage,"
149+
"ResourceClassORM.name,ResourceClassORM.id]"
150+
),
147151
)
148152
idle_threshold: Mapped[Optional[int]] = mapped_column(default=None)
149153
hibernation_threshold: Mapped[Optional[int]] = mapped_column(default=None)

test/bases/renku_data_services/data_api/test_resource_pools.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,86 @@ async def test_resource_class_filtering(
147147
assert len(res.json[0]["classes"]) == len(new_classes)
148148

149149

150+
@pytest.mark.asyncio
151+
async def test_resource_class_ordering(
152+
sanic_client: SanicASGITestClient, admin_headers: dict[str, str], valid_resource_pool_payload: dict[str, Any]
153+
) -> None:
154+
new_classes = [
155+
{
156+
"name": "resource class 5",
157+
"cpu": 0.1,
158+
"memory": 1,
159+
"gpu": 1,
160+
"max_storage": 1,
161+
"default": False,
162+
"default_storage": 1,
163+
"node_affinities": [],
164+
"tolerations": [],
165+
},
166+
{
167+
"name": "resource class 4",
168+
"cpu": 9.0,
169+
"memory": 1,
170+
"gpu": 0,
171+
"max_storage": 1,
172+
"default": False,
173+
"default_storage": 1,
174+
"node_affinities": [],
175+
"tolerations": [],
176+
},
177+
{
178+
"name": "resource class 3",
179+
"cpu": 0.1,
180+
"memory": 100,
181+
"gpu": 0,
182+
"max_storage": 1,
183+
"default": False,
184+
"default_storage": 1,
185+
"node_affinities": [],
186+
"tolerations": [],
187+
},
188+
{
189+
"name": "resource class 2",
190+
"cpu": 0.1,
191+
"memory": 1,
192+
"gpu": 0,
193+
"max_storage": 100,
194+
"default": False,
195+
"default_storage": 1,
196+
"node_affinities": [],
197+
"tolerations": [],
198+
},
199+
{
200+
"name": "resource class 1",
201+
"cpu": 0.1,
202+
"memory": 1,
203+
"gpu": 0,
204+
"max_storage": 10,
205+
"default": True,
206+
"default_storage": 1,
207+
"node_affinities": [],
208+
"tolerations": [],
209+
},
210+
]
211+
payload = valid_resource_pool_payload
212+
payload["quota"] = {"cpu": 100, "memory": 100, "gpu": 100}
213+
payload["classes"] = new_classes
214+
_, res = await create_rp(payload, sanic_client)
215+
assert res.status_code == 201
216+
_, res = await sanic_client.get(
217+
"/api/data/resource_pools",
218+
params={"cpu": 1, "gpu": 1},
219+
headers=admin_headers,
220+
)
221+
assert res.status_code == 200
222+
assert len(res.json) == 1
223+
rp_filtered = res.json[0]
224+
225+
new_classes_names = [c["name"] for c in new_classes]
226+
returned_names = [c["name"] for c in rp_filtered["classes"]]
227+
assert new_classes_names[::-1] == returned_names # classes should show up in reverse order
228+
229+
150230
@pytest.mark.asyncio
151231
async def test_get_single_pool_quota(
152232
sanic_client: SanicASGITestClient, valid_resource_pool_payload: dict[str, Any], admin_headers: dict[str, str]

test/components/renku_data_services/db/test_sqlalchemy_pool_repo.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,7 @@ async def test_resource_pool_update_classes(
111111
old_classes = [asdict(cls) for cls in list(inserted_rp.classes)]
112112
new_classes_dicts = [{**cls, **data.draw(rc_update_reqs_dict)} for cls in old_classes]
113113
new_classes_models = [models.ResourceClass.from_dict(cls) for cls in new_classes_dicts]
114-
new_classes_models = sorted(
115-
new_classes_models, key=lambda x: (x.default, x.cpu, x.memory, x.default_storage, x.name)
116-
)
114+
new_classes_models = sorted(new_classes_models, key=lambda x: (x.gpu, x.cpu, x.memory, x.max_storage, x.name))
117115

118116
updated_rp = await pool_repo.update_resource_pool(
119117
id=inserted_rp.id, classes=new_classes_dicts, api_user=admin_user

0 commit comments

Comments
 (0)