Commit6085ff6fRecorded24 Oct 2026Repositoryfolio
Yield during cold cache fills and task parsing
Changed
src/folio/filecache.sgl | 21 +++++++++++++--------
src/folio/scheduling.sgl | 20 ++++++++++++++++++++
src/folio/task.sgl | 7 +++++--
test/test-cold-cache-scheduling.sgl | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 113 insertions(+), 10 deletions(-)Diff
src/folio/filecache.sglmodified
@@ -25,6 +25,7 @@
25
;;; immutable (folio's structs, dicts and lists are). 26
(define-library (folio filecache) 27
(import (sigil fs)+28
(folio scheduling) 29
(sigil string) 30
(sigil time)) 31
(export make-file-cache@@ -44,10 +45,11 @@
45
(and (< (list-ref signature 3) cutoff-ns) 46
(< (list-ref signature 4) cutoff-ns)))) 47
−47
;; A cache is a box holding a dict: path -> (signature . value).+48
;; A cache holds path -> (signature . value), plus a checkpoint shared+49
;; across misses so a cold batch does not monopolize the HTTP scheduler. 50
(define (make-file-cache) 51
(: -> vector?)−50
(vector (dict)))+52
(vector (dict) (make-work-checkpoint))) 53
54
;;; Return (compute path), reusing the value from an earlier call while 55
;;; the file's stat signature is unchanged. A missing file is never cached.@@ -60,12 +62,15 @@
62
(let ((hit (dict-ref (vector-ref cache 0) path #f))) 63
(if (and hit (equal? (car hit) before)) 64
(cdr hit)−63
(let* ((value (compute path))−64
(after (file-stat-fingerprint path)))−65
(when (and (equal? before after) (settled? before))−66
(vector-set! cache 0−67
(dict-set (vector-ref cache 0) path (cons before value))))−68
value))))))+65
(let ((value (compute path)))+66
((vector-ref cache 1))+67
;; Recheck after the checkpoint: a writer may have run+68
;; while this task was paused. Cache only matching bytes.+69
(let ((after (file-stat-fingerprint path)))+70
(when (and (equal? before after) (settled? before))+71
(vector-set! cache 0+72
(dict-set (vector-ref cache 0) path (cons before value))))+73
value))))))) 74
75
(define (file-cache-size cache) 76
(: vector? -> integer?)src/folio/scheduling.sgladded
@@ -0,0 +1,20 @@
+1
;;; Cooperative checkpoints for bulk file parsing in the shared server.+2
;;; They do nothing outside an async scheduler. A checkpoint cannot interrupt+3
;;; one native operation; callers place it between bounded pieces of work.+4
(define-library (folio scheduling)+5
(import (sigil async scheduler)+6
(sigil time))+7
(export make-work-checkpoint)+8
(begin+9
+10
(define (make-work-checkpoint)+11
(: -> procedure?)+12
(let ((last-poll (current-jiffy))+13
(budget (/ (jiffies-per-second) 100)))+14
(lambda ()+15
(when (and (current-scheduler)+16
(>= (- (current-jiffy) last-poll) budget))+17
;; A positive timer lets the scheduler poll sockets promptly,+18
;; even when other runnable tasks keep its queue nonempty.+19
(sleep 0.001)+20
(set! last-poll (current-jiffy))))))))src/folio/task.sglmodified
@@ -45,7 +45,7 @@
45
;;; archive/tasks/folio-inbox-integrity-and-bulk-moves. 46
47
(define-library (folio task)−48
(import (sigil core)+48
(import (folio scheduling) 49
(sigil struct) 50
(sigil string)) 51
(export folio-task@@ -161,8 +161,10 @@
161
;;; 1-based header line number). 162
(define (find-task-blocks lines . opts) 163
(: list? -> list?)−164
(let ((file (if (pair? opts) (car opts) #f)))+164
(let ((file (if (pair? opts) (car opts) #f))+165
(checkpoint (make-work-checkpoint))) 166
(let loop ((rest lines) (idx 0) (blocks '()))+167
(checkpoint) 168
(cond 169
((null? rest) (reverse blocks)) 170
((task-line? (car rest))@@ -174,6 +176,7 @@
176
;; non-blank under-indented line ends the run. 177
(end (let scan ((r (cdr rest)) (i (+ idx 1)) (last idx) 178
(blanks 0))+179
(checkpoint) 180
(cond 181
((null? r) last) 182
((blank-line? (car r))test/test-cold-cache-scheduling.sgladded
@@ -0,0 +1,75 @@
+1
;;; Cold parsing must let another request run without caching a raced value.+2
(import (sigil test)+3
(sigil fs)+4
(sigil path)+5
(sigil time)+6
(sigil async)+7
(sigil channels)+8
(folio lock)+9
(folio filecache)+10
(folio task))+11
+12
(test-group "cold parsing checkpoints"+13
(test "a writer during a cache-fill pause invalidates the computed value"+14
(call-with-temp-directory+15
(lambda (root)+16
(let* ((path (path-join root "value.md"))+17
(cache (make-file-cache))+18
(ready (make-channel 1))+19
(first #f)+20
(size-after-first #f)+21
(writer-ran? #f))+22
(write-file-string path "old")+23
(parameterize ((file-cache-settle-seconds 0))+24
(with-async+25
(go+26
(channel-receive ready)+27
(write-file-atomic path "new")+28
(set! writer-ran? #t))+29
(go+30
;; Model a native CPU phase; explicit checkpoints must work+31
;; without relying on the VM's automatic preemption.+32
(set-vm-yield-requested! #f)+33
(set! first+34
(file-cache-ref cache path+35
(lambda (p)+36
(let ((value (read-file-string p))+37
(start (current-jiffy)))+38
(channel-send ready #t)+39
(let busy ()+40
(when (< (- (current-jiffy) start)+41
(/ (jiffies-per-second) 50))+42
(busy)))+43
value))))+44
(set! size-after-first (file-cache-size cache))))+45
(display (format "COLD-CACHE writer-ran=~a first=~a cached=~a\n"+46
writer-ran? first size-after-first))+47
(assert-true writer-ran?)+48
(assert-equal "old" first)+49
(assert-equal 0 size-after-first)+50
(assert-equal "new" (file-cache-ref cache path read-file-string)))))))+51
+52
(test "a sibling runs before a cold task-block parse finishes"+53
(let ((ready (make-channel 1))+54
(finished? #f)+55
(interleaved? #f)+56
(blocks '())+57
(lines+58
(let loop ((n 0) (acc '()))+59
(if (= n 2000)+60
(reverse acc)+61
(loop (+ n 1)+62
(cons (format "- [ ] Task ~a {id: t-~a}" n n) acc))))))+63
(with-async+64
(go+65
(channel-receive ready)+66
(set! interleaved? (not finished?)))+67
(go+68
(set-vm-yield-requested! #f)+69
(channel-send ready #t)+70
(set! blocks (find-task-blocks lines))+71
(set! finished? #t)))+72
(display (format "COLD-PARSE interleaved=~a blocks=~a\n"+73
interleaved? (length blocks)))+74
(assert-true interleaved?)+75
(assert-equal 2000 (length blocks)))))