Skip to content

Commit caf3300

Browse files
jvstmeCopilot
andauthored
Allow creating instances without waiting (#80)
* Allow creating instances without waiting Add `InstancesService.create_nowait` method that returns immediately after sending a create request to the API. * `create_nowait` -> `create(wait_for_status)` * Update verda/instances/_instances.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent e47a629 commit caf3300

File tree

2 files changed

+67
-2
lines changed

2 files changed

+67
-2
lines changed

tests/unit_tests/instances/test_instances.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import copy
12
import json
23

34
import pytest
45
import responses
56

6-
from verda.constants import Actions, ErrorCodes, Locations
7+
from verda.constants import Actions, ErrorCodes, InstanceStatus, Locations
78
from verda.exceptions import APIException
89
from verda.instances import Instance, InstancesService, OSVolume
910

@@ -333,6 +334,61 @@ def test_create_instance_attached_os_volume_successful(self, instances_service,
333334
assert responses.assert_call_count(endpoint, 1) is True
334335
assert responses.assert_call_count(url, 1) is True
335336

337+
@pytest.mark.parametrize(
338+
('wait_for_status', 'expected_status', 'expected_get_instance_call_count'),
339+
[
340+
(None, InstanceStatus.ORDERED, 1),
341+
(InstanceStatus.ORDERED, InstanceStatus.ORDERED, 1),
342+
(InstanceStatus.PROVISIONING, InstanceStatus.PROVISIONING, 2),
343+
(lambda status: status != InstanceStatus.ORDERED, InstanceStatus.PROVISIONING, 2),
344+
(InstanceStatus.RUNNING, InstanceStatus.RUNNING, 3),
345+
],
346+
)
347+
def test_create_wait_for_status(
348+
self,
349+
instances_service,
350+
endpoint,
351+
wait_for_status,
352+
expected_status,
353+
expected_get_instance_call_count,
354+
):
355+
# arrange - add response mock
356+
# create instance
357+
responses.add(responses.POST, endpoint, body=INSTANCE_ID, status=200)
358+
# First get instance by id - ordered
359+
get_instance_url = endpoint + '/' + INSTANCE_ID
360+
payload = copy.deepcopy(PAYLOAD[0])
361+
payload['status'] = InstanceStatus.ORDERED
362+
responses.add(responses.GET, get_instance_url, json=payload, status=200)
363+
# Second get instance by id - provisioning
364+
payload = copy.deepcopy(PAYLOAD[0])
365+
payload['status'] = InstanceStatus.PROVISIONING
366+
responses.add(responses.GET, get_instance_url, json=payload, status=200)
367+
# Third get instance by id - running
368+
payload = copy.deepcopy(PAYLOAD[0])
369+
payload['status'] = InstanceStatus.RUNNING
370+
responses.add(responses.GET, get_instance_url, json=payload, status=200)
371+
372+
# act
373+
instance = instances_service.create(
374+
instance_type=INSTANCE_TYPE,
375+
image=OS_VOLUME_ID,
376+
hostname=INSTANCE_HOSTNAME,
377+
description=INSTANCE_DESCRIPTION,
378+
wait_for_status=wait_for_status,
379+
max_interval=0,
380+
max_wait_time=1,
381+
)
382+
383+
# assert
384+
assert isinstance(instance, Instance)
385+
assert instance.id == INSTANCE_ID
386+
assert instance.status == expected_status
387+
assert responses.assert_call_count(endpoint, 1) is True
388+
assert (
389+
responses.assert_call_count(get_instance_url, expected_get_instance_call_count) is True
390+
)
391+
336392
def test_create_instance_failed(self, instances_service, endpoint):
337393
# arrange - add response mock
338394
responses.add(

verda/instances/_instances.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import itertools
22
import time
3+
from collections.abc import Callable
34
from dataclasses import dataclass
45
from typing import Literal
56

@@ -150,6 +151,7 @@ def create(
150151
pricing: Pricing | None = None,
151152
coupon: str | None = None,
152153
*,
154+
wait_for_status: str | Callable[[str], bool] | None = lambda s: s != InstanceStatus.ORDERED,
153155
max_wait_time: float = 180,
154156
initial_interval: float = 0.5,
155157
max_interval: float = 5,
@@ -172,6 +174,7 @@ def create(
172174
contract: Optional contract type for the instance.
173175
pricing: Optional pricing model for the instance.
174176
coupon: Optional coupon code for discounts.
177+
wait_for_status: Status to wait for the instance to reach, or callable that returns True when the desired status is reached. Default to any status other than ORDERED. If None, no wait is performed.
175178
max_wait_time: Maximum total wait for the instance to start provisioning, in seconds (default: 180)
176179
initial_interval: Initial interval, in seconds (default: 0.5)
177180
max_interval: The longest single delay allowed between retries, in seconds (default: 5)
@@ -203,12 +206,18 @@ def create(
203206
payload['pricing'] = pricing
204207
id = self._http_client.post(INSTANCES_ENDPOINT, json=payload).text
205208

209+
if wait_for_status is None:
210+
return self.get_by_id(id)
211+
206212
# Wait for instance to enter provisioning state with timeout
207213
# TODO(shamrin) extract backoff logic, _clusters module has the same code
208214
deadline = time.monotonic() + max_wait_time
209215
for i in itertools.count():
210216
instance = self.get_by_id(id)
211-
if instance.status != InstanceStatus.ORDERED:
217+
if callable(wait_for_status):
218+
if wait_for_status(instance.status):
219+
return instance
220+
elif instance.status == wait_for_status:
212221
return instance
213222

214223
now = time.monotonic()

0 commit comments

Comments
 (0)