thread-master/apps/backend/internal/module/job/repository/mongo.go

254 lines
6.9 KiB
Go
Raw Normal View History

2026-07-13 01:15:30 +00:00
package repository
import (
"context"
libmongo "apps/backend/internal/lib/mongo"
"apps/backend/internal/module/job/domain"
"github.com/zeromicro/go-zero/core/stores/mon"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
)
const colJobs = "jobs"
type MonStore struct {
jobs *mon.Model
}
func NewMonStore(uri, database string) *MonStore {
uri = libmongo.MustMongoURI(uri)
return &MonStore{jobs: mon.MustNewModel(uri, database, colJobs)}
}
func (s *MonStore) Insert(ctx context.Context, j *domain.Job) error {
_, err := s.jobs.InsertOne(ctx, j)
return err
}
func (s *MonStore) Update(ctx context.Context, j *domain.Job) error {
2026-07-15 15:23:59 +00:00
return s.update(ctx, j, nil)
}
func (s *MonStore) UpdateOwned(ctx context.Context, j *domain.Job, leaseOwner string) error {
filter := bson.M{"status": domain.StatusRunning, "lease_owner": leaseOwner}
return s.update(ctx, j, filter)
}
func (s *MonStore) update(ctx context.Context, j *domain.Job, guard bson.M) error {
2026-07-13 08:59:13 +00:00
if j == nil {
return domain.ErrNotFound
}
expectedVersion := j.Version
2026-07-13 01:15:30 +00:00
j.UpdatedAt = domain.NowNano()
2026-07-13 08:59:13 +00:00
candidate := *j
candidate.Version = expectedVersion + 1
2026-07-15 15:23:59 +00:00
filter := bson.M{"_id": j.ID, "version": expectedVersion}
for key, value := range guard {
filter[key] = value
}
res, err := s.jobs.ReplaceOne(ctx, filter, &candidate)
2026-07-13 01:15:30 +00:00
if err != nil {
return err
}
if res.MatchedCount == 0 {
2026-07-13 08:59:13 +00:00
return domain.ErrIllegalStatus
2026-07-13 01:15:30 +00:00
}
2026-07-13 08:59:13 +00:00
j.Version = candidate.Version
2026-07-13 01:15:30 +00:00
return nil
}
2026-07-15 15:23:59 +00:00
func (s *MonStore) RenewLease(ctx context.Context, id, leaseOwner string, leaseExpiresAt int64) error {
res, err := s.jobs.UpdateOne(ctx, bson.M{
"_id": id, "status": domain.StatusRunning, "lease_owner": leaseOwner,
}, bson.M{
"$set": bson.M{"lease_expires_at": leaseExpiresAt, "updated_at": domain.NowNano()},
})
if err != nil {
return err
}
if res.MatchedCount == 0 {
return domain.ErrIllegalStatus
}
return nil
}
2026-07-13 01:15:30 +00:00
func (s *MonStore) FindByID(ctx context.Context, id string) (*domain.Job, error) {
var j domain.Job
err := s.jobs.FindOne(ctx, &j, bson.M{"_id": id})
if err != nil {
if err == mon.ErrNotFound {
return nil, domain.ErrNotFound
}
return nil, err
}
return &j, nil
}
func (s *MonStore) ListByOwner(ctx context.Context, ownerUID int64) ([]*domain.Job, error) {
var list []*domain.Job
err := s.jobs.Find(ctx, &list, bson.M{"owner_uid": ownerUID},
2026-07-13 03:18:08 +00:00
options.Find().SetSort(bson.D{{Key: "updated_at", Value: -1}}))
2026-07-13 01:15:30 +00:00
return list, err
}
2026-07-13 03:18:08 +00:00
func jobTabFilter(ownerUID int64, tab string, nowNs int64) bson.M {
base := bson.M{"owner_uid": ownerUID}
switch domain.NormalizeListTab(tab) {
case domain.TabHistory:
base["status"] = bson.M{"$in": []string{
domain.StatusSucceeded, domain.StatusFailed, domain.StatusCancelled,
}}
case domain.TabRecurring:
base["status"] = bson.M{"$in": []string{domain.StatusPending, domain.StatusQueued}}
base["run_after"] = bson.M{"$gt": nowNs}
default: // active
base["$or"] = []bson.M{
{"status": domain.StatusRunning},
{
"status": bson.M{"$in": []string{domain.StatusPending, domain.StatusQueued}},
"$or": []bson.M{
{"run_after": bson.M{"$exists": false}},
{"run_after": 0},
{"run_after": bson.M{"$lte": nowNs}},
},
},
}
}
return base
}
func (s *MonStore) ListByOwnerTab(ctx context.Context, ownerUID int64, tab string, page, pageSize int) ([]*domain.Job, int64, error) {
page, pageSize = domain.ClampPage(page, pageSize)
now := domain.NowNano()
filter := jobTabFilter(ownerUID, tab, now)
total, err := s.jobs.CountDocuments(ctx, filter)
if err != nil {
return nil, 0, err
}
sort := bson.D{{Key: "updated_at", Value: -1}}
if domain.NormalizeListTab(tab) == domain.TabRecurring {
sort = bson.D{{Key: "run_after", Value: 1}, {Key: "created_at", Value: 1}}
} else if domain.NormalizeListTab(tab) == domain.TabHistory {
sort = bson.D{{Key: "completed_at", Value: -1}, {Key: "updated_at", Value: -1}}
}
skip := int64((page - 1) * pageSize)
var list []*domain.Job
err = s.jobs.Find(ctx, &list, filter,
options.Find().SetSort(sort).SetSkip(skip).SetLimit(int64(pageSize)))
if err != nil {
return nil, 0, err
}
return list, total, nil
}
2026-07-13 01:15:30 +00:00
func (s *MonStore) ClaimNext(ctx context.Context, workerID string) (*domain.Job, error) {
now := domain.NowNano()
filter := bson.M{
"$or": []bson.M{
2026-07-15 15:23:59 +00:00
{
"status": bson.M{"$in": []string{domain.StatusPending, domain.StatusQueued}},
"$or": []bson.M{
{"run_after": bson.M{"$exists": false}},
{"run_after": 0},
{"run_after": bson.M{"$lte": now}},
},
},
{
"status": domain.StatusRunning,
"$or": []bson.M{
{"lease_expires_at": bson.M{"$exists": false}},
{"lease_expires_at": 0},
{"lease_expires_at": bson.M{"$lte": now}},
},
},
2026-07-13 01:15:30 +00:00
},
}
update := bson.M{"$set": bson.M{
"status": domain.StatusRunning,
"worker_id": workerID,
2026-07-15 15:23:59 +00:00
"lease_owner": workerID,
"lease_expires_at": now + domain.DefaultLeaseDurationNs,
2026-07-13 01:15:30 +00:00
"updated_at": now,
"progress_summary": "已由 worker 領取(" + workerID + "",
2026-07-15 15:23:59 +00:00
}, "$inc": bson.M{"version": 1, "attempt": 1}}
2026-07-13 01:15:30 +00:00
opts := options.FindOneAndUpdate().
SetSort(bson.D{{Key: "run_after", Value: 1}, {Key: "created_at", Value: 1}}).
SetReturnDocument(options.After)
var j domain.Job
err := s.jobs.FindOneAndUpdate(ctx, &j, filter, update, opts)
if err != nil {
if err == mon.ErrNotFound {
return nil, domain.ErrNotFound
}
return nil, err
}
return &j, nil
}
func (s *MonStore) CancelPendingByRef(ctx context.Context, ownerUID int64, template, refID string) error {
if refID == "" {
return nil
}
filter := bson.M{
"owner_uid": ownerUID,
"template_type": template,
"ref_id": refID,
"status": bson.M{"$in": []string{domain.StatusPending, domain.StatusQueued}},
}
now := domain.NowNano()
// list then update each (mon may not expose UpdateMany consistently)
var list []*domain.Job
if err := s.jobs.Find(ctx, &list, filter); err != nil {
return err
}
for _, j := range list {
j.Status = domain.StatusCancelled
j.ProgressSummary = "已由新的定期排程取代"
j.CompletedAt = now
j.UpdatedAt = now
_ = s.Update(ctx, j)
}
return nil
}
func (s *MonStore) Delete(ctx context.Context, id string) error {
res, err := s.jobs.DeleteOne(ctx, bson.M{"_id": id})
if err != nil {
return err
}
if res == 0 {
return domain.ErrNotFound
}
return nil
}
func (s *MonStore) DeleteTerminalBefore(ctx context.Context, beforeNs int64) (int64, error) {
// completed_at 優先;缺則用 updated_at舊資料
filter := bson.M{
"status": bson.M{"$in": []string{
domain.StatusSucceeded, domain.StatusFailed, domain.StatusCancelled,
}},
"$or": []bson.M{
{"completed_at": bson.M{"$gt": 0, "$lt": beforeNs}},
{
"$and": []bson.M{
{"$or": []bson.M{
{"completed_at": bson.M{"$exists": false}},
{"completed_at": 0},
}},
{"updated_at": bson.M{"$lt": beforeNs}},
},
},
},
}
// go-zero mon.DeleteMany returns deleted count (int64)
return s.jobs.DeleteMany(ctx, filter)
}
var _ domain.Repository = (*MonStore)(nil)