Skip to content

Commit db5401f

Browse files
authored
Adds Python version of durable objects examples (#26809)
1 parent 5d52290 commit db5401f

File tree

8 files changed

+574
-8
lines changed

8 files changed

+574
-8
lines changed

src/content/docs/durable-objects/examples/alarms-api.mdx

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@ sidebar:
77
description: Use the Durable Objects Alarms API to batch requests to a Durable Object.
88
---
99

10-
import { GlossaryTooltip, WranglerConfig } from "~/components";
10+
import { GlossaryTooltip, TabItem, Tabs, WranglerConfig } from "~/components";
1111

1212
This example implements an <GlossaryTooltip term="alarm">`alarm()`</GlossaryTooltip> handler that allows batching of requests to a single Durable Object.
1313

1414
When a request is received and no alarm is set, it sets an alarm for 10 seconds in the future. The `alarm()` handler processes all requests received within that 10-second window.
1515

1616
If no new requests are received, no further alarms will be set until the next request arrives.
1717

18+
<Tabs> <TabItem label="JavaScript" icon="seti:javascript">
19+
1820
```js
1921
import { DurableObject } from "cloudflare:workers";
2022

@@ -25,8 +27,6 @@ export default {
2527
},
2628
};
2729

28-
const SECONDS = 10;
29-
3030
// Durable Object
3131
export class Batcher extends DurableObject {
3232
constructor(ctx, env) {
@@ -45,7 +45,7 @@ export class Batcher extends DurableObject {
4545
// Any further POSTs in the next 10 seconds will be part of this batch.
4646
let currentAlarm = await this.storage.getAlarm();
4747
if (currentAlarm == null) {
48-
this.storage.setAlarm(Date.now() + 1000 * SECONDS);
48+
this.storage.setAlarm(Date.now() + 1000 * 10);
4949
}
5050

5151
// Add the request to the batch.
@@ -69,6 +69,59 @@ export class Batcher extends DurableObject {
6969
}
7070
```
7171

72+
</TabItem><TabItem label="Python" icon="seti:python">
73+
74+
```py
75+
from workers import DurableObject, Response, WorkerEntrypoint, fetch
76+
import time
77+
78+
# Worker
79+
class Default(WorkerEntrypoint):
80+
async def fetch(self, request):
81+
stub = self.env.BATCHER.getByName("foo")
82+
return await stub.fetch(request)
83+
84+
# Durable Object
85+
class Batcher(DurableObject):
86+
def __init__(self, ctx, env):
87+
super().__init__(ctx, env)
88+
self.storage = ctx.storage
89+
90+
@self.ctx.blockConcurrencyWhile
91+
async def initialize():
92+
vals = await self.storage.list(reverse=True, limit=1)
93+
self.count = 0
94+
if len(vals) > 0:
95+
self.count = int(vals.keys().next().value)
96+
97+
async def fetch(self, request):
98+
self.count += 1
99+
100+
# If there is no alarm currently set, set one for 10 seconds from now
101+
# Any further POSTs in the next 10 seconds will be part of this batch.
102+
current_alarm = await self.storage.getAlarm()
103+
if current_alarm is None:
104+
self.storage.setAlarm(int(time.time() * 1000) + 1000 * 10)
105+
106+
# Add the request to the batch.
107+
await self.storage.put(self.count, await request.text())
108+
return Response.json(
109+
{"queued": self.count}
110+
)
111+
112+
async def alarm(self):
113+
vals = await self.storage.list()
114+
await fetch(
115+
"http://example.com/some-upstream-service",
116+
method="POST",
117+
body=list(vals.values())
118+
)
119+
await self.storage.deleteAll()
120+
self.count = 0
121+
```
122+
123+
</TabItem> </Tabs>
124+
72125
The `alarm()` handler will be called once every 10 seconds. If an unexpected error terminates the Durable Object, the `alarm()` handler will be re-instantiated on another machine. Following a short delay, the `alarm()` handler will run from the beginning on the other machine.
73126

74127
Finally, configure your Wrangler file to include a Durable Object [binding](/durable-objects/get-started/#4-configure-durable-object-bindings) and [migration](/durable-objects/reference/durable-objects-migrations/) based on the namespace and class name chosen previously.

src/content/docs/durable-objects/examples/build-a-counter.mdx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,68 @@ export class Counter extends DurableObject {
148148
}
149149
```
150150

151+
</TabItem> <TabItem label="Python" icon="seti:python">
152+
153+
```py
154+
from workers import DurableObject, Response, WorkerEntrypoint
155+
from urllib.parse import urlparse, parse_qs
156+
157+
# Worker
158+
class Default(WorkerEntrypoint):
159+
async def fetch(self, request):
160+
parsed_url = urlparse(request.url)
161+
query_params = parse_qs(parsed_url.query)
162+
name = query_params.get('name', [None])[0]
163+
164+
if not name:
165+
return Response(
166+
"Select a Durable Object to contact by using"
167+
+ " the `name` URL query string parameter, for example, ?name=A"
168+
)
169+
170+
# A stub is a client Object used to send messages to the Durable Object.
171+
stub = self.env.COUNTERS.getByName(name)
172+
173+
# Send a request to the Durable Object using RPC methods, then await its response.
174+
count = None
175+
176+
if parsed_url.path == "/increment":
177+
count = await stub.increment()
178+
elif parsed_url.path == "/decrement":
179+
count = await stub.decrement()
180+
elif parsed_url.path == "" or parsed_url.path == "/":
181+
# Serves the current value.
182+
count = await stub.getCounterValue()
183+
else:
184+
return Response("Not found", status=404)
185+
186+
return Response(f"Durable Object '{name}' count: {count}")
187+
188+
# Durable Object
189+
class Counter(DurableObject):
190+
def __init__(self, ctx, env):
191+
super().__init__(ctx, env)
192+
193+
async def getCounterValue(self):
194+
value = await self.ctx.storage.get("value")
195+
return value if value is not None else 0
196+
197+
async def increment(self, amount=1):
198+
value = await self.ctx.storage.get("value")
199+
value = (value if value is not None else 0) + amount
200+
# You do not have to worry about a concurrent request having modified the value in storage.
201+
# "input gates" will automatically protect against unwanted concurrency.
202+
# Read-modify-write is safe.
203+
await self.ctx.storage.put("value", value)
204+
return value
205+
206+
async def decrement(self, amount=1):
207+
value = await self.ctx.storage.get("value")
208+
value = (value if value is not None else 0) - amount
209+
await self.ctx.storage.put("value", value)
210+
return value
211+
```
212+
151213
</TabItem> </Tabs>
152214

153215
Finally, configure your Wrangler file to include a Durable Object [binding](/durable-objects/get-started/#4-configure-durable-object-bindings) and [migration](/durable-objects/reference/durable-objects-migrations/) based on the namespace and class name chosen previously.

src/content/docs/durable-objects/examples/build-a-rate-limiter.mdx

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,70 @@ export class RateLimiter extends DurableObject {
198198
}
199199
```
200200

201+
</TabItem> <TabItem label="Python" icon="seti:python">
202+
203+
```py
204+
from workers import DurableObject, Response, WorkerEntrypoint
205+
import time
206+
207+
# Worker
208+
class Default(WorkerEntrypoint):
209+
async def fetch(self, request):
210+
# Determine the IP address of the client
211+
ip = request.headers.get("CF-Connecting-IP")
212+
if ip is None:
213+
return Response("Could not determine client IP", status=400)
214+
215+
try:
216+
stub = self.env.RATE_LIMITER.getByName(ip)
217+
milliseconds_to_next_request = await stub.getMillisecondsToNextRequest()
218+
if milliseconds_to_next_request > 0:
219+
# Alternatively one could sleep for the necessary length of time
220+
return Response("Rate limit exceeded", status=429)
221+
except Exception as error:
222+
return Response("Could not connect to rate limiter", status=502)
223+
224+
# TODO: Implement me
225+
return Response("Call some upstream resource...")
226+
227+
# Durable Object
228+
class RateLimiter(DurableObject):
229+
milliseconds_per_request = 1
230+
milliseconds_for_updates = 5000
231+
capacity = 10000
232+
233+
def __init__(self, ctx, env):
234+
super().__init__(ctx, env)
235+
self.tokens = RateLimiter.capacity
236+
237+
async def getMillisecondsToNextRequest(self):
238+
await self.checkAndSetAlarm()
239+
240+
milliseconds_to_next_request = RateLimiter.milliseconds_per_request
241+
if self.tokens > 0:
242+
self.tokens -= 1
243+
milliseconds_to_next_request = 0
244+
245+
return milliseconds_to_next_request
246+
247+
async def checkAndSetAlarm(self):
248+
current_alarm = await self.ctx.storage.getAlarm()
249+
if current_alarm is None:
250+
self.ctx.storage.setAlarm(
251+
int(time.time() * 1000)
252+
+ RateLimiter.milliseconds_for_updates
253+
* RateLimiter.milliseconds_per_request
254+
)
255+
256+
async def alarm(self):
257+
if self.tokens < RateLimiter.capacity:
258+
self.tokens = min(
259+
RateLimiter.capacity,
260+
self.tokens + RateLimiter.milliseconds_for_updates,
261+
)
262+
await self.checkAndSetAlarm()
263+
```
264+
201265
</TabItem>
202266
</Tabs>
203267

@@ -267,6 +331,34 @@ export class RateLimiter extends DurableObject {
267331
}
268332
```
269333

334+
</TabItem> <TabItem label="Python" icon="seti:python">
335+
336+
```py
337+
from workers import DurableObject
338+
import time
339+
340+
# Durable Object
341+
class RateLimiter(DurableObject):
342+
milliseconds_per_request = 1
343+
milliseconds_for_grace_period = 5000
344+
345+
def __init__(self, ctx, env):
346+
super().__init__(ctx, env)
347+
self.nextAllowedTime = 0
348+
349+
async def getMillisecondsToNextRequest(self):
350+
now = int(time.time() * 1000)
351+
352+
self.nextAllowedTime = max(now, self.nextAllowedTime)
353+
self.nextAllowedTime += RateLimiter.milliseconds_per_request
354+
355+
value = max(
356+
0,
357+
self.nextAllowedTime - now - RateLimiter.milliseconds_for_grace_period,
358+
)
359+
return value
360+
```
361+
270362
</TabItem>
271363
</Tabs>
272364

src/content/docs/durable-objects/examples/durable-object-in-memory-state.mdx

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ description: Create a Durable Object that stores the last location it was
99
accessed from in-memory.
1010
---
1111

12-
import { WranglerConfig } from "~/components";
12+
import { TabItem, Tabs, WranglerConfig } from "~/components";
1313

1414
This example shows you how Durable Objects are stateful, meaning in-memory state can be retained between requests. After a brief period of inactivity, the Durable Object will be evicted, and all in-memory state will be lost. The next request will reconstruct the object, but instead of showing the city of the previous request, it will display a message indicating that the object has been reinitialized. If you need your applications state to survive eviction, write the state to storage by using the [Storage API](/durable-objects/api/sqlite-storage-api/), or by storing your data elsewhere.
1515

16+
<Tabs> <TabItem label="JavaScript" icon="seti:javascript">
17+
1618
```js
1719
import { DurableObject } from "cloudflare:workers";
1820

@@ -65,6 +67,55 @@ New Location: ${request.cf.city}`);
6567
}
6668
```
6769

70+
</TabItem> <TabItem label="Python" icon="seti:python">
71+
72+
```py
73+
from workers import DurableObject, Response, WorkerEntrypoint
74+
75+
# Worker
76+
class Default(WorkerEntrypoint):
77+
async def fetch(self, request):
78+
return await handle_request(request, self.env)
79+
80+
async def handle_request(request, env):
81+
stub = env.LOCATION.getByName("A")
82+
# Forward the request to the remote Durable Object.
83+
resp = await stub.fetch(request)
84+
# Return the response to the client.
85+
return Response(await resp.text())
86+
87+
# Durable Object
88+
class Location(DurableObject):
89+
def __init__(self, ctx, env):
90+
super().__init__(ctx, env)
91+
# Upon construction, you do not have a location to provide.
92+
# This value will be updated as people access the Durable Object.
93+
# When the Durable Object is evicted from memory, this will be reset.
94+
self.location = None
95+
96+
# Handle HTTP requests from clients.
97+
async def fetch(self, request):
98+
response = None
99+
100+
if self.location is None:
101+
response = f"""
102+
This is the first request, you called the constructor, so this.location was null.
103+
You will set this.location to be your city: ({request.js_object.cf.city}). Try reloading the page."""
104+
else:
105+
response = f"""
106+
The Durable Object was already loaded and running because it recently handled a request.
107+
108+
Previous Location: {self.location}
109+
New Location: {request.js_object.cf.city}"""
110+
111+
# You set the new location to be the new city.
112+
self.location = request.js_object.cf.city
113+
print(response)
114+
return Response(response)
115+
```
116+
117+
</TabItem> </Tabs>
118+
68119
Finally, configure your Wrangler file to include a Durable Object [binding](/durable-objects/get-started/#4-configure-durable-object-bindings) and [migration](/durable-objects/reference/durable-objects-migrations/) based on the namespace and class name chosen previously.
69120

70121
<WranglerConfig>

src/content/docs/durable-objects/examples/durable-object-ttl.mdx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,35 @@ export default {
8989
} satisfies ExportedHandler<Env>;
9090
```
9191

92+
</TabItem> <TabItem label="Python" icon="seti:python">
93+
94+
```py
95+
from workers import DurableObject, Response, WorkerEntrypoint
96+
import time
97+
98+
# Durable Object
99+
class MyDurableObject(DurableObject):
100+
# Time To Live (TTL) in milliseconds
101+
timeToLiveMs = 1000
102+
103+
def __init__(self, ctx, env):
104+
super().__init__(ctx, env)
105+
106+
async def fetch(self, _request):
107+
# Extend the TTL immediately following every fetch request to a Durable Object.
108+
await self.ctx.storage.setAlarm(int(time.time() * 1000) + self.timeToLiveMs)
109+
...
110+
111+
async def alarm(self):
112+
await self.ctx.storage.deleteAll()
113+
114+
# Worker
115+
class Default(WorkerEntrypoint):
116+
async def fetch(self, request):
117+
stub = self.env.MY_DURABLE_OBJECT.getByName("foo")
118+
return await stub.fetch(request)
119+
```
120+
92121
</TabItem> </Tabs>
93122

94123
To test and deploy this example, configure your Wrangler file to include a Durable Object [binding](/durable-objects/get-started/#4-configure-durable-object-bindings) and [migration](/durable-objects/reference/durable-objects-migrations/) based on the namespace and class name chosen previously.

0 commit comments

Comments
 (0)