A distributed, transactional,
fault-tolerant object store

Go client

The Go client is Go gettable:

> go get goshawkdb.io/client

The client API is documented at https://godoc.org/goshawkdb.io/client.

The code on this page, along with the Howtos are available in the examples repository.

The Collections Library is available for the Go client.

Some general notes:

  • It is recommended that you read the guide to the data-model in conjunction with this guide.
  • One connection to a server can do one transaction at a time. If you need to run multiple transactions concurrently then you need to use multiple connections.
  • As with any data store which supports transactions, given that the transaction can fail to commit, you need to ensure that your transaction function is side-effect free: it should not alter any state until after the transaction commits. The client will automatically re-run transaction functions whenever necessary.
  • Nested transactions are very useful for keeping your code clean: using nested transactions ensures that you don't need to keep track of whether you already have a transaction open somewhere in your stack. Nested transactions are implemented as a client-side feature: committing a nested transaction does not need any communication with the server so they have very little cost.

Connection life-cycle

The Go client authenticates to the server using X509 certificates. These certificates can be generated by the goshawkdb server, and this is demonstrated in the getting started guide. The client also optionally uses the public certificate of the server to verify the identity of the server to which it is connecting. The examples in this guide directly embed the client certificate and key, and the server certificate, in the source code. These require adjusting for your own installation. If you wish to instead store these certificates in an external file, you may find Go's ioutil.ReadFile method useful.

To create a connection, use NewConnection. To dispose of the connection, call Shutdown on it:

package main

import (
	"fmt"
	"goshawkdb.io/client"
)

const (
	clusterCertPEM = `-----BEGIN CERTIFICATE-----
MIIBxzCCAW2gAwIBAgIIQqu37k6KPOIwCgYIKoZIzj0EAwIwOjESMBAGA1UEChMJ
R29zaGF3a0RCMSQwIgYDVQQDExtDbHVzdGVyIENBIFJvb3QgQ2VydGlmaWNhdGUw
IBcNMTYwMTAzMDkwODE2WhgPMjIxNjAxMDMwOTA4MTZaMDoxEjAQBgNVBAoTCUdv
c2hhd2tEQjEkMCIGA1UEAxMbQ2x1c3RlciBDQSBSb290IENlcnRpZmljYXRlMFkw
EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjHBXt+0n477zVZHTsGgu9rLYzNz/WMLm
l7/KC5v2nx+RC9yfkyfBKq8jJk3KYoB/YJ7s8BH0T456/+nRQIUo7qNbMFkwDgYD
VR0PAQH/BAQDAgIEMA8GA1UdEwEB/wQFMAMBAf8wGQYDVR0OBBIEEL9sxrcr6QTw
wk5csm2ZcfgwGwYDVR0jBBQwEoAQv2zGtyvpBPDCTlyybZlx+DAKBggqhkjOPQQD
AgNIADBFAiAy9NW3zE1ACYDWcp+qeTjQOfEtED3c/LKIXhrbzg2N/QIhANLb4crz
9ENxIifhZcJ/S2lqf49xZZS91dLF4x5ApKci
-----END CERTIFICATE-----`
	clientCertAndKeyPEM = `-----BEGIN CERTIFICATE-----
MIIBszCCAVmgAwIBAgIIfOmxD9dF8ZMwCgYIKoZIzj0EAwIwOjESMBAGA1UEChMJ
R29zaGF3a0RCMSQwIgYDVQQDExtDbHVzdGVyIENBIFJvb3QgQ2VydGlmaWNhdGUw
IBcNMTYwMTAzMDkwODUwWhgPMjIxNjAxMDMwOTA4NTBaMBQxEjAQBgNVBAoTCUdv
c2hhd2tEQjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABFrAPcdlw5DWQmS9mCFX
FlD6R8ABaBf4LA821wVmPa9tiM6n8vRJvbmHuSjy8LwJJRRjo9GJq7KD6ZmsK9P9
sXijbTBrMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAjAMBgNV
HRMBAf8EAjAAMBkGA1UdDgQSBBBX9qcbG4ofUoUTHGwOgGvFMBsGA1UdIwQUMBKA
EL9sxrcr6QTwwk5csm2ZcfgwCgYIKoZIzj0EAwIDSAAwRQIgOK9PVJt7KdvDU/9v
z9gQI8JnVLZm+6gsh6ro9WnaZ8YCIQDXhjfQAWaUmJNTgKq3rLHiEbPS4Mxl7h7S
kbkX/2GIjg==
-----END CERTIFICATE-----
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIN9Mf6CzDgCs1EbzJqDK3+12wcr7Ua3Huz6qNhyXCrS1oAoGCCqGSM49
AwEHoUQDQgAEWsA9x2XDkNZCZL2YIVcWUPpHwAFoF/gsDzbXBWY9r22Izqfy9Em9
uYe5KPLwvAklFGOj0YmrsoPpmawr0/2xeA==
-----END EC PRIVATE KEY-----`
)

func main() {
	conn, err := client.NewConnection("hostname:7894", []byte(clientCertAndKeyPEM), []byte(clusterCertPEM))
	if err != nil {
		fmt.Println(err)
		return
	}
	defer conn.Shutdown()
	// now do some work
}

hostname:port may specify a hostname, an FQDN or an IP address. The port component is optional: if omitted the default port of 7894 is used. The clientCertAndKeyPEM is used to authenticate the client. If the fingerprint of the client certificate does not exist in the cluster configuration, the client will not be authenticated and the connection will be terminated. The clusterCertPEM is recommended but not required: if nil, the client will not be able to verify the server to which it connections. The clusterCertPEM is the public certificate part of the certificate and key pair generated with the -gen-cluster-cert command line parameter. NewConnection will not return until either a connection is fully established and ready for use, or has failed. Similarly, Shutdown will not return until the connection is closed.

Running a transaction

Use the Connection.RunTransaction method. This method automatically detects if a transaction is already in progress and will create nested transactions as necessary. There is no limit to how many nested transactions you can create. The function that you supply is the transaction function and will automatically be run as many times as necessary until it is either able to successfully commit or chooses to abort. The final values that the transaction function returns are returned to the caller of RunTransaction along with some statistics regarding how the transaction progressed.

package main

import (
	"fmt"
	"goshawkdb.io/client"
)

const (
	clusterCertPEM      = `...`
	clientCertAndKeyPEM = `...`
)

func main() {
	conn, err := client.NewConnection("hostname:7894", []byte(clientCertAndKeyPEM), []byte(clusterCertPEM))
	if err != nil {
		fmt.Println(err)
		return
	}
	defer conn.Shutdown()
	result, _, err := conn.RunTransaction(func(txn *client.Txn) (interface{}, error) {
		return "hello", nil
	})
	fmt.Println(result, err)
}

This will print out hello, nil.

To abort a transaction, return a custom non-nil error. To start a retry, return Retry.

Objects

GoshawkDB stores an object graph. Every object has a unique Id and objects can have references (pointers) to other objects. Every object also has a value, which is a byte array ([]byte). GoshawkDB never needs to inspect the value of objects so the value is completely opaque and you are free to use any serialization you wish. The only requirement is that if one object points to another, you must declare that.

When the client first connects to the server, the server informs the client of the object Ids of the various root objects to which the client has access. Initially, the root objects are the only objects that you can access. As soon as you access an object, you gain access to the objects that are referenced (pointed to), as per the restrictions of the capabilities on each reference. Thus you can navigate the object graph. There are no restrictions on the shape of the object graph: cycles are permitted as is aliasing - it is a full graph, not a tree (unlike, say JSON).

Objects are loaded on demand silently in the background as you navigate. Sometimes this will lead the client to realise that there is no point continuing with the current transaction: it cannot possibly commit so it should be restarted. For example: consider that client A loads x and finds that x has the value 7. Now client B runs a transaction that sets both x = 8 and y = 3 and this transaction commits. Now client A loads y. At this point, client A is informed of the transaction that client B committed and so it knows both the value of y and that the value of x has changed. As a result client A knows its transaction cannot commit and must be restarted. Allowing client A's transaction function to continue at this point would both waste time and CPU, and also violate isolation (though it's normally acceptable for isolation to be violated provided the transaction is never going to commit anyway).

Because Go doesn't support any advanced control flow functions (e.g. call-with-continuation), when this situation occurs any attempt to access the references or values of any object will return an error, and that error will be the Restart constant. When this occurs, you should immediately return the same Restart constant as the error in the transaction function and the transaction function will immediately be restarted as necessary.

package main

import (
	"encoding/binary"
	"errors"
	"fmt"
	"goshawkdb.io/client"
)

const (
	clusterCertPEM      = `...`
	clientCertAndKeyPEM = `...`
)

func main() {
	conn, err := client.NewConnection("hostname:7894", []byte(clientCertAndKeyPEM), []byte(clusterCertPEM))
	if err != nil {
		return
	}
	defer conn.Shutdown()

	result, _, err := conn.RunTransaction(func(txn *client.Txn) (interface{}, error) {
		rootObjs, err := txn.GetRootObjects()
		if err != nil {
			return nil, err
		}
		rootObj, found := rootObjs["myRoot1"]
		if !found {
			return nil, errors.New("No root 'myRoot1' found")
		}
		value := make([]byte, 8)
		binary.LittleEndian.PutUint64(value, 42)
		err = rootObj.Set(value)
		if err != nil {
			return nil, err
		}
		return "success!", nil
	})
	fmt.Println(result, err)

	result, _, err = conn.RunTransaction(func(txn *client.Txn) (interface{}, error) {
		rootObjs, err := txn.GetRootObjects()
		if err != nil {
			return nil, err
		}
		rootObj, found := rootObjs["myRoot1"]
		if !found {
			return nil, errors.New("No root 'myRoot1' found")
		}
		value, err := rootObj.Value()
		if err != nil {
			return nil, err
		}
		return fmt.Sprintf("Found value: %v", binary.LittleEndian.Uint64(value)), nil
	})
	fmt.Println(result, err)
}

Creating objects and adding references to them is easy too:

package main

import (
	"errors"
	"fmt"
	"goshawkdb.io/client"
)

const (
	clusterCertPEM      = `...`
	clientCertAndKeyPEM = `...`
)

func main() {
	conn, err := client.NewConnection("hostname:7894", []byte(clientCertAndKeyPEM), []byte(clusterCertPEM))
	if err != nil {
		fmt.Println(err)
		return
	}
	defer conn.Shutdown()

	result, _, err := conn.RunTransaction(func(txn *client.Txn) (interface{}, error) {
		rootObjs, err := txn.GetRootObjects()
		if err != nil {
			return nil, err
		}
		rootObj, found := rootObjs["myRoot1"]
		if !found {
			return nil, errors.New("No root 'myRoot1' found")
		}
		myObj, err := txn.CreateObject([]byte("a new value for a new object"))
		if err != nil {
			return nil, err
		}
		rootObj.Set([]byte{}, myObj) // Root now has no value and one reference
		return "success!", nil
	})
	fmt.Println(result, err)

	result, _, err = conn.RunTransaction(func(txn *client.Txn) (interface{}, error) {
		rootObjs, err := txn.GetRootObjects()
		if err != nil {
			return nil, err
		}
		rootObj, found := rootObjs["myRoot1"]
		if !found {
			return nil, errors.New("No root 'myRoot1' found")
		}
		refs, err := rootObj.References()
		if err != nil {
			return nil, err
		}
		myObj := refs[0]
		value, err := myObj.Value()
		if err != nil {
			return nil, err
		}
		return fmt.Sprintf("Found value: %s", value), nil
	})
	fmt.Println(result, err)
}

Retry

Retry is an incredibly powerful feature. Within a transaction, if you retry, what you're saying is: "Look at all the objects I've read from in this transaction. Put me to sleep, until any of those objects are modified. When they are modified, tell me about them and restart this transaction." Thus retry is a very flexible form of triggers: it allows you to get the server to push data out to you rather than having to poll for it.

The following example shows retry in use. One connection is used as a producer and is writing different values to our client account's root object. The other connection is used as a consumer, using retry to ensure that it's woken up when the value of the root object changes. Note that the consumer isn't guaranteed to receive every value: if the producer overwrites values too quickly then values can be missed, so you would probably want to use a queue structure if you wanted to ensure values couldn't be missed.

package main

import (
	"encoding/binary"
	"errors"
	"fmt"
	"goshawkdb.io/client"
)

const (
	clusterCertPEM      = `...`
	clientCertAndKeyPEM = `...`
)

func main() {
	conn1, err := client.NewConnection("hostname:7894", []byte(clientCertAndKeyPEM), []byte(clusterCertPEM))
	if err != nil {
		fmt.Println(err)
		return
	}
	defer conn1.Shutdown()
	conn2, err := client.NewConnection("hostname:7894", []byte(clientCertAndKeyPEM), []byte(clusterCertPEM))
	if err != nil {
		fmt.Println(err)
		return
	}

	limit := uint64(1000)
	go func() { // producer
		defer conn2.Shutdown()
		buf := make([]byte, 8)
		fmt.Println("Producer starting")
		for i := uint64(0); i < limit; i++ {
			_, _, err := conn2.RunTransaction(func(txn *client.Txn) (interface{}, error) {
				rootObjs, err := txn.GetRootObjects()
				if err != nil {
					return nil, err
				}
				rootObj, found := rootObjs["myRoot1"]
				if !found {
					return nil, errors.New("No root 'myRoot1' found")
				}
				binary.LittleEndian.PutUint64(buf, i)
				return nil, rootObj.Set(buf)
			})
			if err != nil {
				fmt.Println(err)
				return
			}
		}
		fmt.Println("Producer finished")
	}()

	retrieved := uint64(0)
	for retrieved+1 != limit { // consumer
		result, _, err := conn1.RunTransaction(func(txn *client.Txn) (interface{}, error) {
			rootObjs, err := txn.GetRootObjects()
			if err != nil {
				return nil, err
			}
			rootObj, found := rootObjs["myRoot1"]
			if !found {
				return nil, errors.New("No root 'myRoot1' found")
			}
			value, err := rootObj.Value()
			if err != nil {
				return nil, err
			}
			if len(value) == 0 {
				// the producer hasn't written anything yet: go to sleep!
				return client.Retry, nil
			}
			num := binary.LittleEndian.Uint64(value)
			if num == retrieved { // nothing's changed since we last read the root, go to sleep!
				return client.Retry, nil
			} else {
				return num, nil
			}
		})
		if err != nil {
			fmt.Println(err)
			return
		}
		retrieved = result.(uint64)
		fmt.Printf("Consumer retrieved %v\n", retrieved)
	}
}

Running this, you should get output similar to:

2015/12/19 21:04:19 Connection established to localhost:10001 (RM:b975217a)
2015/12/19 21:04:20 Connection established to localhost:10001 (RM:b975217a)
Producer starting
Consumer retrieved 2
Consumer retrieved 36
Consumer retrieved 44
Consumer retrieved 71
Consumer retrieved 78
Consumer retrieved 101
Consumer retrieved 292
Consumer retrieved 326
Consumer retrieved 370
Consumer retrieved 376
Consumer retrieved 382
Consumer retrieved 385
Consumer retrieved 400
Consumer retrieved 417
Consumer retrieved 428
Consumer retrieved 440
Consumer retrieved 452
Consumer retrieved 479
Consumer retrieved 613
Consumer retrieved 617
Consumer retrieved 640
Consumer retrieved 649
Consumer retrieved 656
Consumer retrieved 669
Consumer retrieved 688
Consumer retrieved 689
Consumer retrieved 715
Consumer retrieved 734
Consumer retrieved 739
Consumer retrieved 751
Consumer retrieved 752
Consumer retrieved 823
Consumer retrieved 829
Consumer retrieved 830
Consumer retrieved 924
Consumer retrieved 982
Producer finished
Consumer retrieved 999

In this example I had to use two connections: retry does not release the connection - it blocks the connection, so whilst that connection is blocked, the producer has to use a different connection. Also note that client.Retry is returned as the value, not the error: a transaction that retries is deliberately not returning any value until after some state within the data store has changed.

What's next?

That's it for introducing the client. The API is very small, though quite powerful.