~bigbes/sr-ht-spec

ref: 46048cbc0ada700c621b7c73d8da07deefde2662 sr-ht-spec/hooks/client.go -rw-r--r-- 2.9 KiB
46048cbc — Eugene Blikh chore(beads): spec-ejq.2 re-index verified against repo.bigb.es 13 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package hooks

import (
	"context"
	"fmt"
	"net"
	"time"
)

const (
	// DefaultDialTimeout bounds finding the daemon. A unix socket connect is
	// immediate when the daemon is listening, so this is generous enough to
	// survive a loaded box and short enough that a push against a dead daemon
	// fails while the human is still looking at the terminal.
	DefaultDialTimeout = 5 * time.Second

	// DefaultTimeout bounds one call end to end. Validation walks the pushed
	// tree and asks Postgres about every document id in it, so it is not
	// instantaneous; but a hook that hangs holds the push open indefinitely,
	// and a rejected push is the better failure.
	DefaultTimeout = 60 * time.Second
)

// Client is a hook's end of the RPC: one connection, one request, one
// response, no reuse. Pushes are rare and serial, so a pool would be state to
// get wrong for no gain.
type Client struct {
	// Socket is the daemon's unix socket.
	Socket string

	// DialTimeout and Timeout default to the constants above when zero.
	DialTimeout time.Duration
	Timeout     time.Duration
}

// Call sends one request and returns the daemon's answer.
//
// Every failure here — cannot connect, cannot write, cannot parse — is
// returned as an error, and every caller on the rejecting path turns it into a
// rejection. That is the fail-closed rule: the daemon not answering is never
// permission to proceed.
func (c Client) Call(ctx context.Context, req Request) (Response, error) {
	if c.Socket == "" {
		return Response{}, fmt.Errorf("hooks: no daemon socket to call")
	}
	timeout := c.Timeout
	if timeout <= 0 {
		timeout = DefaultTimeout
	}
	dialTimeout := c.DialTimeout
	if dialTimeout <= 0 {
		dialTimeout = DefaultDialTimeout
	}

	ctx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	dialer := net.Dialer{Timeout: dialTimeout}
	conn, err := dialer.DialContext(ctx, "unix", c.Socket)
	if err != nil {
		return Response{}, fmt.Errorf("hooks: reach the spec.sr.ht daemon on %s: %w", c.Socket, err)
	}
	defer conn.Close()

	if deadline, ok := ctx.Deadline(); ok {
		if err := conn.SetDeadline(deadline); err != nil {
			return Response{}, fmt.Errorf("hooks: set deadline on %s: %w", c.Socket, err)
		}
	}

	if err := WriteRequest(conn, req); err != nil {
		return Response{}, fmt.Errorf("hooks: send %s to %s: %w", req.Method, c.Socket, err)
	}
	// Half-close so a daemon that reads to EOF is not left waiting. The
	// response still arrives on the read half.
	if uc, ok := conn.(*net.UnixConn); ok {
		if err := uc.CloseWrite(); err != nil {
			return Response{}, fmt.Errorf("hooks: finish sending %s to %s: %w", req.Method, c.Socket, err)
		}
	}

	resp, err := ReadResponse(conn)
	if err != nil {
		return Response{}, fmt.Errorf("hooks: read the answer to %s from %s: %w", req.Method, c.Socket, err)
	}
	if err := resp.Validate(); err != nil {
		return Response{}, fmt.Errorf("hooks: %w", err)
	}
	return resp, nil
}