| index 956e202bf..285d74f17 100644 |
| --- a/internal/host/movespaging.go |
| +++ b/internal/host/movespaging.go |
| @@ -51,17 +51,23 @@ import ( |
| // rule first fires: the file being there is the rule's once-per-entry limit, |
| // so a rule that fires again never pages again. The entry carries the page |
| // and its backoff. It is sent when created; one the pager refused is sent |
| -// again by every moves pass (flushPageOutbox) once its backoff is over |
| -// (pageRetryFirst, doubling, up to pageRetryMax), until it is delivered, |
| -// whatever the move and its journals have done since. A delivered entry |
| -// stays as a tombstone, so a rule that fires again still finds it, until the |
| -// move is retired on this box: no listed record carries it and its journal |
| -// is gone. The outbox depends on nothing in the move's journal. |
| +// again by every moves pass (flushPageOutbox, first thing in the pass, before |
| +// any record or fleet read) once its backoff is over (pageRetryFirst, |
| +// doubling, up to pageRetryMax), until it is delivered, whatever the move |
| +// and its journals have done since. A delivered entry stays as a tombstone, |
| +// so a rule that fires again still finds it, until the move is retired on |
| +// this box: no listed record carries it and its journal is gone |
| +// (retirePageTombstones, only after a pass listed every record). The outbox |
| +// depends on nothing in the move's journal. |
| // |
| -// If the outbox cannot keep a page, it is sent at once; if that fails too, |
| -// it is kept in memory and every pass sends it again (pageMemory), with a |
| -// loud line in the agent's log: only a restart during that double fault |
| -// loses it. |
| +// An index in memory (pageIndex, fix round 3) answers for what the disk |
| +// cannot: a firing looks at the disk entry, then the index, and only a page |
| +// in neither is created; only when that create fails is the page sent at |
| +// once, kept in the index, and, if that fails too, sent again by every pass |
| +// with a loud line in the agent's log. A delivery or a backoff the disk |
| +// cannot record is kept in the index too, and a delivered page's tombstone |
| +// is written again every pass until it lands. Only a restart while the index |
| +// holds what the disk does not loses it. |
| // |
| // Unwired (nil Page, as production is until Plan 5 Task 9b wires it), a rule |
| // is never silent: the agent's log says so loudly, once per rule per move |
| @@ -160,11 +166,35 @@ func pageEntryDir(dataDir, host, op string) string { |
| return filepath.Join(pageOutboxDir(dataDir), host, op) |
| } |
| |
| -// pageOutboxMu serializes every change to an outbox entry in this process |
| -// (the moves pass and the probe owners both fire pages); delivery itself is |
| -// outside it, after the attempt is counted. |
| +// pageOutboxMu serializes every change to an outbox entry, and to the page |
| +// index, in this process (the moves pass and the probe owners both fire |
| +// pages); delivery itself is outside it, after the attempt is counted. |
| var pageOutboxMu sync.Mutex |
| |
| +// The page index (fix round 3): what this process knows of a page that the |
| +// disk does not say, keyed by (outbox, host, move, rule). It holds a page |
| +// the outbox could not keep (memory), a backoff deadline the disk could not |
| +// record (next), and a delivery the disk does not record yet (delivered, |
| +// whose tombstone every pass writes again until it lands). A firing looks at |
| +// the disk and then the index, so one (host, move, rule) is one page however |
| +// the storage fails; only a restart while the index holds a page the disk |
| +// does not loses it. |
| +type pageKey struct{ dataDir, host, op, rule string } |
| + |
| +type pageIndexItem struct { |
| + e pageEntry |
| + page pageFunc // the pager that fired a memory page |
| + memory bool // the outbox could not keep it |
| + delivered bool |
| + next time.Time |
| +} |
| + |
| +var pageIndex = map[pageKey]*pageIndexItem{} |
| + |
| +func pageKeyOf(dataDir string, e pageEntry) pageKey { |
| + return pageKey{dataDir, e.Host, e.Op, e.Rule} |
| +} |
| + |
| // readPageEntry reads one entry. |
| func readPageEntry(path string) (pageEntry, error) { |
| var e pageEntry |
| @@ -187,11 +217,18 @@ func writePageEntry(dataDir string, e pageEntry) error { |
| return outboxReplace(pageEntryDir(dataDir, e.Host, e.Op), e.Rule+".json", b) |
| } |
| |
| -// firePage is a rule firing: the first firing of (host, op, rule) creates its |
| -// outbox entry, counting the attempt it is about to make, and sends the page; |
| -// a later firing finds the entry and sends nothing (the passes retry a |
| -// refused one). Unwired, it warns once per rule per move (warned is the |
| -// memory of that) and keeps nothing. |
| +// pageEntryPath is an entry's file. |
| +func pageEntryPath(dataDir, host, op, rule string) string { |
| + return filepath.Join(pageEntryDir(dataDir, host, op), rule+".json") |
| +} |
| + |
| +// firePage is a rule firing (fix round 3, ruling 1). The disk entry is |
| +// looked at first: there, delivered or not, the rule sends nothing (the |
| +// passes retry a refused one). Then the index: there, it sends nothing. |
| +// Only a (host, move, rule) in neither is created, its attempt counted, and |
| +// sent; and only when that create fails is the page sent directly, and kept |
| +// in the index if that fails too. Unwired, it warns once per rule per move |
| +// (warned is the memory of that) and keeps nothing. |
| func firePage(ctx context.Context, page pageFunc, warned *sync.Map, dataDir, host, op, rule string, now time.Time, level, subject, body string) pageOutcome { |
| if page == nil { |
| if _, seen := warned.LoadOrStore(host+"/"+op+"/"+rule, true); !seen { |
| @@ -200,49 +237,69 @@ func firePage(ctx context.Context, page pageFunc, warned *sync.Map, dataDir, hos |
| return pageUnwired |
| } |
| e := pageEntry{V: 1, Host: host, Op: op, Rule: rule, Level: level, Subject: subject, Body: body, At: now.UTC(), Tries: 1, Next: now.UTC().Add(pageBackoff(1))} |
| - created, err := createPageEntry(dataDir, e) |
| - if err != nil { |
| - pagingLogf("moves: %s: WARNING: the page outbox cannot keep the %s page of move %s, so it is sent now: %v", host, rule, op, err) |
| - if perr := page(ctx, level, subject, body); perr == nil { |
| - return pageSent |
| - } else { |
| - pageMemory.keep(page, e) |
| - pagingLogf("moves: %s: WARNING: the %s page of move %s was not delivered either (%v); it is kept in memory only and sent again every pass, and is lost if the agent restarts first", host, rule, op, perr) |
| + if !outboxSegment(host) || !outboxSegment(op) || !outboxSegment(rule) || dataDir == "" { |
| + pagingLogf("moves: %s: WARNING: the %s page of move %s has no usable outbox name, so it is sent now", host, rule, op) |
| + if err := page(ctx, level, subject, body); err != nil { |
| + pagingLogf("moves: %s: WARNING: the %s page of move %s was not delivered: %v", host, rule, op, err) |
| + return pageUndelivered |
| } |
| - return pageUndelivered |
| + return pageSent |
| } |
| - if !created { |
| - pageOutboxMu.Lock() |
| - kept, rerr := readPageEntry(filepath.Join(pageEntryDir(dataDir, host, op), rule+".json")) |
| + key := pageKeyOf(dataDir, e) |
| + pageOutboxMu.Lock() |
| + if kept, err := readPageEntry(pageEntryPath(dataDir, host, op, rule)); err == nil { |
| pageOutboxMu.Unlock() |
| - if rerr == nil && kept.Delivered != nil { |
| + if kept.Delivered != nil { |
| return pagePagedBefore |
| } |
| return pagePending |
| + } else if !errors.Is(err, fs.ErrNotExist) { |
| + if _, serr := os.Lstat(pageEntryPath(dataDir, host, op, rule)); serr == nil { |
| + // There, but not readable now: it is the rule's entry all the same. |
| + pageOutboxMu.Unlock() |
| + return pagePending |
| + } |
| } |
| - return deliverPageEntry(ctx, page, dataDir, e, now) |
| -} |
| - |
| -// createPageEntry creates e's file, create-exclusive. |
| -func createPageEntry(dataDir string, e pageEntry) (bool, error) { |
| - if dataDir == "" { |
| - return false, errors.New("no data directory for the page outbox") |
| - } |
| - if !outboxSegment(e.Host) || !outboxSegment(e.Op) || !outboxSegment(e.Rule) { |
| - return false, fmt.Errorf("an unusable page outbox name: %s/%s/%s", e.Host, e.Op, e.Rule) |
| + if item := pageIndex[key]; item != nil { |
| + pageOutboxMu.Unlock() |
| + if item.delivered { |
| + return pagePagedBefore |
| + } |
| + return pagePending |
| } |
| b, err := json.Marshal(e) |
| + created := false |
| + if err == nil { |
| + created, err = outboxCreate(pageEntryDir(dataDir, host, op), rule+".json", b) |
| + } |
| + if err == nil && !created { |
| + pageOutboxMu.Unlock() |
| + return pagePending |
| + } |
| if err != nil { |
| - return false, err |
| + // The outbox cannot keep it: the index does, before the send, so no |
| + // re-fire sends it again. |
| + item := &pageIndexItem{e: e, page: page, memory: true, next: e.Next} |
| + pageIndex[key] = item |
| + pageOutboxMu.Unlock() |
| + pagingLogf("moves: %s: WARNING: the page outbox cannot keep the %s page of move %s, so it is sent now: %v", host, rule, op, err) |
| + if perr := page(ctx, level, subject, body); perr != nil { |
| + pagingLogf("moves: %s: WARNING: the %s page of move %s was not delivered either (%v); it is kept in memory only and sent again every pass, and is lost if the agent restarts first", host, rule, op, perr) |
| + return pageUndelivered |
| + } |
| + pageOutboxMu.Lock() |
| + item.delivered = true |
| + pageOutboxMu.Unlock() |
| + return pageSent |
| } |
| - pageOutboxMu.Lock() |
| - defer pageOutboxMu.Unlock() |
| - return outboxCreate(pageEntryDir(dataDir, e.Host, e.Op), e.Rule+".json", b) |
| + pageOutboxMu.Unlock() |
| + return deliverPageEntry(ctx, page, dataDir, e, now) |
| } |
| |
| // deliverPageEntry sends an entry whose attempt is already counted, and marks |
| -// it delivered once the pager took it. A refused page stays for the passes; |
| -// a delivered one whose mark cannot be written may be sent again. |
| +// it delivered once the pager took it: on disk, or, when the disk cannot |
| +// record it, in the index, whose tombstone the passes write again until it |
| +// lands. A refused page stays for the passes. |
| func deliverPageEntry(ctx context.Context, page pageFunc, dataDir string, e pageEntry, now time.Time) pageOutcome { |
| if err := page(ctx, e.Level, e.Subject, e.Body); err != nil { |
| pagingLogf("moves: %s: the %s page of move %s was not delivered (try %d), and is sent again after %s: %v", e.Host, e.Rule, e.Op, e.Tries, pageBackoff(e.Tries), err) |
| @@ -250,40 +307,71 @@ func deliverPageEntry(ctx context.Context, page pageFunc, dataDir string, e page |
| } |
| pageOutboxMu.Lock() |
| defer pageOutboxMu.Unlock() |
| - path := filepath.Join(pageEntryDir(dataDir, e.Host, e.Op), e.Rule+".json") |
| - kept, err := readPageEntry(path) |
| - if err == nil { |
| - at := now.UTC() |
| - kept.Delivered = &at |
| - err = writePageEntry(dataDir, kept) |
| - } |
| - if err != nil { |
| - pagingLogf("moves: %s: the %s page of move %s was sent, and the outbox may not say so, so it may be sent again: %v", e.Host, e.Rule, e.Op, err) |
| + if err := markPageDelivered(dataDir, e, now); err != nil { |
| + pageIndex[pageKeyOf(dataDir, e)] = &pageIndexItem{e: e, delivered: true} |
| + pagingLogf("moves: %s: the %s page of move %s was sent; the outbox cannot say so yet, so this agent remembers it and writes it again every pass: %v", e.Host, e.Rule, e.Op, err) |
| } |
| return pageSent |
| } |
| |
| -// flushPageOutbox is every moves pass's half of paging: each entry not yet |
| -// delivered is sent again once its backoff is over (its attempt counted and |
| -// the next put off before the send, so two senders in one window send it |
| -// once); each delivered entry whose move is retired on this box (no record |
| -// in recs carries it and its journal is gone) is removed; and the pages kept |
| -// in memory are sent again. Unwired, nothing is sent. |
| -func (w *movesWorker) flushPageOutbox(ctx context.Context, recs []provision.Record) { |
| +// markPageDelivered writes e's entry as delivered: the entry on disk marked, |
| +// or, when there is none, a tombstone created. The caller holds |
| +// pageOutboxMu. |
| +func markPageDelivered(dataDir string, e pageEntry, now time.Time) error { |
| + at := now.UTC() |
| + kept, err := readPageEntry(pageEntryPath(dataDir, e.Host, e.Op, e.Rule)) |
| + switch { |
| + case err == nil: |
| + if kept.Delivered != nil { |
| + return nil |
| + } |
| + kept.Delivered = &at |
| + return writePageEntry(dataDir, kept) |
| + case errors.Is(err, fs.ErrNotExist): |
| + t := e |
| + t.Delivered = &at |
| + b, err := json.Marshal(t) |
| + if err != nil { |
| + return err |
| + } |
| + _, err = outboxCreate(pageEntryDir(dataDir, e.Host, e.Op), e.Rule+".json", b) |
| + return err |
| + } |
| + return err |
| +} |
| + |
| +// flushPageOutbox is every moves pass's delivery half of paging (fix round 3, |
| +// ruling 4: it runs first, before any record or fleet read, so a bucket that |
| +// cannot be read never holds a page back). The index's pages are sent again |
| +// once their backoff is over and its delivered pages' tombstones written |
| +// again; then each disk entry not yet delivered is sent again once its |
| +// backoff (the later of the disk's and the index's) is over, its attempt |
| +// counted and the next put off before the send, so two senders in one |
| +// window send it once. A deadline the disk cannot record is kept in the |
| +// index (fix round 3, ruling 2). Unwired, the disk's pages wait. |
| +func (w *movesWorker) flushPageOutbox(ctx context.Context) { |
| page, dd := w.cfg.Page, w.cfg.DataDir |
| now := w.now() |
| - pageMemory.flush(ctx, now) |
| + flushPageIndex(ctx, now) |
| if page == nil || dd == "" { |
| return |
| } |
| - live := map[string]bool{} |
| - for _, rec := range recs { |
| - if rec.Move != nil && rec.Move.ID != "" { |
| - live[rec.Host+"/"+rec.Move.ID] = true |
| + root := pageOutboxDir(dd) |
| + err := walkPageOutbox(root, func(p string) error { |
| + if err := ctx.Err(); err != nil { |
| + return err |
| } |
| + w.flushPageEntry(ctx, page, dd, p, now) |
| + return nil |
| + }) |
| + if err != nil { |
| + pagingLogf("moves: the page outbox: %v", err) |
| } |
| - root := pageOutboxDir(dd) |
| - err := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error { |
| +} |
| + |
| +// walkPageOutbox calls fn with each entry file under root. |
| +func walkPageOutbox(root string, fn func(p string) error) error { |
| + return filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error { |
| if errors.Is(err, fs.ErrNotExist) && p == root { |
| return fs.SkipAll |
| } |
| @@ -293,20 +381,12 @@ func (w *movesWorker) flushPageOutbox(ctx context.Context, recs []provision.Reco |
| if d.IsDir() || !strings.HasSuffix(d.Name(), ".json") || strings.HasPrefix(d.Name(), ".") { |
| return nil |
| } |
| - if err := ctx.Err(); err != nil { |
| - return err |
| - } |
| - w.flushPageEntry(ctx, page, dd, p, now, live) |
| - return nil |
| + return fn(p) |
| }) |
| - if err != nil { |
| - pagingLogf("moves: the page outbox: %v", err) |
| - } |
| - pruneEmptyDirs(root) |
| } |
| |
| -// flushPageEntry is one entry of flushPageOutbox. |
| -func (w *movesWorker) flushPageEntry(ctx context.Context, page pageFunc, dd, p string, now time.Time, live map[string]bool) { |
| +// flushPageEntry is one disk entry of flushPageOutbox. |
| +func (w *movesWorker) flushPageEntry(ctx context.Context, page pageFunc, dd, p string, now time.Time) { |
| pageOutboxMu.Lock() |
| e, err := readPageEntry(p) |
| if err != nil || e.Rule+".json" != filepath.Base(p) || e.Op != filepath.Base(filepath.Dir(p)) || e.Host != filepath.Base(filepath.Dir(filepath.Dir(p))) { |
| @@ -315,15 +395,14 @@ func (w *movesWorker) flushPageEntry(ctx context.Context, page pageFunc, dd, p s |
| return |
| } |
| if e.Delivered != nil { |
| - retired := !live[e.Host+"/"+e.Op] && !moveJournalPresent(dd, e.Host, e.Op) |
| - if retired { |
| - if err := os.Remove(p); err != nil { |
| - pagingLogf("moves: the page outbox: %v", err) |
| - } |
| - } |
| pageOutboxMu.Unlock() |
| return |
| } |
| + key := pageKeyOf(dd, e) |
| + if item := pageIndex[key]; item != nil && (item.delivered || now.Before(item.next)) { |
| + pageOutboxMu.Unlock() |
| + return // delivered (its mark is the index's to write) or backing off |
| + } |
| if now.Before(e.Next) { |
| pageOutboxMu.Unlock() |
| return |
| @@ -331,64 +410,82 @@ func (w *movesWorker) flushPageEntry(ctx context.Context, page pageFunc, dd, p s |
| e.Tries++ |
| e.Next = now.UTC().Add(pageBackoff(e.Tries)) |
| if err := writePageEntry(dd, e); err != nil { |
| - pagingLogf("moves: %s: the %s page of move %s is sent without its attempt counted: %v", e.Host, e.Rule, e.Op, err) |
| + pageIndex[key] = &pageIndexItem{e: e, next: e.Next} |
| + pagingLogf("moves: %s: the %s page of move %s is sent with its backoff kept in memory: the outbox cannot record it: %v", e.Host, e.Rule, e.Op, err) |
| } |
| pageOutboxMu.Unlock() |
| deliverPageEntry(ctx, page, dd, e, now) |
| } |
| |
| -// pageMemory is the pages neither the outbox could keep nor the pager could |
| -// take: the passes send them again, until delivered or the agent restarts. |
| -var pageMemory memoryPages |
| - |
| -type memoryPages struct { |
| - mu sync.Mutex |
| - pages []memoryPage |
| -} |
| - |
| -type memoryPage struct { |
| - page pageFunc |
| - e pageEntry |
| -} |
| - |
| -func (m *memoryPages) keep(page pageFunc, e pageEntry) { |
| - m.mu.Lock() |
| - defer m.mu.Unlock() |
| - m.pages = append(m.pages, memoryPage{page: page, e: e}) |
| -} |
| - |
| -func (m *memoryPages) len() int { |
| - m.mu.Lock() |
| - defer m.mu.Unlock() |
| - return len(m.pages) |
| +// flushPageIndex sends each memory page whose backoff is over (a delivered |
| +// one is marked delivered, a refused one waits for its next attempt), and |
| +// writes each delivered page's tombstone, dropping it from the index once |
| +// it lands (fix round 3, ruling 3). |
| +func flushPageIndex(ctx context.Context, now time.Time) { |
| + pageOutboxMu.Lock() |
| + var due []*pageIndexItem |
| + for key, item := range pageIndex { |
| + switch { |
| + case item.delivered: |
| + if err := markPageDelivered(key.dataDir, item.e, now); err == nil { |
| + delete(pageIndex, key) |
| + } |
| + case item.memory && !now.Before(item.next): |
| + item.e.Tries++ |
| + item.next = now.UTC().Add(pageBackoff(item.e.Tries)) |
| + due = append(due, item) |
| + case !item.memory && !now.Before(item.next): |
| + delete(pageIndex, key) // the disk's own deadline governs again |
| + } |
| + } |
| + pageOutboxMu.Unlock() |
| + for _, item := range due { |
| + e := item.e |
| + if err := item.page(ctx, e.Level, e.Subject, e.Body); err != nil { |
| + pagingLogf("moves: %s: WARNING: the %s page of move %s, kept in memory only, was not delivered (try %d): %v", e.Host, e.Rule, e.Op, e.Tries, err) |
| + continue |
| + } |
| + pagingLogf("moves: %s: the %s page of move %s, kept in memory, is delivered", e.Host, e.Rule, e.Op) |
| + pageOutboxMu.Lock() |
| + item.delivered = true |
| + pageOutboxMu.Unlock() |
| + } |
| } |
| |
| -// flush sends each kept page whose backoff is over; a delivered one is |
| -// dropped, a refused one waits for its next attempt. |
| -func (m *memoryPages) flush(ctx context.Context, now time.Time) { |
| - m.mu.Lock() |
| - due := m.pages[:0:0] |
| - var keep []memoryPage |
| - for _, kp := range m.pages { |
| - if now.Before(kp.e.Next) { |
| - keep = append(keep, kp) |
| - continue |
| +// retirePageTombstones removes each delivered disk entry whose move is |
| +// retired on this box: no record in recs carries it and its journal is gone. |
| +// Only a pass that listed every record calls it. |
| +func (w *movesWorker) retirePageTombstones(recs []provision.Record) { |
| + dd := w.cfg.DataDir |
| + if dd == "" { |
| + return |
| + } |
| + live := map[string]bool{} |
| + for _, rec := range recs { |
| + if rec.Move != nil && rec.Move.ID != "" { |
| + live[rec.Host+"/"+rec.Move.ID] = true |
| } |
| - due = append(due, kp) |
| } |
| - m.pages = keep |
| - m.mu.Unlock() |
| - for _, kp := range due { |
| - if err := kp.page(ctx, kp.e.Level, kp.e.Subject, kp.e.Body); err == nil { |
| - pagingLogf("moves: %s: the %s page of move %s, kept in memory, is delivered", kp.e.Host, kp.e.Rule, kp.e.Op) |
| - continue |
| - } else { |
| - kp.e.Tries++ |
| - kp.e.Next = now.UTC().Add(pageBackoff(kp.e.Tries)) |
| - pagingLogf("moves: %s: WARNING: the %s page of move %s, kept in memory only, was not delivered (try %d): %v", kp.e.Host, kp.e.Rule, kp.e.Op, kp.e.Tries, err) |
| + root := pageOutboxDir(dd) |
| + err := walkPageOutbox(root, func(p string) error { |
| + pageOutboxMu.Lock() |
| + defer pageOutboxMu.Unlock() |
| + e, err := readPageEntry(p) |
| + if err != nil || e.Delivered == nil || live[e.Host+"/"+e.Op] || moveJournalPresent(dd, e.Host, e.Op) { |
| + return nil |
| + } |
| + if _, held := pageIndex[pageKeyOf(dd, e)]; held { |
| + return nil |
| + } |
| + if err := os.Remove(p); err != nil { |
| + pagingLogf("moves: the page outbox: %v", err) |
| } |
| - m.keep(kp.page, kp.e) |
| + return nil |
| + }) |
| + if err != nil { |
| + pagingLogf("moves: the page outbox: %v", err) |
| } |
| + pruneEmptyDirs(root) |
| } |
| |
| // firePage is the worker's firePage: its pager, outbox and clock. |