-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstate.go
More file actions
120 lines (109 loc) · 3.37 KB
/
Copy pathstate.go
File metadata and controls
120 lines (109 loc) · 3.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
package main
import (
"encoding/json"
"os"
"path/filepath"
)
// State records which codes have already been sent to Discord, per game key.
// Persisted as JSON in the bind-mounted data dir so restarts never re-send.
type State struct {
path string
// Sent maps game key -> set of code strings already announced (or, for a
// game's first-run backlog / a code we gave up retrying, recorded as done).
Sent map[string]map[string]bool `json:"sent"`
// Attempts maps game key -> code -> number of failed announce attempts. A
// code lives here only while it's being retried; it's cleared once the code
// is marked sent (success or give-up).
Attempts map[string]map[string]int `json:"attempts,omitempty"`
}
func loadState(path string) (*State, error) {
s := &State{
path: path,
Sent: map[string]map[string]bool{},
Attempts: map[string]map[string]int{},
}
data, err := os.ReadFile(path)
if os.IsNotExist(err) {
return s, nil // fresh start
}
if err != nil {
return s, err
}
// Tolerate an empty/corrupt file by starting clean rather than crashing.
var on struct {
Sent map[string]map[string]bool `json:"sent"`
Attempts map[string]map[string]int `json:"attempts"`
}
if err := json.Unmarshal(data, &on); err != nil {
return s, nil
}
if on.Sent != nil {
s.Sent = on.Sent
}
if on.Attempts != nil {
s.Attempts = on.Attempts
}
return s, nil
}
// sent reports whether a code was already announced for a game.
func (s *State) sent(game, code string) bool {
return s.Sent[game][code]
}
// hasGame reports whether we have ever recorded state for a game (used to
// detect a first run and seed silently).
func (s *State) hasGame(game string) bool {
_, ok := s.Sent[game]
return ok
}
// mark records a code as sent for a game and clears any retry counter for it.
func (s *State) mark(game, code string) {
if s.Sent[game] == nil {
s.Sent[game] = map[string]bool{}
}
s.Sent[game][code] = true
if s.Attempts[game] != nil {
delete(s.Attempts[game], code)
if len(s.Attempts[game]) == 0 {
delete(s.Attempts, game)
}
}
}
// recordAttempt increments and returns the failed-announce attempt count for a
// code. Used to cap retries so a persistently failing post can't retry forever.
func (s *State) recordAttempt(game, code string) int {
if s.Attempts == nil {
s.Attempts = map[string]map[string]int{}
}
if s.Attempts[game] == nil {
s.Attempts[game] = map[string]int{}
}
s.Attempts[game][code]++
return s.Attempts[game][code]
}
// seed records that a game has been seen without marking any specific code, so
// an empty first fetch still flips hasGame. Without it, a game that has no
// active codes on its first run stays "first run" and the next fetch that does
// find codes is silently suppressed as backlog instead of announced.
func (s *State) seed(game string) {
if s.Sent[game] == nil {
s.Sent[game] = map[string]bool{}
}
}
// save writes state atomically (temp file + rename).
func (s *State) save() error {
if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil {
return err
}
data, err := json.MarshalIndent(struct {
Sent map[string]map[string]bool `json:"sent"`
Attempts map[string]map[string]int `json:"attempts,omitempty"`
}{Sent: s.Sent, Attempts: s.Attempts}, "", " ")
if err != nil {
return err
}
tmp := s.path + ".tmp"
if err := os.WriteFile(tmp, data, 0o644); err != nil {
return err
}
return os.Rename(tmp, s.path)
}