-
Notifications
You must be signed in to change notification settings - Fork 536
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Second round of request handler changes (#3090)
**Key changes:** 1. IFluidHandleContext no longer implements IFluidRouter. It's request() method is renamed to resolveHandle(). This reduces number of responsibilities for both interfaces and makes it clearer on the purpose of interfaces. Note: This causes DataStoreRuntime to have both request() & resolveHandle(). In next iteration, they will have different behaviors - DDS URIs will fail to resolve when request comes in through request(). I.e. it would be impossible to request DDS from data store unless data store handler implements such access. This is equivalent to making DDSs private on a data store class. 2. 'path' is removed from both IFluidHandle & IFluidRouter. It has been marked deprecated for many versions. 3. Introducing handleFromLegacyUri() for back compat. Using it in couple places in this change to scope amount of churn - all places where it's used needs revisit. More on that below. 4. Fixing how we refer to task manager ID and how we access task manager & agent scheduler. In many places code confuses data store ID vs. type (even though they are the same thing). Exposing task manager on IContainerRuntimeBase. More on future changes here below. 5. Vltava: changing registry creation pattern - hiding component type names, forcing everything to go through factories. Same pattern should be expanded to all of our examples, as name we use in registry not always matches type on a factory. - Also removing using package name as component type - that has pretty big impact on bundle sizes, and is backward compat hazard - type name can't change, which is not obvious looking at package.json. 6. Last edited is simplified - async load path is deprecated, with assumption that it is being used on a critical path of container loading, with detached container creation. 7. PureDataObject method names are busted due to renames for many users (i.e. componentInitializingFirstTime is still used in code in derived classes, when such method was renamed on base class). Follow up for # 3 & 4 above: Next step will be to pass data object dependencies to them directly through scoping mechanism on object creation, and start removing access to "globals" in container. The most obvious example why it's bad is PureDataObject.getService() & getMatchMakerContainerService() implementations. Consumers of those are likely not aware that generateContainerServicesRequestHandler() has to be used by container developer. In fact, getMatchMakerContainerService() is not used in our repo (other than UT)! Better approach is to pass these dependencies through local (to object) scope, and force fluid object implementation to grab and store such dependencies (using handles) within data object itself, for future use. PureDataObjectFactory.instantiateInstance() can validate required dependencies are present. PR #2950 takes a first step on this path.
- Loading branch information
Showing
50 changed files
with
1,975 additions
and
267 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
# Fluid Handles | ||
|
||
A [Fluid Handle](../../packages/loader/core-interfaces/src/handles.ts) is a handle to a `fluid object`. It is | ||
used to represent the object and has a function `get()` that returns the underlying object. Handles move the ownership | ||
of retrieving a `fluid object` from the user of the object to the object itself. The handle can be passed around in the | ||
system and anyone who has the handle can easily get the underlying object by simply calling `get()`. | ||
|
||
## Why use Fluid Handles? | ||
|
||
- You should **always** use handles to represent `fluid objects` and store them in a Distributed Data Structure (DDS). | ||
This tells the runtime, and the storage, about the usage of the object and that it is referenced. The runtime / | ||
storage can then manage the lifetime of the object, and perform important operations such as garbage collection. | ||
Otherwise, if the object is not referenced by a handle, it will be garbage collected. | ||
|
||
The exception to this is when the object has to be handed off to an external entity. For example, when copy / pasting | ||
an object, the `url` of the object should be handed off to the destination so that it can request the object from the | ||
Loader or the Container. In this case, it is the responsiblity of the code doing so to manage the lifetime to this | ||
object / url by storing the handle somewhere, so that the object is not garbage collected. | ||
|
||
- With handles, the user doesn't have to worry about how to get the underlying object since that itself can differ in | ||
different scenarios. It is the responsibility of the handle to retrieve the object and return it. | ||
|
||
For example, the [handle](../../packages/runtime/component-runtime/src/componentHandle.ts) for a `SharedComponent` | ||
simply returns the underlying object. But when this handle is stored in a DDS so that it is serialized and then | ||
de-seriazlied in a remote client, it is represented by a [remote | ||
handle](../../packages/runtime/runtime-utils/src/remoteComponentHandle.ts). The remote handle just has the absolute | ||
url to the object and requests the object from the root and returns it. | ||
|
||
## How to create a handle? | ||
|
||
A handle's primary job is to be able to return the `fluid object` it is representing when `get` is called. So, it needs | ||
to have access to the object either by directly storing it or by having a mechanism to retrieve it when asked. The | ||
creation depends on the usage and the implementation. | ||
|
||
For example, it can be created with the absolute `url` of the object and a `routeContext` which knows how to get the | ||
object via the `url`. When `get` is called, it can request the object from the `routeContext` by providing the `url`. | ||
This is how the [remote handle](../../packages/runtime/runtime-utils/src/remoteComponentHandle.ts) retrieves the | ||
underlying object. | ||
|
||
## Usage | ||
|
||
A handle should always be used to represent a fluid object. Following are couple of examples that outline the usage of | ||
handles to retrieve the underlying object in different scenarios. | ||
|
||
### Basic usage scenario | ||
|
||
One of the basic usage of a Fluid Handle is when a client creates a `fluid object` and wants remote clients to be able | ||
to retrieve and load it. It can store the handle to the object in a DDS and the remote client can retrieve the handle | ||
and `get` the object. | ||
|
||
The following code snippet from the [Pond](../../components/examples/pond/src/index.tsx) Component demonstrates this. It | ||
creates `Clicker` which is a SharedComponent during first time initialization and stores its `handle` in the `root` DDS. | ||
Any remote client can retrieve the `handle` from the `root` DDS and get `Clicker` by calling `get()` on the handle: | ||
|
||
```typescript | ||
protected async initializingFirstTime() { | ||
// The first client creates `Clicker` and stores the handle in the `root` DDS. | ||
const clickerComponent = await Clicker.getFactory().createComponent(this.context); | ||
this.root.set(Clicker.ComponentName, clickerComponent.handle); | ||
} | ||
|
||
protected async hasInitialized() { | ||
// The remote clients retrieve the handle from the `root` DDS and get the `Clicker`. | ||
const clicker = await this.root.get<IComponentHandle>(Clicker.ComponentName).get(); | ||
this.clickerView = new HTMLViewAdapter(clicker); | ||
} | ||
``` | ||
|
||
### A more complex scenario | ||
|
||
Consider a scenario where there are multiple `Containers` and a `fluid object` wants to load another `fluid object`. | ||
|
||
If the `request-response` model is used to acheive this, to `request` the object using its `url`, the object loading it | ||
has to know which `Container` has this object so that it doesn't end up requesting it from the wrong one. It can become | ||
real complicated real fast as the number of `Components` and `Containers` grow. | ||
|
||
This is where Compponent Handles becomes really powerful and make this scenario much simpler. You can pass around the | ||
`handle` to the `fluid object` across `Containers` and to load it from anywhere, you just have to call `get()` on it. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.