Code

client: Try to reconnect after read/write failures.
[sysdb/go.git] / client / client.go
1 //
2 // Copyright (C) 2014 Sebastian 'tokkee' Harl <sh@tokkee.org>
3 // All rights reserved.
4 //
5 // Redistribution and use in source and binary forms, with or without
6 // modification, are permitted provided that the following conditions
7 // are met:
8 // 1. Redistributions of source code must retain the above copyright
9 //    notice, this list of conditions and the following disclaimer.
10 // 2. Redistributions in binary form must reproduce the above copyright
11 //    notice, this list of conditions and the following disclaimer in the
12 //    documentation and/or other materials provided with the distribution.
13 //
14 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15 // ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
16 // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17 // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
18 // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
19 // EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
20 // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
21 // OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
22 // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
23 // OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
24 // ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 /*
27 Package client provides a SysDB client implementation.
29 The Connect function connects to a SysDB server as the specified user:
31         c, err := client.Connect("unix:/var/run/sysdbd.sock", "username")
32         if err != nil {
33                 // handle error
34         }
35         defer c.Close()
37 The github.com/sysdb/go/proto package provides support for handling requests
38 and responses. Use the Send and Receive functions to communicate with the
39 server:
41         m := &proto.Message{
42                 Type: proto.ConnectionQuery,
43                 Raw:  []byte{"LOOKUP hosts MATCHING attribute.architecture = 'amd64';"},
44         }
45         if err := c.Send(m); err != nil {
46                 // handle error
47         }
48         m, err := c.Receive()
49         if err != nil {
50                 // handle error
51         }
52         if m.Type == proto.ConnectionError {
53                 // handle failed query
54         }
55         // ...
56 */
57 package client
59 import (
60         "fmt"
61         "net"
62         "strings"
64         "github.com/sysdb/go/proto"
65 )
67 // A Conn is a connection to a SysDB server instance.
68 //
69 // Multiple goroutines may invoke methods on a Conn simultaneously but since
70 // the SysDB protocol requires a strict ordering of request and response
71 // messages, the communication with the server will usually happen
72 // sequentially.
73 type Conn struct {
74         c                   net.Conn
75         network, addr, user string
76 }
78 func (c *Conn) connect() (err error) {
79         if c.c, err = net.Dial(c.network, c.addr); err != nil {
80                 return err
81         }
82         defer func() {
83                 if err != nil {
84                         c.Close()
85                 }
86         }()
88         m := &proto.Message{
89                 Type: proto.ConnectionStartup,
90                 Raw:  []byte(c.user),
91         }
92         if err := c.Send(m); err != nil {
93                 return err
94         }
96         m, err = c.Receive()
97         if err != nil {
98                 return err
99         }
100         if m.Type == proto.ConnectionError {
101                 return fmt.Errorf("failed to startup session: %s", string(m.Raw))
102         }
103         if m.Type != proto.ConnectionOK {
104                 return fmt.Errorf("failed to startup session: unsupported")
105         }
106         return nil
109 // Connect sets up a client connection to a SysDB server instance at the
110 // specified address using the specified user.
111 //
112 // The address may be a UNIX domain socket, either prefixed with 'unix:' or
113 // specifying an absolute file-system path.
114 func Connect(addr, user string) (*Conn, error) {
115         network := "tcp"
116         if strings.HasPrefix(addr, "unix:") {
117                 network = "unix"
118                 addr = addr[len("unix:"):]
119         } else if addr[0] == '/' {
120                 network = "unix"
121         }
123         c := &Conn{network: network, addr: addr, user: user}
124         if err := c.connect(); err != nil {
125                 return nil, err
126         }
127         return c, nil
130 // Close closes the client connection.
131 //
132 // Any blocked Send or Receive operations will be unblocked and return errors.
133 func (c *Conn) Close() {
134         if c.c == nil {
135                 return
136         }
137         c.c.Close()
138         c.c = nil
141 // Send sends the specified raw message to the server.
142 //
143 // Send operations block until the full message could be written to the
144 // underlying sockets. This ensures that server and client don't get out of
145 // sync.
146 func (c *Conn) Send(m *proto.Message) error {
147         var err error
148         if c.c != nil {
149                 err = proto.Write(c.c, m)
150                 if err == nil {
151                         return nil
152                 }
153                 c.Close()
154         }
156         // Try to reconnect.
157         if e := c.connect(); e == nil {
158                 return proto.Write(c.c, m)
159         } else if err == nil {
160                 err = e
161         }
162         return err
165 // Receive waits for a reply from the server and returns the raw message.
166 //
167 // Receive operations block until a full message could be read from the
168 // underlying socket. This ensures that server and client don't get out of
169 // sync.
170 func (c *Conn) Receive() (*proto.Message, error) {
171         var err error
172         if c.c != nil {
173                 var m *proto.Message
174                 m, err = proto.Read(c.c)
175                 if err == nil {
176                         return m, err
177                 }
178                 c.Close()
179         }
181         // Try to reconnect.
182         if e := c.connect(); e == nil {
183                 return proto.Read(c.c)
184         } else if err == nil {
185                 err = e
186         }
187         return nil, err
190 // vim: set tw=78 sw=4 sw=4 noexpandtab :