semantic bufo search find-bufo.com
bufo
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(bot): persist rate limit across restarts

rate limit was using an in-memory ring buffer that reset on deploy,
allowing a burst of posts. now uses the already-persisted last_posted
timestamps to count recent posts — survives restarts for free.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+16 -14
+1 -14
bot/src/main.zig
··· 20 20 21 21 var global_state: ?*BotState = null; 22 22 23 - const MAX_RATE_LIMIT_SLOTS = 64; 24 - 25 23 const BotState = struct { 26 24 allocator: Allocator, 27 25 config: config.Config, ··· 29 27 bsky_client: bsky.BskyClient, 30 28 mutex: Io.Mutex = Io.Mutex.init, 31 29 stats: stats.Stats, 32 - // ring buffer of recent post timestamps for global rate limiting 33 - recent_posts: [MAX_RATE_LIMIT_SLOTS]i64 = [_]i64{0} ** MAX_RATE_LIMIT_SLOTS, 34 - recent_post_idx: usize = 0, 35 30 }; 36 31 37 32 pub fn main() !void { ··· 180 175 181 176 // global rate limit (0 = disabled) 182 177 if (state.config.max_posts_per_hour > 0) { 183 - const one_hour_ago = now - 3600; 184 - var recent_count: u32 = 0; 185 - for (state.recent_posts) |ts| { 186 - if (ts > one_hour_ago) recent_count += 1; 187 - } 178 + const recent_count = state.stats.recentPostCount(3600); 188 179 if (recent_count >= state.config.max_posts_per_hour) { 189 180 std.debug.print("rate limit: {} posts in last hour, skipping {s}\n", .{ recent_count, match.name }); 190 181 return; ··· 285 276 286 277 std.debug.print("posted bufo quote: {s} (rkey: {s})\n", .{ match.name, our_rkey }); 287 278 state.stats.incPostsCreated(); 288 - 289 - // record for global rate limiting 290 - state.recent_posts[state.recent_post_idx] = now; 291 - state.recent_post_idx = (state.recent_post_idx + 1) % MAX_RATE_LIMIT_SLOTS; 292 279 293 280 // track our post for cleanup on delete/block 294 281 state.stats.addTrackedPost(our_rkey, post.uri, post.did, now);
+15
bot/src/stats.zig
··· 372 372 self.saveUnlocked(); 373 373 } 374 374 375 + /// Count how many bufos were posted within the last `window_secs` seconds. 376 + /// Uses the persisted last_posted timestamps, so survives restarts. 377 + pub fn recentPostCount(self: *Stats, window_secs: i64) u32 { 378 + self.bufo_mutex.lockUncancelable(io); 379 + defer self.bufo_mutex.unlock(io); 380 + 381 + const cutoff = timestamp() - window_secs; 382 + var count: u32 = 0; 383 + var iter = self.last_posted.iterator(); 384 + while (iter.next()) |entry| { 385 + if (entry.value_ptr.* > cutoff) count += 1; 386 + } 387 + return count; 388 + } 389 + 375 390 pub fn addTrackedPost(self: *Stats, our_rkey: []const u8, original_uri: []const u8, original_did: []const u8, ts: i64) void { 376 391 self.bufo_mutex.lockUncancelable(io); 377 392 defer self.bufo_mutex.unlock(io);