temporalio/temporal · error

Can not invoke method on destroyed queue slice

Error message

Can not invoke method on destroyed queue slice

What it means

SliceImpl is a task-queue slice in the history service; once destroy() runs the slice is removed from the monitor and its trackers are nil, so any further method call is a use-after-free bug. stateSanityCheck panics to fail fast instead of operating on nil iterators/trackers. It guards methods like Scope, SplitByPredicate, CanMergeWithSlice, ShrinkScope, SelectTasks, MoreTasks, TaskStats, Clear.

Source

Thrown at service/history/queues/slice.go:447

	s.iterators = []Iterator{
		NewIterator(s.paginationFnProvider, s.scope.Range),
	}
	s.clear()

	s.monitor.SetSlicePendingTaskCount(s, len(s.pendingExecutables))
}

func (s *SliceImpl) destroy() {
	s.destroyed = true
	s.iterators = nil
	s.executableTracker = nil
	s.monitor.RemoveSlice(s)
}

func (s *SliceImpl) stateSanityCheck() {
	if s.destroyed {
		panic("Can not invoke method on destroyed queue slice")
	}
}

func (s *SliceImpl) newSlice(
	scope Scope,
	iterators []Iterator,
	tracker *executableTracker,
) *SliceImpl {
	slice := &SliceImpl{
		paginationFnProvider: s.paginationFnProvider,
		executableFactory:    s.executableFactory,
		scope:                scope,
		iterators:            iterators,
		executableTracker:    tracker,
		monitor:              s.monitor,
		maxPredicateSizeFn:   s.maxPredicateSizeFn,
		maxPendingKeysFn:     s.maxPendingKeysFn,
		metricsHandler:       s.metricsHandler,

View on GitHub (pinned to bde624efd1)

Solutions

  1. Never retain Slice references across queue operations; re-fetch slices from the queue manager after any split/merge
  2. Check slice ownership: only the owning QueueBase/tracker should call mutating methods
  3. Serialize access — queue slices are not goroutine-safe; use the queue's operation flow
  4. If reproducible, report with the stack trace to temporalio/temporal as an internal invariant violation

Example fix

// before: retaining a slice across a merge
slice := queue.GetSlices()[0]
queue.MergeSlices(...)
slice.ShrinkScope() // panics if slice was destroyed by merge
// after: re-acquire current slices from the queue after mutation
queue.MergeSlices(...)
for _, slice := range queue.GetSlices() { slice.ShrinkScope() }
Defensive patterns

Strategy: type-guard

Type guard

func isUsable(s queues.Slice) bool {
  type alive interface{ MoreTasks() bool }
  defer func() { recover() }()
  _ = s.MoreTasks() // panics if destroyed
  return true
}

Try / catch

// Guard retained references at goroutine boundaries:
func safeCall(s queues.Slice, fn func(queues.Slice)) (err error) {
  defer func() { if r := recover(); r != nil { err = fmt.Errorf("slice destroyed: %v", r) } }()
  fn(s)
  return nil
}

Prevention

When it happens

Trigger: Calling any public Slice method after the slice was destroyed — destroy() is invoked internally by appendMergedSlice when a merged slice's scope becomes empty, or by the queue tracker during re-bucketing; keeping a reference to a slice returned by a queue and calling methods on it after the queue merged/split slices.

Common situations: Holding a *SliceImpl across a queue operation that merges slices; concurrent access where one goroutine merges slices while another still uses an old reference; custom code extending the queue manager that retains slice pointers.

Related errors


AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01). Data as JSON: /api/errors/4763cd1d26ccd9e9. Report an issue: GitHub.