coverage~bigbes/sr-ht-spec64cae3afhooks/client.go

Coverage
79.3% 23/29 statements
Δ
+0.0
Blob
a045b67
1 package hooks
2
3 import (
4 "context"
5 "fmt"
6 "net"
7 "time"
8 )
9
10 const (
11 // DefaultDialTimeout bounds finding the daemon. A unix socket connect is
12 // immediate when the daemon is listening, so this is generous enough to
13 // survive a loaded box and short enough that a push against a dead daemon
14 // fails while the human is still looking at the terminal.
15 DefaultDialTimeout = 5 * time.Second
16
17 // DefaultTimeout bounds one call end to end. Validation walks the pushed
18 // tree and asks Postgres about every document id in it, so it is not
19 // instantaneous; but a hook that hangs holds the push open indefinitely,
20 // and a rejected push is the better failure.
21 DefaultTimeout = 60 * time.Second
22 )
23
24 // Client is a hook's end of the RPC: one connection, one request, one
25 // response, no reuse. Pushes are rare and serial, so a pool would be state to
26 // get wrong for no gain.
27 type Client struct {
28 // Socket is the daemon's unix socket.
29 Socket string
30
31 // DialTimeout and Timeout default to the constants above when zero.
32 DialTimeout time.Duration
33 Timeout time.Duration
34 }
35
36 // Call sends one request and returns the daemon's answer.
37 //
38 // Every failure here — cannot connect, cannot write, cannot parse — is
39 // returned as an error, and every caller on the rejecting path turns it into a
40 // rejection. That is the fail-closed rule: the daemon not answering is never
41 // permission to proceed.
42 67 func (c Client) Call(ctx context.Context, req Request) (Response, error) {
43 67 if c.Socket == "" {
44 0 return Response{}, fmt.Errorf("hooks: no daemon socket to call")
45 0 }
46 67 timeout := c.Timeout
47 67 if timeout <= 0 {
48 24 timeout = DefaultTimeout
49 24 }
50 67 dialTimeout := c.DialTimeout
51 67 if dialTimeout <= 0 {
52 58 dialTimeout = DefaultDialTimeout
53 58 }
54
55 67 ctx, cancel := context.WithTimeout(ctx, timeout)
56 67 defer cancel()
57 67
58 67 dialer := net.Dialer{Timeout: dialTimeout}
59 67 conn, err := dialer.DialContext(ctx, "unix", c.Socket)
60 67 if err != nil {
61 4 return Response{}, fmt.Errorf("hooks: reach the spec.sr.ht daemon on %s: %w", c.Socket, err)
62 4 }
63 63 defer conn.Close()
64 63
65 63 if deadline, ok := ctx.Deadline(); ok {
66 63 if err := conn.SetDeadline(deadline); err != nil {
67 0 return Response{}, fmt.Errorf("hooks: set deadline on %s: %w", c.Socket, err)
68 0 }
69 }
70
71 63 if err := WriteRequest(conn, req); err != nil {
72 0 return Response{}, fmt.Errorf("hooks: send %s to %s: %w", req.Method, c.Socket, err)
73 0 }
74 // Half-close so a daemon that reads to EOF is not left waiting. The
75 // response still arrives on the read half.
76 63 if uc, ok := conn.(*net.UnixConn); ok {
77 63 if err := uc.CloseWrite(); err != nil {
78 0 return Response{}, fmt.Errorf("hooks: finish sending %s to %s: %w", req.Method, c.Socket, err)
79 0 }
80 }
81
82 63 resp, err := ReadResponse(conn)
83 63 if err != nil {
84 0 return Response{}, fmt.Errorf("hooks: read the answer to %s from %s: %w", req.Method, c.Socket, err)
85 0 }
86 63 if err := resp.Validate(); err != nil {
87 0 return Response{}, fmt.Errorf("hooks: %w", err)
88 0 }
89 63 return resp, nil
90 }