Repository navigation
Add experimental worker.GetSlotUsage #2858
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| package internal | ||
|
|
||
| import "time" | ||
|
|
||
| // SlotUsage is a point-in-time snapshot of a worker's task slot usage, broken down by task kind. | ||
| // Each task kind is read independently, so the snapshot is not atomic across task kinds. | ||
| // | ||
| // NOTE: Experimental | ||
| // | ||
| // Exposed as: [go.temporal.io/sdk/worker.SlotUsage] | ||
| type SlotUsage struct { | ||
| // Workflow is the usage of workflow task slots. | ||
| Workflow SlotKindUsage | ||
| // Activity is the usage of activity task slots. It includes session activity and session | ||
| // creation tasks, if sessions are enabled. A session creation task holds its slot for the life | ||
| // of the session. | ||
| Activity SlotKindUsage | ||
| // LocalActivity is the usage of local activity slots. | ||
| LocalActivity SlotKindUsage | ||
| // Nexus is the usage of Nexus task slots. | ||
| Nexus SlotKindUsage | ||
| } | ||
|
|
||
| // SlotKindUsage is a snapshot of the task slot usage for one task kind. | ||
| // | ||
| // NOTE: Experimental | ||
| // | ||
| // Exposed as: [go.temporal.io/sdk/worker.SlotKindUsage] | ||
| type SlotKindUsage struct { | ||
| // UsedSlots is the number of tasks currently being processed. Slots reserved by pollers that | ||
| // have not received a task are not counted. | ||
| UsedSlots int | ||
| // UsedSlotsLastChanged is when a task last started or finished, or the zero time if no task has | ||
| // started since the worker was created. A task that starts and finishes between two snapshots | ||
| // still changes it. | ||
| UsedSlotsLastChanged time.Time | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potentially worth discussing if we'd prefer this to be exposed as a time delta (i.e. As a public API, absolute time seems more useful than a time delta, so it's what I tentatively went with here. |
||
| } | ||
|
|
||
| func (u SlotKindUsage) add(other SlotKindUsage) SlotKindUsage { | ||
| lastChanged := u.UsedSlotsLastChanged | ||
| if other.UsedSlotsLastChanged.After(lastChanged) { | ||
| lastChanged = other.UsedSlotsLastChanged | ||
| } | ||
| return SlotKindUsage{ | ||
| UsedSlots: u.UsedSlots + other.UsedSlots, | ||
| UsedSlotsLastChanged: lastChanged, | ||
| } | ||
| } | ||
|
|
||
| // SlotUsage returns a snapshot of the worker's task slot usage. It is safe to call concurrently | ||
| // with any other worker method, including before Start and after Stop. Task kinds whose workers | ||
| // have not been created yet, such as Nexus before Start, report zero usage. | ||
| func (aw *AggregatedWorker) SlotUsage() SlotUsage { | ||
| var u SlotUsage | ||
| if aw.workflowWorker != nil { | ||
| u.Workflow = aw.workflowWorker.worker.slotSupplier.usage() | ||
| u.LocalActivity = aw.workflowWorker.localActivityWorker.slotSupplier.usage() | ||
| } | ||
| if aw.activityWorker != nil { | ||
| u.Activity = u.Activity.add(aw.activityWorker.worker.slotSupplier.usage()) | ||
| } | ||
| if aw.sessionWorker != nil { | ||
| u.Activity = u.Activity.add(aw.sessionWorker.activityWorker.worker.slotSupplier.usage()) | ||
| u.Activity = u.Activity.add(aw.sessionWorker.creationWorker.worker.slotSupplier.usage()) | ||
| } | ||
| if nexusSlots := aw.nexusSlotSupplier.Load(); nexusSlots != nil { | ||
| u.Nexus = nexusSlots.usage() | ||
| } | ||
| return u | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| package internal | ||
|
|
||
| import ( | ||
| "testing" | ||
| "testing/synctest" | ||
| "time" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
| "go.temporal.io/sdk/internal/common/metrics" | ||
| ilog "go.temporal.io/sdk/internal/log" | ||
| ) | ||
|
|
||
| func newTestTrackingSlotSupplier(t *testing.T, numSlots int) *trackingSlotSupplier { | ||
| t.Helper() | ||
| inner, err := NewFixedSizeSlotSupplier(numSlots) | ||
| require.NoError(t, err) | ||
| return newTrackingSlotSupplier(inner, trackingSlotSupplierOptions{ | ||
| logger: ilog.NewNopLogger(), | ||
| metricsHandler: metrics.NopHandler, | ||
| }) | ||
| } | ||
|
|
||
| func reserveTestSlot(t *testing.T, tss *trackingSlotSupplier) *SlotPermit { | ||
| t.Helper() | ||
| permit, err := tss.ReserveSlot(t.Context(), &slotReservationData{taskQueue: "test-task-queue"}) | ||
| require.NoError(t, err) | ||
| return permit | ||
| } | ||
|
|
||
| func TestTrackingSlotSupplierUsage(t *testing.T) { | ||
| synctest.Test(t, func(t *testing.T) { | ||
| tss := newTestTrackingSlotSupplier(t, 3) | ||
|
|
||
| // Reserved slots, like those held by idle pollers, are not in use. | ||
| used := reserveTestSlot(t, tss) | ||
| unused := reserveTestSlot(t, tss) | ||
| require.Equal(t, SlotKindUsage{}, tss.usage()) | ||
|
|
||
| time.Sleep(time.Second) | ||
| startedAt := time.Now() | ||
| tss.MarkSlotUsed(used) | ||
| require.Equal(t, SlotKindUsage{UsedSlots: 1, UsedSlotsLastChanged: startedAt}, tss.usage()) | ||
|
|
||
| // Empty polls are released without being marked used, and must not count as a change. | ||
| time.Sleep(time.Second) | ||
| tss.ReleaseSlot(unused, SlotReleaseReasonTaskProcessed) | ||
| require.Equal(t, SlotKindUsage{UsedSlots: 1, UsedSlotsLastChanged: startedAt}, tss.usage()) | ||
|
|
||
| time.Sleep(time.Second) | ||
| finishedAt := time.Now() | ||
| tss.ReleaseSlot(used, SlotReleaseReasonTaskProcessed) | ||
| require.Equal(t, SlotKindUsage{UsedSlots: 0, UsedSlotsLastChanged: finishedAt}, tss.usage()) | ||
| }) | ||
| } | ||
|
|
||
| func TestAggregatedWorkerSlotUsage(t *testing.T) { | ||
| synctest.Test(t, func(t *testing.T) { | ||
| require.Equal(t, SlotUsage{}, (&AggregatedWorker{}).SlotUsage()) | ||
|
|
||
| workflowSlots := newTestTrackingSlotSupplier(t, 2) | ||
| localActivitySlots := newTestTrackingSlotSupplier(t, 2) | ||
| activitySlots := newTestTrackingSlotSupplier(t, 2) | ||
| sessionActivitySlots := newTestTrackingSlotSupplier(t, 2) | ||
| sessionCreationSlots := newTestTrackingSlotSupplier(t, 2) | ||
| nexusSlots := newTestTrackingSlotSupplier(t, 2) | ||
|
|
||
| aw := &AggregatedWorker{ | ||
| workflowWorker: &workflowWorker{ | ||
| worker: &baseWorker{slotSupplier: workflowSlots}, | ||
| localActivityWorker: &baseWorker{slotSupplier: localActivitySlots}, | ||
| }, | ||
| activityWorker: &activityWorker{worker: &baseWorker{slotSupplier: activitySlots}}, | ||
| sessionWorker: &sessionWorker{ | ||
| activityWorker: &activityWorker{worker: &baseWorker{slotSupplier: sessionActivitySlots}}, | ||
| creationWorker: &activityWorker{worker: &baseWorker{slotSupplier: sessionCreationSlots}}, | ||
| }, | ||
| } | ||
|
|
||
| start := time.Now() | ||
| for _, tss := range []*trackingSlotSupplier{ | ||
| workflowSlots, localActivitySlots, activitySlots, sessionActivitySlots, sessionCreationSlots, nexusSlots, | ||
| } { | ||
| tss.MarkSlotUsed(reserveTestSlot(t, tss)) | ||
| } | ||
| // The latest change across the activity workers wins. | ||
| time.Sleep(time.Second) | ||
| finished := reserveTestSlot(t, activitySlots) | ||
| activitySlots.MarkSlotUsed(finished) | ||
| time.Sleep(time.Second) | ||
| activitySlots.ReleaseSlot(finished, SlotReleaseReasonTaskProcessed) | ||
|
|
||
| // The Nexus worker isn't created until start. | ||
| require.Equal(t, SlotUsage{ | ||
| Workflow: SlotKindUsage{UsedSlots: 1, UsedSlotsLastChanged: start}, | ||
| Activity: SlotKindUsage{UsedSlots: 3, UsedSlotsLastChanged: start.Add(2 * time.Second)}, | ||
| LocalActivity: SlotKindUsage{UsedSlots: 1, UsedSlotsLastChanged: start}, | ||
| }, aw.SlotUsage()) | ||
|
|
||
| aw.nexusSlotSupplier.Store(nexusSlots) | ||
| require.Equal(t, SlotKindUsage{UsedSlots: 1, UsedSlotsLastChanged: start}, aw.SlotUsage().Nexus) | ||
| }) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package worker | ||
|
|
||
| import ( | ||
| "fmt" | ||
|
|
||
| "go.temporal.io/sdk/internal" | ||
| ) | ||
|
|
||
| // SlotUsage is a point-in-time snapshot of a worker's task slot usage, broken down by task kind. | ||
| // Each task kind is read independently, so the snapshot is not atomic across task kinds. | ||
| // | ||
| // NOTE: Experimental | ||
| type SlotUsage = internal.SlotUsage | ||
|
|
||
| // SlotKindUsage is a snapshot of the task slot usage for one task kind. | ||
| // | ||
| // NOTE: Experimental | ||
| type SlotKindUsage = internal.SlotKindUsage | ||
|
|
||
| // SlotUsageProvider is implemented by workers that can report their task slot usage. Workers | ||
| // created by [New] implement it. A type that wraps a worker can implement it by forwarding to the | ||
| // wrapped worker. | ||
| // | ||
| // NOTE: Experimental | ||
| type SlotUsageProvider interface { | ||
| // SlotUsage returns a snapshot of the worker's task slot usage. It must be safe to call | ||
| // concurrently with any other worker method. | ||
| SlotUsage() SlotUsage | ||
| } | ||
|
|
||
| var _ SlotUsageProvider = (*internal.AggregatedWorker)(nil) | ||
|
|
||
| // GetSlotUsage returns a snapshot of w's task slot usage. It returns an error if w does not | ||
| // implement [SlotUsageProvider]. | ||
| // | ||
| // NOTE: Experimental | ||
| func GetSlotUsage(w Worker) (SlotUsage, error) { | ||
| provider, ok := w.(SlotUsageProvider) | ||
| if !ok { | ||
| return SlotUsage{}, fmt.Errorf( | ||
| "worker of type %T does not implement worker.SlotUsageProvider; use a worker created by "+ | ||
| "worker.New", w) | ||
| } | ||
| return provider.SlotUsage(), nil | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This API's a bit more general than just a simple combined slot counter, but pretty free given the internal tracking code, and I think maybe more generally useful as a public API.