diff --git a/dev/World-Transactions.md b/dev/World-Transactions.md index a4d42f8..77a8c6d 100644 --- a/dev/World-Transactions.md +++ b/dev/World-Transactions.md @@ -1,218 +1,280 @@ -This page is a short introduction on the way worlds and entities function as of -Dragonfly v0.10. +Dragonfly serialises access to each world through transactions. This page +explains how to schedule world and entity work as of Dragonfly v0.11. ## Index -* [Worlds and transactions](#worlds-and-transactions) -* [Entities and transactions](#entities-and-transactions) -* [Handlers and transactions](#handlers-and-transactions) -* [Updating from older versions](#updating-from-older-versions) + +* [World owners and transactions](#world-owners-and-transactions) +* [Scheduling world work](#scheduling-world-work) +* [Entities and stable references](#entities-and-stable-references) +* [Deferred work](#deferred-work) +* [Handlers and commands](#handlers-and-commands) +* [Tasks and failures](#tasks-and-failures) +* [Blocking calls with results](#blocking-calls-with-results) * [Troubleshooting](#troubleshooting) -* [Conclusion](#conclusion) -## Worlds and transactions -As of v0.10, all code that modifies worlds must be executed from within a -transaction (`*world.Tx`) to ensure synchronisation with the world. Transactions -can be run as such: +## World owners and transactions + +Each world has an owner: the single goroutine that runs all transactions for +that world, including ticks. Code that receives a `*world.Tx` is already running +on the owner and may use the transaction directly: + ```go -var w *world.World -w.Exec(func(tx *world.Tx) { - // Use tx to edit the world, for example: - tx.SetBlock(pos, block.Dirt{}) -}) +func update(tx *world.Tx, pos cube.Pos) { + tx.SetBlock(pos, block.Dirt{}, nil) +} ``` -A `*world.Tx` is only valid inside of this transaction function. Using it outside -of this scope (e.g. on a different goroutine) is not permitted: + +A `*world.Tx`, `world.Entity` or `*player.Player` is only valid inside the +callback that supplied it. Do not store these values or capture them in a +goroutine: + ```go -var w *world.World -w.Exec(func(tx *world.Tx) { - // tx is valid here. - go func() { - // tx is invalid here. - }() +w.Do(func(tx *world.Tx) { + // tx is valid here. + go func() { + // tx is not valid here. + }() }) -// tx is also invalid here -``` -Trying to use the `*world.Tx` in these invalid cases leads to the following panic: -``` -world.Tx: use of transaction after transaction finishes is not permitted +// tx is not valid here either. ``` -`w.Exec()` returns a `<-chan struct{}` which may be used to wait for the finishing -of the transaction's execution. In cases where code needs to await a transaction, -it can do so as such: -```go -var w *world.World -<-w.Exec(func(tx *world.Tx) {}) -``` +Use a world, entity handle or typed entity reference to schedule another +callback instead. + +## Scheduling world work + +From any goroutine, use `World.Do` to schedule fire-and-forget work: -## Entities and transactions -Following the introduction of transactions to worlds, entities have undergone some -significant changes as well. A new `*world.EntityHandle` was introduced which is -a persistent identifier of a `world.Entity`. Meanwhile, `world.Entity`s are now -only valid in the context of a transaction. Let us examine an example: ```go var w *world.World -// Create a snowball entity using spawn options. -opts := world.EntitySpawnOpts{Position: pos} -handle := entity.NewSnowball(opts, nil) // handle is a *world.EntityHandle +task := w.Do(func(tx *world.Tx) { + tx.SetBlock(pos, block.Dirt{}, nil) +}) +``` + +`Do` returns immediately and is safe to call from anywhere, including another +owner callback. Use `World.DoAfter` when the work should not run until a delay +has passed: -var snowball world.Entity // Don't do this! See the explanation below. -<-w.Exec(func(tx *world.Tx) { - snowball = tx.AddEntity(handle) // AddEntity adds the handle to the world and returns a world.Entity. - fmt.Println(snowball.Position()) // Will equal 'pos' +```go +w.DoAfter(time.Second, func(tx *world.Tx) { + tx.SetBlock(pos, block.Air{}, nil) }) -// snowball is no longer valid here. ``` -In this example, a snowball is created as a `*world.EntityHandle`. As explained before, -these handles are persistent and always valid. The `world.Entity` returned by tx.AddEntity(), -however, is only valid while the transaction is active. This means that `snowball` in -this example is no longer valid when the transaction ends. -If we were to correctly get access to the snowball again using the handle we have -stored, we can open a new transaction using the following: +Both methods return a `*world.Task`. Ignoring the task is fine when the caller +does not need to observe completion or failure. + +`World.Exec`, which was used before v0.11, no longer exists. Replace +fire-and-forget uses with `Do`; use the off-owner `world.Call` helper when a +result is required. + +## Entities and stable references + +An entity handle is a stable identity that may be stored outside a transaction. +Schedule work through the handle to access the live entity safely: + ```go var handle *world.EntityHandle -handle.ExecWorld(func(tx *world.Tx, e world.Entity) { +handle.Do(func(tx *world.Tx, e world.Entity) { tx.RemoveEntity(e) }) ``` -`ExecWorld()` obtains the entity's world in a thread-safe way and opens a transaction -in it when it does. If the entity is not added to a world, `ExecWorld()` will block until -the entity is added to a world and run the transaction function once it is. If the -entity is closed before `ExecWorld()` is called, `ExecWorld()` will return false and not -run the transaction function. - -In short: Avoid storing `world.Entity` implementations (includes `*player.Player`) in -any field that lasts longer than a transaction. Instead, store a `*world.EntityHandle` and -open a new transaction when needed. Another consequence is that `world.Entity` implementations, -such as `*player.Player`, should not be compared for equality with `==`. Instead, compare their -respective handles by checking, e.g., if `(world.Entity).H() == (*player.Player).H()`. - -## Handlers and transactions -Handlers, such as `player.Handler` and `world.Handler`, have the respective `*player.Player`/ -`*world.Tx` passed to them in an `event.Context[T]`, like so: + +Entity work follows the entity between worlds. If a player travels through a +portal before delayed work runs, for example, the callback runs on the owner of +the player's new world: + ```go -func (h Handler) HandleMove(ctx *event.Context[*player.Player], newPos mgl64.Vec3, newRot cube.Rotation) { - p := ctx.Val() // *player.Player -} +handle.DoAfter(time.Second, func(tx *world.Tx, e world.Entity) { + // e is valid only inside this callback. +}) ``` -As explained in the section [Entities and transactions](#entities-and-transactions), these -values are not valid when moved to another goroutine or when stored in a field of the Handler. -## Updating from older versions -Libraries and servers using Dragonfly written before v0.10 require several changes. -Some of the biggest changes and updating suggestions are listed below. +Typed references avoid type assertions. Use `world.EntityRef[T]` for any entity +type and `player.Ref` for players: -### Transactions -Editing worlds now requires opening a transaction in advance. This means that code -such as this: ```go -var w *world.World -w.SetBlock(pos, block.Dirt{}) -w.AddEntity(ent) +type Arena struct { + players []player.Ref // not []*player.Player +} + +for _, ref := range arena.players { + ref.Do(func(tx *world.Tx, p *player.Player) { + p.Message("Round over") + }) +} ``` -must be refactored to be run in a transaction like this: + +Create refs with `world.NewEntityRef[T](handle)` or `player.NewRef(handle)`. A +one-off player operation can use `player.Do` directly: + ```go -var w *world.World -w.Exec(func(tx *world.Tx) { - tx.SetBlock(pos, block.Dirt{}) - tx.AddEntity(ent) +player.Do(handle, func(tx *world.Tx, p *player.Player) { + p.Message("Hello") }) ``` -Most places, such as methods in `world.Block` implementations, will now have a -`*world.Tx` passed around instead of a `*world.World` and should otherwise require -relatively few changes, other than changing `*world.World` => `*world.Tx`. +Compare entities using their stable handles, not by comparing short-lived +`world.Entity` or `*player.Player` values. + +## Deferred work -Pay additional attention in places where your code creates goroutines or calls -`time.AfterFunc()`, because these require a new transaction. +When code already has a transaction and needs work to run immediately after the +current callback, use `Tx.Defer`: -### Handlers -Storing a `*player.Player` in a field of the Handler is no longer valid, as player -entities don't last the entire lifetime. Instead, the player is now passed in all -handle functions as shown in [Handlers and transactions](#handlers-and-transactions). -Consider storing only the data necessary in your handler and, if needed, store -the `*world.EntityHandle` of the player instead of the player itself. +```go +tx.Defer(func(next *world.Tx) { + // next is a fresh transaction. +}) +``` -As a result of these transaction changes, handlers are now entirely thread safe. -This means that data stored in handlers, unless accessed from different goroutines -elsewhere, does not need to be protected using e.g. a mutex. +Deferred callbacks run on the same owner, ahead of the world's normal queue, +and in registration order (FIFO). Never capture the current transaction in the +deferred callback; use the fresh transaction passed to it. -### Commands -Commands now have a transaction passed to them too, and players/entities returned by -a `[]Target` parameter will only include those from the same world as the caller of -the command. +This is useful for mutating entities after iterating `tx.Entities()` or acting +after an event's default behaviour has applied. Use `World.Do` instead when the +work should go through the normal queue. -### Forms -Like with commands, forms are opened within a transaction now. The transaction is -passed to all Submit methods and Close methods. +| Situation | Use | +|---|---| +| Immediately after this callback, in the same world | `tx.Defer` | +| Soon, from anywhere | `w.Do` | +| After a delay | `DoAfter` | +| Wherever an entity is when the work runs | `handle.Do` | + +Player event contexts also have `Context.Defer`, which re-resolves the player +for the deferred callback: -### Accepting players -Accepting players was changed from the following: ```go -for srv.Accept(func(p *player.Player) { - // Use p. +ctx.Defer(func(next *player.Context) { + next.Player().Message("Done") }) ``` -to: + +## Handlers and commands + +Handlers and commands already run on a world owner, so use the transaction or +event context they receive rather than scheduling and waiting for more work. +Player handlers receive a `*player.Context`; call `ctx.Player()` to get the +player, and use embedded world operations directly: + ```go -for p := range srv.Accept() { - // Use p. +func (h Handler) HandleBlockBreak(ctx *player.Context, pos cube.Pos, ...) { + ctx.Player().Message("Broken") + ctx.SetBlock(pos.Side(cube.FaceUp), block.Air{}, nil) } ``` -Like in other places, using `p` outside of this context (different goroutine or -time.AfterFunc()) is not permitted. -## Troubleshooting -You might run into some issues trying to update to the new world transactions. Here are some of the most common issues -and what you can do to solve them: - -#### panic: `world.Tx: use of transaction after transaction finishes is not permitted`: -This panic happens when a transaction is used after it is closed. Common causes for this include using entities outside -of transactions, e.g. when storing `*player.Player` as opposed to its `*world.EntityHandle`. Solving this issue involves -making sure that a `*world.Tx` is not used outside a transaction. This also includes entities that implement -`world.Entity`, such as `*player.Player`. Instead, store their `*world.EntityHandle`, which is persistent, unique and -safe for use outside a transaction. - -#### Deadlock/freeze while opening a transaction: -You might run into deadlocks trying to create world transactions. This results from opening a transaction from within -a transaction. Let's say we have the following function: +Cancellable `world.Handler` events receive a `*world.Context`, which embeds the +transaction. Non-cancellable `HandleEntitySpawn`, `HandleEntityDespawn` and +`HandleClose` events receive a `*world.Tx`. + +For commands, `Runnable.Run` receives a nil transaction when the source is not +attached to a world, such as a console source. Nil-check the transaction before +using it. + +`Server.Accept` and `Server.Players` also run each loop body on the player's +world owner. Keep loop bodies short and do not call `world.Call*` or wait for a +task from inside them. To act on players later, collect their handles: + ```go -func Do(p *player.Player, w *world.World) { - p.Message("Do called") - <-w.Exec(func(tx *world.Tx) { - fmt.Printf("Range: %v\n", tx.Range()) - }) +var handles []*world.EntityHandle +for p := range srv.Players(nil) { + handles = append(handles, p.H()) +} +for _, handle := range handles { + player.Do(handle, func(tx *world.Tx, p *player.Player) { + p.Message("Hello") + }) } ``` -This function will cause a deadlock because the presence of a `*player.Player`, which implements `world.Entity`, means -that this function is called while a transaction is already opened. Because we wait for the new transaction we create to -finish (**`<-`**`w.Exec()`), a deadlock is created. There are two ways to solve this: -If, in this example, `w` is always equal to the player's world, we can simply do the following: +## Tasks and failures + +`Do`, `DoAfter` and `Defer` return a `*world.Task`. A task records whether its +callback ran successfully or failed because the target closed, the task was +cancelled or the callback panicked: + ```go -func Do(p *player.Player, w *world.World) { - p.Message("Do called") - fmt.Printf("Range: %v\n", p.Tx().Range()) -} +task := handle.DoAfter(time.Second, func(tx *world.Tx, e world.Entity) { + // ... +}) +task.OnDone(func(err error) { + switch { + case err == nil: + // The callback completed. + case errors.Is(err, world.ErrEntityClosed): + // The entity closed before the callback ran. + case errors.Is(err, world.ErrWorldClosed): + // The world closed before the callback ran. + case errors.Is(err, world.ErrTaskPanicked): + // The callback panicked. + } +}) ``` -If, on the other hand, `w` is not guaranteed to be the player's world, we can simply remove the arrow so that we do not -wait for the transaction to end inside the current transaction: +`OnDone` always invokes its hook on a fresh goroutine. The hook is therefore +off-owner and must schedule more work before touching world state. + +`Task.Wait`, `Task.Done` and `Task.Err` are intended for off-owner code such as +tests and shutdown paths. `Task.Cancel` prevents a pending task from starting. +Calling `Wait` from the target owner blocks that owner on itself and deadlocks. + +Panics in fire-and-forget callbacks are recovered, logged with their stack and +stored as a `*world.PanicError` on the task. A task may also report +`ErrEntityType`, `ErrEntityNotInWorld` or `ErrTaskCancelled`. + +## Blocking calls with results + +Off-owner code that needs a result may use `world.Call`: + ```go -func Do(p *player.Player, w *world.World) { - p.Message("Do called") - w.Exec(func(tx *world.Tx) { - fmt.Printf("Range: %v\n", tx.Range()) - }) -} +count, err := world.Call(ctx, w, func(tx *world.Tx) (int, error) { + return len(slices.Collect(tx.Entities())), nil +}) ``` -Now, the transaction is run asynchronously once our current transaction ends. -## Conclusion -Transactions help prevent many race conditions and make it easier to optimise -particularly hot code paths. They do make code slightly more complicated and -you may have questions about them. Do not hesitate to ask any questions in the -Bedrock Gophers Discord. \ No newline at end of file +The equivalent helpers are `world.CallEntity` for an entity handle, +`world.CallRef` for a typed entity reference and `player.Call` for a player. + +These functions are only for code outside the target owner, such as background +goroutines, startup code and tests. Never call them from a handler, command, +scheduled callback or other code that already has a transaction: waiting there +deadlocks the owner. Use the existing transaction directly, or schedule +follow-up work with `Defer` or `Do`. + +## Troubleshooting + +### A callback never finishes + +Check whether owner code calls `world.Call*` or `Task.Wait`. The owner cannot +process the scheduled task while it is blocked waiting for that same task. +Use the current transaction directly, `tx.Defer` for immediate follow-up work +or `w.Do` for normal queued work. + +### A task reports a closed-world or closed-entity error + +Scheduled work has defined lifetime failures. Handle `world.ErrWorldClosed` and +`world.ErrEntityClosed` when the operation must be retried or reported. A +player-context defer can also return `world.ErrEntityNotInWorld` if the player +moved to another world; use `player.Do` when the operation should follow the +player. + +### A typed reference reports `world.ErrEntityType` + +The handle still exists, but its live entity no longer has the type expected by +the reference. Treat the reference as stale and stop scheduling through it. + +### Work runs in an unexpected order + +Use `tx.Defer` for strict follow-up work on the same owner. It runs ahead of the +normal queue in FIFO order. `w.Do` uses the normal queue and is the appropriate +choice when other already-queued work may run first. + +Transactions prevent concurrent access to world state while making scheduling +explicit. For a concise list of changes from v0.10, see the +[v0.11.0 migration guide](v0.11.0-Migration-Guide).