]> git.giacomolli.uk - onechat/blob - syncer.go
Limit the time of each Telegram send call
[onechat] / syncer.go
1 package main
2
3 import (
4         "context"
5         "errors"
6         "fmt"
7         "os"
8         "sync"
9
10         "golang.org/x/sys/unix"
11 )
12
13 // syncer makes the writes of a chat durable by group commit. Each write goes to
14 // the operating system at once, so a crash of the process loses nothing. After
15 // each write, Written starts a background sync of the file system of the chat,
16 // unless one runs already. Then the next sync starts when the running one ends.
17 // One sync makes every write before its start durable, so a sync covers many
18 // writes, and there is never more than one sync at a time. A power loss loses at
19 // most the writes after the start of the last finished sync.
20 //
21 // A failed sync can mean that the kernel dropped written data, so the error
22 // stays, and each later Flush returns it.
23 type syncer struct {
24         mutex   sync.Mutex
25         sync    func() error
26         file    *os.File
27         written uint64
28         durable uint64
29         running bool
30         done    chan struct{}
31         failure error
32         workers sync.WaitGroup
33 }
34
35 // openSyncer returns the syncer of the file system that holds dir. It syncs
36 // with syncfs, which writes back only that file system.
37 func openSyncer(dir string) (*syncer, error) {
38         file, err := os.Open(dir)
39         if err != nil {
40                 return nil, fmt.Errorf("open chat directory: %w", err)
41         }
42
43         s := newSyncer(func() error {
44                 return unix.Syncfs(int(file.Fd()))
45         })
46         s.file = file
47
48         return s, nil
49 }
50
51 // newSyncer returns a syncer that makes the writes durable with sync.
52 func newSyncer(sync func() error) *syncer {
53         return &syncer{
54                 sync: sync,
55                 done: make(chan struct{}),
56         }
57 }
58
59 // Written tells the syncer that a write went to the operating system, and
60 // starts a sync when none runs.
61 func (s *syncer) Written() {
62         s.mutex.Lock()
63         defer s.mutex.Unlock()
64
65         s.written++
66         s.start()
67 }
68
69 // start starts a sync of the writes so far when none runs and a write is not
70 // durable. The caller holds the mutex.
71 func (s *syncer) start() {
72         if s.running || s.durable == s.written || s.failure != nil {
73                 return
74         }
75
76         s.running = true
77         s.workers.Add(1)
78         go s.run(s.written)
79 }
80
81 // run syncs once, so the writes up to target become durable, and starts the
82 // next sync when writes came during this one.
83 func (s *syncer) run(target uint64) {
84         defer s.workers.Done()
85
86         err := s.sync()
87
88         s.mutex.Lock()
89         defer s.mutex.Unlock()
90
91         s.running = false
92         if err != nil {
93                 s.failure = fmt.Errorf("sync chat files: %w", err)
94         } else {
95                 s.durable = target
96         }
97
98         close(s.done)
99         s.done = make(chan struct{})
100         s.start()
101 }
102
103 // Flush returns when every write before the call is durable.
104 func (s *syncer) Flush(ctx context.Context) error {
105         s.mutex.Lock()
106         target := s.written
107         s.mutex.Unlock()
108
109         for {
110                 s.mutex.Lock()
111                 reached := s.durable >= target
112                 failure := s.failure
113                 done := s.done
114                 s.mutex.Unlock()
115
116                 if reached {
117                         return nil
118                 }
119
120                 if failure != nil {
121                         return failure
122                 }
123
124                 select {
125                 case <-done:
126                 case <-ctx.Done():
127                         return ctx.Err()
128                 }
129         }
130 }
131
132 // Close makes every write durable, waits for the syncs, and closes the
133 // directory.
134 func (s *syncer) Close() error {
135         err := s.Flush(context.Background())
136         s.workers.Wait()
137         if s.file != nil {
138                 err = errors.Join(err, s.file.Close())
139                 s.file = nil
140         }
141
142         return err
143 }