Internal Architecture

Introduction

Okay, you know you're in trouble if you have to be looking in here. GroupKit's internals are pretty twisted, and if there's any way to avoid having to figure them out, I would really strongly suggest taking advantage of that opportunity right now.

Still here I guess. The first step, before getting into the details of this all, is to try to explain why things are so incredibly complex and what we were trying to accomplish.

As usual, it all comes down to flexibility. We'd long been advocates of making groupware customizable and personalizable for end users. This naturally extended itself to making the toolkit flexible enough so that developers can provide for the flexibility their end users needed. We knew that very little could be set in stone, particularly issues like session management, and how data is shared between processes.

Ted O'Grady did his Masters thesis on applying a technique called open implementations to groupware toolkits. Essentially the underlying idea is that not only is it enough for toolkits to provide an API that developers can use to build their applications, but a second API has to be provided to let developers change how that first API behaves; essentially providing a structured way to let application developers rewrite parts of the toolkit when they find that some of the assumptions made by the toolkit's developers don't fly for their particular application.

In GroupKit, this has focused (after too many iterations to count) on letting developers radically redefine the way that environments behave. As you'll see, environments are the core mechanism for doing anything inside GroupKit. They do a lot of different things. The reason they're able to do so many different things is that they are incredibly customizable.

The rest of this architecture overview consists of two parts. The first looks at how GroupKit uses environments and other mechanisms internally to provide its run-time infrastructure. From this you should learn enough to understand how GroupKit actually works, and how to make some basic changes. The second part talks about how to customize environments; if you need to make some really radical changes, you'll probably need to work through this part. Needless to say, for all of this you should be intimately familiar with the user-level facilities provided by GroupKit.

GroupKit's Run-Time Infrastructure

This section will look at how the existing GroupKit infrastructure is constructed. We'll identify the different types of processes used in GroupKit, talk about the registry which is the central directory of everything, talk about how new processes are created, how things get cleaned up when processes are destroyed, look at session management, and finally consider how broadcasts work.

Processes

Processes in GroupKit are essentially applications. Most of the time they correspond with actual operating system processes, but not always. For example, on the Mac, a session manager and several conferences may all run in a single operating system process, each within its own Tcl interpreter.

There are essentially three types of processes you'll find running in GroupKit. The first is a central process called the registrar; there's usually only one of these. It's most important job is to maintain the global registry; it also provides a number of other important services such as generating unique id's and hosting central environments.

The second type of process is the session manager, of which there are usually one per active user of the system. The main job of the session managers is starting and joining conferences.

Finally, conference processes run the actual GroupKit tools that most people spend their time using.

Every process is identified with a unique id number. The registrar is normally "0". Other processes get their id number from the "idgen" service that runs on the registrar, which may look something like "pumori_cpsc_ucalgary_ca>>>9357+3".

In the current implementation, processes are interconnected by sockets as follows. Session managers maintain a single socket connection to the registrar (i.e. client-server). Therefore, if session managers wish to communicate with each other, they normally do so via the registrar. Conferences maintain a socket to the registrar, a socket to the session manager that created them, and direct socket connections to every other conference process in their session (e.g. if three people have joined the same brainstorming session, each conference process has sockets to the two others). So conferences communicate to each other in a peer-peer fashion, and also have have connections to the central registrar and their session manager.

The Registry

One of the most important jobs of the central registrar is to maintain the global registry. A registry is just an environment that happens to have its data structured a particular way. It is used to look up system information. For example, if a process wants to locate a service it can look up its location in the registry. Similarly, to find an environment to connect up to, a user's name, or all the processes shared by a user, check the registry.

Specifically, GroupKit's global registry is a server environment named ::gk::globalRegistry hosted by the registrar. One of the best ways to understand all the pieces of GroupKit is to examine this environment. To do so, start the registrar and a session manager. Open up the "Debug" window in the session manager, and invoke the command "::gk::globalRegistry debug".

The registry tree is divided into a number of sections. We'll explore each of these in turn.

Processes

Each process has an entry under the "processes" key. This is indexed by the process id, so for example you'd find information on the registrar process under. "::gk::globalRegistry processes.0". The information you'll find stored here is the "host" the process is running on, the "port" where the process is running a socket listener, the "usernum" (which is redundant; this is the same as the process id), and the "group", which is described below.

For example, here is the processes tree of the registry showing the registrar, one session manager, and a conference created by that session manager, all which happen to be running on the same machine:

::gk::globalRegistry
   processes
      0
         host: ratbert
         port: 9357
         usernum: 0
         group: 0
      ratbert>>>9357+2
         host: ratbert
         port: 1108
         usernum: ratbert>>>9357+2  
         group: ratbert>>>9357+2
      ratbert>>>9357+4
         host: ratbert
         port: 1110
         usernum: ratbert>>>9357+4
         group: ratbert>>>9357+2

Groups

Every process in GroupKit created by the same user's session shares the same group, e.g. a session manager and all conferences started from that session manager. The purpose of a group is to hold information like a user's name, color, phone number etc. in one place rather than having multiple copies associated with each process. Not only is this more efficient, but it also means if the information changes, all processes automatically pick up the information.

Each group has a unique id (which is what is stored in the "group" field in the processes tree). In the current implementation, the process id of the session manager is also used as the group id. Information on each group is stored under the "groups" key of the global registry, indexed by the group id.

For each group, GroupKit stores the person's name ("username"), a color associated with them ("color"), and various information that is normally associated with their business card ("title", "dept", "company", "phone", "fax", "email", "www", and "office"). Finally, under the "members" key, GroupKit stores a list of all the processes that are members of the group.

Here is an example showing the group holding the session manager and conference processes from the above example:

::gk::globalRegistry
   groups
      ratbert>>>9357+2
         username: Mark Roseman
         color: red
         title: Reluctant Documenter
         dept: Computer Science
         company: University of Calgary
         phone: 403-220-7259
         fax: 403-284-4707
         email: roseman@cpsc.ucalgary.ca
         www: http://www.teamwave.com/~roseman/
         office: Math Science 618
         members:
            ratbert>>>9357+2: ratbert>>>9357+2
            ratbert>>>9357+4: ratbert>>>9357+4      

Environments

The registry also stores information about shared environments under the "environments" key. In particular, it keeps enough information so that processes can connect to an environment started by another process. This includes the type of environment, e.g. "server" for client-server based environments, and a list of one or more environment "maintainers", which are processes that maintain the environment; to connect to the environment a process would connect to one of the maintainers. While a centralized environment will have only one maintainer, a peer-peer environment may have several, because you can connect to any peer to start using the environment.

The registrar starts up a number of centralized environments which it maintains, as shown in this registry fragment:

::gk::globalRegistry
   environments
      ::gk::globalRegistry
         type: server
         maintainers
            0: 0
      ::gk::sessionManagers
         type: server
         maintainers
            0: 0
Note that peer-peer environments associated with a particular conference are usually not stored in the global registry, but in another environment specifically for the conference. We'll come to this shortly, but it again illustrates the fact that any environment can be used as a registry.

Services

The global registry also stores information about service providers, allowing service subscribers to locate appropriate services to subscribe to. These are stored under the "services" key. From there, services are indexed by the type of service, and within each type by a unique id number generated internally by the service code. Each service stores information about its name ("name"), and the process that is providing the service ("processID").

In this fragment, we see two services provided by the registrar, and a "launcher" service offered by a session manager:

::gk::globalRegistry
   services
      idgenerator
         0x1
            name: global
            processID: 0
      environmenthost
         0x2
            name: global
            processID: 0
      launcher
         ratbert>>>9357+2x2
            name: launcherratbert>>>9357+2
            processID: ratbert>>>9357+2   

Starting New Processes

We'll briefly mention here how a new process gets started and integrates itself into the GroupKit world. On first startup, the process connects to the central registrar, and uses its idgenerator service to generate a process id for itself. It then connects to the global registry, and adds information about itself and its group (using the convenience routines provided by the "gk::registry" command).

Things are essentially the same when a new process is created by an existing process, which is done via a "launcher" service. The launcher passes a number of parameters to the newly created process. For example, the group id is provided, so that the new process becomes part of an existing group, rather than creating its own. Also, the new process connects back to the process launcher service, passing its process id to signify that it was created successfully.

Notice the inconsistency two paragraphs up? The new process connects to the idgenerator service to look up its id, but to do that it would need to look up the service in the global registry. Yet before it can connect to the registry it needs its id. Oops. The way around this bootstrap problem is using the "-socket" option of the "service subscriber" command which just assumes that the requested service exists on the other end of a given socket, and doesn't bother to look it up in the registry.

Cleanup

When processes are shutdown, there is a lot of cleanup work to be done, in particular getting rid of information about the process stored in the registry and other environments throughout the system.

This is actually handled very cleanly using the "addDependent" command of environments. Essentially, this lets you mark a particular node (or subtree) of an environment as dependent on the existence of a process, and when the process goes away, the corresponding nodes will be deleted automatically.

You'll find information about dependent nodes stored in most shared environments under the "_dependents" key. (The leading underscore is a bit of a hack in the environments code; essentially any nodes that start with it won't show up in a traversal using the "keys" command, but the data is still replicated around between instances of environments, which wouldn't be the case if stored under the "option" toplevel key).

Here is an example showing dependents, again with the registrar, one session manager, and one conference running. Items are indexed by process, then by "data" or "option", and within that just by a unique id.

::gk::globalRegistry
   _dependents
      0
         data
            2: environments.::gk::globalRegistry.maintainers.0
            3: environments.::gk::sessionManagers.maintainers.0
            4: environments.::gk::confs.maintainers 0
            5: services.idgenerator.0x1
            6: services.environmenthost.0x2
            11: environments.::gk::confs.ratbert>>>9357+3.maintainers.0
      ratbert>>>9357+2
         option
            8: subnet.defaultserver.clients.ratbert>>>9357+2
         data
            2: groups.ratbert>>>9357+2.members.ratbert>>>9357+2
            5: services.launcher.ratbert>>>9357+2x2
      ratbert>>>9357+4
         option
            13: subnet.defaultserver.clients.ratbert>>>9357+4
         data
            2: groups.ratbert>>>9357+2.members.ratbert>>>9357+4

Broadcasts

Broadcasts in GroupKit are also based on environments. The "gk::to" command is initialized (using "gk::to configure environment") to send all broadcasts through a particular environment.

Environments have a subcommand called "execute" which specifies that the parameters to the subcommand should be evaluated as a Tcl command. The "gk::to" command uses the environment's router (described in excruciating detail below), to run this "execute" subcommand on all of the networked instances of the environment.

This means that if the environment in question uses a replicated architecture, the "gk::to" messages will be distributed in a replicated fashion. If it uses a centralized architecture, broadcasts will be sent around in a centralized fashion. The distribution of broadcasts follows the topology set up by the environment, whatever that may be.

Session Management Support

The GroupKit infrastructure provides certain facilities for supporting session management. The primary interface to these is the "gk::sessionmgr" command, which provides routines for creating and joining conferences.

Certain environments, maintained by the registrar process, are also available. The "gk::sessionManagers" environment is used to manage broadcasts, as described in the previous section.

The "gk::confs" environment simply keeps a list of the currently known conferences. Changes to this environment cause the session manager code to generate higher-level events describing exact changes.

When a conference is created, an environment named "gk::confs.$confid" (where $confid is the conference number) is created on the registrar. This environment contains information pertinent to the conference. It holds the current list of users of the conference for example. It also acts as a registry for any environments created to be used strictly within the conference. As an example, the "gk::awarenessModel" environment is registered with the "gk::confs.$confid" environment, rather than the "gk::globalRegistry".

Building New Environments

This section discusses how to build entirely new types of environments. You might build a new environment to support a new type of network topology, to change the behavior of messages (for example to add support for a concurrency control strategy), or to pretty much do anything else you wanted.

There are several ways that you can customize environments, depending on exactly what you want to accomplish. These vary from adding or changing individual environment commands, changing what processes those commands are sent to, assembling new network topologies from existing components, or even building entirely new types of network configurations.

Adding and Redefining Environment Commands

The simplest thing you can do is redefine what an existing command does, or create a new one. To do this, you use the "command" option of an environment. If you for example wanted to add a new primitive allowing you to lock the environment, you could do so like:
$env command set lock myLockCmdHandler

proc myLockCmdHandler {env cmd args} {
   ...
}
You can also redefine the behavior of the existing commands. This is similar to subclassing them. For example, you could redefine the "set" command, adding functionality but still calling the original, as follows:
$env command rename set _originalset
$env command set set myNewSetHandler

proc myNewSetHandler {env cmd key value} {
   ...
   $env _originalset $key $value
   ...
}

Command Routing

The second thing you can do is change what processes an environment command is executed in. Normally, for example, if you call "$env set x y" this command is executed on the copy of the environment found in the local process. This can be easily changed however to execute in any of the copies of a shared environment running in different processes.

Each environment has a "router" built into it. The router is responsible for delivering messages to different instances of the shared environment, located among different processes. In particular, a router knows how to send messages to "all" (all instances of the shared environment), "others" (instances of the shared environment except the instance in the local process), and to a particular process, identified by its unique process id. Note that the actual mechanism by which the messages get sent is a separate issue (see the next section); in calling the router you just specify who the messages should go to, not how.

To specify that a particular message should be routed, use the "command routing" option for environments. For example, this code specifies that when invoked locally, the set, delete and execute commands should be sent to all processes containing an instance of the shared environment:

foreach i "set delete execute" {
    $env command routing $i all
}

Defining Routes

While the "command routing" option deals with who commands should be sent to, this section discusses how the environment's router can be configured to specify how the messages get sent.

Routers are set up to include one or more attached subnets. Each subnet represents a particular type of network topology. Three pre-made subnets are provided in GroupKit, and others can be defined (see later section).

A "server" subnet resides on an environment in a centralized process, and accepts connections from clients. This subnet therefore has direct knowledge of all its attached clients, and knows how to route messages directly to any of them. A "client" subnet would reside in the client processes, and knows only about the server; to route messages to everyone, it just sends them to the server. Finally, a "peer" subnet is used to establish a fully-replicated topology with no centralized component, where all peers communicate directly with each other.

When setting up a router, each subnet is given a name. This is so you could for example have several subnets of the same type on one router. For example, the router could act as a bridge between two replicated networks by including two different peer subnets.

You can add new subnets using the "router addsubnet" command. This takes the name to assign to a subnet, as well as a handler which implements the subnet. The handler may also take other parameters; for example the handler for "peer" subnets can optionally take the host and port of an existing peer to connect to. Here are two examples:

$env router addsubnet defaultserver gk::EnvServerSubnet

$env router addsubnet defaultpeer gk::EnvPeerSubnet $host $port
Note again that the router itself knows nothing about the internals of the subnets; this is completely encapsulated in their handlers. By calling the handlers, the routers can find out enough information (e.g. is a particular process located on a given subnet?) to route messages to and between different subnets. However, by combining together individual subnet components, arbitrary network architectures can be created.

Defining New Subnets

The three provided subnet types (server, client, peer) should be sufficient for most applications, particularly when combined in different ways. However, if you do need to build a new type of subnet, it is possible to do so.

As mentioned above, to define a new subnet type you need to provide a handler. This is a standard Tcl procedure accepting several parameters. The first is the environment for which this subnet will be a part of, the second is the name given to the particular subnet (remember that several subnets may be added to the router of a single environment), and the third parameter is an operation for the handler to perform. Any additional parameters required by the operation will follow in further parameters.

The first operation to be implemented is "init", which is called when the subnet is first added. You can do anything you want to initialize the subnet here. Any parameters provided by the "router addsubnet" command are passed along here.

The "route" operation is invoked by the router to specify that the subnet should send a message. It takes the following parameters: who the message should be sent to, the message itself, and an optional parameter specifying who this message was received from. The destination of the message will be specified as either "others" (all other processes known by the subnet), "notsender" (all processes except the sender of the message, as specified in the third parameter), or a process id meaning send the message to only that single process. The "notsender" tag essentially helps prevent loops in the routing process.

The third operation is "contains" which is called by the router to determine if a particular process (identified by process id, the only parameter here) can be reached via this particular subnet. It is used by the router for efficiency purposes. The valid responses by the subnet to this query are "yes", "no", or "maybe".

Finally, the "connectfrom" operation is invoked when a subnet on a remote environment attempts to make a connection to the local subnet. You will be passed two parameters, the id of the remote process, and a socket used to connect to the remote process.

Those are all the operations a subnet needs to support. In terms of actual implementation, subnets rely heavily on the "gk::rpc" command to build their communications infrastructure and send messages around. Subnets typically store any necessary housekeeping information in the "option" tree of their environment, under the key "subnet.$subnetname".

Environment Types

The previous sections have talked about individual pieces. This section brings together the pieces, which is necessary to create a new type of environment that can be instantiated. This can combine any or all of redefining environment commands, specifying command routing, and adding subnets.

To define an actual environment type, you need to define a procedure that implements the new type; this procedure takes one parameter, the name of the environment which is to have that type applied to it. You also need to register this procedure, so that the gk::environment command knows about the new type.

Here is a fairly simple example, which implements a server environment type. It contains a single server subnet (which clients can connect to), and all modify commands are routed to everyone. Information about the environment is also stored in the registry, which a client environment type might query to find out how to connect. Users could instantiate an instance of this type of environment using "gk::environment -server".

proc gk::EnvTypeServer {env} {
    $env router addsubnet defaultserver gk::EnvServerSubnet
    foreach i "set delete execute" {
	$env command routing $i all
    }
    registry addenvironment $env server
}

gk::EnvironmentRegisterType -server gk::EnvTypeServer

What Now?

The preceeding sections should give you a bit of an overview of the process of creating new environment types. Your best bet is to look through the existing implementations of subnets and types within the core of GroupKit to get a better understanding of what is involved.


GroupKit Reference Manual. Last updated May 6, 1998 by Mark Roseman.