Skip to content

Commit

Permalink
Second round of request handler changes (#3090)
Browse files Browse the repository at this point in the history
**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
vladsud authored Aug 13, 2020
1 parent d14736b commit 8e892e0
Show file tree
Hide file tree
Showing 50 changed files with 1,975 additions and 267 deletions.
5 changes: 3 additions & 2 deletions BREAKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ Temporarily exposed on IContainerRuntimeBase. The intent is to remove it altoget

## getDataStore() APIs is removed
IContainerRuntime.getDataStore() is removed. Only IContainerRuntime.getRootDataStore() is available to retrieve root data stores.
For couple versions we will allow retrieving non-root data stores using this API, but this functionality is temporary and will be removed soon. We will provide other tools to support backward compatibility.
For couple versions we will allow retrieving non-root data stores using this API, but this functionality is temporary and will be removed soon.
You can use handleFromLegacyUri() for creating handles from container-internal URIs (i.e., in format `/${dataStoreId}`) and resolving those containers to get to non-root data stores. Please note that this functionality is strictly added for legacy files! In future, not using handles to refer to content (and storing handles in DDSs) will result in such data stores not being reachable from roots, and thus garbage collected (deleted) from file.

### Package Renames
As a follow up to the changes in 0.24 we are updating a number of package names
Expand Down Expand Up @@ -312,7 +313,7 @@ getAbsoluteUrl on the container runtime and component context now returns `strin
import { waitForAttach } from "@fluidframework/aqueduct";


protected async componentHasInitialized() {
protected async hasInitialized() {
waitForAttach(this.runtime)
.then(async () => {
const url = await this.context.getAbsoluteUrl(this.url);
Expand Down
12 changes: 6 additions & 6 deletions docs/docs/aqueduct.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,20 +51,20 @@ structures:
/**
* Called the first time the component is initialized.
*/
protected async componentInitializingFirstTime(): Promise<void> { }
protected async initializingFirstTime(): Promise<void> { }

/**
* Called every time *except* first time the component is initialized.
*/
protected async componentInitializingFromExisting(): Promise<void> { }
protected async initializingFromExisting(): Promise<void> { }

/**
* Called every time the component is initialized after create or existing.
*/
protected async componentHasInitialized(): Promise<void> { }
protected async hasInitialized(): Promise<void> { }
```

#### componentInitializingFirstTime
#### initializingFirstTime

ComponentInitializingFirstTime is called only once. It is executed only by the _first_ client to open the component and
all work will resolve before the component is presented to any user. You should overload this method to perform
Expand All @@ -74,7 +74,7 @@ The `root` SharedDirectory can be used in this method.
The following is an example from the Badge component.

```ts{5,10,19}
protected async componentInitializingFirstTime() {
protected async initializingFirstTime() {
// Create a cell to represent the Badge's current state
const current = SharedCell.create(this.runtime);
current.set(this.defaultOptions[0]);
Expand Down Expand Up @@ -106,7 +106,7 @@ SharedDirectory.

:::

#### componentInitializingFromExisting
#### initializingFromExisting

::: danger TODO

Expand Down
78 changes: 78 additions & 0 deletions docs/docs/fluid-handles.md
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.
6 changes: 3 additions & 3 deletions docs/docs/hello-world.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,17 +144,17 @@ use the `root` `SharedDirectory`.
```typescript
/**
* componentInitializingFirstTime is called only once, it is executed only by the first client to open the
* initializingFirstTime is called only once, it is executed only by the first client to open the
* component and all work will resolve before the view is presented to any user.
*
* This method is used to perform component setup, which can include setting an initial schema or initial values.
*/
protected async componentInitializingFirstTime() {
protected async initializingFirstTime() {
this.root.set(diceValueKey, 1);
}
```
The `componentInitializingFirstTime` function is an override lifecycle method that is called the first time the
The `initializingFirstTime` function is an override lifecycle method that is called the first time the
component instance is ever created. This is our opportunity to perform setup work that will only ever be run
once.
Expand Down
Loading

0 comments on commit 8e892e0

Please sign in to comment.