Commit a01ce812 authored by Kirill Smelkov's avatar Kirill Smelkov

.

parent 1bbebed5
This diff is collapsed.
# Copyright (C) 2018 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.
# TODO doc, code
# XXX see lonet.go for lonet organization and protocol.
class SQLiteRegistry(object):
def __init__(self, dburi, network):
# XXX open
self.setup(network)
# TODO
// Copyright (C) 2018 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.
package lonet
import (
"context"
"fmt"
"io"
"net"
"reflect"
"testing"
"golang.org/x/sync/errgroup"
"lab.nexedi.com/kirr/go123/exc"
)
// FIXME dup from pipenet:
// ---- 8< ----
type mklistener interface {
Listen(string) (net.Listener, error)
}
func xlisten(n mklistener, laddr string) net.Listener {
l, err := n.Listen(laddr)
exc.Raiseif(err)
return l
}
func xaccept(l net.Listener) net.Conn {
c, err := l.Accept()
exc.Raiseif(err)
return c
}
type dialer interface {
Dial(context.Context, string) (net.Conn, error)
}
func xdial(n dialer, addr string) net.Conn {
c, err := n.Dial(context.Background(), addr)
exc.Raiseif(err)
return c
}
func xread(r io.Reader) string {
buf := make([]byte, 4096)
n, err := r.Read(buf)
exc.Raiseif(err)
return string(buf[:n])
}
func xwrite(w io.Writer, data string) {
_, err := w.Write([]byte(data))
exc.Raiseif(err)
}
func xwait(w interface { Wait() error }) {
err := w.Wait()
exc.Raiseif(err)
}
func assertEq(t *testing.T, a, b interface{}) {
t.Helper()
if !reflect.DeepEqual(a, b) {
fmt.Printf("not equal:\nhave: %v\nwant: %v\n", a, b)
t.Errorf("not equal:\nhave: %v\nwant: %v", a, b)
exc.Raise(0)
}
}
// ---- 8< ----
// TODO test go-go
// TODO test go-py
// XXX handshake:
// - ok,
// - econnrefused (no such host, port not listening)
// - network mismatch
// - invalid request
func TestLonet(t *testing.T) {
// FIXME ~100% dup from TestPipeNet
X := exc.Raiseif
ctx := context.Background()
subnet, err := Join(ctx, "")
X(err)
// XXX defer shutdown/rm this lonet fully?
xaddr := func(addr string) *Addr {
a, err := subnet.parseAddr(addr)
X(err)
return a
}
, err := subnet.NewHost(ctx, "α")
X(err)
, err := subnet.NewHost(ctx, "β")
X(err)
assertEq(t, .Network(), subnet.Network())
assertEq(t, .Network(), subnet.Network())
assertEq(t, .Name(), "α")
assertEq(t, .Name(), "β")
l1, err := .Listen("")
X(err)
assertEq(t, l1.Addr(), xaddr("α:1"))
// zero port always stays unused even after autobind
_, err = .Dial(ctx, ":0")
assertEq(t, err, &net.OpError{Op: "dial", Net: subnet.Network(), Addr: xaddr("α:0"), Err: errConnRefused})
wg := &errgroup.Group{}
wg.Go(exc.Funcx(func() {
c1s := xaccept(l1)
assertEq(t, c1s.LocalAddr(), xaddr("α:2"))
assertEq(t, c1s.RemoteAddr(), xaddr("β:1"))
assertEq(t, xread(c1s), "ping")
xwrite(c1s, "pong")
c2s := xaccept(l1)
assertEq(t, c2s.LocalAddr(), xaddr("α:3"))
assertEq(t, c2s.RemoteAddr(), xaddr("β:2"))
assertEq(t, xread(c2s), "hello")
xwrite(c2s, "world")
}))
c1c := xdial(, "α:1")
assertEq(t, c1c.LocalAddr(), xaddr("β:1"))
assertEq(t, c1c.RemoteAddr(), xaddr("α:2"))
xwrite(c1c, "ping")
assertEq(t, xread(c1c), "pong")
c2c := xdial(, "α:1")
assertEq(t, c2c.LocalAddr(), xaddr("β:2"))
assertEq(t, c2c.RemoteAddr(), xaddr("α:3"))
xwrite(c2c, "hello")
assertEq(t, xread(c2c), "world")
xwait(wg)
l2 := xlisten(, ":0") // autobind again
assertEq(t, l2.Addr(), xaddr("α:4"))
}
// Copyright (C) 2018 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.
package lonet
// registry of network hosts.
import (
"context"
"errors"
"fmt"
)
// registry represents access to lonet network registry.
//
// The registry holds information about hosts available on the network, and
// for each host its OS listening address. Whenever host α needs to establish XXX dup with lonet.go
// connection to address on host β, it queries the registry for β and further
// talks to β on that address. Correspondingly when a host joins the network,
// it announces itself to the registry so that other hosts could see it.
//
// The registry could be implemented in several ways, for example:
//
// - dedicated network server,
// - hosts broadcasting information to each other similar to ARP,
// - shared memory or file,
// - ...
type registry interface {
// XXX + network name?
// Announce announces host to registry.
//
// The host is named as hostname on lonet network and is listening for
// incoming lonet protocol connections on OS-level osladdr address.
//
// Returned error, if !nil, is *registryError with .Err describing the
// error cause:
//
// - errRegistryDown if registry cannot be accessed, XXX (and its underlying cause?)
// - errHostDup if hostname was already announced,
// - some other error indicating e.g. IO problem.
Announce(ctx context.Context, hostname, osladdr string) error
// Query queries registry for host.
//
// If successful - it returns OS-level network address to connect to
// the host via lonet protocol handshake.
//
// Returned error, if !nil, is *registryError with .Err describing the
// error cause:
//
// - errRegistryDown if registry cannot be accessed, XXX ^^^
// - errNoHost if hostname was not announced to registry,
// - some other error indicating e.g. IO problem.
Query(ctx context.Context, hostname string) (osladdr string, _ error)
// Close closes access to registry.
//
// Close interrupts all in-flight Announce and Query requests started
// via closing registry connection. Those interrupted requests will
// return with errRegistryDown error cause.
Close() error
}
var errRegistryDown = errors.New("registry is down")
var errNoHost = errors.New("no such host")
var errHostDup = errors.New("host already registered")
// registryError represents an error of a registry operation.
type registryError struct {
// XXX name of the network? - XXX yes
Registry string // name of the registry
Op string // operation that failed
Args interface{} // operation arguments, if any
Err error // actual error that occurred during the operation
}
func (e *registryError) Error() string {
s := e.Registry + ": " + e.Op
if e.Args != nil {
s += fmt.Sprintf(" %s", e.Args)
}
s += ": " + e.Err.Error()
return s
}
func (e *registryError) Cause() error {
return e.Err
}
// Copyright (C) 2018 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.
package lonet
// registry implemented as shared SQLite file.
import (
"context"
"errors"
"fmt"
"crawshaw.io/sqlite"
"crawshaw.io/sqlite/sqliteutil"
"lab.nexedi.com/kirr/go123/xerr"
)
// registry schema (keep in sync wrt .setup())
//
// hosts:
// hostname text !null PK
// osladdr text !null
//
// meta:
// name text !null PK
// value text !null
//
// "schemaver" text - version of db schema.
// "network" text - name of lonet network this registry serves.
const schemaVer = "lonet.1"
type sqliteRegistry struct {
dbpool *sqlite.Pool
uri string // URI db was originally opened with
}
// openRegistrySQLite opens SQLite registry located at dburi.
//
// the registry is setup/verified to be serving specified lonet network.
func openRegistrySQLite(ctx context.Context, dburi, network string) (_ *sqliteRegistry, err error) {
r := &sqliteRegistry{uri: dburi}
defer r.regerr(&err, "open")
dbpool, err := sqlite.Open(dburi, 0, /* poolSize= */16) // XXX pool size ok?
if err != nil {
return nil, err
}
r.dbpool = dbpool
// initialize/check db
err = r.setup(ctx, network)
if err != nil {
r.Close()
return nil, err
}
return r, nil
}
// Close implements registry.
func (r *sqliteRegistry) Close() (err error) {
defer r.regerr(&err, "close")
return r.dbpool.Close()
}
// withConn runs f on a dbpool connection.
//
// connection is first allocated from dbpool and put back after call to f.
func (r *sqliteRegistry) withConn(ctx context.Context, f func(*sqlite.Conn) error) error {
conn := r.dbpool.Get(ctx.Done())
if conn == nil {
// either ctx cancel or dbpool close
if ctx.Err() != nil {
return ctx.Err()
}
return errRegistryDown // db closed
}
defer r.dbpool.Put(conn)
return f(conn)
}
var errNoRows = errors.New("query: empty result")
var errManyRows = errors.New("query: multiple results")
// query1 is like sqliteutil.Exec but checks that exactly 1 row is returned.
//
// if query results in no rows - errNoRows is returned.
// if query results in more than 1 row - errManyRows is returned.
func query1(conn *sqlite.Conn, query string, resultf func(stmt *sqlite.Stmt), argv ...interface{}) error {
nrow := 0
err := sqliteutil.Exec(conn, query, func(stmt *sqlite.Stmt) error {
if nrow == 1 {
return errManyRows
}
nrow++
resultf(stmt)
return nil
}, argv...)
if err != nil {
return err
}
if nrow == 0 {
return errNoRows
}
return nil
}
// setup initializes/checks registry database to be of expected schema and configuration.
func (r *sqliteRegistry) setup(ctx context.Context, network string) (err error) {
defer xerr.Contextf(&err, "setup %q", network)
return r.withConn(ctx, func(conn *sqlite.Conn) (err error) {
// NOTE: keep in sync wrt top-level text.
err = sqliteutil.ExecScript(conn, `
CREATE TABLE IF NOT EXISTS hosts (
hostname TEXT NON NULL PRIMARY KEY,
osladdr TEXT NON NULL
);
CREATE TABLE IF NOT EXISTS meta (
name TEXT NON NULL PRIMARY KEY,
value TEXT NON NULL
);
`)
if err != nil {
return err
}
// do whole checks/init under transaction, so that there is
// e.g. no race wrt another process setting config.
defer sqliteutil.Save(conn)(&err)
// check/init schema version
ver, err := r.config(conn, "schemaver")
if err != nil {
return err
}
if ver == "" {
ver = schemaVer
err = r.setConfig(conn, "schemaver", ver)
if err != nil {
return err
}
}
if ver != schemaVer {
return fmt.Errorf("schema version mismatch: want %q; have %q", schemaVer, ver)
}
// check/init network name
dbnetwork, err := r.config(conn, "network")
if err != nil {
return err
}
if dbnetwork == "" {
dbnetwork = network
err = r.setConfig(conn, "network", dbnetwork)
if err != nil {
return err
}
}
if dbnetwork != network {
return fmt.Errorf("network name mismatch: want %q; have %q", network, dbnetwork)
}
return nil
})
}
// config gets one registry configuration value by name.
//
// if there is no record corresponding to name - ("", nil) is returned.
// XXX add ok ret to indicate presence of value?
func (r *sqliteRegistry) config(conn *sqlite.Conn, name string) (value string, err error) {
defer xerr.Contextf(&err, "config: get %q", name)
err = query1(conn, "SELECT value FROM meta WHERE name = ?", func(stmt *sqlite.Stmt) {
value = stmt.ColumnText(0)
}, name)
switch err {
case errNoRows:
return "", nil
case errManyRows:
value = ""
}
return value, err
}
// setConfig sets one registry configuration value by name.
func (r *sqliteRegistry) setConfig(conn *sqlite.Conn, name, value string) (err error) {
defer xerr.Contextf(&err, "config: set %q = %q", name, value)
err = sqliteutil.Exec(conn,
"INSERT OR REPLACE INTO meta (name, value) VALUES (?, ?)", nil,
name, value)
return err
}
// Announce implements registry.
func (r *sqliteRegistry) Announce(ctx context.Context, hostname, osladdr string) (err error) {
defer r.regerr(&err, "announce", hostname, osladdr)
return r.withConn(ctx, func(conn *sqlite.Conn) error {
err := sqliteutil.Exec(conn,
"INSERT INTO hosts (hostname, osladdr) VALUES (?, ?)", nil,
hostname, osladdr)
switch sqlite.ErrCode(err) {
case sqlite.SQLITE_CONSTRAINT_UNIQUE:
err = errHostDup
}
return err
})
}
var errRegDup = errors.New("registry broken: duplicate host entries")
// Query implements registry.
func (r *sqliteRegistry) Query(ctx context.Context, hostname string) (osladdr string, err error) {
defer r.regerr(&err, "query", hostname)
err = r.withConn(ctx, func(conn *sqlite.Conn) error {
err := query1(conn, "SELECT osladdr FROM hosts WHERE hostname = ?",
func (stmt *sqlite.Stmt) {
osladdr = stmt.ColumnText(0)
}, hostname)
switch err {
case errNoRows:
return errNoHost
case errManyRows:
// hostname is PK - we should not get several results
osladdr = ""
return errRegDup
}
return err
})
return osladdr, err
}
// regerr is syntactic sugar to wrap !nil *errp into registryError.
//
// intended too be used like
//
// defer r.regerr(&err, "operation", arg1, arg2, ...)
func (r *sqliteRegistry) regerr(errp *error, op string, args ...interface{}) {
if *errp == nil {
return
}
// XXX name of the network
*errp = &registryError{
Registry: r.uri,
Op: op,
Args: args,
Err: *errp,
}
}
// Copyright (C) 2018 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.
package lonet
import (
"context"
"fmt"
"io/ioutil"
"os"
"testing"
"lab.nexedi.com/kirr/go123/exc"
)
func TestRegistrySQLite(t *testing.T) {
X := exc.Raiseif
work, err := ioutil.TempDir("", "t-registry-sqlite")
X(err)
defer os.RemoveAll(work)
dbpath := work + "/1.db"
ctx := context.Background()
r, err := openRegistrySQLite(ctx, dbpath, "aaa")
X(err)
// query checks that result of Query(hostname) is as expected.
//
// if expect is error - it checks that Query returns error with cause == expect.
// otherwise expect must be string and it will check that Query
// succeeds and returns osladdr == expect.
query := func(r *sqliteRegistry, hostname string, expect interface{}) {
// XXX ^^^ -> `r registry` (needs .Network() to get network name) ?
t.Helper()
osladdr, err := r.Query(ctx, hostname)
if ewant, iserr := expect.(error); iserr {
// error expected
// XXX construct full registry error around ewant + reflect.DeepCompare?
e, ok := err.(*registryError)
if !(ok && e.Err == ewant && osladdr == "") {
t.Fatalf("%s: query %q:\nwant: \"\", %v\nhave: %q, %v",
r.uri, hostname, ewant, osladdr, err)
}
} else {
// !error expected
laddr := expect.(string)
if !(osladdr == laddr && err == nil) {
t.Fatalf("%s: query %q:\nwant: %q, nil\nhave: %q, %v",
r.uri, hostname, laddr, osladdr, err)
}
}
}
// announce checks that result of Announce(hostname, osladdr) is as expected.
//
// if len(errv) == 1 - it checks that Announce returns error with cause == errv[0].
// otherwise it will check that Announce succeeds and returns nil error.
announce := func(r *sqliteRegistry, hostname, osladdr string, errv ...error) {
t.Helper()
err := r.Announce(ctx, hostname, osladdr)
var ewant error
if len(errv) > 0 {
ewant = errv[0]
if len(errv) > 1 {
panic("only 1 error allowed in announce check")
}
}
if ewant != nil {
// error expected
// XXX construct full registry error around ewant + reflect.DeepCompare?
e, ok := err.(*registryError)
if (!ok && e.Err == ewant) {
t.Fatalf("%s: announce %q %q:\nwant %v\nhave: %v",
r.uri, hostname, osladdr, ewant, err)
}
} else {
// !error expected
if err != nil {
t.Fatalf("%s: announce %q %q: %s", r.uri, hostname, osladdr, err)
}
}
}
ø := errNoHost
// r.Network() == ...
query(r, "α", ø)
announce(r, "α", "alpha:1234")
announce(r, "α", "alpha:1234", errHostDup)
announce(r, "α", "alpha:1235", errHostDup)
query(r, "α", "alpha:1234")
query(r, "β", ø)
r2, err := openRegistrySQLite(ctx, dbpath, "aaa")
// r2.Network() == ...
query(r2, "α", "alpha:1234")
query(r2, "β", ø)
announce(r2, "β", "beta:zzz")
query(r2, "β", "beta:zzz")
query(r, "β", "beta:zzz")
X(r.Close())
query(r, "α", errRegistryDown)
query(r, "β", errRegistryDown)
announce(r, "γ", "gamma:qqq", errRegistryDown)
query(r, "γ", errRegistryDown)
query(r2, "α", "alpha:1234")
X(r2.Close())
query(r2, "α", errRegistryDown)
// verify network mismatch detection works
r3, err := openRegistrySQLite(ctx, dbpath, "bbb")
if !(r3 == nil && err != nil) {
t.Fatalf("network mismatch: not detected")
}
errWant := fmt.Sprintf(`%s: open []: setup "bbb": network name mismatch: want "bbb"; have "aaa"`, dbpath)
if err.Error() != errWant {
t.Fatalf("network mismatch: error:\nhave: %q\nwant: %q", err.Error(), errWant)
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment