discovery: add global discovery to mojo

Change-Id: I6577af005091079d67674b7a44c0472bd0402856
diff --git a/go/src/v.io/mojo/discovery/apptest/apptest.go b/go/src/v.io/mojo/discovery/apptest/apptest.go
index 6670a7d..7d78079 100644
--- a/go/src/v.io/mojo/discovery/apptest/apptest.go
+++ b/go/src/v.io/mojo/discovery/apptest/apptest.go
@@ -18,14 +18,16 @@
 
 func RunAppTests(mctx application.Context) int {
 	apptests := []func(*testing.T, application.Context){
-		AppTestBasic,
+		AppTestDiscoveryBasic,
+		AppTestGlobalDiscoveryBasic,
 	}
 
 	var tests []testing.InternalTest
 	for _, apptest := range apptests {
 		qname := runtime.FuncForPC(reflect.ValueOf(apptest).Pointer()).Name()
 		name := qname[strings.LastIndex(qname, ".")+1:]
-		tests = append(tests, testing.InternalTest{name, func(t *testing.T) { apptest(t, mctx) }})
+		f := apptest // To bind the current value of apptest to each closure.
+		tests = append(tests, testing.InternalTest{name, func(t *testing.T) { f(t, mctx) }})
 	}
 
 	// MainStart is not supposed to be called directly, but there is no other way
diff --git a/go/src/v.io/mojo/discovery/apptest/apptest_util.go b/go/src/v.io/mojo/discovery/apptest/apptest_util.go
new file mode 100644
index 0000000..5f0cfa1
--- /dev/null
+++ b/go/src/v.io/mojo/discovery/apptest/apptest_util.go
@@ -0,0 +1,232 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build mojo
+
+package apptest
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"log"
+	"reflect"
+	"strings"
+	"sync"
+	"time"
+
+	"mojo/public/go/bindings"
+	"mojo/public/go/system"
+
+	mojom "mojom/v.io/discovery"
+
+	_ "v.io/x/ref/runtime/factories/generic"
+
+	"v.io/mojo/discovery/internal"
+)
+
+type mockScanHandler struct {
+	ch chan mojom.Update_Pointer
+}
+
+func (h *mockScanHandler) OnUpdate(ptr mojom.Update_Pointer) error {
+	h.ch <- ptr
+	return nil
+}
+
+func scan(d mojom.Discovery, query string) (<-chan mojom.Update_Pointer, func(), error) {
+	ch := make(chan mojom.Update_Pointer)
+	handler := &mockScanHandler{ch}
+	req, ptr := mojom.CreateMessagePipeForScanHandler()
+	stub := mojom.NewScanHandlerStub(req, handler, bindings.GetAsyncWaiter())
+
+	closer, e1, e2 := d.Scan(query, ptr)
+	if e1 != nil {
+		close(ch)
+		return nil, nil, errors.New(e1.Msg)
+	}
+	if e2 != nil {
+		close(ch)
+		return nil, nil, e2
+	}
+
+	wg := new(sync.WaitGroup)
+	wg.Add(1)
+	go func() {
+		defer wg.Done()
+		for {
+			if err := stub.ServeRequest(); err != nil {
+				connErr, ok := err.(*bindings.ConnectionError)
+				if !ok || !connErr.Closed() {
+					log.Println(err)
+				}
+				break
+			}
+		}
+	}()
+
+	stop := func() {
+		p := mojom.NewCloserProxy(*closer, bindings.GetAsyncWaiter())
+		p.Close()
+		p.Close_Proxy()
+		stub.Close()
+		wg.Wait()
+		close(ch)
+	}
+	return ch, stop, nil
+}
+
+func scanAndMatch(d mojom.Discovery, query string, wants ...mojom.Advertisement) error {
+	const timeout = 10 * time.Second
+
+	var err error
+	for now := time.Now(); time.Since(now) < timeout; {
+		var updatePtrs []mojom.Update_Pointer
+		updatePtrs, err = doScan(d, query, len(wants))
+		if err != nil {
+			return err
+		}
+		err = matchFound(updatePtrs, wants...)
+		if err == nil {
+			return nil
+		}
+	}
+	return err
+}
+
+func doScan(d mojom.Discovery, query string, expectedUpdates int) ([]mojom.Update_Pointer, error) {
+	scanCh, stop, err := scan(d, query)
+	if err != nil {
+		return nil, err
+	}
+	defer func() {
+		stop()
+		for range scanCh {
+		}
+	}()
+
+	updatePtrs := make([]mojom.Update_Pointer, 0, expectedUpdates)
+	for {
+		var timer <-chan time.Time
+		if len(updatePtrs) >= expectedUpdates {
+			timer = time.After(5 * time.Millisecond)
+		}
+
+		select {
+		case ptr := <-scanCh:
+			updatePtrs = append(updatePtrs, ptr)
+		case <-timer:
+			return updatePtrs, nil
+		}
+	}
+}
+
+func matchFound(updatePtrs []mojom.Update_Pointer, wants ...mojom.Advertisement) error {
+	return match(updatePtrs, false, wants...)
+}
+
+func matchLost(updatePtrs []mojom.Update_Pointer, wants ...mojom.Advertisement) error {
+	return match(updatePtrs, true, wants...)
+}
+
+func match(updatePtrs []mojom.Update_Pointer, lost bool, wants ...mojom.Advertisement) error {
+	updateMap := make(map[[internal.AdIdLen]uint8]mojom.Update)
+	updates := make([]mojom.Update, 0)
+	for _, ptr := range updatePtrs {
+		update := mojom.NewUpdateProxy(ptr, bindings.GetAsyncWaiter())
+		defer update.Close_Proxy()
+
+		id, _ := update.GetId()
+		updateMap[id] = update
+		updates = append(updates, update)
+	}
+	for _, want := range wants {
+		update := updateMap[*want.Id]
+		if update == nil {
+			break
+		}
+		if got, _ := update.IsLost(); got != lost {
+			break
+		}
+		if !updateEqual(update, want) {
+			break
+		}
+		delete(updateMap, *want.Id)
+	}
+	if len(updateMap) == 0 {
+		return nil
+	}
+	return fmt.Errorf("Match failed; got %v, but wanted %v", updatesToDebugString(updates), adsToDebugString(wants))
+}
+
+func updateEqual(update mojom.Update, ad mojom.Advertisement) bool {
+	if got, _ := update.GetId(); got != *ad.Id {
+		return false
+	}
+	if got, _ := update.GetInterfaceName(); got != ad.InterfaceName {
+		return false
+	}
+	if got, _ := update.GetAddresses(); !reflect.DeepEqual(got, ad.Addresses) {
+		return false
+	}
+	if ad.Attributes != nil {
+		for k, v := range *ad.Attributes {
+			if got, _ := update.GetAttribute(k); got != v {
+				return false
+			}
+		}
+	}
+	if ad.Attachments != nil {
+		for k, v := range *ad.Attachments {
+			h, err := update.GetAttachment(k)
+			if err != nil {
+				return false
+			}
+			defer h.Close()
+			h.Wait(system.MOJO_HANDLE_SIGNAL_READABLE, system.MOJO_DEADLINE_INDEFINITE)
+			r, got := h.ReadData(system.MOJO_READ_DATA_FLAG_ALL_OR_NONE)
+			if r != system.MOJO_RESULT_OK {
+				return false
+			}
+			if !bytes.Equal(got, v) {
+				return false
+			}
+		}
+	}
+	return true
+}
+
+func adsToDebugString(ads []mojom.Advertisement) string {
+	var strs []string
+	for _, ad := range ads {
+		strs = append(strs, adToDebugString(ad))
+	}
+	return "[]" + strings.Join(strs, ", ") + "]"
+}
+
+func adToDebugString(ad mojom.Advertisement) string {
+	return "{" + strings.Join(dumpFields(ad), ", ") + "}"
+}
+
+func updatesToDebugString(updates []mojom.Update) string {
+	var strs []string
+	for _, u := range updates {
+		strs = append(strs, updateToDebugString(u))
+	}
+	return "[]" + strings.Join(strs, ", ") + "]"
+}
+
+func updateToDebugString(update mojom.Update) string {
+	lost, _ := update.IsLost()
+	ad, _ := update.GetAdvertisement()
+	return fmt.Sprintf("{%v, %v}", lost, strings.Join(dumpFields(ad), ", "))
+}
+
+func dumpFields(i interface{}) []string {
+	var fields []string
+	for rv, i := reflect.ValueOf(i), 0; i < rv.NumField(); i++ {
+		fields = append(fields, fmt.Sprint(rv.Field(i)))
+	}
+	return fields
+}
diff --git a/go/src/v.io/mojo/discovery/apptest/discovery_apptest.go b/go/src/v.io/mojo/discovery/apptest/discovery_apptest.go
index 8eb7325..afc6f92 100644
--- a/go/src/v.io/mojo/discovery/apptest/discovery_apptest.go
+++ b/go/src/v.io/mojo/discovery/apptest/discovery_apptest.go
@@ -7,18 +7,10 @@
 package apptest
 
 import (
-	"bytes"
-	"errors"
-	"fmt"
-	"log"
-	"reflect"
-	"strings"
 	"testing"
-	"time"
 
 	"mojo/public/go/application"
 	"mojo/public/go/bindings"
-	"mojo/public/go/system"
 
 	mojom "mojom/v.io/discovery"
 
@@ -33,7 +25,7 @@
 	return mojom.NewDiscoveryProxy(ptr, bindings.GetAsyncWaiter())
 }
 
-func AppTestBasic(t *testing.T, mctx application.Context) {
+func AppTestDiscoveryBasic(t *testing.T, mctx application.Context) {
 	ads := []mojom.Advertisement{
 		{
 			Id:            &[internal.AdIdLen]uint8{1, 2, 3},
@@ -78,7 +70,7 @@
 	}
 
 	// Make sure none of advertisements are discoverable by the same discovery instance.
-	if err := scanAndMatch(d1, ""); err != nil {
+	if err := scanAndMatch(d1, ``); err != nil {
 		t.Error(err)
 	}
 
@@ -97,11 +89,11 @@
 	}
 
 	// Open a new scan channel and consume expected advertisements first.
-	scanCh, stop, err := scan(d2, `v.InterfaceName="v.io/v23/a"`)
+	scanCh, scanStop, err := scan(d2, `v.InterfaceName="v.io/v23/a"`)
 	if err != nil {
 		t.Fatal(err)
 	}
-	defer stop()
+	defer scanStop()
 
 	update := <-scanCh
 	if err := matchFound([]mojom.Update_Pointer{update}, ads[0]); err != nil {
@@ -127,208 +119,3 @@
 		t.Error(err)
 	}
 }
-
-type mockScanHandler struct {
-	ch chan mojom.Update_Pointer
-}
-
-func (h *mockScanHandler) OnUpdate(ptr mojom.Update_Pointer) error {
-	h.ch <- ptr
-	return nil
-}
-
-func scan(d mojom.Discovery, query string) (<-chan mojom.Update_Pointer, func(), error) {
-	ch := make(chan mojom.Update_Pointer)
-	handler := &mockScanHandler{ch}
-	req, ptr := mojom.CreateMessagePipeForScanHandler()
-	stub := mojom.NewScanHandlerStub(req, handler, bindings.GetAsyncWaiter())
-
-	closer, e1, e2 := d.Scan(query, ptr)
-	if e1 != nil {
-		close(ch)
-		return nil, nil, errors.New(e1.Msg)
-	}
-	if e2 != nil {
-		close(ch)
-		return nil, nil, e2
-	}
-
-	go func() {
-		for {
-			if err := stub.ServeRequest(); err != nil {
-				connErr, ok := err.(*bindings.ConnectionError)
-				if !ok || !connErr.Closed() {
-					log.Println(err)
-				}
-				break
-			}
-		}
-	}()
-
-	stop := func() {
-		p := mojom.NewCloserProxy(*closer, bindings.GetAsyncWaiter())
-		p.Close()
-		p.Close_Proxy()
-		stub.Close()
-		close(ch)
-	}
-	return ch, stop, nil
-}
-
-func scanAndMatch(d mojom.Discovery, query string, wants ...mojom.Advertisement) error {
-	const timeout = 10 * time.Second
-
-	var err error
-	for now := time.Now(); time.Since(now) < timeout; {
-		var updatePtrs []mojom.Update_Pointer
-		updatePtrs, err = doScan(d, query, len(wants))
-		if err != nil {
-			return err
-		}
-		err = matchFound(updatePtrs, wants...)
-		if err == nil {
-			return nil
-		}
-	}
-	return err
-}
-
-func doScan(d mojom.Discovery, query string, expectedUpdates int) ([]mojom.Update_Pointer, error) {
-	scanCh, stop, err := scan(d, query)
-	if err != nil {
-		return nil, err
-	}
-	defer func() {
-		stop()
-		for range scanCh {
-		}
-	}()
-
-	updatePtrs := make([]mojom.Update_Pointer, 0, expectedUpdates)
-	for {
-		var timer <-chan time.Time
-		if len(updatePtrs) >= expectedUpdates {
-			timer = time.After(5 * time.Millisecond)
-		}
-
-		select {
-		case ptr := <-scanCh:
-			updatePtrs = append(updatePtrs, ptr)
-		case <-timer:
-			return updatePtrs, nil
-		}
-	}
-}
-
-func matchFound(updatePtrs []mojom.Update_Pointer, wants ...mojom.Advertisement) error {
-	return match(updatePtrs, false, wants...)
-}
-
-func matchLost(updatePtrs []mojom.Update_Pointer, wants ...mojom.Advertisement) error {
-	return match(updatePtrs, true, wants...)
-}
-
-func match(updatePtrs []mojom.Update_Pointer, lost bool, wants ...mojom.Advertisement) error {
-	updateMap := make(map[[internal.AdIdLen]uint8]mojom.Update)
-	updates := make([]mojom.Update, 0)
-	for _, ptr := range updatePtrs {
-		update := mojom.NewUpdateProxy(ptr, bindings.GetAsyncWaiter())
-		defer update.Close_Proxy()
-
-		id, _ := update.GetId()
-		updateMap[id] = update
-		updates = append(updates, update)
-	}
-	for _, want := range wants {
-		update := updateMap[*want.Id]
-		if update == nil {
-			break
-		}
-		if got, _ := update.IsLost(); got != lost {
-			break
-		}
-		if !updateEqual(update, want) {
-			break
-		}
-		delete(updateMap, *want.Id)
-	}
-	if len(updateMap) == 0 {
-		return nil
-	}
-
-	return fmt.Errorf("Match failed; got %v, but wanted %v", updatesToDebugString(updates), adsToDebugString(wants))
-}
-
-func updateEqual(update mojom.Update, ad mojom.Advertisement) bool {
-	if got, _ := update.GetId(); got != *ad.Id {
-		return false
-	}
-	if got, _ := update.GetInterfaceName(); got != ad.InterfaceName {
-		return false
-	}
-	if got, _ := update.GetAddresses(); !reflect.DeepEqual(got, ad.Addresses) {
-		return false
-	}
-	if ad.Attributes != nil {
-		for k, v := range *ad.Attributes {
-			if got, _ := update.GetAttribute(k); got != v {
-				return false
-			}
-		}
-	}
-	if ad.Attachments != nil {
-		for k, v := range *ad.Attachments {
-			h, err := update.GetAttachment(k)
-			if err != nil {
-				return false
-			}
-			defer h.Close()
-			h.Wait(system.MOJO_HANDLE_SIGNAL_READABLE, system.MOJO_DEADLINE_INDEFINITE)
-			r, got := h.ReadData(system.MOJO_READ_DATA_FLAG_ALL_OR_NONE)
-			if r != system.MOJO_RESULT_OK {
-				return false
-			}
-			if !bytes.Equal(got, v) {
-				return false
-			}
-		}
-	}
-	if got, _ := update.GetAdvertisement(); !reflect.DeepEqual(got, ad) {
-		return false
-	}
-	return true
-}
-
-func adsToDebugString(ads []mojom.Advertisement) string {
-	var strs []string
-	for _, ad := range ads {
-		strs = append(strs, adToDebugString(ad))
-	}
-	return "[]" + strings.Join(strs, ", ") + "]"
-}
-
-func adToDebugString(ad mojom.Advertisement) string {
-	return "{" + strings.Join(dumpFields(ad), ", ") + "}"
-}
-
-func updatesToDebugString(updates []mojom.Update) string {
-	var strs []string
-	for _, u := range updates {
-		strs = append(strs, updateToDebugString(u))
-	}
-	return "[]" + strings.Join(strs, ", ") + "]"
-}
-
-func updateToDebugString(update mojom.Update) string {
-	lost, _ := update.IsLost()
-	ad, _ := update.GetAdvertisement()
-	return fmt.Sprintf("{%v, %v}", lost, strings.Join(dumpFields(ad), ", "))
-}
-
-func dumpFields(i interface{}) []string {
-	var fields []string
-	for rv, i := reflect.ValueOf(i), 0; i < rv.NumField(); i++ {
-		fields = append(fields, fmt.Sprint(rv.Field(i)))
-	}
-	return fields
-}
diff --git a/go/src/v.io/mojo/discovery/apptest/global_discovery_apptest.go b/go/src/v.io/mojo/discovery/apptest/global_discovery_apptest.go
new file mode 100644
index 0000000..6dabf9f
--- /dev/null
+++ b/go/src/v.io/mojo/discovery/apptest/global_discovery_apptest.go
@@ -0,0 +1,130 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build mojo
+
+package apptest
+
+import (
+	"encoding/hex"
+	"fmt"
+	"net/url"
+	"testing"
+	"time"
+
+	"mojo/public/go/application"
+	"mojo/public/go/bindings"
+
+	mojom "mojom/v.io/discovery"
+
+	_ "v.io/x/ref/runtime/factories/generic"
+
+	"v.io/mojo/discovery/internal"
+)
+
+// TODO(jhahn): Mojom 'const' is ignored in mojom.go.
+// See https://github.com/domokit/mojo/issues/685.
+const (
+	QueryGlobal       = "global"
+	QueryMountTTL     = "mount_ttl"
+	QueryScanInterval = "scan_interval"
+)
+
+func newGlobalDiscovery(mctx application.Context, scanInterval time.Duration) *mojom.Discovery_Proxy {
+	u, _ := url.Parse("https://mojo.v.io/discovery.mojo")
+	q := u.Query()
+	q.Set(QueryGlobal, "a/b/c")
+	q.Set(QueryScanInterval, fmt.Sprintf("%.3fs", scanInterval.Seconds()))
+	u.RawQuery = q.Encode()
+
+	req, ptr := mojom.CreateMessagePipeForDiscovery()
+	mctx.ConnectToApplication(u.String()).ConnectToService(&req)
+	return mojom.NewDiscoveryProxy(ptr, bindings.GetAsyncWaiter())
+}
+
+func AppTestGlobalDiscoveryBasic(t *testing.T, mctx application.Context) {
+	ads := []mojom.Advertisement{
+		{
+			Id:        &[internal.AdIdLen]uint8{1, 2, 3},
+			Addresses: []string{"/h1:123/x"},
+		},
+		{
+			Addresses: []string{"/h1:123/y"},
+		},
+	}
+
+	d1 := newGlobalDiscovery(mctx, 0)
+	defer d1.Close_Proxy()
+
+	var stops []func()
+	for i, ad := range ads {
+		id, closer, e1, e2 := d1.Advertise(ad, nil)
+		if e1 != nil || e2 != nil {
+			t.Fatalf("ad[%d]: failed to advertise: %v, %v", i, e1, e2)
+		}
+		if id == nil {
+			t.Errorf("ad[%d]: got nil id", i)
+			continue
+		}
+		if ad.Id == nil {
+			ads[i].Id = id
+		} else if *id != *ad.Id {
+			t.Errorf("ad[%d]: got ad id %v, but wanted %v", i, *id, *ad.Id)
+		}
+
+		stop := func() {
+			p := mojom.NewCloserProxy(*closer, bindings.GetAsyncWaiter())
+			p.Close()
+			p.Close_Proxy()
+		}
+		stops = append(stops, stop)
+	}
+
+	// Make sure none of advertisements are discoverable by the same discovery instance.
+	if err := scanAndMatch(d1, ``); err != nil {
+		t.Error(err)
+	}
+
+	// Create a new discovery instance. All advertisements should be discovered with that.
+	d2 := newGlobalDiscovery(mctx, 1*time.Millisecond)
+	defer d2.Close_Proxy()
+
+	if err := scanAndMatch(d2, `k="01020300000000000000000000000000"`, ads[0]); err != nil {
+		t.Error(err)
+	}
+	if err := scanAndMatch(d2, ``, ads...); err != nil {
+		t.Error(err)
+	}
+
+	// Open a new scan channel and consume expected advertisements first.
+	scanCh, scanStop, err := scan(d2, `k="01020300000000000000000000000000"`)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer scanStop()
+
+	update := <-scanCh
+	if err := matchFound([]mojom.Update_Pointer{update}, ads[0]); err != nil {
+		t.Error(err)
+	}
+
+	// Make sure scan returns the lost advertisement when advertising is stopped.
+	stops[0]()
+
+	update = <-scanCh
+	if err := matchLost([]mojom.Update_Pointer{update}, ads[0]); err != nil {
+		t.Error(err)
+	}
+
+	// Also it shouldn't affect the other.
+	if err := scanAndMatch(d2, fmt.Sprintf(`k="%s"`, hex.EncodeToString(ads[1].Id[:])), ads[1]); err != nil {
+		t.Error(err)
+	}
+
+	// Stop advertising the remaining one; Shouldn't discover any advertisements.
+	stops[1]()
+	if err := scanAndMatch(d2, ``); err != nil {
+		t.Error(err)
+	}
+}
diff --git a/go/src/v.io/mojo/discovery/discovery.go b/go/src/v.io/mojo/discovery/discovery.go
index 181f4cc..6006cbf 100644
--- a/go/src/v.io/mojo/discovery/discovery.go
+++ b/go/src/v.io/mojo/discovery/discovery.go
@@ -18,11 +18,13 @@
 
 	"v.io/v23"
 	"v.io/v23/context"
+	"v.io/v23/naming"
 
 	idiscovery "v.io/x/ref/lib/discovery"
 	fdiscovery "v.io/x/ref/lib/discovery/factory"
 	"v.io/x/ref/lib/discovery/plugins/mock"
 	"v.io/x/ref/runtime/factories/roaming"
+	"v.io/x/ref/services/mounttable/mounttablelib"
 
 	"v.io/mojo/discovery/internal"
 )
@@ -31,7 +33,7 @@
 import "C"
 
 var (
-	flagUseMock = flag.Bool("use-mock", false, "Use a mock plugin for mojo apptests.")
+	flagTestMode = flag.Bool("test-mode", false, "should only be true for apptests.")
 )
 
 type delegate struct {
@@ -50,21 +52,34 @@
 	roaming.SetArgs(mctx)
 	d.ctx, d.shutdown = v23.Init()
 
-	if *flagUseMock {
+	if *flagTestMode {
+		// Inject a mock plugin.
 		df, _ := idiscovery.NewFactory(d.ctx, mock.New())
 		fdiscovery.InjectFactory(df)
+
+		// Start a mounttable and set the namespace roots.
+		name, _, err := mounttablelib.StartServers(d.ctx, v23.GetListenSpec(d.ctx), "", "", "", "", "mounttable")
+		if err != nil {
+			panic(err)
+		}
+		ns := v23.GetNamespace(d.ctx)
+		ns.SetRoots(name)
+		ns.CacheCtl(naming.DisableCache(true))
 	}
 }
 
-func (d *delegate) Create(request mojom.Discovery_Request) {
-	discovery := internal.NewDiscovery(d.ctx)
-	stub := mojom.NewDiscoveryStub(request, discovery, bindings.GetAsyncWaiter())
+func (d *delegate) AcceptConnection(connection *application.Connection) {
+	f := &factory{d, connection.ConnectionURL()}
+	connection.ProvideServices(&mojom.Discovery_ServiceFactory{f})
+}
+
+func (d *delegate) run(stub *bindings.Stub, done func()) {
 	d.mu.Lock()
 	d.stubs[stub] = struct{}{}
 	d.mu.Unlock()
 
 	go func() {
-		defer discovery.Close()
+		defer done()
 
 		for {
 			if err := stub.ServeRequest(); err != nil {
@@ -82,10 +97,6 @@
 	}()
 }
 
-func (d *delegate) AcceptConnection(connection *application.Connection) {
-	connection.ProvideServices(&mojom.Discovery_ServiceFactory{d})
-}
-
 func (d *delegate) Quit() {
 	d.mu.Lock()
 	for stub := range d.stubs {
@@ -95,6 +106,22 @@
 	d.shutdown()
 }
 
+type factory struct {
+	d   *delegate
+	url string
+}
+
+func (f *factory) Create(request mojom.Discovery_Request) {
+	discovery, err := internal.NewDiscovery(f.d.ctx, f.url)
+	if err != nil {
+		f.d.ctx.Error(err)
+		request.Close()
+		return
+	}
+	stub := mojom.NewDiscoveryStub(request, discovery, bindings.GetAsyncWaiter())
+	f.d.run(stub, discovery.Close)
+}
+
 //export MojoMain
 func MojoMain(handle C.MojoHandle) C.MojoResult {
 	application.Run(&delegate{stubs: map[*bindings.Stub]struct{}{}}, system.MojoHandle(handle))
diff --git a/go/src/v.io/mojo/discovery/internal/discovery.go b/go/src/v.io/mojo/discovery/internal/discovery.go
index 17f5056..b6a900d 100644
--- a/go/src/v.io/mojo/discovery/internal/discovery.go
+++ b/go/src/v.io/mojo/discovery/internal/discovery.go
@@ -5,7 +5,9 @@
 package internal
 
 import (
+	"net/url"
 	"sync"
+	"time"
 
 	"mojo/public/go/bindings"
 
@@ -14,11 +16,19 @@
 	"v.io/v23"
 	"v.io/v23/context"
 	"v.io/v23/discovery"
+
+	"v.io/x/ref/lib/discovery/global"
 )
 
 // TODO(jhahn): Mojom 'const' is ignored in mojom.go.
 // See https://github.com/domokit/mojo/issues/685.
-const AdIdLen = 16
+const (
+	AdIdLen = 16
+
+	QueryGlobal       = "global"
+	QueryMountTTL     = "mount_ttl"
+	QueryScanInterval = "scan_interval"
+)
 
 // closer implements the mojom.Closer.
 type closer struct {
@@ -30,7 +40,7 @@
 	return nil
 }
 
-type discoveryCloser interface {
+type DiscoveryCloser interface {
 	mojom.Discovery
 
 	// Close closes all active tasks.
@@ -141,29 +151,47 @@
 	}
 }
 
-// ediscovery always returns the given error.
-type ediscovery struct{ err error }
-
-func (d *ediscovery) Advertise(mojom.Advertisement, *[]string) (*[AdIdLen]uint8, *mojom.Closer_Pointer, *mojom.Error, error) {
-	return nil, nil, v2mError(d.err), nil
-}
-func (d *ediscovery) Scan(string, mojom.ScanHandler_Pointer) (*mojom.Closer_Pointer, *mojom.Error, error) {
-	return nil, v2mError(d.err), nil
-}
-func (d *ediscovery) Close() {}
-
 // NewDiscovery returns a new Vanadium discovery instance.
-func NewDiscovery(ctx *context.T) discoveryCloser {
-	d, err := v23.NewDiscovery(ctx)
+func NewDiscovery(ctx *context.T, connectionUrl string) (DiscoveryCloser, error) {
+	d, err := newDiscovery(ctx, connectionUrl)
 	if err != nil {
-		return &ediscovery{err}
+		return nil, err
 	}
 
 	ctx, cancel := context.WithCancel(ctx)
-	return &mdiscovery{
+	md := &mdiscovery{
 		ctx:    ctx,
 		cancel: cancel,
 		d:      d,
 		stubs:  make(map[*bindings.Stub]struct{}),
 	}
+	return md, nil
+}
+
+func newDiscovery(ctx *context.T, connectionUrl string) (discovery.T, error) {
+	u, err := url.ParseRequestURI(connectionUrl)
+	if err != nil {
+		return nil, err
+	}
+
+	q := u.Query()
+	if _, ok := q[QueryGlobal]; ok {
+		mountTTL, err := parseDuration(q.Get(QueryMountTTL))
+		if err != nil {
+			return nil, err
+		}
+		scanInterval, err := parseDuration(q.Get(QueryScanInterval))
+		if err != nil {
+			return nil, err
+		}
+		return global.NewWithTTL(ctx, q.Get(QueryGlobal), mountTTL, scanInterval)
+	}
+	return v23.NewDiscovery(ctx)
+}
+
+func parseDuration(s string) (time.Duration, error) {
+	if len(s) == 0 {
+		return 0, nil
+	}
+	return time.ParseDuration(s)
 }
diff --git a/java/app/build.gradle b/java/app/build.gradle
index 1a63fbf..160b242 100644
--- a/java/app/build.gradle
+++ b/java/app/build.gradle
@@ -64,7 +64,7 @@
 }
 
 dependencies {
-    compile 'io.v:vanadium-android:1.11+'
+    compile 'io.v:vanadium-android:1.12+'
     compile 'com.android.support:multidex:1.0.0'
 
     // Mojo system should not be included in binary.
diff --git a/java/app/src/main/java/io/v/mojo/discovery/DiscoveryApp.java b/java/app/src/main/java/io/v/mojo/discovery/DiscoveryApp.java
index 83433d1..62cbd18 100644
--- a/java/app/src/main/java/io/v/mojo/discovery/DiscoveryApp.java
+++ b/java/app/src/main/java/io/v/mojo/discovery/DiscoveryApp.java
@@ -7,6 +7,10 @@
 import android.content.Context;
 import android.util.Log;
 
+import com.google.common.collect.ImmutableList;
+
+import java.io.File;
+
 import org.chromium.mojo.application.ApplicationConnection;
 import org.chromium.mojo.application.ApplicationDelegate;
 import org.chromium.mojo.application.ApplicationRunner;
@@ -18,9 +22,9 @@
 
 import io.v.android.v23.V;
 import io.v.v23.context.VContext;
-import io.v.v23.verror.VException;
 
 import io.v.impl.google.lib.discovery.FactoryUtil;
+import io.v.impl.google.services.mounttable.MountTableServer;
 
 /**
  * Android mojo application providing the vanadium discovery service.
@@ -29,20 +33,39 @@
     static final String TAG = "DiscoveryApp";
 
     private final Core mCore;
-    private final VContext mContext;
+    private VContext mContext;
+
+    private File mCacheDir;
 
     DiscoveryApp(Core core, Context context) {
         mCore = core;
         mContext = V.init(context);
+
+        // TODO(jhahn): MountTableServer always requires a storage directory for now.
+        // Remove this once this bug is fixed.
+        mCacheDir = context.getCacheDir();
     }
 
     @Override
     public void initialize(Shell shell, String[] args, String url) {
         for (String arg : args) {
-            if (arg.matches("-{1,2}use-mock")) {
+            if (arg.matches("-{1,2}test-mode")) {
                 try {
+                    // Inject a mock plugin.
                     FactoryUtil.injectMockPlugin(mContext);
-                } catch (VException e) {
+
+                    // Start a mounttable and set the namespace roots.
+                    File storageRoot = new File(mCacheDir, "mounttable");
+                    storageRoot.mkdirs();
+                    mContext =
+                            MountTableServer.withNewServer(
+                                    mContext,
+                                    new MountTableServer.Params()
+                                            .withStatsPrefix("mounttable")
+                                            .withStorageRootDir(storageRoot.getAbsolutePath()));
+                    String name = V.getServer(mContext).getStatus().getEndpoints()[0].name();
+                    V.getNamespace(mContext).setRoots(ImmutableList.of(name));
+                } catch (Exception e) {
                     Log.e(TAG, e.toString());
                 }
                 break;
@@ -51,14 +74,17 @@
     }
 
     @Override
-    public boolean configureIncomingConnection(ApplicationConnection applicationConnection) {
+    public boolean configureIncomingConnection(final ApplicationConnection applicationConnection) {
         applicationConnection.addService(
                 new ServiceFactoryBinder<Discovery>() {
                     @Override
                     public void bind(InterfaceRequest<Discovery> request) {
                         try {
-                            Discovery.MANAGER.bind(new DiscoveryImpl(mCore, mContext), request);
-                        } catch (VException e) {
+                            Discovery.MANAGER.bind(
+                                    new DiscoveryImpl(
+                                            mCore, mContext, applicationConnection.connectionUrl()),
+                                    request);
+                        } catch (Exception e) {
                             Log.e(TAG, e.toString());
                             request.close();
                         }
diff --git a/java/app/src/main/java/io/v/mojo/discovery/DiscoveryImpl.java b/java/app/src/main/java/io/v/mojo/discovery/DiscoveryImpl.java
index 134ac89..406e4e4 100644
--- a/java/app/src/main/java/io/v/mojo/discovery/DiscoveryImpl.java
+++ b/java/app/src/main/java/io/v/mojo/discovery/DiscoveryImpl.java
@@ -4,6 +4,8 @@
 
 package io.v.mojo.discovery;
 
+import android.net.Uri;
+
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.MoreExecutors;
 
@@ -12,6 +14,11 @@
 import org.chromium.mojo.system.Core;
 import org.chromium.mojo.system.MojoException;
 import org.chromium.mojo.system.RunLoop;
+import org.joda.time.DateTimeZone;
+import org.joda.time.Duration;
+import org.joda.time.format.PeriodFormatter;
+import org.joda.time.format.PeriodFormatterBuilder;
+import org.joda.time.tz.UTCProvider;
 
 import io.v.android.v23.V;
 import io.v.v23.InputChannelCallback;
@@ -20,15 +27,34 @@
 import io.v.v23.security.BlessingPattern;
 import io.v.v23.verror.VException;
 
+import io.v.impl.google.lib.discovery.GlobalDiscovery;
+
 class DiscoveryImpl implements Discovery {
     private final Core mCore;
     private final VContext mContext;
     private final io.v.v23.discovery.Discovery mDiscovery;
 
-    DiscoveryImpl(Core core, VContext context) throws VException {
+    static {
+        // TODO(jhahn): To avoid IOException: 'Resource not found: "org/joda/time/tz/data/ZoneInfoMap"'
+        // Load resources or find other way to parse duration strings.
+        DateTimeZone.setProvider(new UTCProvider());
+    }
+
+    DiscoveryImpl(Core core, VContext context, String connectionUrl) throws Exception {
         mCore = core;
         mContext = context.withCancel();
-        mDiscovery = V.newDiscovery(mContext);
+
+        Uri uri = Uri.parse(connectionUrl);
+        String global = uri.getQueryParameter(DiscoveryConstants.QUERY_GLOBAL);
+        if (global != null) {
+            Duration mountTTL =
+                    parseDuration(uri.getQueryParameter(DiscoveryConstants.QUERY_MOUNT_TTL));
+            Duration scanInterval =
+                    parseDuration(uri.getQueryParameter(DiscoveryConstants.QUERY_SCAN_INTERVAL));
+            mDiscovery = GlobalDiscovery.newDiscovery(mContext, global, mountTTL, scanInterval);
+        } else {
+            mDiscovery = V.newDiscovery(mContext);
+        }
     }
 
     private static class CloserImpl implements Closer {
@@ -76,6 +102,22 @@
         public void onConnectionError(MojoException e) {}
     }
 
+    private static Duration parseDuration(String duration) {
+        if (duration == null || duration.equals("0")) {
+            return Duration.ZERO;
+        }
+        PeriodFormatter formatter =
+                new PeriodFormatterBuilder()
+                        .appendHours()
+                        .appendSuffix("h")
+                        .appendMinutes()
+                        .appendSuffix("m")
+                        .appendSecondsWithOptionalMillis()
+                        .appendSuffix("s")
+                        .toFormatter();
+        return formatter.parsePeriod(duration).toStandardDuration();
+    }
+
     @Override
     public void advertise(Advertisement ad, String[] visibility, AdvertiseResponse callback) {
         try {
diff --git a/lib/gen/dart-gen/mojom/lib/discovery/discovery.mojom.dart b/lib/gen/dart-gen/mojom/lib/discovery/discovery.mojom.dart
index 827cac0..ab497bb 100644
--- a/lib/gen/dart-gen/mojom/lib/discovery/discovery.mojom.dart
+++ b/lib/gen/dart-gen/mojom/lib/discovery/discovery.mojom.dart
@@ -7,6 +7,9 @@
 import 'package:mojo/bindings.dart' as bindings;
 import 'package:mojo/core.dart' as core;
 import 'package:mojo/mojo/bindings/types/service_describer.mojom.dart' as service_describer;
+const String queryGlobal = "global";
+const String queryMountTtl = "mount_ttl";
+const String queryScanInterval = "scan_interval";
 
 
 
@@ -1870,8 +1873,8 @@
   }
 }
 
-const int _Discovery_advertiseName = 0;
-const int _Discovery_scanName = 1;
+const int _discoveryMethodAdvertiseName = 0;
+const int _discoveryMethodScanName = 1;
 
 class _DiscoveryServiceDescription implements service_describer.ServiceDescription {
   dynamic getTopLevelInterface([Function responseFactory]) =>
@@ -1911,7 +1914,7 @@
 
   void handleResponse(bindings.ServiceMessage message) {
     switch (message.header.type) {
-      case _Discovery_advertiseName:
+      case _discoveryMethodAdvertiseName:
         var r = DiscoveryAdvertiseResponseParams.deserialize(
             message.payload);
         if (!message.header.hasRequestId) {
@@ -1931,7 +1934,7 @@
         }
         c.complete(r);
         break;
-      case _Discovery_scanName:
+      case _discoveryMethodScanName:
         var r = DiscoveryScanResponseParams.deserialize(
             message.payload);
         if (!message.header.hasRequestId) {
@@ -1975,7 +1978,7 @@
       params.visibility = visibility;
       return _proxyImpl.sendMessageWithRequestId(
           params,
-          _Discovery_advertiseName,
+          _discoveryMethodAdvertiseName,
           -1,
           bindings.MessageHeader.kMessageExpectsResponse);
     }
@@ -1985,7 +1988,7 @@
       params.handler = handler;
       return _proxyImpl.sendMessageWithRequestId(
           params,
-          _Discovery_scanName,
+          _discoveryMethodScanName,
           -1,
           bindings.MessageHeader.kMessageExpectsResponse);
     }
@@ -2070,18 +2073,18 @@
   }
 
 
-  DiscoveryAdvertiseResponseParams _DiscoveryAdvertiseResponseParamsFactory(List<int> instanceId, Object closer, Error err) {
-    var mojo_factory_result = new DiscoveryAdvertiseResponseParams();
-    mojo_factory_result.instanceId = instanceId;
-    mojo_factory_result.closer = closer;
-    mojo_factory_result.err = err;
-    return mojo_factory_result;
+  DiscoveryAdvertiseResponseParams _discoveryAdvertiseResponseParamsFactory(List<int> instanceId, Object closer, Error err) {
+    var result = new DiscoveryAdvertiseResponseParams();
+    result.instanceId = instanceId;
+    result.closer = closer;
+    result.err = err;
+    return result;
   }
-  DiscoveryScanResponseParams _DiscoveryScanResponseParamsFactory(Object closer, Error err) {
-    var mojo_factory_result = new DiscoveryScanResponseParams();
-    mojo_factory_result.closer = closer;
-    mojo_factory_result.err = err;
-    return mojo_factory_result;
+  DiscoveryScanResponseParams _discoveryScanResponseParamsFactory(Object closer, Error err) {
+    var result = new DiscoveryScanResponseParams();
+    result.closer = closer;
+    result.err = err;
+    return result;
   }
 
   dynamic handleMessage(bindings.ServiceMessage message) {
@@ -2092,16 +2095,16 @@
     }
     assert(_impl != null);
     switch (message.header.type) {
-      case _Discovery_advertiseName:
+      case _discoveryMethodAdvertiseName:
         var params = _DiscoveryAdvertiseParams.deserialize(
             message.payload);
-        var response = _impl.advertise(params.ad,params.visibility,_DiscoveryAdvertiseResponseParamsFactory);
+        var response = _impl.advertise(params.ad,params.visibility,_discoveryAdvertiseResponseParamsFactory);
         if (response is Future) {
           return response.then((response) {
             if (response != null) {
               return buildResponseWithId(
                   response,
-                  _Discovery_advertiseName,
+                  _discoveryMethodAdvertiseName,
                   message.header.requestId,
                   bindings.MessageHeader.kMessageIsResponse);
             }
@@ -2109,21 +2112,21 @@
         } else if (response != null) {
           return buildResponseWithId(
               response,
-              _Discovery_advertiseName,
+              _discoveryMethodAdvertiseName,
               message.header.requestId,
               bindings.MessageHeader.kMessageIsResponse);
         }
         break;
-      case _Discovery_scanName:
+      case _discoveryMethodScanName:
         var params = _DiscoveryScanParams.deserialize(
             message.payload);
-        var response = _impl.scan(params.query,params.handler,_DiscoveryScanResponseParamsFactory);
+        var response = _impl.scan(params.query,params.handler,_discoveryScanResponseParamsFactory);
         if (response is Future) {
           return response.then((response) {
             if (response != null) {
               return buildResponseWithId(
                   response,
-                  _Discovery_scanName,
+                  _discoveryMethodScanName,
                   message.header.requestId,
                   bindings.MessageHeader.kMessageIsResponse);
             }
@@ -2131,7 +2134,7 @@
         } else if (response != null) {
           return buildResponseWithId(
               response,
-              _Discovery_scanName,
+              _discoveryMethodScanName,
               message.header.requestId,
               bindings.MessageHeader.kMessageIsResponse);
         }
@@ -2165,7 +2168,7 @@
   }
 }
 
-const int _Closer_closeName = 0;
+const int _closerMethodCloseName = 0;
 
 class _CloserServiceDescription implements service_describer.ServiceDescription {
   dynamic getTopLevelInterface([Function responseFactory]) =>
@@ -2204,7 +2207,7 @@
 
   void handleResponse(bindings.ServiceMessage message) {
     switch (message.header.type) {
-      case _Closer_closeName:
+      case _closerMethodCloseName:
         var r = CloserCloseResponseParams.deserialize(
             message.payload);
         if (!message.header.hasRequestId) {
@@ -2246,7 +2249,7 @@
       var params = new _CloserCloseParams();
       return _proxyImpl.sendMessageWithRequestId(
           params,
-          _Closer_closeName,
+          _closerMethodCloseName,
           -1,
           bindings.MessageHeader.kMessageExpectsResponse);
     }
@@ -2331,9 +2334,9 @@
   }
 
 
-  CloserCloseResponseParams _CloserCloseResponseParamsFactory() {
-    var mojo_factory_result = new CloserCloseResponseParams();
-    return mojo_factory_result;
+  CloserCloseResponseParams _closerCloseResponseParamsFactory() {
+    var result = new CloserCloseResponseParams();
+    return result;
   }
 
   dynamic handleMessage(bindings.ServiceMessage message) {
@@ -2344,14 +2347,14 @@
     }
     assert(_impl != null);
     switch (message.header.type) {
-      case _Closer_closeName:
-        var response = _impl.close(_CloserCloseResponseParamsFactory);
+      case _closerMethodCloseName:
+        var response = _impl.close(_closerCloseResponseParamsFactory);
         if (response is Future) {
           return response.then((response) {
             if (response != null) {
               return buildResponseWithId(
                   response,
-                  _Closer_closeName,
+                  _closerMethodCloseName,
                   message.header.requestId,
                   bindings.MessageHeader.kMessageIsResponse);
             }
@@ -2359,7 +2362,7 @@
         } else if (response != null) {
           return buildResponseWithId(
               response,
-              _Closer_closeName,
+              _closerMethodCloseName,
               message.header.requestId,
               bindings.MessageHeader.kMessageIsResponse);
         }
@@ -2393,7 +2396,7 @@
   }
 }
 
-const int _ScanHandler_onUpdateName = 0;
+const int _scanHandlerMethodOnUpdateName = 0;
 
 class _ScanHandlerServiceDescription implements service_describer.ServiceDescription {
   dynamic getTopLevelInterface([Function responseFactory]) =>
@@ -2457,7 +2460,7 @@
       }
       var params = new _ScanHandlerOnUpdateParams();
       params.update = update;
-      _proxyImpl.sendMessage(params, _ScanHandler_onUpdateName);
+      _proxyImpl.sendMessage(params, _scanHandlerMethodOnUpdateName);
     }
 }
 
@@ -2549,7 +2552,7 @@
     }
     assert(_impl != null);
     switch (message.header.type) {
-      case _ScanHandler_onUpdateName:
+      case _scanHandlerMethodOnUpdateName:
         var params = _ScanHandlerOnUpdateParams.deserialize(
             message.payload);
         _impl.onUpdate(params.update);
@@ -2583,13 +2586,13 @@
   }
 }
 
-const int _Update_isLostName = 0;
-const int _Update_getIdName = 1;
-const int _Update_getInterfaceNameName = 2;
-const int _Update_getAddressesName = 3;
-const int _Update_getAttributeName = 4;
-const int _Update_getAttachmentName = 5;
-const int _Update_getAdvertisementName = 6;
+const int _updateMethodIsLostName = 0;
+const int _updateMethodGetIdName = 1;
+const int _updateMethodGetInterfaceNameName = 2;
+const int _updateMethodGetAddressesName = 3;
+const int _updateMethodGetAttributeName = 4;
+const int _updateMethodGetAttachmentName = 5;
+const int _updateMethodGetAdvertisementName = 6;
 
 class _UpdateServiceDescription implements service_describer.ServiceDescription {
   dynamic getTopLevelInterface([Function responseFactory]) =>
@@ -2634,7 +2637,7 @@
 
   void handleResponse(bindings.ServiceMessage message) {
     switch (message.header.type) {
-      case _Update_isLostName:
+      case _updateMethodIsLostName:
         var r = UpdateIsLostResponseParams.deserialize(
             message.payload);
         if (!message.header.hasRequestId) {
@@ -2654,7 +2657,7 @@
         }
         c.complete(r);
         break;
-      case _Update_getIdName:
+      case _updateMethodGetIdName:
         var r = UpdateGetIdResponseParams.deserialize(
             message.payload);
         if (!message.header.hasRequestId) {
@@ -2674,7 +2677,7 @@
         }
         c.complete(r);
         break;
-      case _Update_getInterfaceNameName:
+      case _updateMethodGetInterfaceNameName:
         var r = UpdateGetInterfaceNameResponseParams.deserialize(
             message.payload);
         if (!message.header.hasRequestId) {
@@ -2694,7 +2697,7 @@
         }
         c.complete(r);
         break;
-      case _Update_getAddressesName:
+      case _updateMethodGetAddressesName:
         var r = UpdateGetAddressesResponseParams.deserialize(
             message.payload);
         if (!message.header.hasRequestId) {
@@ -2714,7 +2717,7 @@
         }
         c.complete(r);
         break;
-      case _Update_getAttributeName:
+      case _updateMethodGetAttributeName:
         var r = UpdateGetAttributeResponseParams.deserialize(
             message.payload);
         if (!message.header.hasRequestId) {
@@ -2734,7 +2737,7 @@
         }
         c.complete(r);
         break;
-      case _Update_getAttachmentName:
+      case _updateMethodGetAttachmentName:
         var r = UpdateGetAttachmentResponseParams.deserialize(
             message.payload);
         if (!message.header.hasRequestId) {
@@ -2754,7 +2757,7 @@
         }
         c.complete(r);
         break;
-      case _Update_getAdvertisementName:
+      case _updateMethodGetAdvertisementName:
         var r = UpdateGetAdvertisementResponseParams.deserialize(
             message.payload);
         if (!message.header.hasRequestId) {
@@ -2796,7 +2799,7 @@
       var params = new _UpdateIsLostParams();
       return _proxyImpl.sendMessageWithRequestId(
           params,
-          _Update_isLostName,
+          _updateMethodIsLostName,
           -1,
           bindings.MessageHeader.kMessageExpectsResponse);
     }
@@ -2804,7 +2807,7 @@
       var params = new _UpdateGetIdParams();
       return _proxyImpl.sendMessageWithRequestId(
           params,
-          _Update_getIdName,
+          _updateMethodGetIdName,
           -1,
           bindings.MessageHeader.kMessageExpectsResponse);
     }
@@ -2812,7 +2815,7 @@
       var params = new _UpdateGetInterfaceNameParams();
       return _proxyImpl.sendMessageWithRequestId(
           params,
-          _Update_getInterfaceNameName,
+          _updateMethodGetInterfaceNameName,
           -1,
           bindings.MessageHeader.kMessageExpectsResponse);
     }
@@ -2820,7 +2823,7 @@
       var params = new _UpdateGetAddressesParams();
       return _proxyImpl.sendMessageWithRequestId(
           params,
-          _Update_getAddressesName,
+          _updateMethodGetAddressesName,
           -1,
           bindings.MessageHeader.kMessageExpectsResponse);
     }
@@ -2829,7 +2832,7 @@
       params.name = name;
       return _proxyImpl.sendMessageWithRequestId(
           params,
-          _Update_getAttributeName,
+          _updateMethodGetAttributeName,
           -1,
           bindings.MessageHeader.kMessageExpectsResponse);
     }
@@ -2838,7 +2841,7 @@
       params.name = name;
       return _proxyImpl.sendMessageWithRequestId(
           params,
-          _Update_getAttachmentName,
+          _updateMethodGetAttachmentName,
           -1,
           bindings.MessageHeader.kMessageExpectsResponse);
     }
@@ -2846,7 +2849,7 @@
       var params = new _UpdateGetAdvertisementParams();
       return _proxyImpl.sendMessageWithRequestId(
           params,
-          _Update_getAdvertisementName,
+          _updateMethodGetAdvertisementName,
           -1,
           bindings.MessageHeader.kMessageExpectsResponse);
     }
@@ -2931,40 +2934,40 @@
   }
 
 
-  UpdateIsLostResponseParams _UpdateIsLostResponseParamsFactory(bool lost) {
-    var mojo_factory_result = new UpdateIsLostResponseParams();
-    mojo_factory_result.lost = lost;
-    return mojo_factory_result;
+  UpdateIsLostResponseParams _updateIsLostResponseParamsFactory(bool lost) {
+    var result = new UpdateIsLostResponseParams();
+    result.lost = lost;
+    return result;
   }
-  UpdateGetIdResponseParams _UpdateGetIdResponseParamsFactory(List<int> id) {
-    var mojo_factory_result = new UpdateGetIdResponseParams();
-    mojo_factory_result.id = id;
-    return mojo_factory_result;
+  UpdateGetIdResponseParams _updateGetIdResponseParamsFactory(List<int> id) {
+    var result = new UpdateGetIdResponseParams();
+    result.id = id;
+    return result;
   }
-  UpdateGetInterfaceNameResponseParams _UpdateGetInterfaceNameResponseParamsFactory(String interfaceName) {
-    var mojo_factory_result = new UpdateGetInterfaceNameResponseParams();
-    mojo_factory_result.interfaceName = interfaceName;
-    return mojo_factory_result;
+  UpdateGetInterfaceNameResponseParams _updateGetInterfaceNameResponseParamsFactory(String interfaceName) {
+    var result = new UpdateGetInterfaceNameResponseParams();
+    result.interfaceName = interfaceName;
+    return result;
   }
-  UpdateGetAddressesResponseParams _UpdateGetAddressesResponseParamsFactory(List<String> addresses) {
-    var mojo_factory_result = new UpdateGetAddressesResponseParams();
-    mojo_factory_result.addresses = addresses;
-    return mojo_factory_result;
+  UpdateGetAddressesResponseParams _updateGetAddressesResponseParamsFactory(List<String> addresses) {
+    var result = new UpdateGetAddressesResponseParams();
+    result.addresses = addresses;
+    return result;
   }
-  UpdateGetAttributeResponseParams _UpdateGetAttributeResponseParamsFactory(String attribute) {
-    var mojo_factory_result = new UpdateGetAttributeResponseParams();
-    mojo_factory_result.attribute = attribute;
-    return mojo_factory_result;
+  UpdateGetAttributeResponseParams _updateGetAttributeResponseParamsFactory(String attribute) {
+    var result = new UpdateGetAttributeResponseParams();
+    result.attribute = attribute;
+    return result;
   }
-  UpdateGetAttachmentResponseParams _UpdateGetAttachmentResponseParamsFactory(core.MojoDataPipeConsumer data) {
-    var mojo_factory_result = new UpdateGetAttachmentResponseParams();
-    mojo_factory_result.data = data;
-    return mojo_factory_result;
+  UpdateGetAttachmentResponseParams _updateGetAttachmentResponseParamsFactory(core.MojoDataPipeConsumer data) {
+    var result = new UpdateGetAttachmentResponseParams();
+    result.data = data;
+    return result;
   }
-  UpdateGetAdvertisementResponseParams _UpdateGetAdvertisementResponseParamsFactory(Advertisement ad) {
-    var mojo_factory_result = new UpdateGetAdvertisementResponseParams();
-    mojo_factory_result.ad = ad;
-    return mojo_factory_result;
+  UpdateGetAdvertisementResponseParams _updateGetAdvertisementResponseParamsFactory(Advertisement ad) {
+    var result = new UpdateGetAdvertisementResponseParams();
+    result.ad = ad;
+    return result;
   }
 
   dynamic handleMessage(bindings.ServiceMessage message) {
@@ -2975,14 +2978,14 @@
     }
     assert(_impl != null);
     switch (message.header.type) {
-      case _Update_isLostName:
-        var response = _impl.isLost(_UpdateIsLostResponseParamsFactory);
+      case _updateMethodIsLostName:
+        var response = _impl.isLost(_updateIsLostResponseParamsFactory);
         if (response is Future) {
           return response.then((response) {
             if (response != null) {
               return buildResponseWithId(
                   response,
-                  _Update_isLostName,
+                  _updateMethodIsLostName,
                   message.header.requestId,
                   bindings.MessageHeader.kMessageIsResponse);
             }
@@ -2990,19 +2993,19 @@
         } else if (response != null) {
           return buildResponseWithId(
               response,
-              _Update_isLostName,
+              _updateMethodIsLostName,
               message.header.requestId,
               bindings.MessageHeader.kMessageIsResponse);
         }
         break;
-      case _Update_getIdName:
-        var response = _impl.getId(_UpdateGetIdResponseParamsFactory);
+      case _updateMethodGetIdName:
+        var response = _impl.getId(_updateGetIdResponseParamsFactory);
         if (response is Future) {
           return response.then((response) {
             if (response != null) {
               return buildResponseWithId(
                   response,
-                  _Update_getIdName,
+                  _updateMethodGetIdName,
                   message.header.requestId,
                   bindings.MessageHeader.kMessageIsResponse);
             }
@@ -3010,19 +3013,19 @@
         } else if (response != null) {
           return buildResponseWithId(
               response,
-              _Update_getIdName,
+              _updateMethodGetIdName,
               message.header.requestId,
               bindings.MessageHeader.kMessageIsResponse);
         }
         break;
-      case _Update_getInterfaceNameName:
-        var response = _impl.getInterfaceName(_UpdateGetInterfaceNameResponseParamsFactory);
+      case _updateMethodGetInterfaceNameName:
+        var response = _impl.getInterfaceName(_updateGetInterfaceNameResponseParamsFactory);
         if (response is Future) {
           return response.then((response) {
             if (response != null) {
               return buildResponseWithId(
                   response,
-                  _Update_getInterfaceNameName,
+                  _updateMethodGetInterfaceNameName,
                   message.header.requestId,
                   bindings.MessageHeader.kMessageIsResponse);
             }
@@ -3030,19 +3033,19 @@
         } else if (response != null) {
           return buildResponseWithId(
               response,
-              _Update_getInterfaceNameName,
+              _updateMethodGetInterfaceNameName,
               message.header.requestId,
               bindings.MessageHeader.kMessageIsResponse);
         }
         break;
-      case _Update_getAddressesName:
-        var response = _impl.getAddresses(_UpdateGetAddressesResponseParamsFactory);
+      case _updateMethodGetAddressesName:
+        var response = _impl.getAddresses(_updateGetAddressesResponseParamsFactory);
         if (response is Future) {
           return response.then((response) {
             if (response != null) {
               return buildResponseWithId(
                   response,
-                  _Update_getAddressesName,
+                  _updateMethodGetAddressesName,
                   message.header.requestId,
                   bindings.MessageHeader.kMessageIsResponse);
             }
@@ -3050,21 +3053,21 @@
         } else if (response != null) {
           return buildResponseWithId(
               response,
-              _Update_getAddressesName,
+              _updateMethodGetAddressesName,
               message.header.requestId,
               bindings.MessageHeader.kMessageIsResponse);
         }
         break;
-      case _Update_getAttributeName:
+      case _updateMethodGetAttributeName:
         var params = _UpdateGetAttributeParams.deserialize(
             message.payload);
-        var response = _impl.getAttribute(params.name,_UpdateGetAttributeResponseParamsFactory);
+        var response = _impl.getAttribute(params.name,_updateGetAttributeResponseParamsFactory);
         if (response is Future) {
           return response.then((response) {
             if (response != null) {
               return buildResponseWithId(
                   response,
-                  _Update_getAttributeName,
+                  _updateMethodGetAttributeName,
                   message.header.requestId,
                   bindings.MessageHeader.kMessageIsResponse);
             }
@@ -3072,21 +3075,21 @@
         } else if (response != null) {
           return buildResponseWithId(
               response,
-              _Update_getAttributeName,
+              _updateMethodGetAttributeName,
               message.header.requestId,
               bindings.MessageHeader.kMessageIsResponse);
         }
         break;
-      case _Update_getAttachmentName:
+      case _updateMethodGetAttachmentName:
         var params = _UpdateGetAttachmentParams.deserialize(
             message.payload);
-        var response = _impl.getAttachment(params.name,_UpdateGetAttachmentResponseParamsFactory);
+        var response = _impl.getAttachment(params.name,_updateGetAttachmentResponseParamsFactory);
         if (response is Future) {
           return response.then((response) {
             if (response != null) {
               return buildResponseWithId(
                   response,
-                  _Update_getAttachmentName,
+                  _updateMethodGetAttachmentName,
                   message.header.requestId,
                   bindings.MessageHeader.kMessageIsResponse);
             }
@@ -3094,19 +3097,19 @@
         } else if (response != null) {
           return buildResponseWithId(
               response,
-              _Update_getAttachmentName,
+              _updateMethodGetAttachmentName,
               message.header.requestId,
               bindings.MessageHeader.kMessageIsResponse);
         }
         break;
-      case _Update_getAdvertisementName:
-        var response = _impl.getAdvertisement(_UpdateGetAdvertisementResponseParamsFactory);
+      case _updateMethodGetAdvertisementName:
+        var response = _impl.getAdvertisement(_updateGetAdvertisementResponseParamsFactory);
         if (response is Future) {
           return response.then((response) {
             if (response != null) {
               return buildResponseWithId(
                   response,
-                  _Update_getAdvertisementName,
+                  _updateMethodGetAdvertisementName,
                   message.header.requestId,
                   bindings.MessageHeader.kMessageIsResponse);
             }
@@ -3114,7 +3117,7 @@
         } else if (response != null) {
           return buildResponseWithId(
               response,
-              _Update_getAdvertisementName,
+              _updateMethodGetAdvertisementName,
               message.header.requestId,
               bindings.MessageHeader.kMessageIsResponse);
         }
diff --git a/mojoapptests b/mojoapptests
index 88c0b2f..992d966 100644
--- a/mojoapptests
+++ b/mojoapptests
@@ -5,7 +5,7 @@
   {
     "test": "https://test.v.io/discovery_apptests.mojo",
     "test-args": [],
-    "shell-args": ["--args-for=https://mojo.v.io/discovery.mojo --use-mock"],
+    "shell-args": ["--args-for=https://mojo.v.io/discovery.mojo --test-mode"],
     "timeout": 120,
   },
   # Testing dart apptests timeout failure with a large timeout value.
@@ -14,7 +14,7 @@
     "test": "https://dart.test.mojo.v.io/discovery_apptests.dart",
     "type": "dart",
     "dart_strict_mode": True,
-    "shell-args": ["--args-for=https://mojo.v.io/discovery.mojo --use-mock"],
-    "timeout": 1200,
+    "shell-args": ["--args-for=https://mojo.v.io/discovery.mojo --test-mode"],
+    "timeout": 120,
   }
 ]
diff --git a/mojom/v.io/discovery.mojom b/mojom/v.io/discovery.mojom
index 41786cb..12f60bd 100644
--- a/mojom/v.io/discovery.mojom
+++ b/mojom/v.io/discovery.mojom
@@ -53,7 +53,23 @@
   string msg;
 };
 
+// Url query parameters for global discovery service.
+const string QUERY_GLOBAL = "global";
+const string QUERY_MOUNT_TTL = "mount_ttl";
+const string QUERY_SCAN_INTERVAL = "scan_interval";
+
 // Discovery provides Vanadium discovery service.
+//
+// Global discovery:
+//   You can connect to a global discovery service that uses the Vanadium namespace
+//   under |path| by passing a query string "global=<path>". Optionally you can
+//   set |mount_ttl| (default is 120s) and |scan_interval| (default is 90s) with
+//   query strings "mount_ttl=<mount_ttl>" and "scan_interval=<scan_interval>".
+//   A duration string is a sequence of decimal numbers, each with a unit suffix,
+//   such as "90s" or "1m30s". Valid time units are "s", "m", and "h".
+//
+//   Global discovery is an experimental work to see its feasibility and set the
+//   long-term goal, and can be changed without notice.
 [ServiceName="v23::discovery::Discovery"]
 interface Discovery {
   // Broadcasts the advertisement to be discovered by |Scan| operations.