Skip to content

Commit b97ea63

Browse files
new: Add event polling functionality (#293)
## 📝 Description This change introduces various event-polling-related methods and classes to be reused across both official and unofficial Linode API integrations. Additionally, this change adds documentation and guide for using the new event polling system. This event polling system is derived from the event polling system implemented in the Linode Ansible Collection, but is _not_ backwards compatbile.
1 parent 66df89e commit b97ea63

13 files changed

Lines changed: 724 additions & 1 deletion

File tree

docs/guides/event_polling.rst

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
Polling for Events
2+
==================
3+
4+
There are often situations where an API request will trigger a
5+
long-running operation (e.g. Instance shutdown) that will run
6+
after the request has been made. These operations are tracked
7+
through `Linode Account Events`_ which reflect the target entity,
8+
progress, and status of these operations.
9+
10+
.. _Linode Account Events: https://www.linode.com/docs/api/account/#events-list
11+
12+
There are often cases where you would like for your application to
13+
halt until these operations have succeeded. The most reliable and
14+
efficient way to achieve this is by using the :py:class:`EventPoller`
15+
object.
16+
17+
Polling on Basic Operations
18+
---------------------------
19+
20+
In order to poll for an operation, we must create an :py:class:`EventPoller`
21+
object *before* the endpoint that triggers the operation has been called.
22+
23+
Assuming a :py:class:`LinodeClient` object has already been created with the name
24+
"client" and an :py:class:`Instance` object has already been created with the name "my_instance",
25+
an :py:class:`EventPoller` can be created using the
26+
:meth:`LinodeClient.polling.event_poller_create(...) <PollingGroup.event_poller_create>`
27+
method::
28+
29+
poller = client.polling.event_poller_create(
30+
"linode", # The type of the target entity
31+
"linode_shutdown", # The action to poll for
32+
entity_id=my_instance.id, # The ID of your Linode Instance
33+
)
34+
35+
Valid values for the `type` and `action` fields can be found in the `Events Response Documentation`_.
36+
37+
.. _Events Response Documentation: https://www.linode.com/docs/api/account/#events-list__responses
38+
39+
From here, we can send the request to trigger the long-running operation::
40+
41+
my_instance.shutdown()
42+
43+
To wait for this operation to finish, we can call the
44+
:meth:`poller.wait_for_next_event_finished(...) <EventPoller.wait_for_next_event_finished>`
45+
method::
46+
47+
poller.wait_for_next_event_finished()
48+
49+
The :py:class:`timeout` (default 240) and :py:class:`interval` (default 5) arguments can optionally be used to configure the timeout
50+
and poll frequency for this operation.
51+
52+
Bringing this together, we get the following::
53+
54+
from linode_api4 import LinodeClient, Instance
55+
56+
# Construct a client
57+
client = LinodeClient("MY_LINODE_TOKEN")
58+
59+
# Fetch an existing Linode Instance
60+
my_instance = client.load(Instance, 12345)
61+
62+
# Create the event poller
63+
poller = client.polling.event_poller_create(
64+
"linode", # The type of the target entity
65+
"linode_shutdown", # The action to poll for
66+
entity_id=my_instance.id, # The ID of your Linode Instance
67+
)
68+
69+
# Shutdown the Instance
70+
my_instance.shutdown()
71+
72+
# Wait until the event has finished
73+
poller.wait_for_next_event_finished()
74+
75+
print("Linode has been successfully shutdown!")
76+
77+
Polling for an Entity to be Free
78+
--------------------------------
79+
80+
In many cases, certain operations cannot be run until any other operations running on a resource have
81+
been completed. To ensure these operation are run reliably and do not encounter conflicts,
82+
you can use the
83+
:meth:`LinodeClient.polling.wait_for_entity_free(...) <PollingGroup.wait_for_entity_free>` method
84+
to wait until a resource has no running or queued operations.
85+
86+
For example::
87+
88+
# Construct a client
89+
client = LinodeClient("MY_LINODE_TOKEN")
90+
91+
# Load an existing instance
92+
my_instance = client.load(Instance, 12345)
93+
94+
# Wait until the Linode is not busy
95+
client.polling.wait_for_entity_free(
96+
"linode",
97+
my_instance.id
98+
)
99+
100+
# Boot the Instance
101+
my_instance.boot()
102+
103+
The :py:class:`timeout` (default 240) and :py:class:`interval` (default 5) arguments can optionally be used to configure the timeout
104+
and poll frequency for this operation.

docs/index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@ Table of Contents
3232

3333
guides/getting_started
3434
guides/core_concepts
35+
guides/event_polling
3536
guides/oauth
3637
linode_api4/linode_client
3738
linode_api4/login_client
3839
linode_api4/objects/models
40+
linode_api4/polling
3941
linode_api4/paginated_list
4042
linode_api4/objects/filtering

docs/linode_api4/linode_client.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,15 @@ with buckets and objects, use the s3 API directly with a library like `boto3`_.
146146

147147
.. _boto3: https://github.com/boto/boto3
148148

149+
PollingGroup
150+
^^^^^^^^^^^^
151+
152+
Includes methods related to account event polling.
153+
154+
.. autoclass:: linode_api4.linode_client.PollingGroup
155+
:members:
156+
:special-members:
157+
149158
ProfileGroup
150159
^^^^^^^^^^^^
151160

docs/linode_api4/objects/models.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,3 @@ Volume Models
139139
:exclude-members: api_endpoint, properties, derived_url_path, id_attribute, parent_id_name
140140
:undoc-members:
141141
:inherited-members:
142-

docs/linode_api4/polling.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Event Polling
2+
==========
3+
4+
This project exposes a framework for dynamically polling on long-running Linode Events.
5+
6+
See the :doc:`Event Polling Guide<../guides/event_polling>` for more details.
7+
8+
EventPoller class
9+
-------------------
10+
11+
.. autoclass:: linode_api4.EventPoller
12+
:members:

linode_api4/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
from linode_api4.linode_client import LinodeClient
55
from linode_api4.login_client import LinodeLoginClient, OAuthScopes
66
from linode_api4.paginated_list import PaginatedList
7+
from linode_api4.polling import EventPoller

linode_api4/groups/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .networking import *
1212
from .nodebalancer import *
1313
from .object_storage import *
14+
from .polling import *
1415
from .profile import *
1516
from .region import *
1617
from .support import *

linode_api4/groups/polling.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import polling
2+
3+
from linode_api4.groups import Group
4+
from linode_api4.objects.account import Event
5+
from linode_api4.polling import EventPoller, TimeoutContext
6+
7+
8+
class PollingGroup(Group):
9+
"""
10+
This group contains various helper functions for polling on Linode events.
11+
"""
12+
13+
def event_poller_create(
14+
self,
15+
entity_type: str,
16+
action: str,
17+
entity_id: int = None,
18+
) -> EventPoller:
19+
"""
20+
Creates a new instance of the EventPoller class.
21+
22+
:param entity_type: The type of the entity to poll for events on.
23+
Valid values for this field can be found here: https://www.linode.com/docs/api/account/#events-list__responses
24+
:type entity_type: str
25+
:param action: The action that caused the Event to poll for.
26+
Valid values for this field can be found here: https://www.linode.com/docs/api/account/#events-list__responses
27+
:type action: str
28+
:param entity_id: The ID of the entity to poll for.
29+
:type entity_id: int
30+
:param poll_interval: The interval in seconds to wait between polls.
31+
:type poll_interval: int
32+
33+
:returns: The new EventPoller object.
34+
:rtype: EventPoller
35+
"""
36+
37+
return EventPoller(
38+
self.client,
39+
entity_type,
40+
action,
41+
entity_id=entity_id,
42+
)
43+
44+
def wait_for_entity_free(
45+
self,
46+
entity_type: str,
47+
entity_id: int,
48+
timeout: int = 240,
49+
interval: int = 5,
50+
):
51+
"""
52+
Waits for all events relevant events to not be scheduled or in-progress.
53+
54+
:param entity_type: The type of the entity to poll for events on.
55+
Valid values for this field can be found here: https://www.linode.com/docs/api/account/#events-list__responses
56+
:type entity_type: str
57+
:param entity_id: The ID of the entity to poll for.
58+
:type entity_id: int
59+
:param timeout: The timeout in seconds for this polling operation.
60+
:type timeout: int
61+
:param interval: The interval in seconds to wait between polls.
62+
:type interval: int
63+
"""
64+
65+
timeout_ctx = TimeoutContext(timeout_seconds=timeout)
66+
67+
api_filter = {
68+
"+order": "desc",
69+
"+order_by": "created",
70+
"entity.id": entity_id,
71+
"entity.type": entity_type,
72+
}
73+
74+
def poll_func():
75+
events = self.client.get("/account/events", filters=api_filter)[
76+
"data"
77+
]
78+
return all(
79+
event["status"] not in ("scheduled", "started")
80+
for event in events
81+
)
82+
83+
if poll_func():
84+
return
85+
86+
polling.poll(
87+
poll_func,
88+
step=interval,
89+
timeout=timeout_ctx.seconds_remaining,
90+
)

linode_api4/linode_client.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,9 @@ def __init__(
168168
#: Access methods related to Images - See :any:`ImageGroup` for more information.
169169
self.images = ImageGroup(self)
170170

171+
#: Access methods related to Event polling - See :any:`PollingGroup` for more information.
172+
self.polling = PollingGroup(self)
173+
171174
@property
172175
def _user_agent(self):
173176
return "{}python-linode_api4/{} {}".format(

0 commit comments

Comments
 (0)