summaryrefslogtreecommitdiff
path: root/internal/stats/stats.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-13 08:09:33 +0300
committerPaul Buetow <paul@buetow.org>2026-04-13 08:10:16 +0300
commitf2dd8d8a515c1a2a220836231ad1a671a5e9b73d (patch)
tree5b19585afb01b60d03d24a96b57bc7b986ea4cc0 /internal/stats/stats.go
parent56002ff942de1bfb0ce467ec37a692b8c4ca01e9 (diff)
ask: serialize concurrent CLI with repo lock and stale PID recovery
Add advisory lock under .git/hexai-ask.lock around Taskwarrior execution, with metadata (PID and process basename) and Linux /proc comm checks to remove orphan lock files when the recorded holder is gone or not ask. Extract internal/filelock for shared flock helpers; stats uses it too. Made-with: Cursor
Diffstat (limited to 'internal/stats/stats.go')
-rw-r--r--internal/stats/stats.go31
1 files changed, 3 insertions, 28 deletions
diff --git a/internal/stats/stats.go b/internal/stats/stats.go
index bd91e20..a5c5cf1 100644
--- a/internal/stats/stats.go
+++ b/internal/stats/stats.go
@@ -16,6 +16,8 @@ import (
"strings"
"sync/atomic"
"time"
+
+ "codeberg.org/snonux/hexai/internal/filelock"
)
const (
@@ -27,8 +29,6 @@ const (
var windowSeconds int64 = int64(defaultWindow.Seconds())
-var errLockWouldBlock = errors.New("stats: lock would block")
-
// nowFunc is the clock source for event timestamps and pruning cutoffs.
// Replaced in tests to control time without sleeping.
var nowFunc = time.Now
@@ -141,7 +141,7 @@ func lockStatsFile(ctx context.Context, dir string) (func() error, error) {
if err != nil {
return nil, err
}
- unlock, err := acquireFileLock(ctx, f)
+ unlock, err := filelock.AcquireExclusive(ctx, f)
if err != nil {
_ = f.Close()
return nil, err
@@ -215,31 +215,6 @@ func writeStatsFileAtomic(dir, path string, sf *File) error {
return nil
}
-// acquireFileLock spins on tryLockFile until it succeeds, the context is
-// cancelled, or an unexpected error occurs. A single timer is reused across
-// retries to avoid leaking timers/channels on every loop iteration.
-func acquireFileLock(ctx context.Context, f *os.File) (func() error, error) {
- fd := f.Fd()
- retryTimer := time.NewTimer(5 * time.Millisecond)
- defer retryTimer.Stop()
- for {
- err := tryLockFile(fd)
- if err == nil {
- return func() error { return unlockFile(fd) }, nil
- }
- if errors.Is(err, errLockWouldBlock) {
- retryTimer.Reset(5 * time.Millisecond)
- select {
- case <-ctx.Done():
- return nil, ctx.Err()
- case <-retryTimer.C:
- }
- continue
- }
- return nil, err
- }
-}
-
// TakeSnapshot reads the stats file and aggregates events within the stored
// window (falling back to the process-level Window() if the file has none).
// This is a pure read — it does not mutate global state.