Skip to content
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

Bug: Actor with system ID '...' already exists. (v5) #4364

Closed
rlaffers opened this issue Oct 17, 2023 · 1 comment · Fixed by #4405
Closed

Bug: Actor with system ID '...' already exists. (v5) #4364

rlaffers opened this issue Oct 17, 2023 · 1 comment · Fixed by #4405
Assignees
Labels

Comments

@rlaffers
Copy link
Contributor

Description

Invoked actors are not cleared from the system upon re-entry.

const machine = createMachine({
  initial: 'Listening',
  states: {
    Listening: {
      invoke: {
        systemId: 'listener', // <-- This will clash with the subsequently invoked listener
        src: fromCallback(() => {
          console.log('✅ invoked the listener')
          return () => {
            console.log('⛔ clearing the listener')
          }
        }),
      },
    },
  },
  on: {
    RESTART: {
      // reenters Listening, tries to invoke the listener, but:
      // 🪲 Actor with system ID 'listener' already exists.
      target: '.Listening',
    },
  },
})

Expected result

After receiving the RESTART event, the first listener is cleared first (its unsubscribe function should run), its systemId should be removed from actor.system, and then a fresh listener should be invoked.

Actual result

Error upon receiving the RESTART event:

Error: Actor with system ID 'listener' already exists.

Reproduction

https://codesandbox.io/s/optimistic-bush-7lh38j?file=/src/index.js

Additional context

This is an issue with xstate v5.
Occurs with: [email protected]

@rlaffers rlaffers added the bug label Oct 17, 2023
@davidkpiano davidkpiano self-assigned this Oct 17, 2023
@Andarist
Copy link
Member

The issue is caused by the fact that we defer stopping the previous child:

function executeStop(
actorContext: AnyActorContext,
actorRef: ActorRef<any, any> | undefined
) {
if (!actorRef) {
return;
}
// this allows us to prevent an actor from being started if it gets stopped within the same macrostep
// this can happen, for example, when the invoking state is being exited immediately by an always transition
if (actorRef.status !== ActorStatus.Running) {
actorContext.stopChild(actorRef);
return;
}
// stopping a child enqueues a stop event in the child actor's mailbox
// we need for all of the already enqueued events to be processed before we stop the child
// the parent itself might want to send some events to a child (for example from exit actions on the invoking state)
// and we don't want to ignore those events
actorContext.defer(() => {
actorContext.stopChild(actorRef);
});
}

but we register a new one in the system asap, when it's created. An actor instantiation is not deferred (and can't be deferred).

So the fix for this is to unregister the old actor from the system eagerly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants