refactor: remove completed migration logic

This commit is contained in:
tiennm99 committed 2026-07-01 16:20:30 +07:00
1 parent 3049af2800
commit 2fb10ff632
8 files changed
+11 -355

No files matched your search

+6 -141
View File
@@ -2,140 +2,33 @@ package stats
import (
"context"
"errors"
"fmt"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
"github.com/tiennm99/miti99bot/internal/storage"
"github.com/tiennm99/miti99bot/internal/systemstate"
)
const (
miscCommandRenameMigrationName = "stats-command-renames-misc-20260701"
statsCommandUsersIndexName = "stats_cmd_n_user"
statsUserCommandsIndexName = "stats_uid_n_cmd"
statsUsernameLookupIndexName = "stats_user_uid"
statsLegacyUserUpdatedIndexName = "stats_user_updated_at"
statsCommandUsersIndexName = "stats_cmd_n_user"
statsUserCommandsIndexName = "stats_uid_n_cmd"
statsUsernameLookupIndexName = "stats_user_uid"
)
type commandRename struct {
old string
new string
}
var miscCommandRenames = []commandRename{
{old: "mstats", new: "ping_stats"},
{old: "fortytwo", new: "the_answer"},
}
// InitStore performs stats collection startup maintenance. It is safe to call
// every boot: MongoDB indexes are created idempotently and one-time migrations
// are guarded by the shared system collection.
func InitStore(ctx context.Context, statsColl, systemColl storage.Collection) error {
// every boot: MongoDB indexes are created idempotently and memory storage is a
// no-op.
func InitStore(ctx context.Context, statsColl storage.Collection) error {
if mongoColl, ok := storage.MongoCollection(statsColl); ok {
if err := ensureUsageIndexes(ctx, mongoColl); err != nil {
return err
}
if err := dropIndexIfExists(ctx, mongoColl, statsLegacyUserUpdatedIndexName); err != nil {
return err
}
}
if err := runMiscCommandRenameMigration(ctx, statsColl, systemColl); err != nil {
return err
}
return nil
}
func runMiscCommandRenameMigration(ctx context.Context, statsColl, systemColl storage.Collection) error {
sys := systemstate.New(systemColl)
key := "migration:" + miscCommandRenameMigrationName
if rec, ok, err := sys.Get(ctx, key); err != nil {
return fmt.Errorf("stats migration state %s: %w", miscCommandRenameMigrationName, err)
} else if ok && rec.Status == "complete" {
return nil
}
count, err := migrateCommandStats(ctx, storage.Typed[usageEntry](statsColl), miscCommandRenames)
if err != nil {
return err
}
now := time.Now().UTC().UnixMilli()
if err := sys.Put(ctx, key, systemstate.Record{
Kind: "migration",
Name: miscCommandRenameMigrationName,
Status: "complete",
Count: count,
CompletedAt: now,
UpdatedAt: now,
}); err != nil {
return fmt.Errorf("stats migration mark complete %s: %w", miscCommandRenameMigrationName, err)
}
return nil
}
func migrateCommandStats(ctx context.Context, docs storage.DocStore[usageEntry], renames []commandRename) (int64, error) {
renameByOld := make(map[string]string, len(renames))
for _, r := range renames {
renameByOld[r.old] = r.new
}
keys, err := docs.List(ctx, "")
if err != nil {
return 0, fmt.Errorf("stats command rename list: %w", err)
}
var moved int64
for _, key := range keys {
src, _, err := docs.Get(ctx, key)
if err != nil {
return moved, fmt.Errorf("stats command rename get %s: %w", key, err)
}
if src.Cmd == "" || src.Deleted {
continue
}
newCmd, ok := renameByOld[src.Cmd]
if !ok {
continue
}
dstKey := usageKey(newCmd, src.UserID)
dst, _, err := docs.Get(ctx, dstKey)
switch {
case errors.Is(err, storage.ErrNotFound):
dst = usageEntry{}
case err != nil:
return moved, fmt.Errorf("stats command rename get %s: %w", dstKey, err)
}
dst.Cmd = newCmd
dst.N += src.N
dst.Deleted = false
if src.UserID != 0 {
dst.UserID = src.UserID
if dst.Username == "" {
dst.Username = src.Username
}
} else {
dst.UserID = 0
dst.Username = ""
}
if err := docs.Put(ctx, dstKey, dst); err != nil {
return moved, fmt.Errorf("stats command rename put %s: %w", dstKey, err)
}
if err := docs.Delete(ctx, key); err != nil {
return moved, fmt.Errorf("stats command rename delete %s: %w", key, err)
}
moved += src.N
}
return moved, nil
}
func ensureUsageIndexes(ctx context.Context, coll *mongo.Collection) error {
models := []mongo.IndexModel{
{
@@ -156,31 +49,3 @@ func ensureUsageIndexes(ctx context.Context, coll *mongo.Collection) error {
}
return nil
}
func dropIndexIfExists(ctx context.Context, coll *mongo.Collection, name string) error {
cur, err := coll.Indexes().List(ctx)
if err != nil {
return fmt.Errorf("stats list indexes before drop %s: %w", name, err)
}
defer func() { _ = cur.Close(ctx) }()
for cur.Next(ctx) {
var doc struct {
Name string `bson:"name"`
}
if err := cur.Decode(&doc); err != nil {
return fmt.Errorf("stats decode index before drop %s: %w", name, err)
}
if doc.Name != name {
continue
}
if err := coll.Indexes().DropOne(ctx, name); err != nil {
return fmt.Errorf("stats drop legacy index %s: %w", name, err)
}
return nil
}
if err := cur.Err(); err != nil {
return fmt.Errorf("stats index cursor before drop %s: %w", name, err)
}
return nil
}
+2 -16
View File
@@ -7,10 +7,6 @@ import (
"testing"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
"github.com/tiennm99/miti99bot/internal/storage"
)
@@ -38,23 +34,16 @@ func TestInitStore_MongoCreatesIndexes(t *testing.T) {
provider := storage.NewMongoProvider(db)
statsColl := provider.Collection("stats")
systemColl := provider.Collection("system")
rawStatsColl, ok := storage.MongoCollection(statsColl)
if !ok {
t.Fatal("stats collection is not Mongo-backed")
}
if _, err := rawStatsColl.Indexes().CreateOne(ctx, mongo.IndexModel{
Keys: bson.D{bsonField("user", 1), bsonField("updatedAt", -1)},
Options: options.Index().SetName(statsLegacyUserUpdatedIndexName),
}); err != nil {
t.Fatalf("seed legacy index: %v", err)
}
if err := InitStore(ctx, statsColl, systemColl); err != nil {
if err := InitStore(ctx, statsColl); err != nil {
t.Fatalf("InitStore: %v", err)
}
if err := InitStore(ctx, statsColl, systemColl); err != nil {
if err := InitStore(ctx, statsColl); err != nil {
t.Fatalf("InitStore second run: %v", err)
}
@@ -82,7 +71,4 @@ func TestInitStore_MongoCreatesIndexes(t *testing.T) {
t.Fatalf("missing index %s; indexes=%v", name, found)
}
}
if found[statsLegacyUserUpdatedIndexName] {
t.Fatalf("legacy index %s was not dropped; indexes=%v", statsLegacyUserUpdatedIndexName, found)
}
}
-75
View File
@@ -1,75 +0,0 @@
package stats
import (
"context"
"errors"
"testing"
"github.com/tiennm99/miti99bot/internal/storage"
"github.com/tiennm99/miti99bot/internal/systemstate"
)
func TestInitStore_RenamesMiscCommandStatsOnce(t *testing.T) {
ctx := context.Background()
provider := storage.NewMemoryProvider()
statsColl := provider.Collection("stats")
systemColl := provider.Collection(systemstate.CollectionName)
docs := storage.Typed[usageEntry](statsColl)
seed := map[string]usageEntry{
usageKey("mstats", 0): {Cmd: "mstats", N: 2},
usageKey("mstats", 7): {Cmd: "mstats", UserID: 7, Username: "alice", N: 3},
usageKey("ping_stats", 7): {Cmd: "ping_stats", UserID: 7, Username: "alice", N: 5},
usageKey("fortytwo", 0): {Cmd: "fortytwo", N: 1},
usageKey("the_answer", 0): {Cmd: "the_answer", N: 4},
usageKey("fortytwo", 100): {Cmd: "fortytwo", UserID: 100, Username: "owner", N: 8},
usageKey("the_answer", 100): {Cmd: "the_answer", UserID: 100, Username: "owner", N: 9},
}
for key, entry := range seed {
if err := docs.Put(ctx, key, entry); err != nil {
t.Fatalf("seed %s: %v", key, err)
}
}
if err := InitStore(ctx, statsColl, systemColl); err != nil {
t.Fatalf("InitStore first run: %v", err)
}
if err := InitStore(ctx, statsColl, systemColl); err != nil {
t.Fatalf("InitStore second run: %v", err)
}
want := map[string]int64{
usageKey("ping_stats", 0): 2,
usageKey("ping_stats", 7): 8,
usageKey("the_answer", 0): 5,
usageKey("the_answer", 100): 17,
}
for key, wantN := range want {
got, _, err := docs.Get(ctx, key)
if err != nil {
t.Fatalf("get %s: %v", key, err)
}
if got.N != wantN {
t.Fatalf("%s N = %d, want %d", key, got.N, wantN)
}
}
for _, key := range []string{
usageKey("mstats", 0),
usageKey("mstats", 7),
usageKey("fortytwo", 0),
usageKey("fortytwo", 100),
} {
if _, _, err := docs.Get(ctx, key); !errors.Is(err, storage.ErrNotFound) {
t.Fatalf("old stats key %s still exists: %v", key, err)
}
}
rec, ok, err := systemstate.New(systemColl).Get(ctx, "migration:"+miscCommandRenameMigrationName)
if err != nil {
t.Fatalf("migration marker get: %v", err)
}
if !ok || rec.Status != "complete" || rec.Count != 14 {
t.Fatalf("migration marker = %+v, ok=%v; want complete count 14", rec, ok)
}
}