Files
zeekay 40351cce6f rip: delete temporal fork — native ZAP-only tasksd
Goes from 135 MB binary with 1153 files importing go.temporal.io and 224
importing google.golang.org/grpc to a 10 MB native binary with ZERO of
either.

Deleted:
- tasks/, tasktests/, service/, chasm/, client/, api/, proto/, tools/
- cmd/tools/, components/, docker/
- schema/{cassandra,elasticsearch,mysql,postgresql,sqlite}/
- 40 tainted common/ subdirs + 7 top-level common files
- tests/testutils (no longer referenced after surviving deps drop common)

New cmd/tasksd (~100 lines) boots tasks.Embed (zap.Node) + http.Server.
New pkg/tasks/embed.go registers ZAP handlers for the SDK opcode surface
(0x0060-0x00A5). Workflow handlers return a 501 "not yet implemented"
envelope; native engine lands behind this contract.

Build:  go build ./cmd/tasksd → 10 MB
Vet:    go vet ./... → clean
Boot:   tasksd --zap-port 9999 --http :7243
Probes: /healthz, /v1/tasks/health → 200
UI:     /_/tasks/, / → 200 (embedded Vite bundle)
ZAP:    _tasks._tcp on :9999 (mDNS disabled; direct connect)

go.mod: tidy. No go.temporal.io. No google.golang.org/grpc.
2026-04-26 14:06:21 -07:00

954 lines
32 KiB
Go

package matching
import (
"cmp"
"context"
"fmt"
"math"
"slices"
"sync"
"time"
"go.temporal.io/api/serviceerror"
persistencespb "go.temporal.io/server/api/persistence/v1"
"go.temporal.io/server/common/log"
"go.temporal.io/server/common/log/tag"
"go.temporal.io/server/common/metrics"
"go.temporal.io/server/common/persistence"
"go.temporal.io/server/common/primitives/timestamp"
"go.temporal.io/server/common/softassert"
"go.temporal.io/server/service/matching/counter"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
)
const (
initialRangeID = 1 // Id of the first range of a new task queue
// Subqueue zero corresponds to "the queue" before migrating metadata to subqueues.
// For backwards compatibility, some operations only apply to subqueue zero for now.
subqueueZero = subqueueIndex(0)
)
type (
taskQueueDB struct {
// constants
config *taskQueueConfig
queue *PhysicalTaskQueueKey
isDraining bool
store persistence.TaskManager
logger log.Logger
metricsHandler metrics.Handler
// mutable
sync.Mutex
rangeID int64
subqueues []*dbSubqueue
otherHasTasks bool
// used to avoid unnecessary metadata writes:
lastChange time.Time // updated when metadata is changed in memory
lastWrite time.Time // updated when metadata is successfully written to db
}
dbSubqueue struct {
persistencespb.SubqueueInfo
maxReadLevel int64
oldestTime time.Time // time of oldest task if backlog, otherwise zero time
}
taskQueueState struct {
rangeID int64
ackLevel int64 // TODO(pri): old matcher cleanup, delete later
subqueues []persistencespb.SubqueueInfo
otherHasTasks bool
}
subqueueIndex int
createTasksResponse struct {
bySubqueue map[subqueueIndex]subqueueCreateTasksResponse
}
subqueueCreateTasksResponse struct {
tasks []*persistencespb.AllocatedTaskInfo
maxReadLevelBefore int64
maxReadLevelAfter int64
}
createFairTasksResponse map[subqueueIndex][]*persistencespb.AllocatedTaskInfo // subqueue -> tasks
)
// newTaskQueueDB returns an instance of an object that represents
// persistence view of a physical task queue. All mutations / reads to queues
// wrt persistence go through this object.
//
// This class will serialize writes to persistence that do condition updates. There are
// two reasons for doing this:
// - To work around known Cassandra issue where concurrent LWT to the same partition cause timeout errors
// - To provide the guarantee that there is only writer who updates queue in persistence at any given point in time
// This guarantee makes some of the other code simpler and there is no impact to perf because updates to taskqueue are
// spread out and happen in background routines
func newTaskQueueDB(
config *taskQueueConfig,
store persistence.TaskManager,
queue *PhysicalTaskQueueKey,
logger log.Logger,
metricsHandler metrics.Handler,
isDraining bool,
) *taskQueueDB {
return &taskQueueDB{
config: config,
queue: queue,
isDraining: isDraining,
store: store,
logger: logger,
metricsHandler: metricsHandler,
}
}
// RangeID returns the current persistence view of rangeID
func (db *taskQueueDB) RangeID() int64 {
db.Lock()
defer db.Unlock()
return db.rangeID
}
// GetMaxReadLevel returns the current maxReadLevel
func (db *taskQueueDB) GetMaxReadLevel(subqueue subqueueIndex) int64 {
db.Lock()
defer db.Unlock()
return db.getMaxReadLevelLocked(subqueue)
}
func (db *taskQueueDB) getMaxReadLevelLocked(subqueue subqueueIndex) int64 {
return db.subqueues[subqueue].maxReadLevel
}
// GetMaxReadLevel returns the current maxReadLevel
func (db *taskQueueDB) GetMaxFairReadLevel(subqueue subqueueIndex) fairLevel {
db.Lock()
defer db.Unlock()
return db.getMaxFairReadLevelLocked(subqueue)
}
func (db *taskQueueDB) getMaxFairReadLevelLocked(subqueue subqueueIndex) fairLevel {
return fairLevelFromProto(db.subqueues[subqueue].FairMaxReadLevel)
}
// This is only exposed for testing!
func (db *taskQueueDB) setMaxReadLevelForTesting(subqueue subqueueIndex, level int64) {
db.Lock()
defer db.Unlock()
db.subqueues[subqueue].maxReadLevel = level
}
// RenewLease renews the lease on a taskqueue. If there is no previous lease,
// this method will attempt to steal taskqueue from current owner
func (db *taskQueueDB) RenewLease(
ctx context.Context,
) (taskQueueState, error) {
db.Lock()
defer db.Unlock()
if db.rangeID == 0 {
if err := db.takeOverTaskQueueLocked(ctx); err != nil {
return taskQueueState{}, err
}
} else {
if err := db.updateTaskQueueLocked(ctx, true); err != nil {
return taskQueueState{}, err
}
}
return taskQueueState{
rangeID: db.rangeID,
ackLevel: db.subqueues[subqueueZero].AckLevel, // TODO(pri): cleanup, only used by old backlog manager
subqueues: db.cloneSubqueues(),
otherHasTasks: !db.isDraining && db.otherHasTasks,
}, nil
}
func (db *taskQueueDB) takeOverTaskQueueLocked(
ctx context.Context,
) error {
response, err := db.store.GetTaskQueue(ctx, &persistence.GetTaskQueueRequest{
NamespaceID: db.queue.NamespaceId(),
TaskQueue: db.queue.PersistenceName(),
TaskType: db.queue.TaskType(),
})
switch err.(type) {
case nil:
db.rangeID = response.RangeID
// If we are the draining one, then assume the other has tasks, so we can migrate
// backwards safely.
db.otherHasTasks = response.TaskQueueInfo.OtherHasTasks || db.isDraining
db.subqueues = db.ensureDefaultSubqueuesLocked(
response.TaskQueueInfo.Subqueues,
response.TaskQueueInfo.AckLevel,
response.TaskQueueInfo.ApproximateBacklogCount,
)
err := db.updateTaskQueueLocked(ctx, true)
if err != nil {
db.rangeID = 0
return err
}
db.lastWrite = time.Now()
// We took over the task queue and are not sure what tasks may have been written
// before. Set max read level id of all subqueues to just before our new block.
maxReadLevel := rangeIDToTaskIDBlock(db.rangeID, db.config.RangeSize).start - 1
for _, s := range db.subqueues {
s.maxReadLevel = maxReadLevel
}
return nil
case *serviceerror.NotFound:
db.rangeID = initialRangeID
db.subqueues = db.ensureDefaultSubqueuesLocked(nil, 0, 0)
// If we are the draining one, then assume the other has tasks, so we can migrate
// backwards safely. Also assume other has tasks if the config allows for migration
// (and the partition supports fairness) since we may have just turned on fairness and need to migrate.
canMigrate := (db.config.NewMatcher || db.config.EnableFairness) && db.queue.Partition().SupportsFairness()
db.otherHasTasks = canMigrate || db.isDraining
if _, err := db.store.CreateTaskQueue(ctx, &persistence.CreateTaskQueueRequest{
RangeID: db.rangeID,
TaskQueueInfo: db.cachedQueueInfo(),
}); err != nil {
db.rangeID = 0
return err
}
db.lastWrite = time.Now()
// In this case, ensureDefaultSubqueuesLocked already initialized subqueue 0 to have
// ackLevel and maxReadLevel 0, so we don't need to initialize them.
softassert.That(db.logger, db.subqueues[0].maxReadLevel == 0, "should have maxReadLevel 0 here")
softassert.That(db.logger, db.subqueues[0].FairMaxReadLevel == nil, "should have maxReadLevel 0 here")
softassert.That(db.logger, db.subqueues[0].AckLevel == 0, "should have ackLevel 0 here")
softassert.That(db.logger, db.subqueues[0].FairAckLevel == nil, "should have ackLevel 0 here")
return nil
default:
return err
}
}
func (db *taskQueueDB) updateTaskQueueLocked(ctx context.Context, incrementRangeId bool) error {
newRangeID := db.rangeID
if incrementRangeId {
newRangeID++
}
if _, err := db.store.UpdateTaskQueue(ctx, &persistence.UpdateTaskQueueRequest{
RangeID: newRangeID,
TaskQueueInfo: db.cachedQueueInfo(),
PrevRangeID: db.rangeID,
}); err != nil {
return err
}
db.lastWrite = time.Now()
db.rangeID = newRangeID
return nil
}
// OldUpdateState updates the queue state with the given value. This is used by old backlog
// manager (not subqueue-enabled).
// TODO(pri): old matcher cleanup
func (db *taskQueueDB) OldUpdateState(
ctx context.Context,
ackLevel int64,
) error {
db.Lock()
defer db.Unlock()
// We don't need to update lastWrite/lastChange in here since this function is only used by
// the old backlog manager and those fields are only used by the new backlog manager.
// Reset approximateBacklogCount to fix the count divergence issue
maxReadLevel := db.getMaxReadLevelLocked(subqueueZero)
if ackLevel == maxReadLevel {
db.subqueues[subqueueZero].ApproximateBacklogCount = 0
db.subqueues[subqueueZero].oldestTime = time.Time{} // zero time means no backlog
}
prevAckLevel := db.subqueues[subqueueZero].AckLevel
db.subqueues[subqueueZero].AckLevel = ackLevel
err := db.updateTaskQueueLocked(ctx, false)
if err != nil {
db.subqueues[subqueueZero].AckLevel = prevAckLevel
}
db.emitPhysicalBacklogGaugesLocked()
return err
}
// shouldUpdateMetadataOnAppendLocked returns whether a task append should also write the
// metadata blob. This is always true when enough time has passed since the last metadata
// write (controlled by MetadataUpdateOnAppendInterval), so that backlog counts stay
// reasonably fresh. When the interval is zero, metadata is updated on every append
// (previous behavior). Caller must hold db.Mutex.
func (db *taskQueueDB) shouldUpdateMetadataOnAppendLocked() bool {
interval := db.config.MetadataUpdateOnAppendInterval()
return interval <= 0 || time.Since(db.lastWrite) >= interval
}
func (db *taskQueueDB) SyncState(ctx context.Context) error {
db.Lock()
defer db.Unlock()
defer db.emitPhysicalBacklogGaugesLocked()
// We only need to write if something changed, or if we're past half of the persistence TTL.
// Cap at 24h so that the scavenger (which looks for metadata not updated in 48h) doesn't
// mistake the queue for idle, even if a future partition kind has a longer TTL.
ttl := min(24*time.Hour, cmp.Or(db.queue.Partition().PersistenceTTL(), 24*time.Hour))
needWrite := db.lastChange.After(db.lastWrite) || time.Since(db.lastWrite) > ttl/2
if !needWrite {
// If we don't write, though, we wouldn't know if someone else has stolen ownership
// momentarily (this could happen due to eventual consistency of membership updates).
// So instead, do a (cheaper) read to just check the range id.
return db.verifyOwnershipLocked(ctx)
}
return db.updateTaskQueueLocked(ctx, false)
}
func (db *taskQueueDB) verifyOwnershipLocked(ctx context.Context) error {
response, err := db.store.GetTaskQueue(ctx, &persistence.GetTaskQueueRequest{
NamespaceID: db.queue.NamespaceId(),
TaskQueue: db.queue.PersistenceName(),
TaskType: db.queue.TaskType(),
})
if err != nil {
return err
}
if response.RangeID != db.rangeID {
return &persistence.ConditionFailedError{
Msg: fmt.Sprintf("task queue ownership lost: stored rangeID %d, in-memory rangeID %d",
response.RangeID, db.rangeID),
}
}
return nil
}
func (db *taskQueueDB) updateAckLevelAndBacklogStats(subqueue subqueueIndex, newAckLevel int64, countDelta int64, oldestTime time.Time) {
db.Lock()
defer db.Unlock()
dbQueue := db.subqueues[subqueue]
if newAckLevel < dbQueue.AckLevel {
softassert.Fail(db.logger,
"ack level in subqueue should not move backwards",
tag.Int("subqueue-id", int(subqueue)),
tag.Any("cur-ack-level", dbQueue.AckLevel),
tag.Any("new-ack-level", newAckLevel))
}
if dbQueue.AckLevel != newAckLevel {
db.lastChange = time.Now()
dbQueue.AckLevel = newAckLevel
}
if newAckLevel == db.getMaxReadLevelLocked(subqueue) {
// Reset approximateBacklogCount to fix the count divergence issue
if dbQueue.ApproximateBacklogCount != 0 || !dbQueue.oldestTime.Equal(oldestTime) {
db.lastChange = time.Now()
dbQueue.ApproximateBacklogCount = 0
dbQueue.oldestTime = oldestTime
}
} else if countDelta != 0 {
db.lastChange = time.Now()
db.updateBacklogStatsLocked(subqueue, countDelta, oldestTime)
}
}
func (db *taskQueueDB) updateFairAckLevel(subqueue subqueueIndex, newAckLevel fairLevel, countDelta, knownCount int64, oldestTime time.Time) {
db.Lock()
defer db.Unlock()
db.lastChange = time.Now()
dbQueue := db.subqueues[subqueue]
if prev := fairLevelFromProto(dbQueue.FairAckLevel); newAckLevel.less(prev) {
softassert.Fail(db.logger,
"ack level in subqueue should not move backwards",
tag.Int("subqueue-id", int(subqueue)),
tag.Any("cur-ack-level", prev),
tag.Any("new-ack-level", newAckLevel))
}
dbQueue.FairAckLevel = newAckLevel.toProto()
if knownCount >= 0 {
// Reset approximateBacklogCount to fix the count divergence issue
dbQueue.ApproximateBacklogCount = knownCount
dbQueue.oldestTime = oldestTime
} else if countDelta != 0 {
db.updateBacklogStatsLocked(subqueue, countDelta, oldestTime)
}
}
// Use this to reset ApproximateBacklogCount when the backlog count is known, e.g. when you're
// read to the end of the backlog.
func (db *taskQueueDB) setKnownFairBacklogCount(subqueue subqueueIndex, count int64) {
db.Lock()
defer db.Unlock()
if db.subqueues[subqueue].ApproximateBacklogCount != count {
db.lastChange = time.Now()
db.subqueues[subqueue].ApproximateBacklogCount = count
if count == 0 {
db.subqueues[subqueue].oldestTime = time.Time{}
}
}
}
// updateApproximateBacklogCount updates the in-memory DB state with the given delta value
// TODO(pri): old matcher cleanup
func (db *taskQueueDB) updateBacklogStats(countDelta int64, oldestTime time.Time) {
db.Lock()
defer db.Unlock()
db.lastChange = time.Now()
db.updateBacklogStatsLocked(subqueueZero, countDelta, oldestTime)
}
func (db *taskQueueDB) updateBacklogStatsLocked(subqueue subqueueIndex, countDelta int64, oldestTime time.Time) {
// Prevent under-counting
count := &db.subqueues[subqueue].ApproximateBacklogCount
if *count+countDelta < 0 {
db.logger.Info("ApproximateBacklogCount could have under-counted.",
tag.WorkerVersion(db.queue.Version().MetricsTagValue()),
tag.WorkflowNamespaceID(db.queue.Partition().NamespaceId()))
*count = 0
} else {
*count += countDelta
}
db.subqueues[subqueue].oldestTime = oldestTime
}
func (db *taskQueueDB) persistTopKFairnessKeys(subqueue subqueueIndex, entries []counter.TopKEntry) {
db.Lock()
defer db.Unlock()
counts := make([]*persistencespb.FairnessKeyCount, len(entries))
for i, entry := range entries {
counts[i] = &persistencespb.FairnessKeyCount{Key: entry.Key, Count: entry.Count}
}
db.subqueues[subqueue].TopKFairnessCounts = counts
db.lastChange = time.Now()
}
func (db *taskQueueDB) getTopKFairnessKeys(subqueue subqueueIndex) []counter.TopKEntry {
db.Lock()
defer db.Unlock()
if subqueue >= subqueueIndex(len(db.subqueues)) {
return nil
}
counts := db.subqueues[subqueue].TopKFairnessCounts
entries := make([]counter.TopKEntry, len(counts))
for i, count := range counts {
entries[i] = counter.TopKEntry{Key: count.Key, Count: count.Count}
}
return entries
}
// getApproximateBacklogCountsBySubqueue return the approximate backlog count for each subqueue.
// The index corresponds to the subqueue id.
func (db *taskQueueDB) getApproximateBacklogCountsBySubqueue() []int64 {
db.Lock()
defer db.Unlock()
result := make([]int64, len(db.subqueues))
for id, s := range db.subqueues {
result[id] = s.ApproximateBacklogCount
}
return result
}
func (db *taskQueueDB) getApproximateBacklogCountAndMaxReadLevel(subqueue subqueueIndex) (int64, fairLevel) {
db.Lock()
defer db.Unlock()
s := db.subqueues[subqueue]
return s.ApproximateBacklogCount, fairLevelFromProto(s.FairMaxReadLevel)
}
func (db *taskQueueDB) getTotalApproximateBacklogCount() int64 {
db.Lock()
defer db.Unlock()
var total int64
for _, s := range db.subqueues {
total += s.ApproximateBacklogCount
}
return total
}
// SetOtherHasTasks updates the otherHasTasks flag and attempts to persist immediately.
// The in-memory state is updated regardless of whether persistence succeeds.
// Returns any error from the persistence attempt.
func (db *taskQueueDB) SetOtherHasTasks(ctx context.Context, value bool) error {
db.Lock()
defer db.Unlock()
if db.otherHasTasks == value {
return nil
}
db.otherHasTasks = value
db.lastChange = time.Now()
return db.updateTaskQueueLocked(ctx, false)
}
// CreateTasks creates a batch of given tasks for this task queue
func (db *taskQueueDB) CreateTasks(
ctx context.Context,
reqs []*writeTaskRequest,
) (createTasksResponse, error) {
if db.isDraining {
return createTasksResponse{}, softassert.UnexpectedInternalErr(db.logger, "CreateTasks can't be used in draining mode", nil)
}
db.Lock()
defer db.Unlock()
if len(reqs) == 0 {
return createTasksResponse{}, nil
}
updates := make(map[subqueueIndex]subqueueCreateTasksResponse)
allTasks := make([]*persistencespb.AllocatedTaskInfo, len(reqs))
allSubqueues := make([]int, len(reqs))
for i, req := range reqs {
task := &persistencespb.AllocatedTaskInfo{
TaskId: req.id,
Data: req.taskInfo,
}
allTasks[i] = task
allSubqueues[i] = int(req.subqueue)
u := updates[req.subqueue]
updates[req.subqueue] = subqueueCreateTasksResponse{
tasks: append(u.tasks, task),
maxReadLevelBefore: db.getMaxReadLevelLocked(req.subqueue),
maxReadLevelAfter: task.TaskId, // task ids are in order so this is the max
}
}
for sq, update := range updates {
db.subqueues[sq].ApproximateBacklogCount += int64(len(update.tasks))
}
// Decide whether to include metadata in the write. We always need the LWT for the
// range ID check, but updating the full metadata blob on every append has extra cost.
// We piggyback the metadata update if enough time has passed since the last write.
updateMetadata := db.shouldUpdateMetadataOnAppendLocked()
resp, err := db.store.CreateTasks(
ctx,
&persistence.CreateTasksRequest{
TaskQueueInfo: &persistence.PersistedTaskQueueInfo{
Data: db.cachedQueueInfo(),
RangeID: db.rangeID,
},
Tasks: allTasks,
Subqueues: allSubqueues,
UpdateMetadata: updateMetadata,
})
// Update the maxReadLevel after the writes are completed, but before we send the response,
// so that taskReader is guaranteed to see the new read level when SpoolTask wakes it up.
// Do this even if the write fails, we won't reuse the task ids.
for sq, update := range updates {
db.subqueues[sq].maxReadLevel = update.maxReadLevelAfter
}
if err == nil {
// Only update lastWrite for persistence implementations that update metadata on CreateTasks,
// otherwise we have a change to ApproximateBacklogCount we need to write.
if resp.UpdatedMetadata {
db.lastWrite = time.Now()
} else {
db.lastChange = time.Now()
}
} else if _, ok := err.(*persistence.ConditionFailedError); ok {
// tasks definitely were not created, restore the counter. For other errors tasks may or may not be created.
// In those cases we keep the count incremented, hence it may be an overestimate.
for i, update := range updates {
db.subqueues[i].ApproximateBacklogCount -= int64(len(update.tasks))
}
}
return createTasksResponse{bySubqueue: updates}, err
}
// CreateFairTasks creates a batch of given tasks for this task queue
func (db *taskQueueDB) CreateFairTasks(
ctx context.Context,
reqs []*writeTaskRequest,
) (createFairTasksResponse, error) {
if db.isDraining {
return createFairTasksResponse{}, softassert.UnexpectedInternalErr(db.logger, "CreateTasks can't be used in draining mode", nil)
}
db.Lock()
defer db.Unlock()
if len(reqs) == 0 {
return nil, nil
}
newTasks := make(createFairTasksResponse)
newMaxLevel := make(map[subqueueIndex]fairLevel)
allTasks := make([]*persistencespb.AllocatedTaskInfo, len(reqs))
allSubqueues := make([]int, len(reqs))
for i, req := range reqs {
task := &persistencespb.AllocatedTaskInfo{
TaskId: req.id,
TaskPass: req.pass,
Data: req.taskInfo,
}
allTasks[i] = task
allSubqueues[i] = int(req.subqueue)
newTasks[req.subqueue] = append(newTasks[req.subqueue], task)
newMaxLevel[req.subqueue] = newMaxLevel[req.subqueue].max(req.fairLevel)
}
for sq, tasks := range newTasks {
db.subqueues[sq].ApproximateBacklogCount += int64(len(tasks))
}
// Unlike in CreateTasks, we can set the persisted FairMaxReadLevel before persisting.
// This means that for stores that update metadata along with writing tasks (i.e. Cassandra),
// the FairMaxReadLevel will be more up-to-date. The max read level is not used by
// fairTaskReader, so there's no correctness issue with doing this.
for sq, level := range newMaxLevel {
db.subqueues[sq].FairMaxReadLevel = fairLevelFromProto(db.subqueues[sq].FairMaxReadLevel).max(level).toProto()
}
updateMetadata := db.shouldUpdateMetadataOnAppendLocked()
resp, err := db.store.CreateTasks(
ctx,
&persistence.CreateTasksRequest{
TaskQueueInfo: &persistence.PersistedTaskQueueInfo{
Data: db.cachedQueueInfo(),
RangeID: db.rangeID,
},
Tasks: allTasks,
Subqueues: allSubqueues,
UpdateMetadata: updateMetadata,
})
if err == nil {
// Only update lastWrite for persistence implementations that update metadata on CreateTasks,
// otherwise we have a change to ApproximateBacklogCount we need to write.
if resp.UpdatedMetadata {
db.lastWrite = time.Now()
} else {
db.lastChange = time.Now()
}
} else if _, ok := err.(*persistence.ConditionFailedError); ok {
// Tasks definitely were not created, restore the counter. For other errors tasks may or may not be created.
// In those cases we keep the count incremented, hence it may be an overestimate.
// Don't bother restoring MaxReadLevel, it's okay if that's too high.
for i, tasks := range newTasks {
db.subqueues[i].ApproximateBacklogCount -= int64(len(tasks))
}
}
return newTasks, err
}
// GetTasks returns a batch of tasks between the given range
func (db *taskQueueDB) GetTasks(
ctx context.Context,
subqueue subqueueIndex,
inclusiveMinTaskID int64,
exclusiveMaxTaskID int64,
batchSize int,
) (*persistence.GetTasksResponse, error) {
return db.store.GetTasks(ctx, &persistence.GetTasksRequest{
NamespaceID: db.queue.NamespaceId(),
TaskQueue: db.queue.PersistenceName(),
TaskType: db.queue.TaskType(),
InclusiveMinTaskID: inclusiveMinTaskID,
ExclusiveMaxTaskID: exclusiveMaxTaskID,
Subqueue: int(subqueue),
PageSize: batchSize,
})
}
// GetFairTasks returns a batch of tasks after the given level
func (db *taskQueueDB) GetFairTasks(
ctx context.Context,
subqueue subqueueIndex,
inclusiveMinLevel fairLevel,
batchSize int,
) (*persistence.GetTasksResponse, error) {
return db.store.GetTasks(ctx, &persistence.GetTasksRequest{
NamespaceID: db.queue.NamespaceId(),
TaskQueue: db.queue.PersistenceName(),
TaskType: db.queue.TaskType(),
InclusiveMinPass: inclusiveMinLevel.pass,
InclusiveMinTaskID: inclusiveMinLevel.id,
ExclusiveMaxTaskID: math.MaxInt64,
Subqueue: int(subqueue),
PageSize: batchSize,
UseLimit: true,
})
}
// CompleteTasksLessThan deletes of tasks less than the given taskID. Limit is
// the upper bound of number of tasks that can be deleted by this method. It may
// or may not be honored
func (db *taskQueueDB) CompleteTasksLessThan(
ctx context.Context,
exclusiveMaxTaskID int64,
limit int,
subqueue subqueueIndex,
) (int, error) {
n, err := db.store.CompleteTasksLessThan(ctx, &persistence.CompleteTasksLessThanRequest{
NamespaceID: db.queue.NamespaceId(),
TaskQueueName: db.queue.PersistenceName(),
TaskType: db.queue.TaskType(),
ExclusiveMaxTaskID: exclusiveMaxTaskID,
Subqueue: int(subqueue),
Limit: limit,
})
if err != nil {
db.logger.Error("Persistent store operation failure",
tag.StoreOperationCompleteTasksLessThan,
tag.Error(err),
tag.TaskID(exclusiveMaxTaskID),
tag.WorkflowTaskQueueType(db.queue.TaskType()),
tag.WorkflowTaskQueueName(db.queue.PersistenceName()),
)
}
return n, err
}
// CompleteFairTasksLessThan deletes of tasks less than the given taskID. Limit is
// the upper bound of number of tasks that can be deleted by this method. It may
// or may not be honored
func (db *taskQueueDB) CompleteFairTasksLessThan(
ctx context.Context,
exclusiveMaxLevel fairLevel,
limit int,
subqueue subqueueIndex,
) (int, error) {
n, err := db.store.CompleteTasksLessThan(ctx, &persistence.CompleteTasksLessThanRequest{
NamespaceID: db.queue.NamespaceId(),
TaskQueueName: db.queue.PersistenceName(),
TaskType: db.queue.TaskType(),
ExclusiveMaxPass: exclusiveMaxLevel.pass,
ExclusiveMaxTaskID: exclusiveMaxLevel.id,
Subqueue: int(subqueue),
Limit: limit,
})
if err != nil {
db.logger.Error("Persistent store operation failure",
tag.StoreOperationCompleteTasksLessThan,
tag.Error(err),
tag.AckLevel(exclusiveMaxLevel),
tag.WorkflowTaskQueueType(db.queue.TaskType()),
tag.WorkflowTaskQueueName(db.queue.PersistenceName()),
)
}
return n, err
}
func (db *taskQueueDB) AllocateSubqueue(
ctx context.Context,
key *persistencespb.SubqueueKey,
) ([]persistencespb.SubqueueInfo, error) {
db.Lock()
defer db.Unlock()
newSubqueue := db.newSubqueueLocked(key)
db.subqueues = append(db.subqueues, newSubqueue)
// ensure written to metadata before returning
err := db.updateTaskQueueLocked(ctx, false)
if err != nil {
// If this was a conflict, caller will shut down partition. Otherwise, we don't know
// for sure if this write made it to persistence or not. We should forget about the new
// subqueue and let a future call to AllocateSubqueue add it again. If we crash and
// reload, the new owner may see the subqueue present, which is also fine.
db.subqueues = db.subqueues[:len(db.subqueues)-1]
return nil, err
}
return db.cloneSubqueues(), nil
}
func (db *taskQueueDB) expiryTime() *timestamppb.Timestamp {
if ttl := db.queue.Partition().PersistenceTTL(); ttl > 0 {
return timestamppb.New(time.Now().Add(ttl))
}
return nil
}
func (db *taskQueueDB) cachedQueueInfo() *persistencespb.TaskQueueInfo {
infos := make([]*persistencespb.SubqueueInfo, len(db.subqueues))
for i := range db.subqueues {
infos[i] = &db.subqueues[i].SubqueueInfo
}
return &persistencespb.TaskQueueInfo{
NamespaceId: db.queue.NamespaceId(),
Name: db.queue.PersistenceName(),
TaskType: db.queue.TaskType(),
Kind: db.queue.Partition().Kind(),
AckLevel: db.subqueues[subqueueZero].AckLevel, // backwards compatibility
ExpiryTime: db.expiryTime(),
LastUpdateTime: timestamp.TimeNowPtrUtc(),
ApproximateBacklogCount: db.subqueues[subqueueZero].ApproximateBacklogCount, // backwards compatibility
Subqueues: infos,
OtherHasTasks: db.otherHasTasks,
}
}
// emitPhysicalBacklogGaugesLocked emits backlog gauges tagged by priority key, along with
// the legacy task_lag_per_tl gauge.
//
// When version-attributed backlog metrics are enabled (BacklogMetricsEmitInterval > 0), this
// emits physical_approximate_backlog_count and physical_approximate_backlog_age_seconds for
// the unversioned queue only. Version-attributed metrics (including appropriate attribution of
// the default queue's tasks to current and ramping versions) are emitted separately by the
// partition manager via fetchAndEmitLogicalBacklogMetrics.
//
// When version-attributed metrics are disabled (BacklogMetricsEmitInterval == 0), this falls back
// to emitting the original approximate_backlog_count and approximate_backlog_age_seconds for
// all queues (including versioned queues when BreakdownMetricsByBuildID is enabled).
func (db *taskQueueDB) emitPhysicalBacklogGaugesLocked() {
if !db.config.BreakdownMetricsByTaskQueue() || !db.config.BreakdownMetricsByPartition() {
return
}
attributionEnabled := db.config.BacklogMetricsEmitInterval() > 0
if attributionEnabled {
if db.queue.IsVersioned() {
return
}
} else {
if db.queue.IsVersioned() && !db.config.BreakdownMetricsByBuildID() {
return
}
}
var totalLag int64
var oldestTime time.Time
counts := make(map[int32]int64)
for _, s := range db.subqueues {
counts[s.Key.Priority] += s.ApproximateBacklogCount
oldestTime = minNonZeroTime(oldestTime, s.oldestTime)
// note: this metric is only an estimation for the lag.
// taskID in DB may not be continuous, especially when task list ownership changes.
if s.FairMaxReadLevel != nil && s.FairAckLevel != nil {
// TODO(fairness): this is not a good estimate of anything, we should probably just
// get rid of this metric.
totalLag += s.FairMaxReadLevel.TaskId - s.FairAckLevel.TaskId
} else {
totalLag += s.maxReadLevel - s.AckLevel
}
}
backlogCountGauge := metrics.ApproximateBacklogCount
backlogAgeGauge := metrics.ApproximateBacklogAgeSeconds
if attributionEnabled {
backlogCountGauge = metrics.PhysicalApproximateBacklogCount
backlogAgeGauge = metrics.PhysicalApproximateBacklogAgeSeconds
}
for priority, count := range counts {
backlogCountGauge.With(db.metricsHandler).Record(float64(count), metrics.MatchingTaskPriorityTag(priority))
}
if oldestTime.IsZero() {
backlogAgeGauge.With(db.metricsHandler).Record(0)
} else {
backlogAgeGauge.With(db.metricsHandler).Record(time.Since(oldestTime).Seconds())
}
metrics.TaskLagPerTaskQueueGauge.With(db.metricsHandler).Record(float64(totalLag))
}
func (db *taskQueueDB) ensureDefaultSubqueuesLocked(
infos []*persistencespb.SubqueueInfo,
initAckLevel int64,
initApproxCount int64,
) []*dbSubqueue {
// convert+copy protos to []*dbSubqueue
subqueues := make([]*dbSubqueue, len(infos))
for i, info := range infos {
subqueues[i] = &dbSubqueue{}
proto.Merge(&subqueues[i].SubqueueInfo, info)
}
// check for default priority and add if not present (this may be initializing subqueue 0)
defKey := &persistencespb.SubqueueKey{
Priority: int32(db.config.DefaultPriorityKey),
}
hasDefault := slices.ContainsFunc(subqueues, func(s *dbSubqueue) bool {
return proto.Equal(s.Key, defKey)
})
if !hasDefault {
subqueues = append(subqueues, db.newSubqueueLocked(defKey))
// If we are transitioning from no-subqueues to subqueues, initialize subqueue 0 with
// the ack level and approx count from TaskQueueInfo.
if len(subqueues) == 1 {
subqueues[subqueueZero].AckLevel = initAckLevel
subqueues[subqueueZero].ApproximateBacklogCount = initApproxCount
}
}
return subqueues
}
func (db *taskQueueDB) newSubqueueLocked(key *persistencespb.SubqueueKey) *dbSubqueue {
// For fifo queues: start ack level + max read level just before the current block.
// For fair queues: ack level and max read level don't matter here.
initAckLevel := rangeIDToTaskIDBlock(db.rangeID, db.config.RangeSize).start - 1
softassert.That(db.logger, initAckLevel >= 0, "initAckLevel should not be negative")
s := &dbSubqueue{maxReadLevel: initAckLevel}
s.Key = key
s.AckLevel = initAckLevel
return s
}
// clone db.subqueues so we can return it outside our lock
func (db *taskQueueDB) cloneSubqueues() []persistencespb.SubqueueInfo {
infos := make([]persistencespb.SubqueueInfo, len(db.subqueues))
for i := range db.subqueues {
proto.Merge(&infos[i], &db.subqueues[i].SubqueueInfo)
}
return infos
}
func (db *taskQueueDB) emitZeroPhysicalBacklogGauges() {
if !db.config.BreakdownMetricsByTaskQueue() || !db.config.BreakdownMetricsByPartition() {
return
}
attributionEnabled := db.config.BacklogMetricsEmitInterval() > 0
if attributionEnabled {
if db.queue.IsVersioned() {
return
}
} else {
if db.queue.IsVersioned() && !db.config.BreakdownMetricsByBuildID() {
return
}
}
priorities := make(map[int32]struct{})
db.Lock()
for _, s := range db.subqueues {
priorities[s.Key.Priority] = struct{}{}
}
db.Unlock()
backlogCountGauge := metrics.ApproximateBacklogCount
backlogAgeGauge := metrics.ApproximateBacklogAgeSeconds
if attributionEnabled {
backlogCountGauge = metrics.PhysicalApproximateBacklogCount
backlogAgeGauge = metrics.PhysicalApproximateBacklogAgeSeconds
}
for k := range priorities {
backlogCountGauge.With(db.metricsHandler).Record(0, metrics.MatchingTaskPriorityTag(k))
}
backlogAgeGauge.With(db.metricsHandler).Record(0)
metrics.TaskLagPerTaskQueueGauge.With(db.metricsHandler).Record(0)
}