mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-04 02:04:03 +00:00
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.
440 lines
14 KiB
Go
440 lines
14 KiB
Go
package matching
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"slices"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
taskqueuepb "go.temporal.io/api/taskqueue/v1"
|
|
"go.temporal.io/server/api/matchingservice/v1"
|
|
persistencespb "go.temporal.io/server/api/persistence/v1"
|
|
taskqueuespb "go.temporal.io/server/api/taskqueue/v1"
|
|
"go.temporal.io/server/common"
|
|
"go.temporal.io/server/common/backoff"
|
|
"go.temporal.io/server/common/future"
|
|
"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/softassert"
|
|
"google.golang.org/protobuf/types/known/durationpb"
|
|
)
|
|
|
|
// this retry policy is currently only used for matching persistence operations
|
|
// that, if failed, the entire task queue needs to be reloaded
|
|
// TODO(pri): old matcher cleanup: move here
|
|
// var persistenceOperationRetryPolicy = backoff.NewExponentialRetryPolicy(50 * time.Millisecond).
|
|
// WithMaximumInterval(1 * time.Second).
|
|
// WithExpirationInterval(30 * time.Second)
|
|
|
|
type (
|
|
// backlogManager manages the backlog and persistence of a physical task queue
|
|
// TODO(pri): old matcher cleanup: move here
|
|
// backlogManager interface {
|
|
// Start()
|
|
// Stop()
|
|
// WaitUntilInitialized(context.Context) error
|
|
// SpoolTask(taskInfo *persistencespb.TaskInfo) error
|
|
// BacklogCountHint() int64
|
|
// BacklogStatus() *taskqueuepb.TaskQueueStatus
|
|
// TotalApproximateBacklogCount() int64
|
|
// BacklogHeadAge() time.Duration
|
|
// }
|
|
|
|
priBacklogManagerImpl struct {
|
|
pqMgr physicalTaskQueueManager
|
|
config *taskQueueConfig
|
|
tqCtx context.Context
|
|
isDraining bool
|
|
db *taskQueueDB
|
|
taskWriter *priTaskWriter
|
|
|
|
subqueueLock sync.Mutex
|
|
subqueues []*priTaskReader // subqueue index -> fairTaskReader
|
|
subqueuesByPriority map[priorityKey]subqueueIndex
|
|
priorityBySubqueue map[subqueueIndex]priorityKey
|
|
|
|
logger log.Logger
|
|
throttledLogger log.ThrottledLogger
|
|
matchingClient matchingservice.MatchingServiceClient
|
|
metricsHandler metrics.Handler
|
|
initializedError *future.FutureImpl[struct{}]
|
|
// skipFinalUpdate controls behavior on Stop: if it's false, we try to write one final
|
|
// update before unloading
|
|
skipFinalUpdate atomic.Bool
|
|
}
|
|
|
|
priorityKey int32
|
|
)
|
|
|
|
var _ backlogManager = (*priBacklogManagerImpl)(nil)
|
|
|
|
func newPriBacklogManager(
|
|
tqCtx context.Context,
|
|
pqMgr physicalTaskQueueManager,
|
|
config *taskQueueConfig,
|
|
taskManager persistence.TaskManager,
|
|
logger log.Logger,
|
|
throttledLogger log.ThrottledLogger,
|
|
matchingClient matchingservice.MatchingServiceClient,
|
|
metricsHandler metrics.Handler,
|
|
isDraining bool,
|
|
) *priBacklogManagerImpl {
|
|
bmg := &priBacklogManagerImpl{
|
|
pqMgr: pqMgr,
|
|
config: config,
|
|
tqCtx: tqCtx,
|
|
isDraining: isDraining,
|
|
db: newTaskQueueDB(config, taskManager, pqMgr.QueueKey(), logger, metricsHandler, isDraining),
|
|
subqueuesByPriority: make(map[priorityKey]subqueueIndex),
|
|
priorityBySubqueue: make(map[subqueueIndex]priorityKey),
|
|
matchingClient: matchingClient,
|
|
metricsHandler: metricsHandler,
|
|
logger: logger,
|
|
throttledLogger: throttledLogger,
|
|
initializedError: future.NewFuture[struct{}](),
|
|
}
|
|
bmg.taskWriter = newPriTaskWriter(bmg)
|
|
return bmg
|
|
}
|
|
|
|
// signalIfFatal calls UnloadFromPartitionManager of the physicalTaskQueueManager
|
|
// if and only if the supplied error represents a fatal condition, e.g. the existence
|
|
// of a newer lease by another backlogManager. Returns true if the unload signal
|
|
// is emitted, false otherwise.
|
|
func (c *priBacklogManagerImpl) signalIfFatal(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
var condfail *persistence.ConditionFailedError
|
|
if errors.As(err, &condfail) {
|
|
c.metricsHandler.Counter(metrics.ConditionFailedErrorPerTaskQueueCounter.Name()).Record(1)
|
|
c.skipFinalUpdate.Store(true)
|
|
c.pqMgr.UnloadFromPartitionManager(unloadCauseConflict)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *priBacklogManagerImpl) Start() {
|
|
c.taskWriter.Start()
|
|
}
|
|
|
|
func (c *priBacklogManagerImpl) Stop() {
|
|
// Maybe try to write one final update of ack level. Skip the update if we never
|
|
// initialized. Also skip if we're stopping due to lost ownership (the update will
|
|
// fail in that case). Ignore any errors. Don't bother with GC, the next reload will
|
|
// handle that.
|
|
if !c.initializedError.Ready() || c.skipFinalUpdate.Load() {
|
|
return
|
|
}
|
|
|
|
c.subqueueLock.Lock()
|
|
for i, r := range c.subqueues {
|
|
_, ackLevel := r.getLevels()
|
|
// oldestTime can be time.Time{} here since countDelta is 0
|
|
c.db.updateAckLevelAndBacklogStats(subqueueIndex(i), ackLevel, 0, time.Time{})
|
|
}
|
|
c.subqueueLock.Unlock()
|
|
|
|
ctx, cancel := context.WithTimeout(c.tqCtx, ioTimeout)
|
|
_ = c.db.SyncState(ctx)
|
|
cancel()
|
|
}
|
|
|
|
func (c *priBacklogManagerImpl) initState(state taskQueueState, err error) {
|
|
defer c.initializedError.Set(struct{}{}, err)
|
|
|
|
if err != nil {
|
|
// We can't recover from here without starting over, so unload the whole task queue.
|
|
// Skip final update since we never initialized.
|
|
c.skipFinalUpdate.Store(true)
|
|
c.pqMgr.UnloadFromPartitionManager(unloadCauseInitError)
|
|
return
|
|
}
|
|
|
|
if state.otherHasTasks {
|
|
c.pqMgr.SetupDraining()
|
|
}
|
|
|
|
c.subqueueLock.Lock()
|
|
defer c.subqueueLock.Unlock()
|
|
|
|
c.loadSubqueuesLocked(state.subqueues)
|
|
go c.periodicSync()
|
|
}
|
|
|
|
func (c *priBacklogManagerImpl) WaitUntilInitialized(ctx context.Context) error {
|
|
_, err := c.initializedError.Get(ctx)
|
|
return err
|
|
}
|
|
|
|
func (c *priBacklogManagerImpl) loadSubqueuesLocked(subqueues []persistencespb.SubqueueInfo) {
|
|
// TODO(pri): This assumes that subqueues never shrinks, and priority/fairness index of
|
|
// existing subqueues never changes. If we change that, this logic will need to change.
|
|
for i := range subqueues {
|
|
if i >= len(c.subqueues) {
|
|
r := newPriTaskReader(c, subqueueIndex(i), subqueues[i].AckLevel)
|
|
r.Start()
|
|
c.subqueues = append(c.subqueues, r)
|
|
}
|
|
c.subqueuesByPriority[priorityKey(subqueues[i].Key.Priority)] = subqueueIndex(i)
|
|
c.priorityBySubqueue[subqueueIndex(i)] = priorityKey(subqueues[i].Key.Priority)
|
|
}
|
|
}
|
|
|
|
func (c *priBacklogManagerImpl) getSubqueueForPriority(priority priorityKey) subqueueIndex {
|
|
priority = c.config.clipPriority(priority)
|
|
|
|
c.subqueueLock.Lock()
|
|
defer c.subqueueLock.Unlock()
|
|
|
|
if i, ok := c.subqueuesByPriority[priority]; ok {
|
|
return i
|
|
}
|
|
|
|
// We need to allocate a new subqueue. Note this is doing io under subqueueLock,
|
|
// but we want to serialize these updates.
|
|
// TODO(pri): maybe we can improve that
|
|
subqueues, err := c.db.AllocateSubqueue(c.tqCtx, &persistencespb.SubqueueKey{
|
|
Priority: int32(priority),
|
|
})
|
|
if err != nil {
|
|
c.signalIfFatal(err)
|
|
// If we failed to write the metadata update, just use subqueueZero.
|
|
// If err was a fatal error (most likely case), the subsequent call to SpoolTask will fail.
|
|
return subqueueZero
|
|
}
|
|
|
|
c.loadSubqueuesLocked(subqueues)
|
|
|
|
// After AllocateSubqueue added a subqueue for this priority, and we merged the result into
|
|
// our state with loadSubqueuesLocked, this lookup should now find a subqueue.
|
|
if i, ok := c.subqueuesByPriority[priority]; ok {
|
|
return i
|
|
}
|
|
|
|
// But if something went wrong, return zero.
|
|
return subqueueZero
|
|
}
|
|
|
|
func (c *priBacklogManagerImpl) periodicSync() {
|
|
for {
|
|
select {
|
|
case <-c.tqCtx.Done():
|
|
return
|
|
case <-time.After(c.config.UpdateAckInterval()):
|
|
ctx, cancel := context.WithTimeout(c.tqCtx, ioTimeout)
|
|
err := c.db.SyncState(ctx)
|
|
cancel()
|
|
c.signalIfFatal(err)
|
|
|
|
if c.hasFinishedDraining() {
|
|
c.pqMgr.FinishedDraining()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *priBacklogManagerImpl) SpoolTask(taskInfo *persistencespb.TaskInfo) error {
|
|
subqueue := c.getSubqueueForPriority(priorityKey(taskInfo.Priority.GetPriorityKey()))
|
|
err := c.taskWriter.appendTask(subqueue, taskInfo)
|
|
c.signalIfFatal(err)
|
|
return err
|
|
}
|
|
|
|
func (c *priBacklogManagerImpl) signalReaders(resp createTasksResponse) {
|
|
c.subqueueLock.Lock()
|
|
subqueues := slices.Clone(c.subqueues)
|
|
c.subqueueLock.Unlock()
|
|
|
|
for subqueue, subqueueResp := range resp.bySubqueue {
|
|
subqueues[subqueue].signalNewTasks(subqueueResp)
|
|
}
|
|
}
|
|
|
|
func (c *priBacklogManagerImpl) addSpooledTask(task *internalTask) error {
|
|
return c.pqMgr.AddSpooledTask(task)
|
|
}
|
|
|
|
func (c *priBacklogManagerImpl) BacklogCountHint() (total int64) {
|
|
c.subqueueLock.Lock()
|
|
defer c.subqueueLock.Unlock()
|
|
for _, r := range c.subqueues {
|
|
total += int64(r.getLoadedTasks())
|
|
}
|
|
return
|
|
}
|
|
|
|
func (c *priBacklogManagerImpl) BacklogStatsByPriority() map[int32]*taskqueuepb.TaskQueueStats {
|
|
c.subqueueLock.Lock()
|
|
defer c.subqueueLock.Unlock()
|
|
|
|
result := make(map[int32]*taskqueuepb.TaskQueueStats)
|
|
backlogCounts := c.db.getApproximateBacklogCountsBySubqueue()
|
|
for subqueueIdx, priorityKey := range c.priorityBySubqueue {
|
|
pk := int32(priorityKey)
|
|
|
|
// Note that there could be more than one subqueue for the same priority.
|
|
if _, ok := result[pk]; !ok {
|
|
result[pk] = &taskqueuepb.TaskQueueStats{
|
|
// TODO(pri): returning 0 to match existing behavior, but maybe emptyBacklogAge would
|
|
// be more appropriate in the future.
|
|
ApproximateBacklogAge: durationpb.New(0),
|
|
}
|
|
}
|
|
|
|
// Add backlog counts together across all subqueues for the same priority.
|
|
result[pk].ApproximateBacklogCount += backlogCounts[subqueueIdx]
|
|
|
|
// Find greatest backlog age for across all subqueues for the same priority.
|
|
oldestBacklogTime := c.subqueues[subqueueIdx].getOldestBacklogTime()
|
|
if !oldestBacklogTime.IsZero() {
|
|
oldestBacklogAge := time.Since(oldestBacklogTime)
|
|
if oldestBacklogAge > result[pk].ApproximateBacklogAge.AsDuration() {
|
|
result[pk].ApproximateBacklogAge = durationpb.New(oldestBacklogAge)
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (c *priBacklogManagerImpl) BacklogStatus() *taskqueuepb.TaskQueueStatus {
|
|
c.subqueueLock.Lock()
|
|
defer c.subqueueLock.Unlock()
|
|
|
|
// TODO(pri): needs more work for subqueues, for now just return read/ack level for subqueue 0
|
|
var readLevel, ackLevel int64
|
|
if len(c.subqueues) > 0 {
|
|
readLevel, ackLevel = c.subqueues[subqueueZero].getLevels()
|
|
}
|
|
|
|
taskIDBlock := rangeIDToTaskIDBlock(c.db.RangeID(), c.config.RangeSize)
|
|
return &taskqueuepb.TaskQueueStatus{
|
|
ReadLevel: readLevel,
|
|
AckLevel: ackLevel,
|
|
// use getTotalApproximateBacklogCount instead of BacklogCountHint since it's more accurate
|
|
BacklogCountHint: c.db.getTotalApproximateBacklogCount(),
|
|
TaskIdBlock: &taskqueuepb.TaskIdBlock{
|
|
StartId: taskIDBlock.start,
|
|
EndId: taskIDBlock.end,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (c *priBacklogManagerImpl) InternalStatus() []*taskqueuespb.InternalTaskQueueStatus {
|
|
currentTaskIDBlock := c.taskWriter.getCurrentTaskIDBlock()
|
|
|
|
c.subqueueLock.Lock()
|
|
defer c.subqueueLock.Unlock()
|
|
|
|
status := make([]*taskqueuespb.InternalTaskQueueStatus, len(c.subqueues))
|
|
backlogCountsBySubqueue := c.db.getApproximateBacklogCountsBySubqueue()
|
|
for i, r := range c.subqueues {
|
|
readLevel, ackLevel := r.getLevels()
|
|
status[i] = &taskqueuespb.InternalTaskQueueStatus{
|
|
ReadLevel: readLevel,
|
|
AckLevel: ackLevel,
|
|
TaskIdBlock: &taskqueuepb.TaskIdBlock{
|
|
StartId: currentTaskIDBlock.start,
|
|
EndId: currentTaskIDBlock.end,
|
|
},
|
|
LoadedTasks: int64(r.getLoadedTasks()),
|
|
MaxReadLevel: c.db.GetMaxReadLevel(subqueueIndex(i)),
|
|
ApproximateBacklogCount: backlogCountsBySubqueue[i],
|
|
BacklogDrained: r.isDrained(),
|
|
}
|
|
}
|
|
return status
|
|
}
|
|
|
|
func (c *priBacklogManagerImpl) respoolTaskAfterError(task *persistencespb.TaskInfo) error {
|
|
// We cannot just remove it from persistence because then it will be lost.
|
|
// We handle this by writing the task back to persistence with a higher taskID.
|
|
// This will allow subsequent tasks to make progress, and hopefully by the time this task is picked-up
|
|
// again the underlying reason for failing to start will be resolved.
|
|
// Note the task may get written to a different subqueue than it came from.
|
|
metrics.TaskRewrites.With(c.metricsHandler).Record(1)
|
|
err := backoff.ThrottleRetryContext(c.tqCtx, func(context.Context) error {
|
|
return c.SpoolTask(task)
|
|
}, persistenceOperationRetryPolicy, common.IsPersistenceTransientError)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
// OK, we also failed to write to persistence.
|
|
// This should only happen in very extreme cases where persistence is completely down.
|
|
// We still can't lose the old task, so we just unload the entire task queue.
|
|
// We haven't advanced the ack level past this task, so when the task queue reloads,
|
|
// it will see this task again.
|
|
c.logger.Error("Persistent store operation failure",
|
|
tag.StoreOperationStopTaskQueue,
|
|
tag.Error(err),
|
|
tag.WorkflowTaskQueueName(c.queueKey().PersistenceName()),
|
|
tag.WorkflowTaskQueueType(c.queueKey().TaskType()))
|
|
// Skip final update since persistence is having problems.
|
|
c.skipFinalUpdate.Store(true)
|
|
c.pqMgr.UnloadFromPartitionManager(unloadCauseOtherError)
|
|
return err
|
|
}
|
|
|
|
// TODO(pri): old matcher cleanup: move here
|
|
// func rangeIDToTaskIDBlock(rangeID int64, rangeSize int64) taskIDBlock {
|
|
// return taskIDBlock{
|
|
// start: (rangeID-1)*rangeSize + 1,
|
|
// end: rangeID * rangeSize,
|
|
// }
|
|
// }
|
|
|
|
func (c *priBacklogManagerImpl) queueKey() *PhysicalTaskQueueKey {
|
|
return c.pqMgr.QueueKey()
|
|
}
|
|
|
|
func (c *priBacklogManagerImpl) getDB() *taskQueueDB {
|
|
return c.db
|
|
}
|
|
|
|
// hasFinishedDraining returns true if this is a draining backlog manager and all tasks have
|
|
// been fully drained (read and acked).
|
|
func (c *priBacklogManagerImpl) hasFinishedDraining() bool {
|
|
if !c.isDraining {
|
|
return false
|
|
}
|
|
|
|
c.subqueueLock.Lock()
|
|
defer c.subqueueLock.Unlock()
|
|
|
|
for _, r := range c.subqueues {
|
|
if !r.isDrained() {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// FinalGC does a final gc pass on all subqueues.
|
|
func (c *priBacklogManagerImpl) FinalGC() {
|
|
if !softassert.That(c.logger, c.isDraining, "FinalGC called on non-draining backlog manager") {
|
|
return
|
|
}
|
|
c.subqueueLock.Lock()
|
|
subqueues := slices.Clone(c.subqueues)
|
|
c.subqueueLock.Unlock()
|
|
|
|
for _, r := range subqueues {
|
|
r.finalGC()
|
|
}
|
|
}
|
|
|
|
func (c *priBacklogManagerImpl) setPriority(task *internalTask) {
|
|
c.config.setDefaultPriority(task)
|
|
if c.isDraining {
|
|
// draining goes before active backlog so we're guaranteed to finish migration
|
|
task.effectivePriority -= effectivePriorityFactor * maxPriorityLevels
|
|
}
|
|
}
|