Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 51bac22

Browse files
committed
Add cache and rate limiting APIs
1 parent 5e2c5f9 commit 51bac22

7 files changed

Lines changed: 931 additions & 19 deletions

File tree

README.md

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ The easiest way to write Fastly Compute services in Zig.
2929
- [Device Detection](#device-detection)
3030
- [Cache Purging](#cache-purging)
3131
- [Runtime Metrics](#runtime-metrics)
32+
- [Cache Transactions](#cache-transactions)
33+
- [Rate Limiting](#rate-limiting)
3234
- [Deployment to Fastly's platform](#deployment-to-fastlys-platform)
3335

3436
## What is Fastly Compute?
@@ -214,8 +216,8 @@ try query.headers.set("X-Custom-Header", "Custom value");
214216
Body content can also be pushed, even as chunks:
215217

216218
```zig
217-
try query.body.write("X");
218-
try query.body.write("Y");
219+
_ = try query.body.write("X");
220+
_ = try query.body.write("Y");
219221
try query.body.close();
220222
```
221223

@@ -410,6 +412,89 @@ const vcpu_ms = try zigly.runtime.getVcpuMs();
410412
// Returns the amount of vCPU time used in milliseconds
411413
```
412414

415+
#### Cache Transactions
416+
417+
The cache API provides both simple caching and request-collapsing transactions:
418+
419+
```zig
420+
const cache = zigly.cache;
421+
422+
// Simple cache insert
423+
var body = try cache.insert("my-cache-key", .{
424+
.max_age_ns = cache.secondsToNs(3600), // 1 hour TTL
425+
});
426+
_ = try body.write("Cached content");
427+
try body.close();
428+
429+
// Simple cache lookup
430+
var entry = try cache.lookup("my-cache-key", .{});
431+
const state = try entry.getState();
432+
if (state.isFound() and state.isUsable()) {
433+
var cached_body = try entry.getBody(null);
434+
const content = try cached_body.readAll(allocator, 0);
435+
// Use cached content
436+
}
437+
try entry.close();
438+
```
439+
440+
For request-collapsing (preventing thundering herd), use transactional lookups:
441+
442+
```zig
443+
// Transactional lookup - only one request will fetch/generate content
444+
var tx = try cache.transactionLookup("my-key", .{});
445+
const tx_state = try tx.getState();
446+
447+
if (tx_state.mustInsertOrUpdate()) {
448+
// We won the race - insert new content
449+
var result = try tx.insert(.{
450+
.max_age_ns = cache.secondsToNs(60),
451+
});
452+
_ = try result.body.write("Fresh content");
453+
try result.body.close();
454+
} else if (tx_state.isUsable()) {
455+
// Content is available
456+
var cached_body = try tx.getBody(null);
457+
const content = try cached_body.readAll(allocator, 0);
458+
}
459+
460+
try tx.close();
461+
```
462+
463+
#### Rate Limiting
464+
465+
Edge Rate Limiting provides rate counters and penalty boxes for traffic control:
466+
467+
```zig
468+
const erl = zigly.erl;
469+
470+
// Rate counter - track request rates
471+
const rc = erl.RateCounter.open("my_rate_counter");
472+
try rc.increment("client-ip-192.168.1.1", 1);
473+
474+
const rate = try rc.lookupRate("client-ip-192.168.1.1", 10); // 10-second window
475+
const count = try rc.lookupCount("client-ip-192.168.1.1", 60); // 60-second window
476+
477+
// Penalty box - block bad actors
478+
const pb = erl.PenaltyBox.open("my_penalty_box");
479+
if (try pb.has("bad-actor")) {
480+
// Client is in penalty box, reject request
481+
}
482+
try pb.add("bad-actor", 300); // Block for 5 minutes
483+
484+
// Combined rate limiter
485+
const limiter = erl.RateLimiter.init(.{
486+
.rate_counter = "my_rate_counter",
487+
.penalty_box = "my_penalty_box",
488+
.window_seconds = 10,
489+
.limit = 100, // 100 requests per 10 seconds
490+
.ttl_seconds = 300, // 5 minute penalty
491+
});
492+
493+
if (!try limiter.isAllowed("client-id", 1)) {
494+
// Rate limited - reject request
495+
}
496+
```
497+
413498
## Deployment to Fastly's platform
414499

415500
The `fastly` command-line tool only supports compilation of Rust and AssemblyScript at the moment.

build.zig

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,17 @@ pub fn build(b: *std.Build) !void {
3636
.root_module = exe_module,
3737
});
3838
b.installArtifact(exe);
39+
40+
const readme_module = b.createModule(.{
41+
.root_source_file = b.path("tmp/readme_examples_test.zig"),
42+
.target = target,
43+
.optimize = optimize,
44+
});
45+
readme_module.addImport("zigly", lib_module);
46+
47+
const readme_exe = b.addExecutable(.{
48+
.name = "readme-examples-test",
49+
.root_module = readme_module,
50+
});
51+
b.installArtifact(readme_exe);
3952
}

src/tests.zig

Lines changed: 184 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ const Request = zigly.http.Request;
1111
const Logger = zigly.Logger;
1212
const Backend = zigly.Backend;
1313
const DynamicBackend = zigly.DynamicBackend;
14+
const cache = zigly.cache;
15+
const erl = zigly.erl;
1416

1517
fn start() !void {
1618
var gpa = std.heap.GeneralPurposeAllocator(.{ .safety = true }){};
@@ -60,14 +62,26 @@ fn start() !void {
6062
_ = try request.isPost();
6163
}
6264

63-
{
65+
google_test: {
6466
var arena = ArenaAllocator.init(allocator);
6567
defer arena.deinit();
66-
var query = try Request.new("GET", "https://www.google.com");
67-
try query.setCachingPolicy(.{ .no_cache = true });
68-
var response = try query.send("google");
69-
const body = try response.body.readAll(arena.allocator(), 0);
70-
std.debug.print("{s}\n", .{body});
68+
var query = Request.new("GET", "https://www.google.com") catch |err| {
69+
std.debug.print("Google request creation error: {}\n", .{err});
70+
break :google_test;
71+
};
72+
query.setCachingPolicy(.{ .no_cache = true }) catch |err| {
73+
std.debug.print("Cache policy error: {}\n", .{err});
74+
break :google_test;
75+
};
76+
var response = query.send("google") catch |err| {
77+
std.debug.print("Google send error: {}\n", .{err});
78+
break :google_test;
79+
};
80+
const body = response.body.readAll(arena.allocator(), 0) catch |err| {
81+
std.debug.print("Google body read error: {}\n", .{err});
82+
break :google_test;
83+
};
84+
std.debug.print("Google response length: {}\n", .{body.len});
7185
}
7286

7387
// Test the Apache combined log format function
@@ -84,12 +98,12 @@ fn start() !void {
8498
}
8599

86100
// Test dynamic backend registration (must be before finishing downstream response)
87-
{
101+
dyn_backend_test: {
88102
std.debug.print("Testing dynamic backends...\n", .{});
89103

90104
// Register a dynamic backend to httpbin.org
91105
const dynamic_backend = DynamicBackend{
92-
.name = "httpbin",
106+
.name = "httpbin_dyn",
93107
.target = "httpbin.org:443",
94108
.use_ssl = true,
95109
.host_override = "httpbin.org",
@@ -100,38 +114,191 @@ fn start() !void {
100114
.between_bytes_timeout_ms = 10000,
101115
};
102116

103-
const dyn_backend = try dynamic_backend.register();
117+
const dyn_backend = dynamic_backend.register() catch |err| {
118+
std.debug.print("Dynamic backend registration error: {}\n", .{err});
119+
break :dyn_backend_test;
120+
};
104121
std.debug.print("Dynamic backend registered: {s}\n", .{dyn_backend.name});
105122

106123
// Check if backend exists
107-
const exists = try Backend.exists("httpbin");
124+
const exists = Backend.exists("httpbin_dyn") catch false;
108125
std.debug.print("Backend exists: {}\n", .{exists});
109126

110127
// Check if backend is dynamic
111-
const is_dynamic = try dyn_backend.isDynamic();
128+
const is_dynamic = dyn_backend.isDynamic() catch false;
112129
std.debug.print("Backend is dynamic: {}\n", .{is_dynamic});
113130

114131
// Check if backend uses SSL
115-
const is_ssl = try dyn_backend.isSsl();
132+
const is_ssl = dyn_backend.isSsl() catch false;
116133
std.debug.print("Backend uses SSL: {}\n", .{is_ssl});
117134

118135
// Get backend port
119-
const port = try dyn_backend.getPort();
136+
const port = dyn_backend.getPort() catch 0;
120137
std.debug.print("Backend port: {}\n", .{port});
121138

122139
// Make a request using the dynamic backend
123140
var arena = ArenaAllocator.init(allocator);
124141
defer arena.deinit();
125142

126-
var query = try Request.new("GET", "https://httpbin.org/get");
127-
var dyn_response = try query.send("httpbin");
128-
const status = try dyn_response.getStatus();
143+
var query = Request.new("GET", "https://httpbin.org/get") catch |err| {
144+
std.debug.print("Request creation error: {}\n", .{err});
145+
break :dyn_backend_test;
146+
};
147+
var dyn_response = query.send("httpbin_dyn") catch |err| {
148+
std.debug.print("Request send error: {}\n", .{err});
149+
break :dyn_backend_test;
150+
};
151+
const status = dyn_response.getStatus() catch 0;
129152
std.debug.print("Response status from dynamic backend: {}\n", .{status});
130153

131-
const body = try dyn_response.body.readAll(arena.allocator(), 1024);
154+
const body = dyn_response.body.readAll(arena.allocator(), 1024) catch |err| {
155+
std.debug.print("Body read error: {}\n", .{err});
156+
break :dyn_backend_test;
157+
};
132158
std.debug.print("Response body (first 200 chars): {s}\n", .{body[0..@min(body.len, 200)]});
133159
}
134160

161+
// Test cache transactions
162+
cache_test: {
163+
std.debug.print("Testing cache transactions...\n", .{});
164+
165+
const cache_key = "test-cache-key-12345";
166+
const cache_body = "Hello from cache!";
167+
168+
// Insert into cache (simple test without metadata)
169+
var insert_body = cache.insert(cache_key, .{
170+
.max_age_ns = cache.secondsToNs(60),
171+
}) catch |err| {
172+
std.debug.print("Cache insert error: {}\n", .{err});
173+
break :cache_test;
174+
};
175+
_ = try insert_body.write(cache_body);
176+
try insert_body.close();
177+
178+
std.debug.print("Cache insert completed\n", .{});
179+
180+
// Lookup from cache
181+
var entry = cache.lookup(cache_key, .{}) catch |err| {
182+
std.debug.print("Cache lookup error: {}\n", .{err});
183+
break :cache_test;
184+
};
185+
const state = try entry.getState();
186+
std.debug.print("Cache state - found: {}, usable: {}, stale: {}\n", .{
187+
state.isFound(),
188+
state.isUsable(),
189+
state.isStale(),
190+
});
191+
192+
if (state.isFound()) {
193+
var arena = ArenaAllocator.init(allocator);
194+
defer arena.deinit();
195+
196+
var body = try entry.getBody(null);
197+
const content = try body.readAll(arena.allocator(), 1024);
198+
std.debug.print("Cache body: {s}\n", .{content});
199+
}
200+
201+
try entry.close();
202+
std.debug.print("Cache test completed\n", .{});
203+
}
204+
205+
// Test transactional cache lookup
206+
tx_test: {
207+
std.debug.print("Testing transactional cache...\n", .{});
208+
209+
const tx_key = "test-transaction-key-67890";
210+
211+
// Transactional lookup (request-collapsing)
212+
var tx = cache.transactionLookup(tx_key, .{}) catch |err| {
213+
std.debug.print("Transaction lookup error: {}\n", .{err});
214+
break :tx_test;
215+
};
216+
const tx_state = try tx.getState();
217+
218+
if (tx_state.mustInsertOrUpdate()) {
219+
std.debug.print("Transaction requires insert\n", .{});
220+
var result = try tx.insert(.{
221+
.max_age_ns = cache.secondsToNs(30),
222+
});
223+
_ = try result.body.write("Transaction cached content");
224+
try result.body.close();
225+
std.debug.print("Transaction insert completed\n", .{});
226+
} else {
227+
std.debug.print("Transaction found existing entry\n", .{});
228+
}
229+
230+
try tx.close();
231+
std.debug.print("Transaction test completed\n", .{});
232+
}
233+
234+
// Test rate limiting (ERL)
235+
erl_test: {
236+
std.debug.print("Testing rate limiting...\n", .{});
237+
238+
// Test rate counter
239+
const rc = erl.RateCounter.open("test_rc");
240+
rc.increment("client-ip-192.168.1.1", 1) catch |err| {
241+
std.debug.print("Rate counter increment error: {}\n", .{err});
242+
break :erl_test;
243+
};
244+
std.debug.print("Rate counter incremented\n", .{});
245+
246+
const rate = rc.lookupRate("client-ip-192.168.1.1", 10) catch |err| {
247+
std.debug.print("Rate lookup error: {}\n", .{err});
248+
break :erl_test;
249+
};
250+
std.debug.print("Rate for client: {}\n", .{rate});
251+
252+
const count = rc.lookupCount("client-ip-192.168.1.1", 60) catch |err| {
253+
std.debug.print("Count lookup error: {}\n", .{err});
254+
break :erl_test;
255+
};
256+
std.debug.print("Count for client: {}\n", .{count});
257+
258+
// Test penalty box
259+
const pb = erl.PenaltyBox.open("test_pb");
260+
const in_pb_before = pb.has("bad-actor") catch |err| {
261+
std.debug.print("Penalty box has error: {}\n", .{err});
262+
break :erl_test;
263+
};
264+
std.debug.print("Bad actor in penalty box before: {}\n", .{in_pb_before});
265+
266+
pb.add("bad-actor", 300) catch |err| {
267+
std.debug.print("Penalty box add error: {}\n", .{err});
268+
break :erl_test;
269+
};
270+
std.debug.print("Added bad actor to penalty box\n", .{});
271+
272+
const in_pb_after = pb.has("bad-actor") catch |err| {
273+
std.debug.print("Penalty box has (after) error: {}\n", .{err});
274+
break :erl_test;
275+
};
276+
std.debug.print("Bad actor in penalty box after: {}\n", .{in_pb_after});
277+
278+
// Test combined rate limiter
279+
const limiter = erl.RateLimiter.init(.{
280+
.rate_counter = "test_rc",
281+
.penalty_box = "test_pb",
282+
.window_seconds = 10,
283+
.limit = 100,
284+
.ttl_seconds = 300,
285+
});
286+
287+
const result = limiter.checkRate("test-entry", 1) catch |err| {
288+
std.debug.print("Rate check error: {}\n", .{err});
289+
break :erl_test;
290+
};
291+
std.debug.print("Rate check result: {s}\n", .{if (result == .allowed) "allowed" else "blocked"});
292+
293+
const is_allowed = limiter.isAllowed("test-entry", 1) catch |err| {
294+
std.debug.print("Is allowed error: {}\n", .{err});
295+
break :erl_test;
296+
};
297+
std.debug.print("Is allowed: {}\n", .{is_allowed});
298+
299+
std.debug.print("Rate limiting test completed\n", .{});
300+
}
301+
135302
// Final response to client
136303
{
137304
var response = downstream.response;

src/zigly.zig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,9 @@ pub const Acl = lib.Acl;
1717
pub const purge = lib.purge;
1818
pub const device = lib.device;
1919
pub const runtime = lib.runtime;
20+
pub const cache = lib.cache;
21+
pub const erl = lib.erl;
22+
pub const RateLimiter = lib.RateLimiter;
23+
pub const RateCounter = lib.RateCounter;
24+
pub const PenaltyBox = lib.PenaltyBox;
2025
pub const compatibilityCheck = lib.compatibilityCheck;

0 commit comments

Comments
 (0)