Shared Substance

Introduction

Shared Substance is a middleware and run-time environment that we have created to support the development of multi-surface applications. It defines a distributed application model based on sharing and provides a set of basic resources for applications, including networking, discovery, file and device access, and integration with GUI toolkits.

SharedSubstance is a distributed application model implemented in Substance that introduces the concepts of environments and shares. Environments are uniquely named and remotely discoverable Shared Substance processes. A distributed application consists of several environments running on different machines. Each environment can host specific functionality of a distributed application, such as rendering shared data on a particular surface, as well as generic functionality that can be used by multiple distributed applications. In order for an environment to share data, resources or functionality, it must provide shares.

Shares describe subtrees that are publicly available. Shares may be remotely accessed through mounting, a strategy similar to remote method invocation (RMI), and replication, a strategy similar to shared-state. The distributed application model of Shared Substance relies on announcing and accessing shared subtrees and their facets, and replicating and/or mounting them for remote access. Since everything in Substance is represented at run-time by trees of nodes and facets, everything (data, resources, functionality, arbitrary segments of an application) may be shared.

Shared Substance defines a standard organization of the nodes of an environment. The root has four subtrees:

Environments and shares are publicly announced for remote dynamic discovery (we currently use Bonjour, but SharedSubstance is agnostic towards other mechanisms, such as UPnP).

Sharing

In Shared Substance every subtree can be shared. Sharing is done by adding a sharer facet on a node, and calling its sharer handler:

from substance.std.sharing.sharer import Sharer

class SomeFacet(Facet):

    def __init__(self):
        Facet.__init__(self, "SomeFacet")

    def instantiate(self, with_state):
        self.st = local.new_child(self, "SharedSubtree", "Our shared subtree")
        self.st.new_value(self, "SomeValue", "A description", "foo")
        self.st.new_value(self, "AnotherValue", "Another description", 42)

        self.st.add_facet(self, Sharer(), True, True)
        self.st.Sharer.share(self, "SomeShare", "The master environment", "examples.substance")

Now SomeShare will be announced on the network, shared under the domain examples.substace. Sharing this on the network will result in this share appearing in the world subtree of all the other environments on the network (in /world/shares/examples.substance).

Sharing a subtree will enable other environments to access it either through mounting or replication. They will also be able to call facets installed on the nodes, manipulate the tree as they please, and even install facets that will be made accessible to the host environment.

Mounting and replicating

Mounting and replicating are two techniques for including a shared subtree in a local environment. The use of the two techniques are very similar however there are some key differences. When mounting a shared subtree in a local Substance environment all events to the mount will be send directly over the network to the host of the shared tree. This means that minimum state regarding the share is kept in the local environment. When replicating on the other hand, a replica of the shared subtree will be created in the local environment and kept in synch with the one of the host. NB: Facets are not replicated, but instead proxied i.e. when calling a facet on a replicated subtree, the facet install on the host will be called. Replication is well suited for uses where the read frequency is high, e.g. for replicating a subtree for rendering.

Mounting

To mount the shared subtree of the example above the following code is needed:

from substance.environment.bootstrap import *
from substance.std.sharing.mounter import Mounter

class MountingFacet(Facet):
    def __init__(self):
        Facet.__init__(self, "MountingFacet")

    def instantiate(self, with_state):
        local.add_facet(self, Mounter())
        mount = local.Mounter.mount(self, "SomeShare", "examples.substance")

boot_std_default(name = "MountingEnv", description = "Environment for mounting")
boot.add_facet(Slave(), environment / "app")
done_boot()

Now the mount path will give us access to the shared subtree. NB: When mounting it is not possible to install listeners or facets on the mounted subtree. If this is needed use replication instead.

To unmount call for instance:

local.Replicator.unmount(self, "SomeShare", "examples.substance")

Replicating

To replicate the shared subtree of the example above the following code is needed:

from substance.environment.bootstrap import *
from substance.std.sharing.replicator import Replicator

class ReplicatingFacet(Facet):
    def __init__(self):
        Facet.__init__(self, "ReplicatingFacet")

    def instantiate(self, with_state):
        local.add_facet(self, Replicator())
        replica = local.Replicator.replicate(self, "SomeShare", "examples.substance")

boot_std_default(name = "ReplicatingEnv", description = "Environment for replicating")
boot.add_facet(Slave(), environment / "app")
done_boot()

To unreplicate call for instance:

local.Replicator.unreplicate(self, "SomeShare", "examples.substance")

NB: This will disconnect the replicated data from the host subtree and uninstall proxied facets. However, the data will persist and must be deleted manually.

On a replica it is possible to install facets. Per default these will only be accessible locally, and will not be proxied on other environments replicating the same share. However it is possible to install a facet on a node in a replicated subtree where it will be made accessible across the network in the following manner:

replica.add_facet(self, SomeFacet(), True, True, True)

The signature of add_facet looks like the following:

def add_facet(self, facet, with_state=True, with_dependencies=True, remote=False):
    ...

Callbacks

When mounting or replicating as above the replicator will throw and error if the share is not available on the network. It is possible to provide a callback path for both the Mounter and the Replicator that will call a handler when the share appears on the network. This means that it is possible to run the replicating or mounting environment before the sharing environment is running.

from substance.environment.bootstrap import *
from substance.std.sharing.replicator import Replicator

class ReplicatingFacet(Facet):
    def __init__(self):
        Facet.__init__(self, "ReplicatingFacet")

    def instantiate(self, with_state):
        local.add_facet(self, Replicator())
        local.Replicator.replicate(self, "SomeShare", "examples.substance", self.get_path())

    @Facet.HANDLER("<void>::<share:string>:<domain:string>")
    def share_replicated(self, share, domain, share_path):
        self._replica = share_path

    @Facet.HANDLER("<void>::<share:string>:<domain:string>")
    def share_disappeared(self, share, domain):
        self._replica = None

boot_std_default(name = "ReplicatingEnv", description = "Environment for replicating")
boot.add_facet(Slave(), environment / "app")
done_boot()

NB: It is required that the callback facet has a handler named share_replicated and one named share_disappeared with the signature above. When mounting, the latter must be share_mounted but with the same signature.