diff --git a/frontend-partials.lisp b/frontend-partials.lisp index 203ddf8..cec0665 100644 --- a/frontend-partials.lisp +++ b/frontend-partials.lisp @@ -68,7 +68,7 @@ (shuffle-now-playing mount) (harmony-now-playing mount)))) -(define-api-with-limit asteroid/partial/now-playing (&optional mount) (:limit 30 :timeout 60) +(define-api-with-limit asteroid/partial/now-playing (&optional mount) (:limit 120 :timeout 60) "Get Partial HTML with live now-playing status. Optional MOUNT parameter specifies which stream to get metadata from. Returns partial HTML with current track info." @@ -91,7 +91,7 @@ :connection-error t :stats nil)))))) -(define-api-with-limit asteroid/partial/now-playing-inline (&optional mount) (:limit 30 :timeout 60) +(define-api-with-limit asteroid/partial/now-playing-inline (&optional mount) (:limit 120 :timeout 60) "Get inline text with now playing info (for admin dashboard and widgets). Optional MOUNT parameter specifies which stream to get metadata from." (with-error-handling diff --git a/limiter.lisp b/limiter.lisp index 305f0c8..9c65fec 100644 --- a/limiter.lisp +++ b/limiter.lisp @@ -1,4 +1,10 @@ ;;;; limiter.lisp - Rate limiter definitions for the application +;;;; +;;;; Includes monkey-patches for r-simple-rate's sliding-window bug: +;;;; upstream tax-rate updates the timestamp on EVERY request, which +;;;; prevents the window from ever resetting while polling is active. +;;;; Our overrides use a proper fixed window — the timestamp is only +;;;; updated when the window expires and the counter resets. (in-package :asteroid) @@ -15,6 +21,56 @@ (error (e) (l:warn :rate-limiter "Failed to cleanup rate limits: ~a" e)))) +;;; ——— r-simple-rate fixed-window overrides ——— + +(defun simple-rate::tax-rate (limit &key (ip (remote *request*))) + "Fixed-window version of tax-rate. + Only updates the timestamp when the window resets, not on every request. + This prevents the sliding-window bug where continuous polling starves + the counter because the reset condition never triggers." + (let* ((limit (simple-rate::limit limit)) + (tracking (dm:get-one 'simple-rate::tracking + (db:query (:and (:= 'limit (simple-rate::name limit)) + (:= 'ip ip)))))) + (cond (tracking + ;; If the window has expired, reset counter AND timestamp + (when (<= (+ (dm:field tracking "time") + (simple-rate::timeout limit)) + (get-universal-time)) + (setf (dm:field tracking "amount") (simple-rate::amount limit)) + (setf (dm:field tracking "time") (get-universal-time))) + ;; Tax it (do NOT touch timestamp here — fixed window) + (decf (dm:field tracking "amount")) + (dm:save tracking)) + (t + (db:insert 'simple-rate::tracking + `((limit . ,(simple-rate::name limit)) + (time . ,(get-universal-time)) + (amount . ,(simple-rate::amount limit)) + (ip . ,ip))))))) + +(defun rate:left (limit &key (ip (remote *request*))) + "Fixed-window version of rate:left. + Returns correct remaining amount even for expired windows, so that + with-limitation does not block on stale tracking entries." + (let* ((limit (simple-rate::limit limit)) + (tracking (dm:get-one 'simple-rate::tracking + (db:query (:and (:= 'limit (simple-rate::name limit)) + (:= 'ip ip)))))) + (if tracking + (let ((window-end (+ (dm:field tracking "time") + (simple-rate::timeout limit))) + (now (get-universal-time))) + (if (<= window-end now) + ;; Window expired — report full budget + (values (simple-rate::amount limit) 0) + ;; Window still active + (values (dm:field tracking "amount") + (- window-end now)))) + ;; No tracking entry yet — full budget + (values (simple-rate::amount limit) + (simple-rate::timeout limit))))) + (define-trigger db:connected () "Clean up any corrupted rate limit entries on startup" (cleanup-corrupted-rate-limits)) diff --git a/parenscript/front-page.lisp b/parenscript/front-page.lisp index 7a136b1..9658fc3 100644 --- a/parenscript/front-page.lisp +++ b/parenscript/front-page.lisp @@ -776,7 +776,7 @@ (setf (ps:@ curated-option text-content) (+ "🎧 " current-channel-name))))))))))) (catch (lambda (error) (ps:chain console (log "Could not fetch channel name:" error)))))) - 10000)) ;; Poll every 10 seconds + 15000)) ;; Poll every 15 seconds ;; Listen for messages from popout window (ps:chain window