logtail: a log rotated while the panel was down is read to its end (stage 2b)

At start, a saved offset whose fingerprint was not the current file's meant
"start on the new file": what Postfix wrote between the panel's stop and the
rotation was never read - delivery statuses, stored lines, refusals, inbound
journal rows, the deferred counter.

- tracker.resume marks the case (behind, prev); follow calls readRotated
  before the new file: it opens mail.log.1 (delaycompress leaves it plain),
  checks by fingerprint that it is the file the place belongs to, reads it
  from the saved offset to its end through handle (catching up: the
  once-protections apply) and flushes the stored lines.
- Not found, not that file, or the offset past its end (rotated twice):
  logged, with the saved place's time - from when nothing was read.
- The fingerprint gains the file's identity (device.inode, fileid_unix.go;
  empty elsewhere): two logs that begin alike are two files - the
  review-2026-08-followups row on collisions. A fingerprint saved before
  (hash alone) still matches by its hash (sameLog), so the first start after
  the upgrade resumes where it was.
- store.LogtailState reads back updated_at.

Tests: stop, append, rotate, append, start - both halves read, once each,
the earlier line not again; a mail.log.1 that is another file is not read
from the place; sameLog with an old and a new fingerprint.

CHANGELOG [Unreleased], audit plan line ticked, roadmap 23/62.
Evidence: GOOS=linux go vet ./internal/logtail clean; go test ./internal/logtail
ok on Windows but for TestFollowTailsAndRotates; the server run follows
before the push.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-14 16:25:29 +03:00
co-authored by Claude Opus 5.5
parent 5be109b871
commit 4678015bbd
9 changed files with 219 additions and 10 deletions
+9 -3
View File
@@ -14,17 +14,23 @@ import (
type LogtailState struct {
Fingerprint string
Offset int64
// UpdatedAt is when the position was saved; read back only (zero on save).
UpdatedAt time.Time
}
// LogtailState returns the stored read position for path. ok is false the first
// time a path is followed (nothing persisted yet), which the tailer treats as
// "start at the end".
func (s *Store) LogtailState(path string) (LogtailState, bool, error) {
var st LogtailState
var (
st LogtailState
updated string
)
err := s.db.QueryRow(
`SELECT fingerprint, read_offset FROM logtail_state WHERE path = ?`,
`SELECT fingerprint, read_offset, updated_at FROM logtail_state WHERE path = ?`,
path,
).Scan(&st.Fingerprint, &st.Offset)
).Scan(&st.Fingerprint, &st.Offset, &updated)
st.UpdatedAt, _ = time.Parse(time.RFC3339, updated)
if errors.Is(err, sql.ErrNoRows) {
return LogtailState{}, false, nil
}