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.
- The client API is fully documented at https://godoc.org/goshawkdb.io/client.
- There are some howtos available which feature longer worked examples.
- You may wish to have a look through GoshawkDB's
integration
tests repository. Whilst these use a helper which
wraps the
Connection
object, they may help to demonstrate the client further.