check whether mailboxes have message/etc counts through an "upgrade" boolean flag

Instead of using the per-mailbox flag, and going through all mailboxes when
opening an account.
This commit is contained in:
Mechiel Lukkien 2025-03-23 12:45:21 +01:00
parent b37faa06bd
commit a68a9d8a48
No known key found for this signature in database
4 changed files with 36 additions and 29 deletions

2
ctl.go
View File

@ -1522,7 +1522,7 @@ func servectlcmd(ctx context.Context, ctl *ctl, cid int64, shutdown func()) {
} }
totalSize += mc.Size totalSize += mc.Size
if !mb.HaveCounts || mc != mb.MailboxCounts { if mc != mb.MailboxCounts {
_, err := fmt.Fprintf(w, "for %s setting new counts %s (was %s)\n", mb.Name, mc, mb.MailboxCounts) _, err := fmt.Fprintf(w, "for %s setting new counts %s (was %s)\n", mb.Name, mc, mb.MailboxCounts)
ctl.xcheck(err, "write") ctl.xcheck(err, "write")
mb.HaveCounts = true mb.HaveCounts = true

View File

@ -221,7 +221,7 @@ type Mailbox struct {
// lower case (for JMAP), sorted. // lower case (for JMAP), sorted.
Keywords []string Keywords []string
HaveCounts bool // Whether MailboxCounts have been initialized. HaveCounts bool // Deprecated. Covered by Upgrade.MailboxCounts. No longer read.
MailboxCounts // Statistics about messages, kept up to date whenever a change happens. MailboxCounts // Statistics about messages, kept up to date whenever a change happens.
} }
@ -949,6 +949,7 @@ type Upgrade struct {
Threads byte // 0: None, 1: Adding MessageID's completed, 2: Adding ThreadID's completed. Threads byte // 0: None, 1: Adding MessageID's completed, 2: Adding ThreadID's completed.
MailboxModSeq bool // Whether mailboxes have been assigned modseqs. MailboxModSeq bool // Whether mailboxes have been assigned modseqs.
MailboxParentID bool // Setting ParentID on mailboxes. MailboxParentID bool // Setting ParentID on mailboxes.
MailboxCounts bool // Global flag about whether we have mailbox flags. Instead of previous per-mailbox boolean.
} }
// upgradeInit is the value to for new account database, that don't need any upgrading. // upgradeInit is the value to for new account database, that don't need any upgrading.
@ -957,6 +958,7 @@ var upgradeInit = Upgrade{
Threads: 2, Threads: 2,
MailboxModSeq: true, MailboxModSeq: true,
MailboxParentID: true, MailboxParentID: true,
MailboxCounts: true,
} }
// InitialUIDValidity returns a UIDValidity used for initializing an account. // InitialUIDValidity returns a UIDValidity used for initializing an account.
@ -1143,11 +1145,10 @@ func OpenAccountDB(log mlog.Log, accountDir, accountName string) (a *Account, re
return acc, nil return acc, nil
} }
// Ensure singletons are present. Mailbox counts and total message size, Settings. // Ensure singletons are present, like DiskUsage and Settings.
// Process pending MessageErase records. Check that next the message ID assigned by // Process pending MessageErase records. Check that next the message ID assigned by
// the database does not already have a file on disk, or increase the sequence so // the database does not already have a file on disk, or increase the sequence so
// it doesn't. // it doesn't.
var mentioned bool
err = db.Write(context.TODO(), func(tx *bstore.Tx) error { err = db.Write(context.TODO(), func(tx *bstore.Tx) error {
if tx.Get(&Settings{ID: 1}) == bstore.ErrAbsent { if tx.Get(&Settings{ID: 1}) == bstore.ErrAbsent {
if err := tx.Insert(&Settings{ID: 1, ShowAddressSecurity: true}); err != nil { if err := tx.Insert(&Settings{ID: 1, ShowAddressSecurity: true}); err != nil {
@ -1155,23 +1156,6 @@ func OpenAccountDB(log mlog.Log, accountDir, accountName string) (a *Account, re
} }
} }
err := bstore.QueryTx[Mailbox](tx).FilterEqual("HaveCounts", false).ForEach(func(mb Mailbox) error {
if !mentioned {
mentioned = true
log.Info("first calculation of mailbox counts for account", slog.String("account", accountName))
}
mc, err := mb.CalculateCounts(tx)
if err != nil {
return err
}
mb.HaveCounts = true
mb.MailboxCounts = mc
return tx.Update(&mb)
})
if err != nil {
return err
}
du := DiskUsage{ID: 1} du := DiskUsage{ID: 1}
err = tx.Get(&du) err = tx.Get(&du)
if err == bstore.ErrAbsent { if err == bstore.ErrAbsent {
@ -1308,7 +1292,6 @@ func OpenAccountDB(log mlog.Log, accountDir, accountName string) (a *Account, re
return nil, fmt.Errorf("calculating counts for mailbox, inserting settings, expunging messages: %v", err) return nil, fmt.Errorf("calculating counts for mailbox, inserting settings, expunging messages: %v", err)
} }
// Start adding threading if needed.
up := Upgrade{ID: 1} up := Upgrade{ID: 1}
err = db.Write(context.TODO(), func(tx *bstore.Tx) error { err = db.Write(context.TODO(), func(tx *bstore.Tx) error {
err := tx.Get(&up) err := tx.Get(&up)
@ -1458,6 +1441,34 @@ func OpenAccountDB(log mlog.Log, accountDir, accountName string) (a *Account, re
} }
} }
if !up.MailboxCounts {
log.Debug("upgrade: ensuring all mailboxes have message counts")
err := acc.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
err := bstore.QueryTx[Mailbox](tx).FilterEqual("HaveCounts", false).ForEach(func(mb Mailbox) error {
mc, err := mb.CalculateCounts(tx)
if err != nil {
return err
}
mb.HaveCounts = true
mb.MailboxCounts = mc
return tx.Update(&mb)
})
if err != nil {
return err
}
up.MailboxCounts = true
if err := tx.Update(&up); err != nil {
return fmt.Errorf("marking upgrade done: %w", err)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("upgrade: ensuring message counts on all mailboxes")
}
}
if up.Threads == 2 { if up.Threads == 2 {
close(acc.threadsCompleted) close(acc.threadsCompleted)
return acc, nil return acc, nil
@ -1686,7 +1697,6 @@ func (a *Account) SetSkipMessageModSeqZeroCheck(skip bool) {
// //
// - Missing or unexpected on-disk message files. // - Missing or unexpected on-disk message files.
// - Mismatch between message size and length of MsgPrefix and on-disk file. // - Mismatch between message size and length of MsgPrefix and on-disk file.
// - Missing HaveCounts.
// - Incorrect mailbox counts. // - Incorrect mailbox counts.
// - Incorrect total message size. // - Incorrect total message size.
// - Message with UID >= mailbox uid next. // - Message with UID >= mailbox uid next.
@ -1935,10 +1945,7 @@ func (a *Account) CheckConsistency() error {
var totalMailboxSize int64 var totalMailboxSize int64
for _, mb := range mailboxNames { for _, mb := range mailboxNames {
totalMailboxSize += mb.Size totalMailboxSize += mb.Size
if !mb.HaveCounts { if mb.MailboxCounts != counts[mb.ID] {
errmsg := fmt.Sprintf("mailbox %q (id %d) does not have counts, should be %#v", mb.Name, mb.ID, counts[mb.ID])
errmsgs = append(errmsgs, errmsg)
} else if mb.MailboxCounts != counts[mb.ID] {
mbcounterr := fmt.Sprintf("mailbox %q (id %d) has wrong counts %s, should be %s", mb.Name, mb.ID, mb.MailboxCounts, counts[mb.ID]) mbcounterr := fmt.Sprintf("mailbox %q (id %d) has wrong counts %s, should be %s", mb.Name, mb.ID, mb.MailboxCounts, counts[mb.ID])
errmsgs = append(errmsgs, mbcounterr) errmsgs = append(errmsgs, mbcounterr)
} }

View File

@ -1695,7 +1695,7 @@
}, },
{ {
"Name": "HaveCounts", "Name": "HaveCounts",
"Docs": "Whether MailboxCounts have been initialized.", "Docs": "Deprecated. Covered by Upgrade.MailboxCounts. No longer read.",
"Typewords": [ "Typewords": [
"bool" "bool"
] ]

View File

@ -201,7 +201,7 @@ export interface Mailbox {
Sent: boolean Sent: boolean
Trash: boolean Trash: boolean
Keywords?: string[] | null // Keywords as used in messages. Storing a non-system keyword for a message automatically adds it to this list. Used in the IMAP FLAGS response. Only "atoms" are allowed (IMAP syntax), keywords are case-insensitive, only stored in lower case (for JMAP), sorted. Keywords?: string[] | null // Keywords as used in messages. Storing a non-system keyword for a message automatically adds it to this list. Used in the IMAP FLAGS response. Only "atoms" are allowed (IMAP syntax), keywords are case-insensitive, only stored in lower case (for JMAP), sorted.
HaveCounts: boolean // Whether MailboxCounts have been initialized. HaveCounts: boolean // Deprecated. Covered by Upgrade.MailboxCounts. No longer read.
Total: number // Total number of messages, excluding \Deleted. For JMAP. Total: number // Total number of messages, excluding \Deleted. For JMAP.
Deleted: number // Number of messages with \Deleted flag. Used for IMAP message count that includes messages with \Deleted. Deleted: number // Number of messages with \Deleted flag. Used for IMAP message count that includes messages with \Deleted.
Unread: number // Messages without \Seen, excluding those with \Deleted, for JMAP. Unread: number // Messages without \Seen, excluding those with \Deleted, for JMAP.