Files
tasks/chasm/lib/scheduler/generator_tasks_test.go
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

186 lines
6.6 KiB
Go

package scheduler_test
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/require"
schedulepb "go.temporal.io/api/schedule/v1"
"go.temporal.io/api/workflowservice/v1"
"go.temporal.io/server/chasm"
"go.temporal.io/server/chasm/lib/scheduler"
"go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1"
"go.temporal.io/server/common/metrics"
queueerrors "go.temporal.io/server/service/history/queues/errors"
"go.temporal.io/server/service/history/tasks"
legacyscheduler "go.temporal.io/server/service/worker/scheduler"
"go.uber.org/mock/gomock"
"google.golang.org/protobuf/types/known/timestamppb"
)
func newGeneratorHandler(env *testEnv) *scheduler.GeneratorTaskHandler {
return scheduler.NewGeneratorTaskHandler(scheduler.GeneratorTaskHandlerOptions{
Config: defaultConfig(),
MetricsHandler: metrics.NoopMetricsHandler,
BaseLogger: env.Logger,
SpecProcessor: env.SpecProcessor,
SpecBuilder: legacyscheduler.NewSpecBuilder(),
})
}
func TestGeneratorTask_Execute_ProcessTimeRangeFails(t *testing.T) {
// Create a custom mock spec processor that fails on ProcessTimeRange.
ctrl := gomock.NewController(t)
specProcessor := scheduler.NewMockSpecProcessor(ctrl)
now := time.Now()
// First call during newTestEnv's CloseTransaction should succeed.
specProcessor.EXPECT().ProcessTimeRange(
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(),
).Return(&scheduler.ProcessedTimeRange{
NextWakeupTime: now.Add(defaultInterval),
LastActionTime: now,
}, nil).Times(1)
// Second call during test should fail.
specProcessor.EXPECT().ProcessTimeRange(
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(),
).Return(nil, errors.New("processTimeRange bug"))
specProcessor.EXPECT().NextTime(gomock.Any(), gomock.Any()).Return(legacyscheduler.GetNextTimeResult{
Next: now.Add(defaultInterval),
Nominal: now.Add(defaultInterval),
}, nil).AnyTimes()
env := newTestEnv(t, withSpecProcessor(specProcessor))
handler := newGeneratorHandler(env)
ctx := env.MutableContext()
generator := env.Scheduler.Generator.Get(ctx)
// If ProcessTimeRange fails, we should fail the task as an internal error.
err := handler.Execute(ctx, generator, chasm.TaskAttributes{}, &schedulerpb.GeneratorTask{})
var target *queueerrors.UnprocessableTaskError
require.ErrorAs(t, err, &target)
require.Equal(t, "failed to process a time range: processTimeRange bug", target.Message)
}
func TestGeneratorTask_ExecuteBufferTask_Basic(t *testing.T) {
env := newTestEnv(t)
handler := newGeneratorHandler(env)
ctx := env.MutableContext()
sched := env.Scheduler
generator := sched.Generator.Get(ctx)
// Move high water mark back in time (Generator always compares high water mark
// against system time) to generate buffered actions.
highWatermark := ctx.Now(generator).UTC().Add(-defaultInterval * 5)
generator.LastProcessedTime = timestamppb.New(highWatermark)
// Execute the generate task.
err := handler.Execute(ctx, generator, chasm.TaskAttributes{}, &schedulerpb.GeneratorTask{})
require.NoError(t, err)
// We expect 5 buffered starts.
invoker := sched.Invoker.Get(ctx)
require.Len(t, invoker.BufferedStarts, 5)
// Validate RequestId -> WorkflowId mapping.
for _, start := range invoker.BufferedStarts {
require.Equal(t, start.WorkflowId, invoker.RunningWorkflowID(start.RequestId))
}
// Generator's high water mark should have advanced.
newHighWatermark := generator.LastProcessedTime.AsTime()
require.True(t, newHighWatermark.After(highWatermark))
// Ensure we scheduled a physical side-effect task on the tree at immediate time.
// The InvokerExecuteTask is a side-effect task that starts workflows.
// The InvokerProcessBufferTask (pure) executes inline during CloseTransaction.
require.NoError(t, env.CloseTransaction())
require.True(t, env.HasTask(&tasks.ChasmTask{}, chasm.TaskScheduledTimeImmediate))
}
func TestGeneratorTask_UpdateFutureActionTimes_UnlimitedActions(t *testing.T) {
env := newTestEnv(t)
handler := newGeneratorHandler(env)
ctx := env.MutableContext()
generator := env.Scheduler.Generator.Get(ctx)
err := handler.Execute(ctx, generator, chasm.TaskAttributes{}, &schedulerpb.GeneratorTask{})
require.NoError(t, err)
require.NotEmpty(t, generator.FutureActionTimes)
require.Len(t, generator.FutureActionTimes, 10)
}
func TestGeneratorTask_UpdateFutureActionTimes_LimitedActions(t *testing.T) {
env := newTestEnv(t)
handler := newGeneratorHandler(env)
ctx := env.MutableContext()
sched := env.Scheduler
generator := sched.Generator.Get(ctx)
sched.Schedule.State.LimitedActions = true
sched.Schedule.State.RemainingActions = 2
err := handler.Execute(ctx, generator, chasm.TaskAttributes{}, &schedulerpb.GeneratorTask{})
require.NoError(t, err)
require.Len(t, generator.FutureActionTimes, 2)
}
func TestGeneratorTask_UpdateFutureActionTimes_SkipsBeforeUpdateTime(t *testing.T) {
env := newTestEnv(t)
handler := newGeneratorHandler(env)
ctx := env.MutableContext()
sched := env.Scheduler
generator := sched.Generator.Get(ctx)
// UpdateTime acts as a floor - action times at or before it are skipped.
baseTime := ctx.Now(generator).UTC()
updateTime := baseTime.Add(defaultInterval / 2)
sched.Info.UpdateTime = timestamppb.New(updateTime)
err := handler.Execute(ctx, generator, chasm.TaskAttributes{}, &schedulerpb.GeneratorTask{})
require.NoError(t, err)
require.NotEmpty(t, generator.FutureActionTimes)
for _, futureTime := range generator.FutureActionTimes {
require.True(t, futureTime.AsTime().After(updateTime))
}
}
func TestUnpause_ResumesProcessing(t *testing.T) {
env := newTestEnv(t)
// Pause the schedule.
env.Scheduler.Schedule.State.Paused = true
require.NoError(t, env.CloseTransaction())
// Clear tasks from setup, then unpause. UpdateTime is captured at T0.
env.NodeBackend.TasksByCategory = nil
ctx := env.MutableContext()
_, err := env.Scheduler.Patch(ctx, &schedulerpb.PatchScheduleRequest{
FrontendRequest: &workflowservice.PatchScheduleRequest{
Patch: &schedulepb.SchedulePatch{Unpause: "resuming"},
},
})
require.NoError(t, err)
// Advance time before closing so the generator has actions to process.
env.TimeSource.Update(env.TimeSource.Now().Add(defaultInterval * 3))
require.NoError(t, env.CloseTransaction())
// With the fix, Patch kicks an immediate generator task. During CloseTransaction
// it processes the elapsed interval, buffers starts, and the invoker schedules
// side-effect tasks to start workflows. Without the fix, nothing runs.
require.True(t, env.HasTask(&tasks.ChasmTask{}, chasm.TaskScheduledTimeImmediate),
"schedule should resume processing after unpause")
}