stride3d/stride · error · InvalidOperationException

Already scheduled, call Unschedule before running this…

Error message

Already scheduled, call Unschedule before running this method

What it means

Scheduler.ScheduleUnsafe registers a SchedulerEntry into a priority queue; each entry may only be in one queue at a time. If newEntry.CurrentQueue is already set, it throws InvalidOperationException telling you to Unschedule first before scheduling again.

Solutions

  1. Call Unschedule on the entry before scheduling it again
  2. Track whether the entry is already scheduled (check CurrentQueue != null) before calling Schedule
  3. Move scheduling logic to one place so an entry is enqueued at most once per frame

Example fix

// before
scheduler.Schedule(entry, priority);
scheduler.Schedule(entry, newPriority); // throws
// after
if (entry.CurrentQueue != null) scheduler.Unschedule(entry);
scheduler.Schedule(entry, newPriority);
Defensive patterns

Strategy: validation

Validate before calling

if (entry.CurrentQueue != null) scheduler.Unschedule(entry);

Prevention

When it happens

Trigger: Calling Schedule (-> ScheduleUnsafe) on the same entry/instance twice without Unschedule; scheduling an entity/micro-thread already queued this frame.

Common situations: Double-scheduling in an update loop (e.g. both on spawn and per-frame); re-scheduling after priority change without unscheduling; racing schedulers enqueueing the same entry.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of stride3d/stride@96fad776d2 (2026-09-14). Data as JSON: /api/errors/bc9e607511c3e7e1. Report an issue: GitHub.

Appendix: source

Thrown at sources/core/Stride.Core.MicroThreading/Scheduler.cs:368

        {
            callbackList.Add(node);
            if (schedulerEntry.CurrentQueue == null)
                Schedule(schedulerEntry, priority, scheduleMode);
        }
    }

    internal void Schedule(SchedulerEntry newEntry, long priority, ScheduleMode scheduleMode)
    {
        lock (bucketsLock)
        {
            ScheduleUnsafe(newEntry, priority, scheduleMode);
        }
    }

    private void ScheduleUnsafe(SchedulerEntry newEntry, long priority, ScheduleMode scheduleMode)
    {
        if (newEntry.CurrentQueue != null)
            throw new InvalidOperationException($"Already scheduled, call {nameof(Unschedule)} before running this method");

        if (newEntry.PreviousQueue is { } previousQueue 
            && previousQueue.Owner == this
            && previousQueue.Priority == priority
            && previousQueue.InBucketPool == false)
        {
            if (previousQueue.Deque.Count == 0)
                emptyBuckets.Remove(previousQueue.Priority);

            newEntry.CurrentQueue = previousQueue;
        }
        // Edge case: this entry has never been scheduled, or its priority changed, or the priority was unused last schedule
        else if (buckets.TryGetValue(priority, out newEntry.CurrentQueue) == false 
                 && emptyBuckets.Remove(priority, out newEntry.CurrentQueue) == false)
        {
            if (bucketPool.TryPop(out newEntry.CurrentQueue))
                newEntry.CurrentQueue.InBucketPool = false;
            else

View on GitHub (pinned to 96fad776d2)