Skip to content

Commit 5ef76ae

Browse files
ec2: Fix Fleet on-demand instance provisioning
EC2 Fleet was not launching the correct number of on-demand instances in two scenarios: * When OnDemandTargetCapacity was greater than 1, only a single instance was launched instead of the requested amount. * When OnDemandTargetCapacity was omitted from the request, the remaining capacity fallback did not behave correctly.
1 parent 7ac979b commit 5ef76ae

2 files changed

Lines changed: 134 additions & 12 deletions

File tree

moto/ec2/models/fleets.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,7 @@ def __init__(
131131
if self.spot_target_capacity > 0:
132132
self.create_spot_requests(self.spot_target_capacity)
133133
self.on_demand_target_capacity = int(
134-
target_capacity_specification.get(
135-
"OnDemandTargetCapacity", self.target_capacity
136-
)
134+
target_capacity_specification.get("OnDemandTargetCapacity", 0)
137135
)
138136
if self.on_demand_target_capacity > 0:
139137
self.create_on_demand_requests(self.on_demand_target_capacity)
@@ -267,15 +265,14 @@ def create_on_demand_requests(self, weight_to_add: float) -> None:
267265
tags=launch_spec.tag_specifications,
268266
)
269267

270-
# get the instance from the reservation
271-
instance = reservation.instances[0]
272-
self.on_demand_instances.append(
273-
{
274-
"id": reservation.id,
275-
"instance": instance,
276-
"launch_spec": launch_spec,
277-
}
278-
)
268+
for instance in reservation.instances:
269+
self.on_demand_instances.append(
270+
{
271+
"id": reservation.id,
272+
"instance": instance,
273+
"launch_spec": launch_spec,
274+
}
275+
)
279276
self.fulfilled_capacity += added_weight
280277

281278
def get_launch_spec_counts(

tests/test_ec2/test_fleets.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,131 @@ def test_create_fleet_dryrun(ec2_client=None):
114114
assert len(instances_res["Reservations"]) == reservations_before
115115

116116

117+
@pytest.mark.aws_verified
118+
@ec2_aws_verified()
119+
@pytest.mark.parametrize(
120+
["target_capacity_specification", "expected_on_demand", "expected_spot"],
121+
[
122+
pytest.param(
123+
{"TotalTargetCapacity": 2, "DefaultTargetCapacityType": "on-demand"},
124+
2,
125+
0,
126+
id="default-on-demand-only",
127+
),
128+
pytest.param(
129+
{"TotalTargetCapacity": 2, "DefaultTargetCapacityType": "spot"},
130+
0,
131+
2,
132+
id="default-spot-only",
133+
),
134+
pytest.param(
135+
{
136+
"TotalTargetCapacity": 2,
137+
"OnDemandTargetCapacity": 2,
138+
"SpotTargetCapacity": 0,
139+
"DefaultTargetCapacityType": "on-demand",
140+
},
141+
2,
142+
0,
143+
id="explicit-on-demand-only",
144+
),
145+
pytest.param(
146+
{
147+
"TotalTargetCapacity": 2,
148+
"OnDemandTargetCapacity": 0,
149+
"SpotTargetCapacity": 2,
150+
"DefaultTargetCapacityType": "spot",
151+
},
152+
0,
153+
2,
154+
id="explicit-spot-only",
155+
),
156+
pytest.param(
157+
{
158+
"TotalTargetCapacity": 2,
159+
"OnDemandTargetCapacity": 1,
160+
"SpotTargetCapacity": 1,
161+
"DefaultTargetCapacityType": "on-demand",
162+
},
163+
1,
164+
1,
165+
id="explicit-mixed-default-on-demand",
166+
),
167+
pytest.param(
168+
{
169+
"TotalTargetCapacity": 2,
170+
"OnDemandTargetCapacity": 1,
171+
"SpotTargetCapacity": 1,
172+
"DefaultTargetCapacityType": "spot",
173+
},
174+
1,
175+
1,
176+
id="explicit-mixed-default-spot",
177+
),
178+
pytest.param(
179+
{
180+
"TotalTargetCapacity": 3,
181+
"OnDemandTargetCapacity": 1,
182+
"DefaultTargetCapacityType": "spot",
183+
},
184+
1,
185+
2,
186+
id="partial-on-demand-remainder-to-spot",
187+
),
188+
pytest.param(
189+
{
190+
"TotalTargetCapacity": 3,
191+
"SpotTargetCapacity": 1,
192+
"DefaultTargetCapacityType": "on-demand",
193+
},
194+
2,
195+
1,
196+
id="partial-spot-remainder-to-on-demand",
197+
),
198+
],
199+
)
200+
def test_create_instant_fleet_target_capacity_combinations(
201+
target_capacity_specification,
202+
expected_on_demand,
203+
expected_spot,
204+
ec2_client=None,
205+
):
206+
with launch_template_context(region=ec2_client.meta.region_name) as ctxt:
207+
fleet_res = ctxt.ec2.create_fleet(
208+
LaunchTemplateConfigs=[
209+
{
210+
"LaunchTemplateSpecification": {
211+
"LaunchTemplateId": ctxt.lt_id,
212+
"Version": "1",
213+
},
214+
},
215+
],
216+
TargetCapacitySpecification=target_capacity_specification,
217+
Type="instant",
218+
)
219+
fleet_id = fleet_res["FleetId"]
220+
instances = fleet_res.get("Instances", [])
221+
222+
on_demand_ids = [
223+
iid
224+
for entry in instances
225+
if entry.get("Lifecycle") == "on-demand"
226+
for iid in entry.get("InstanceIds", [])
227+
]
228+
spot_ids = [
229+
iid
230+
for entry in instances
231+
if entry.get("Lifecycle") == "spot"
232+
for iid in entry.get("InstanceIds", [])
233+
]
234+
235+
try:
236+
assert len(on_demand_ids) == expected_on_demand
237+
assert len(spot_ids) == expected_spot
238+
finally:
239+
ctxt.ec2.delete_fleets(FleetIds=[fleet_id], TerminateInstances=True)
240+
241+
117242
@mock_aws
118243
def test_create_spot_fleet_with_lowest_price():
119244
conn = boto3.client("ec2", region_name="us-west-2")

0 commit comments

Comments
 (0)