Skip to content

Commit 097e1e3

Browse files
committed
ovsdb/internal/jsonrpc: initial commit
1 parent c910586 commit 097e1e3

File tree

4 files changed

+361
-0
lines changed

4 files changed

+361
-0
lines changed

ovsdb/internal/jsonrpc/conn.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Copyright 2017 DigitalOcean.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package jsonrpc
16+
17+
import (
18+
"encoding/json"
19+
"fmt"
20+
"io"
21+
"log"
22+
"sync"
23+
)
24+
25+
// A Request is a JSON-RPC request.
26+
type Request struct {
27+
ID int `json:"id"`
28+
Method string `json:"method"`
29+
Params []interface{} `json:"params"`
30+
}
31+
32+
// A Response is a JSON-RPC response.
33+
type Response struct {
34+
ID int `json:"id"`
35+
Result interface{} `json:"result"`
36+
Error interface{} `json:"error"`
37+
}
38+
39+
// NewConn creates a new Conn with the input io.ReadWriteCloser.
40+
// If a logger is specified, it is used for debug logs.
41+
func NewConn(rwc io.ReadWriteCloser, ll *log.Logger) *Conn {
42+
if ll != nil {
43+
rwc = &debugReadWriteCloser{
44+
rwc: rwc,
45+
ll: ll,
46+
}
47+
}
48+
49+
return &Conn{
50+
enc: json.NewEncoder(rwc),
51+
dec: json.NewDecoder(rwc),
52+
closer: rwc,
53+
}
54+
}
55+
56+
// A Conn is a JSON-RPC connection.
57+
type Conn struct {
58+
mu sync.RWMutex
59+
enc *json.Encoder
60+
dec *json.Decoder
61+
closer io.Closer
62+
seq int
63+
}
64+
65+
// Close closes the connection.
66+
func (c *Conn) Close() error {
67+
return c.closer.Close()
68+
}
69+
70+
// Execute executes a single request and unmarshals its results into out.
71+
func (c *Conn) Execute(req Request, out interface{}) error {
72+
c.mu.Lock()
73+
defer c.mu.Unlock()
74+
c.seq++
75+
76+
// Use auto-increment sequence, or user-defined if requested.
77+
seq := c.seq
78+
if req.ID != 0 {
79+
seq = req.ID
80+
} else {
81+
req.ID = seq
82+
}
83+
84+
// Non-nil array required for ovsdb-server to reply.
85+
if req.Params == nil {
86+
req.Params = []interface{}{}
87+
}
88+
89+
if err := c.enc.Encode(req); err != nil {
90+
return fmt.Errorf("failed to encode JSON-RPC request: %v", err)
91+
}
92+
93+
res := Response{
94+
Result: out,
95+
}
96+
97+
if err := c.dec.Decode(&res); err != nil {
98+
return fmt.Errorf("failed to decode JSON-RPC request: %v", err)
99+
}
100+
101+
if res.ID != seq {
102+
return fmt.Errorf("bad JSON-RPC sequence: %d, want: %d", res.ID, seq)
103+
}
104+
105+
// TODO(mdlayher): better errors.
106+
if res.Error != nil {
107+
return fmt.Errorf("received JSON-RPC error: %#v", res.Error)
108+
}
109+
110+
return nil
111+
}
112+
113+
type debugReadWriteCloser struct {
114+
rwc io.ReadWriteCloser
115+
ll *log.Logger
116+
}
117+
118+
func (rwc *debugReadWriteCloser) Read(b []byte) (int, error) {
119+
n, err := rwc.rwc.Read(b)
120+
if err != nil {
121+
return n, err
122+
}
123+
124+
rwc.ll.Printf(" read: %s", string(b[:n]))
125+
return n, nil
126+
}
127+
128+
func (rwc *debugReadWriteCloser) Write(b []byte) (int, error) {
129+
n, err := rwc.rwc.Write(b)
130+
if err != nil {
131+
return n, err
132+
}
133+
134+
rwc.ll.Printf("write: %s", string(b[:n]))
135+
return n, nil
136+
}
137+
138+
func (rwc *debugReadWriteCloser) Close() error {
139+
err := rwc.rwc.Close()
140+
rwc.ll.Println("close:", err)
141+
return err
142+
}

ovsdb/internal/jsonrpc/conn_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright 2017 DigitalOcean.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package jsonrpc_test
16+
17+
import (
18+
"fmt"
19+
"testing"
20+
21+
"github.com/digitalocean/go-openvswitch/ovsdb/internal/jsonrpc"
22+
"github.com/google/go-cmp/cmp"
23+
)
24+
25+
func TestConnExecuteBadSequence(t *testing.T) {
26+
req := jsonrpc.Request{
27+
ID: 10,
28+
Method: "test",
29+
}
30+
31+
c, done := jsonrpc.TestConn(t, func(_ jsonrpc.Request) jsonrpc.Response {
32+
return jsonrpc.Response{
33+
// Bad sequence.
34+
ID: 1,
35+
}
36+
})
37+
defer done()
38+
39+
if err := c.Execute(req, nil); err == nil {
40+
t.Fatal("expected an error, but none occurred")
41+
}
42+
}
43+
44+
func TestConnExecuteError(t *testing.T) {
45+
// TODO(mdlayher): what does this actually look like?
46+
type rpcError struct {
47+
Details string
48+
}
49+
50+
c, done := jsonrpc.TestConn(t, func(_ jsonrpc.Request) jsonrpc.Response {
51+
return jsonrpc.Response{
52+
ID: 10,
53+
Error: rpcError{
54+
Details: "some error",
55+
},
56+
}
57+
})
58+
defer done()
59+
60+
if err := c.Execute(jsonrpc.Request{ID: 10}, nil); err == nil {
61+
t.Fatal("expected an error, but none occurred")
62+
}
63+
}
64+
65+
func TestConnExecuteOK(t *testing.T) {
66+
req := jsonrpc.Request{
67+
Method: "hello",
68+
Params: []interface{}{"world"},
69+
}
70+
71+
type message struct {
72+
Message string `json:"message"`
73+
}
74+
75+
want := message{
76+
Message: "hello world",
77+
}
78+
79+
c, done := jsonrpc.TestConn(t, func(got jsonrpc.Request) jsonrpc.Response {
80+
req.ID = 1
81+
82+
if diff := cmp.Diff(req, got); diff != "" {
83+
panicf("unexpected request (-want +got):\n%s", diff)
84+
}
85+
86+
return jsonrpc.Response{
87+
ID: 1,
88+
Result: want,
89+
}
90+
})
91+
defer done()
92+
93+
var out message
94+
if err := c.Execute(req, &out); err != nil {
95+
t.Fatalf("failed to execute: %v", err)
96+
}
97+
98+
if diff := cmp.Diff(want, out); diff != "" {
99+
t.Fatalf("unexpected response (-want +got):\n%s", diff)
100+
}
101+
}
102+
103+
func panicf(format string, a ...interface{}) {
104+
panic(fmt.Sprintf(format, a...))
105+
}

ovsdb/internal/jsonrpc/doc.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright 2017 DigitalOcean.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package jsonrpc is a minimal JSON-RPC 1.0 implementation.
16+
package jsonrpc

ovsdb/internal/jsonrpc/testconn.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright 2017 DigitalOcean.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package jsonrpc
16+
17+
import (
18+
"encoding/json"
19+
"fmt"
20+
"net"
21+
"strings"
22+
"sync"
23+
"testing"
24+
)
25+
26+
// A TestFunc is used to create RPC responses in TestConn.
27+
type TestFunc func(req Request) Response
28+
29+
// TestConn creates a Conn backed by a server that calls a TestFunc.
30+
// Invoke the returned closure to clean up its resources.
31+
func TestConn(t *testing.T, fn TestFunc) (*Conn, func()) {
32+
t.Helper()
33+
34+
conn, done := TestNetConn(t, fn)
35+
36+
c := NewConn(conn, nil)
37+
38+
return c, func() {
39+
_ = c.Close()
40+
done()
41+
}
42+
}
43+
44+
// TestNetConn creates a net.Conn backed by a server that calls a TestFunc.
45+
// Invoke the returned closure to clean up its resources.
46+
func TestNetConn(t *testing.T, fn TestFunc) (net.Conn, func()) {
47+
t.Helper()
48+
49+
l, err := net.Listen("tcp", ":0")
50+
if err != nil {
51+
t.Fatalf("failed to listen: %v", err)
52+
}
53+
54+
var wg sync.WaitGroup
55+
wg.Add(1)
56+
57+
go func() {
58+
defer wg.Done()
59+
60+
for {
61+
c, err := l.Accept()
62+
if err != nil {
63+
if strings.Contains(err.Error(), "use of closed network") {
64+
return
65+
}
66+
67+
panicf("failed to accept: %v", err)
68+
}
69+
70+
var req Request
71+
if err := json.NewDecoder(c).Decode(&req); err != nil {
72+
panicf("failed to decode request: %v", err)
73+
}
74+
75+
res := fn(req)
76+
if err := json.NewEncoder(c).Encode(res); err != nil {
77+
panicf("failed to encode response: %v", err)
78+
}
79+
_ = c.Close()
80+
}
81+
}()
82+
83+
c, err := net.Dial("tcp", l.Addr().String())
84+
if err != nil {
85+
t.Fatalf("failed to dial: %v", err)
86+
}
87+
88+
return c, func() {
89+
// Ensure types are cleaned up, and ensure goroutine stops.
90+
_ = l.Close()
91+
_ = c.Close()
92+
wg.Wait()
93+
}
94+
}
95+
96+
func panicf(format string, a ...interface{}) {
97+
panic(fmt.Sprintf(format, a...))
98+
}

0 commit comments

Comments
 (0)