Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit ffe52c1

Browse files
sandereggCopilot
andauthored
🎨Computational backend: Make sure the number of threads of a dask-worker is computed for autoscaling 🚨🚨🚨 (#8423)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 379e430 commit ffe52c1

File tree

30 files changed

+1099
-228
lines changed

30 files changed

+1099
-228
lines changed

‎packages/aws-library/src/aws_library/ec2/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
EC2InstanceData,
1818
EC2InstanceType,
1919
EC2Tags,
20+
GenericResourceValueType,
2021
Resources,
2122
)
2223

@@ -36,6 +37,7 @@
3637
"EC2NotConnectedError",
3738
"EC2RuntimeError",
3839
"EC2Tags",
40+
"GenericResourceValueType",
3941
"Resources",
4042
"SimcoreEC2API",
4143
)

‎packages/aws-library/src/aws_library/ec2/_models.py‎

Lines changed: 144 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import contextlib
12
import datetime
23
import re
34
import tempfile
@@ -14,45 +15,171 @@
1415
Field,
1516
NonNegativeFloat,
1617
NonNegativeInt,
18+
StrictFloat,
19+
StrictInt,
1720
StringConstraints,
21+
TypeAdapter,
22+
ValidationError,
1823
field_validator,
1924
)
2025
from pydantic.config import JsonDict
2126
from types_aiobotocore_ec2.literals import InstanceStateNameType, InstanceTypeType
2227

28+
GenericResourceValueType: TypeAlias = StrictInt | StrictFloat | str
29+
2330

2431
class Resources(BaseModel, frozen=True):
2532
cpus: NonNegativeFloat
2633
ram: ByteSize
34+
generic_resources: Annotated[
35+
dict[str, GenericResourceValueType],
36+
Field(
37+
default_factory=dict,
38+
description=(
39+
"Arbitrary additional resources (e.g. {'threads': 8}). "
40+
"Numeric values are treated as quantities and participate in add/sub/compare."
41+
),
42+
),
43+
] = DEFAULT_FACTORY
2744

2845
@classmethod
2946
def create_as_empty(cls) -> "Resources":
3047
return cls(cpus=0, ram=ByteSize(0))
3148

3249
def __ge__(self, other: "Resources") -> bool:
33-
return self.cpus >= other.cpus and self.ram >= other.ram
50+
"""operator for >= comparison
51+
if self has greater or equal resources than other, returns True
52+
This will return True only if all of the resources in self are greater or equal to other
53+
54+
Note that generic_resources are compared only if they are numeric
55+
Non-numeric generic resources must be equal in both or only defined in self
56+
to be considered greater or equal
57+
"""
58+
if self == other:
59+
return True
60+
return self > other
3461

3562
def __gt__(self, other: "Resources") -> bool:
36-
return self.cpus > other.cpus or self.ram > other.ram
63+
"""operator for > comparison
64+
if self has resources greater than other, returns True
65+
This will return True only if all of the resources in self are greater than other
66+
67+
Note that generic_resources are compared only if they are numeric
68+
Non-numeric generic resources must only be defined in self
69+
to be considered greater
70+
"""
71+
if (self.cpus < other.cpus) or (self.ram < other.ram):
72+
return False
73+
74+
keys = set(self.generic_resources) | set(other.generic_resources)
75+
for k in keys:
76+
a = self.generic_resources.get(k)
77+
b = other.generic_resources.get(k)
78+
if a is None:
79+
return False
80+
if b is None:
81+
# a is greater as b is not defined
82+
continue
83+
if isinstance(a, int | float) and isinstance(b, int | float):
84+
if a < b:
85+
return False
86+
else:
87+
# remaining options is a is str and b is str or mixed types
88+
# NOTE: we cannot compare strings unless they are equal or some kind of boolean (e.g. "true", "false", "yes", "no", "1", "0")
89+
assert isinstance(a, str) # nosec
90+
assert isinstance(b, int | float | str) # nosec
91+
# let's try to get a boolean out of the values to compare them
92+
with contextlib.suppress(ValidationError):
93+
a_as_boolean = TypeAdapter(bool).validate_python(a)
94+
b_as_boolean = TypeAdapter(bool).validate_python(b)
95+
if not a_as_boolean and b_as_boolean:
96+
return False
97+
98+
# here we have either everything greater or equal or non-comparable strings
99+
100+
return self != other
37101

38102
def __add__(self, other: "Resources") -> "Resources":
103+
"""operator for adding two Resources
104+
Note that only numeric generic resources are added
105+
Non-numeric generic resources are ignored
106+
"""
107+
merged: dict[str, GenericResourceValueType] = {}
108+
keys = set(self.generic_resources) | set(other.generic_resources)
109+
for k in keys:
110+
a = self.generic_resources.get(k)
111+
b = other.generic_resources.get(k)
112+
# adding non numeric values does not make sense, so we skip those for the resulting resource
113+
if isinstance(a, int | float) and isinstance(b, int | float):
114+
merged[k] = a + b
115+
elif a is None and isinstance(b, int | float):
116+
merged[k] = b
117+
elif b is None and isinstance(a, int | float):
118+
merged[k] = a
119+
39120
return Resources.model_construct(
40-
**{
41-
key: a + b
42-
for (key, a), b in zip(
43-
self.model_dump().items(), other.model_dump().values(), strict=True
44-
)
45-
}
121+
cpus=self.cpus + other.cpus,
122+
ram=self.ram + other.ram,
123+
generic_resources=merged,
46124
)
47125

48126
def __sub__(self, other: "Resources") -> "Resources":
127+
"""operator for subtracting two Resources
128+
Note that only numeric generic resources are subtracted
129+
Non-numeric generic resources are ignored
130+
"""
131+
merged: dict[str, GenericResourceValueType] = {}
132+
keys = set(self.generic_resources) | set(other.generic_resources)
133+
for k in keys:
134+
a = self.generic_resources.get(k)
135+
b = other.generic_resources.get(k)
136+
# subtracting non numeric values does not make sense, so we skip those for the resulting resource
137+
if isinstance(a, int | float) and isinstance(b, int | float):
138+
merged[k] = a - b
139+
elif a is None and isinstance(b, int | float):
140+
merged[k] = -b
141+
elif b is None and isinstance(a, int | float):
142+
merged[k] = a
143+
49144
return Resources.model_construct(
50-
**{
51-
key: a - b
52-
for (key, a), b in zip(
53-
self.model_dump().items(), other.model_dump().values(), strict=True
54-
)
55-
}
145+
cpus=self.cpus - other.cpus,
146+
ram=self.ram - other.ram,
147+
generic_resources=merged,
148+
)
149+
150+
def __hash__(self) -> int:
151+
"""Deterministic hash including cpus, ram (in bytes) and generic_resources."""
152+
# sort generic_resources items to ensure order-independent hashing
153+
generic_items: tuple[tuple[str, GenericResourceValueType], ...] = tuple(
154+
sorted(self.generic_resources.items())
155+
)
156+
return hash((self.cpus, self.ram, generic_items))
157+
158+
def as_flat_dict(self) -> dict[str, int | float | str]:
159+
"""Like model_dump, but flattens generic_resources to top level keys"""
160+
base = self.model_dump()
161+
base.update(base.pop("generic_resources"))
162+
return base
163+
164+
@classmethod
165+
def from_flat_dict(
166+
cls,
167+
data: dict[str, int | float | str],
168+
*,
169+
mapping: dict[str, str] | None = None,
170+
) -> "Resources":
171+
"""Inverse of as_flat_dict with optional key mapping"""
172+
mapped_data = data
173+
if mapping:
174+
mapped_data = {mapping.get(k, k): v for k, v in data.items()}
175+
generic_resources = {
176+
k: v for k, v in mapped_data.items() if k not in {"cpus", "ram"}
177+
}
178+
179+
return cls(
180+
cpus=float(mapped_data.get("cpus", 0)),
181+
ram=ByteSize(mapped_data.get("ram", 0)),
182+
generic_resources=generic_resources,
56183
)
57184

58185
@field_validator("cpus", mode="before")
@@ -174,8 +301,9 @@ def validate_bash_calls(cls, v):
174301
temp_file.flush()
175302
# NOTE: this will not capture runtime errors, but at least some syntax errors such as invalid quotes
176303
sh.bash(
177-
"-n", temp_file.name
178-
) # pyright: ignore[reportCallIssue] # sh is untyped, but this call is safe for bash syntax checking
304+
"-n",
305+
temp_file.name, # pyright: ignore[reportCallIssue] - sh is untyped but safe for bash syntax checking
306+
)
179307
except sh.ErrorReturnCode as exc:
180308
msg = f"Invalid bash call in custom_boot_scripts: {v}, Error: {exc.stderr}"
181309
raise ValueError(msg) from exc

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /