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

Document stopping actors. #1434

Closed

Conversation

nathanhammond
Copy link

This PR includes two pieces of information that @Andarist explained to me in issue #1403, actions.pure() and actions.stop().

However, after attempting to document (and reading the code in interpreter.ts for 4.X), I have additional questions that imply to me that there may be additional unaddressed issues here.

  • Just sending xstate.stop to a StateNode does not seem to be complete to me as it doesn't seem like it is recursive. The receiver of xstate.stop may also be a machine with children which would also need to be stopped. The event does not appear to be propagated. Code.
  • It seems like you should still call .stop() on the interpreter as well. Code.

Assuming that my hypotheses are correct, maybe Interpreter#stop should also send (from leaf to root?) xstate.stop?

@changeset-bot
Copy link

changeset-bot bot commented Sep 3, 2020

💥 No Changeset

Latest commit: ce786dd

Merging this PR will not cause any packages to be released. If these changes should not cause updates to packages in this repo, this is fine 🙂

If these changes should be published to npm, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@codesandbox-ci
Copy link

codesandbox-ci bot commented Sep 3, 2020

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit ce786dd:

Sandbox Source
XState Example Template Configuration
XState React Template Configuration

Comment on lines +366 to +367
context.childMachine.stop();
return actions.stop(context.childMachine.id);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I included both versions of stop here because I believe that they are both required.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that context.childMachine.stop(); should not be needed, but I would have to recheck this. Have you encountered any problems with actions.stop(context.childMachine.id); alone?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not experienced issues, but my WIP machine is not complicated enough to trigger the thing I believe could be an issue. (And I wanted to get a first pass of the documentation together and ask before trying to create a repro of a hypothetical issue.)

My hypothesis comes out of scanning the implementations of these to get a bit more context before filing this PR. The test case that this would need is:

  • Parent machine.
  • Spawns a child machine (so that it is an Interpreter, not an actor).
  • Child machine starts an activity (and stays in that state).
  • Child machine's interpreter reference from parent machine's context has a listener attached to it.
  • Test sending xstate.stop, calling childMachine.stop(), or doing both of those things.

Confirm:

  • No activities still running. (I think this is what xstate.stop does.)
  • No listeners still bound. (I think this is what Interpreter#stop does.)
  • Make sure that it isn't just getting GC'd by way of clobbering all references.

I suppose that if the sent xstate.stop actually bubbled up into something that eventually triggered listener removal on the interpreter then it's good, but in a quick recheck I can't identify where that might happen.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I have created the test case:

https://codesandbox.io/s/brave-fermi-nyg8e?file=/src/index.js

My findings:

  • Either one of Interpreter#stop or actions.stop() seem to result in proper teardown. (One of the stopListeners sends a session.stop via Interpreter#stop.)
  • What I thought was happening with activities doesn't appear to be relevant. I'm not sure why.

I also tried to check the behavior for sending actions.stop() as an event to a top-level Interpreter which appears to not work, but I'm not entirely sure what it's doing.

@Andarist Can you also take a look at this test case yourself to make sure that my understanding is correct and that my conclusions are valid?

If my analysis holds, I believe that Interpreter#stop/<actor>.stop() is actually a complete solution.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Andarist Can you also take a look at this test case yourself to make sure that my understanding is correct and that my conclusions are valid?

Sure thing! I've forked your demo so it doesn't restart the spawned child all over again - it was easier for me to inspect stuff this way: https://codesandbox.io/s/magical-lamarr-b09k1?file=/src/index.js

Either one of Interpreter#stop or actions.stop() seem to result in proper teardown. (One of the stopListeners sends a session.stop via Interpreter#stop.)

if we are talking about calling those from within that pure action then structures within child get cleaned up nicely but context.childMachine.stop() doesn't clean this child from the parent's structure, whereas actions.pure((context) => actions.stop(context.childMachine.id) }) does both correctly.

I also tried to check the behavior for sending actions.stop() as an event to a top-level Interpreter which appears to not work, but I'm not entirely sure what it's doing.

actions.stop doesn't create an event (well, actually it is compatible with an event interface so it could be abused, sort of, to be sent and processed). It creates an action object - think about actions as of instructions for the interpreter that you'd like something to happen. Those actions are received by the interpreter through other means though than by sending it events so to make service.send(actions.stop()) work you would have to create a transition for the xstate.stop event and create a stop action there. This is not what you are looking for though, I'm just describing this for completeness.

// ALSO: Test the parent behavior.
// service.stop();

This cleans up stuff correctly and I have confirmed that using this demo.

// HELP: Is this correct for trying to send a stop action to an un-parented Interpreter?
// service.send(actions.stop(service.id));

Do you want to stop the service here or some child service of a particular id? If the first one it should be totally valid to just call stop method like this service.stop().

// 2. Check to see if window.childMachineFirst's StateNode has activities.

It seems that we do not clean children when stopping but they are being stopped. So references are held to them - gonna prepare a fix for this later as semantically it seems right the good thing to do. The child services are being stopped though so GC should kick in most of the time and collect the whole tree of services.

// 3. Check to see if window.childMachineFirst has listeners.

This works correctly - those structures are being cleaned up just OK.

Copy link
Member

@Andarist Andarist left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for doing this, such docs PRs from external contributors are super rare and yet they are very, very valuable, often more valuable than code contributions. Much love for that ❤️

I have left some minor comments, the overall content is very good and very needed for our users. I bet others had similar problems in the past and they couldn't figure out how to do this properly with the documentation alone.

As to the whole stopping thing - yes, pure is required now, but it should be fairly easy to just allow for its dynamic resolution (stop(ctx => ctx.child)) and I think we should implement this unless @davidkpiano has a better idea about how one should manage stopping of the spawned actors.

The PR itself doesn't have to be adjusted to account for those possible changes though - it should describe current possibilities. If we decide to move with the dynamic resolution of stop I will be able to implement it rather quickly and adjust the content of those paragraphs here accordingly.

@@ -332,6 +332,79 @@ parentService.send('LOCAL.WAKE');
// => 'connected'
```

### Stopping Spawned Machines

The object returned from a successful `spawn` action is an `Interpreter`. It implements the `actor` interface, but has slightly different behavior. If you call `stop()` on an `Interpreter` it does not clean up any ongoing `activities` in the active `StateNode`. To properly stop spawned machines you must additionally send them an action using `actions.stop()`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is stopping a machine any different than stopping other types of actors/services? In general, in v5 we'll stop exposing Interpreters here at all, all spawned things will just be actors and nothing more than that. It would be best to refrain from v4-specific explanations (if not needed, of course, after all - v4 will still be the latest stable major version for some time).

I would expect the issue to be the same for all types of actors right now as well, as the interpreter doesn't really care about this - it just happens that we do not "wrap" the spawned interpreters as they already implement the actor interface. This leaks the information about the spawned things and gives access to structures that should not be exposed - and that's why we plan to wrap those as just plain actors in the v5.

Right now the actor interface still has the stop method and this is related to the issue you have stumbled upon because how one should stop a spawned service was not documented but at the same time the stop method is super easy to discover, so it was quite natural to just use it.

The underlying reason why stop shouldn't be used in this context is that the spawned actor is "controlled" by the parent and thus they are linked together. So it becomes the responsibility of a parent to manage the lifecycle of the spawned actor - which, again, was not that obvious at all. We need this to clear up references etc. I think it would be best to focus on that high-level aspect here - I think it brings a little bit of a perspective for the reader on why this is needed. I think it should be doable to warn at runtime about incorrect .stop() invocations - but we'd have to look into it. This would certainly improve the overall DX as we could both warn about this operation not being correct and we could point to this section of the documentation for further reading. Could you create an issue about it? We probably won't be able to jump into any time soon, but would still be great to not forget about this.

I would also refrain from mentioning activities here as they will be gone in v5, StateNode is also a class not really mentioned at other places (I think?). Simply mentioning that we need to clean up internal structures should be sufficient.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of this is paragraph is based upon my hypothesis of a possible issue existing. I'll pull together that test case and get back to you on that.

Other notes:

  • I will revise to add more of the conceptual model components you describe. That's actually the thing that helps the most from the conversations we've been having in different issue threads. Though I've turned it into a recipe, teaching the conceptual model is probably more important.
  • Yeah, got a bit into the weeds.
  • I also need to compare this to V5 so I can write this maybe generically enough to apply in both worlds.

docs/guides/actors.md Show resolved Hide resolved
Comment on lines +366 to +367
context.childMachine.stop();
return actions.stop(context.childMachine.id);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that context.childMachine.stop(); should not be needed, but I would have to recheck this. Have you encountered any problems with actions.stop(context.childMachine.id); alone?

docs/guides/actors.md Show resolved Hide resolved

Further, as `assign` actions are run in advance of custom user actions, by the time you would usually attempt to stop a spawned machine, you may need to take care that the reference to the machine in `context` is not overwritten.

For actions which do not have side effects (such as mutating `context`), you can use `actions.pure()` to ensure that your custom actions is run in order of definition, and before `assign` actions' updates are applied to `context`. Depending upon why you need to stop a spawned machine, this technique may be required.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would actually make a great section of its own - the whole builtin vs custom actions and their ordering. The example itself is also a great explaining why this matters. If you don't have time to move this around or you are unsure where you would fit this - then let us know and we'll figure out something.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is only half of the things from #1403 I assigned myself to write. I do plan to drop some more information into Actions for ordering, and maybe in Context too.

I went ahead and included a first pass of that information here; I think that "JIT" reinforcement of this concept is useful.

@Andarist
Copy link
Member

Andarist commented Sep 3, 2020

Oh, I've missed your questions in the PR comment itself. Let's get to them:

Just sending xstate.stop to a StateNode does not seem to be complete to me as it doesn't seem like it is recursive. The receiver of xstate.stop may also be a machine with children which would also need to be stopped. The event does not appear to be propagated. Code.

StateNode is a "pure" representation of a machine, it's stateless but the information in it holds a list (or a map, don't remember) of things like activities and children`, those shouldn't be "live" things though.

OTOH the Interpreter is this "live" thing that manages the pure machine that it references. It receives actions that should be executed (apart from assigns, those are completely evaluated & resolved within StateNode). In here it stops a particular child upon receiving a stop action:
https://github.com/davidkpiano/xstate/blob/78e998674b8ac8cbcf926f43b2588787b5631953/packages/core/src/interpreter.ts#L885-L886
and stop should recursively stop all "nested" machines as well:
https://github.com/davidkpiano/xstate/blob/78e998674b8ac8cbcf926f43b2588787b5631953/packages/core/src/interpreter.ts#L492-L497

This should also answer the second question as those were highly interconnected.

@nathanhammond
Copy link
Author

How do y'all prefer your modifications to PRs?
A. New commits.
B. Update-in-place (continuously squash to a single commit).
C. Other

And prepping for landing:
A. Squash
B. Commit History
C. Other

And landing strategy:
A. Merge
B. Rebase
C. Other

And test strategy:
A. Failing test in a separate commit first. (red => green)
B. Tests pass at every commit.
C. Other

(Can also drop this into CONTRIBUTING.md if you'd like.)

@Andarist
Copy link
Member

Sorry for the delay - was quite busy lately with other stuff. I haven't forgotten and will get back to you this week.

@Andarist
Copy link
Member

How do y'all prefer your modifications to PRs?

IMHO - whatever works best for you. Ideally, we'd like to receive often contributions from the community, and requiring a particular git-flow makes it harder to achieve that, not everyone is well-versed with more advanced git techniques etc. As to the landing strategy - this one can actually be quite easily chosen from the GitHub UI so it's not much of a concern either way.

@davidkpiano
Copy link
Member

@nathanhammond Sorry for the delay in reviewing this! @Andarist and I discussed the need for a dynamic stop(ctx => ctx.child) and how it could be useful (and simpler) with regard to this, so I'll make those changes and then re-review this PR with the necessary changes.

Thanks so much for doing this!

@davidkpiano
Copy link
Member

Now that #1577 is in, we can simplify these docs:


Stopping Spawned Machines

To stop a spawned machine, use the actions.stop(...) action creator and reference the spawned machine as an expression in the first argument:

on: {
  STOP_SPAWNED: {
    actions: actions.stop(context => context.someSpawnedMachine)
  }
}

@davidkpiano
Copy link
Member

The docs have moved to https://github.com/statelyai/docs and we will be addressing suggestions & improvements there.

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

Successfully merging this pull request may close these issues.

None yet

3 participants