The Suffix - Part I

Wherein for the first time you build a server with multiple services and use the server's namespace to address them. This is an advanced tutorial.

Prerequisites:
All tutorials require Vanadium installation, and must run in a bash shell.
Further, please download this script then source it:

source ~/Downloads/scenario-c-setup.sh

This command wipes tutorial files and defines shell environment variables. If you start a new terminal, and just want to re-establish the environment, see instructions here.

If you would like to generate files from this tutorial without the copy/paste steps, download and source this script. Files will be created in the $V_TUT directory.

Introduction

In an earlier tutorial, a fortune server was mounted in a table with the name fortuneAlpha.

Since that server contained only one service, clients needed nothing more than that name, as seen in the mount table, to start using the service.

If a server contains multiple services, a suffix is needed to select one. A mount table doesn't know about suffixes directly. A server encapsulates that knowledge in its dispatcher.

Let's build an example.

Requirements

Your company, Prophecies Inc, needs a server to support its team of prophetic consultants - Cassandra, Nostradamus, etc. Each consultant needs its own data and customers.

  1. Many services: To support hiring new consultants, the server must be able to create fortune services on the fly, each with its own set of fortunes.

  2. Discovery: Customers must be able to discover all the available services.

To do this, we'll use a custom dispatcher.

One thing we won't need is a new service implementation. The one we have can be used as is.

New dispatcher

Recall the basic program layout:

The dispatcher owns all the services in a server. All previous tutorials used servers with just one service, so a default dispatcher was used - its only job was to figure out which method to invoke on the single service.

In this tutorial, we need a custom dispatcher that maps from suffixes to services.

Examples below will show a client asking for a fortune from a service, named prophInc/cassandra. The resolution process will determine that prophInc is a name associated with a server endpoint in a mount table (exactly like fortuneAlpha was earlier), and that cassandra is a suffix.

This suffix cassandra is ultimately passed to the Lookup method in a dispatcher interface. Lookup accepts a suffix and returns a service, and the authorizer that guards it.

The examples will also show that cassandra and all other service names under prophInc are discoverable in the namespace.

Here's an implementation satisfying the service requirements:

 cat - <<EOF >$V_TUT/src/fortune/server/util/dispatcher.go
package util

import (
  "errors"
  "strings"
  "sync"
  "fortune/ifc"
  "fortune/service"
  "v.io/v23/context"
  "v.io/v23/rpc"
  "v.io/v23/security"
)

type myDispatcher struct {
  mu sync.Mutex
  registry map[string]interface{}
}

func (d *myDispatcher) Lookup(
    _ *context.T, suffix string) (interface{}, security.Authorizer, error) {
  if strings.Contains(suffix, "/") {
    return nil, nil, errors.New("unsupported service name")
  }
  auth := MakeAuthorizer()
  d.mu.Lock()
  defer d.mu.Unlock()
  if suffix == "" {
    names := make([]string, 0, len(d.registry))
    for name, _ := range d.registry {
      names = append(names, name)
    }
    return rpc.ChildrenGlobberInvoker(names...), auth, nil
  }
  s, ok := d.registry[suffix]
  if !ok {
    // Make the service on first attempt to use.
    s = ifc.FortuneServer(service.Make())
    d.registry[suffix] = s
  }
  return s, auth, nil;
}

func MakeDispatcher() rpc.Dispatcher {
  return &myDispatcher {
    registry: make(map[string]interface{}),
  }
}
EOF

Code walk

The server's root object is accessed with an empty suffix (""). In this example, the root object is a ChildrenGlobber service that makes the fortune service names discoverable in the server's namespace.

This dispatcher uses the suffix as a key in a string-to-service map called registry. If the map lookup fails, a new service is created on the fly, and stored in the map using the suffix.

The service object associated with the suffix is returned to the caller of Lookup.

Lookup must also return an authorizer. A dispatcher's job is to determine which authorizer to use with a given service. In this example, we use the default authorizer for simplicity.

Since any server should support concurrent access, its service map should be protected my a mutex.

Try it

Principal

In this example, we run the client and the server as the same [principal]. We can re-use the tutorial principal from the basics tutorial.

Start the server

Fire up the server with the new dispatcher code:

go install fortune/server
kill_tut_process TUT_PID_SERVER
$V_TUT/bin/server \
    --v23.credentials $V_TUT/cred/basics \
    --endpoint-file-name $V_TUT/server.txt &
TUT_PID_SERVER=$!

Hire Cassandra

Try to run a client as Cassandra:

Expect it to fail.

$V_TUT/bin/client \
    --v23.credentials $V_TUT/cred/basics \
    --server `cat $V_TUT/server.txt`

The error is Method does not exist: Get. This is because the root object, i.e. the one associated with an empty suffix, does not implement the Fortune interface.

To get past this, specify a suffix:

$V_TUT/bin/client \
    --v23.credentials $V_TUT/cred/basics \
    --server `cat $V_TUT/server.txt`/cassandra

That works. This time the server was specified with the suffix cassandra.

Likewise, the client should be able to write to that service:

$V_TUT/bin/client \
    --v23.credentials $V_TUT/cred/basics \
    --server `cat $V_TUT/server.txt`/cassandra \
    --add 'Do not visit Sparta!'

But how would customers know that the cassandra service exists? The answer is in the server's namespace.

The server's namespace is just like the mount table namespace. It allows the server to publish the name of the services that it hosts. In this example, this is accomplished by the ChildrenGlobber service at the server's root.

ChildrenGlobber is covered in more details in the Globber tutorial.

$V_BIN/namespace \
    --v23.credentials $V_TUT/cred/basics \
    --v23.namespace.root `cat $V_TUT/server.txt` \
    glob "*"

Let's insert a table to make service specifications easier to read.

Mount it

Start a mount table and save it in $V23_NAMESPACE:

PORT_MT=23000  # Pick an unused port.
kill_tut_process TUT_PID_MT
$V_BIN/mounttabled \
    --v23.credentials $V_TUT/cred/basics \
    --v23.tcp.address :$PORT_MT &
TUT_PID_MT=$!
export V23_NAMESPACE=/localhost:$PORT_MT

Restart the fortune server to publish itself in the mount table at the name prophInc:

kill_tut_process TUT_PID_SERVER
$V_TUT/bin/server \
    --v23.credentials $V_TUT/cred/basics \
    --service-name prophInc \
    --endpoint-file-name $V_TUT/server.txt &
TUT_PID_SERVER=$!

Now add a new fortune via a mount table lookup:

$V_TUT/bin/client \
    --v23.credentials $V_TUT/cred/basics \
    --server prophInc/cassandra \
    --add 'Troy is doomed!'

It's this last usage that makes the term suffix clear.

The mount name prophInc is followed by the suffix cassandra to specify which service to access in the server.

Customers can discover the service names via the mount table:

$V_BIN/namespace \
    --v23.credentials $V_TUT/cred/basics \
    glob "prophInc/*"

The server's namespace transparently extends the mount table's namespace.

Exercises

Add other services

Create another service for nostradamus and watch it appear in the namespace next to cassandra.

$V_TUT/bin/client \
    --v23.credentials $V_TUT/cred/basics \
    --server prophInc/nostradamus

$V_BIN/namespace \
    --v23.credentials $V_TUT/cred/basics \
    glob "prophInc/*"

Cleanup

kill_tut_process TUT_PID_SERVER
kill_tut_process TUT_PID_MT
unset V23_NAMESPACE

Summary