mirror of
https://github.com/octoleo/syncthing.git
synced 2024-11-09 23:00:58 +00:00
New Cluster Configuration message replaces Options (fixes #63)
This commit is contained in:
parent
41c228cb56
commit
5064f846fc
@ -61,7 +61,7 @@ func connect(target string) {
|
|||||||
|
|
||||||
remoteID := certID(conn.ConnectionState().PeerCertificates[0].Raw)
|
remoteID := certID(conn.ConnectionState().PeerCertificates[0].Raw)
|
||||||
|
|
||||||
pc = protocol.NewConnection(remoteID, conn, conn, Model{}, nil)
|
pc = protocol.NewConnection(remoteID, conn, conn, Model{})
|
||||||
|
|
||||||
select {}
|
select {}
|
||||||
}
|
}
|
||||||
@ -127,6 +127,11 @@ func (m Model) IndexUpdate(nodeID string, repo string, files []protocol.FileInfo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Model) ClusterConfig(nodeID string, config protocol.ClusterConfigMessage) {
|
||||||
|
log.Println("Received cluster config")
|
||||||
|
log.Printf("%#v", config)
|
||||||
|
}
|
||||||
|
|
||||||
func (m Model) Request(nodeID, repo string, name string, offset int64, size int) ([]byte, error) {
|
func (m Model) Request(nodeID, repo string, name string, offset int64, size int) ([]byte, error) {
|
||||||
log.Println("Received request")
|
log.Println("Received request")
|
||||||
return nil, io.EOF
|
return nil, io.EOF
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
@ -231,15 +229,6 @@ func (l NodeConfigurationList) Len() int {
|
|||||||
return len(l)
|
return len(l)
|
||||||
}
|
}
|
||||||
|
|
||||||
func clusterHash(nodes []NodeConfiguration) string {
|
|
||||||
sort.Sort(NodeConfigurationList(nodes))
|
|
||||||
h := sha256.New()
|
|
||||||
for _, n := range nodes {
|
|
||||||
h.Write([]byte(n.NodeID))
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%x", h.Sum(nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanNodeList(nodes []NodeConfiguration, myID string) []NodeConfiguration {
|
func cleanNodeList(nodes []NodeConfiguration, myID string) []NodeConfiguration {
|
||||||
var myIDExists bool
|
var myIDExists bool
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
|
@ -219,15 +219,9 @@ func main() {
|
|||||||
m.ScanRepos()
|
m.ScanRepos()
|
||||||
m.SaveIndexes(confDir)
|
m.SaveIndexes(confDir)
|
||||||
|
|
||||||
connOpts := map[string]string{
|
|
||||||
"clientId": "syncthing",
|
|
||||||
"clientVersion": Version,
|
|
||||||
"clusterHash": clusterHash(cfg.Repositories[0].Nodes),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Routine to connect out to configured nodes
|
// Routine to connect out to configured nodes
|
||||||
disc := discovery()
|
disc := discovery()
|
||||||
go listenConnect(myID, disc, m, tlsCfg, connOpts)
|
go listenConnect(myID, disc, m, tlsCfg)
|
||||||
|
|
||||||
for _, repo := range cfg.Repositories {
|
for _, repo := range cfg.Repositories {
|
||||||
// Routine to pull blocks from other nodes to synchronize the local
|
// Routine to pull blocks from other nodes to synchronize the local
|
||||||
@ -325,7 +319,7 @@ func saveConfig() {
|
|||||||
saveConfigCh <- struct{}{}
|
saveConfigCh <- struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func listenConnect(myID string, disc *discover.Discoverer, m *Model, tlsCfg *tls.Config, connOpts map[string]string) {
|
func listenConnect(myID string, disc *discover.Discoverer, m *Model, tlsCfg *tls.Config) {
|
||||||
var conns = make(chan *tls.Conn)
|
var conns = make(chan *tls.Conn)
|
||||||
|
|
||||||
// Listen
|
// Listen
|
||||||
@ -438,7 +432,7 @@ next:
|
|||||||
if rateBucket != nil {
|
if rateBucket != nil {
|
||||||
wr = &limitedWriter{conn, rateBucket}
|
wr = &limitedWriter{conn, rateBucket}
|
||||||
}
|
}
|
||||||
protoConn := protocol.NewConnection(remoteID, conn, wr, m, connOpts)
|
protoConn := protocol.NewConnection(remoteID, conn, wr, m)
|
||||||
m.AddConnection(conn, protoConn)
|
m.AddConnection(conn, protoConn)
|
||||||
continue next
|
continue next
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ type Model struct {
|
|||||||
|
|
||||||
protoConn map[string]protocol.Connection
|
protoConn map[string]protocol.Connection
|
||||||
rawConn map[string]io.Closer
|
rawConn map[string]io.Closer
|
||||||
|
nodeVer map[string]string
|
||||||
pmut sync.RWMutex // protects protoConn and rawConn
|
pmut sync.RWMutex // protects protoConn and rawConn
|
||||||
|
|
||||||
sup suppressor
|
sup suppressor
|
||||||
@ -56,6 +57,7 @@ func NewModel(maxChangeBw int) *Model {
|
|||||||
cm: cid.NewMap(),
|
cm: cid.NewMap(),
|
||||||
protoConn: make(map[string]protocol.Connection),
|
protoConn: make(map[string]protocol.Connection),
|
||||||
rawConn: make(map[string]io.Closer),
|
rawConn: make(map[string]io.Closer),
|
||||||
|
nodeVer: make(map[string]string),
|
||||||
sup: suppressor{threshold: int64(maxChangeBw)},
|
sup: suppressor{threshold: int64(maxChangeBw)},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,7 +89,6 @@ func (m *Model) StartRepoRO(repo string) {
|
|||||||
type ConnectionInfo struct {
|
type ConnectionInfo struct {
|
||||||
protocol.Statistics
|
protocol.Statistics
|
||||||
Address string
|
Address string
|
||||||
ClientID string
|
|
||||||
ClientVersion string
|
ClientVersion string
|
||||||
Completion int
|
Completion int
|
||||||
}
|
}
|
||||||
@ -105,8 +106,7 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
|
|||||||
for node, conn := range m.protoConn {
|
for node, conn := range m.protoConn {
|
||||||
ci := ConnectionInfo{
|
ci := ConnectionInfo{
|
||||||
Statistics: conn.Statistics(),
|
Statistics: conn.Statistics(),
|
||||||
ClientID: conn.Option("clientId"),
|
ClientVersion: m.nodeVer[node],
|
||||||
ClientVersion: conn.Option("clientVersion"),
|
|
||||||
}
|
}
|
||||||
if nc, ok := m.rawConn[node].(remoteAddrer); ok {
|
if nc, ok := m.rawConn[node].(remoteAddrer); ok {
|
||||||
ci.Address = nc.RemoteAddr().String()
|
ci.Address = nc.RemoteAddr().String()
|
||||||
@ -245,15 +245,37 @@ func (m *Model) IndexUpdate(nodeID string, repo string, fs []protocol.FileInfo)
|
|||||||
m.rmut.RUnlock()
|
m.rmut.RUnlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Model) ClusterConfig(nodeID string, config protocol.ClusterConfigMessage) {
|
||||||
|
compErr := compareClusterConfig(m.clusterConfig(nodeID), config)
|
||||||
|
if debugNet {
|
||||||
|
dlog.Printf("ClusterConfig: %s: %#v", nodeID, config)
|
||||||
|
dlog.Printf(" ... compare: %s: %v", nodeID, compErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if compErr != nil {
|
||||||
|
warnf("%s: %v", nodeID, compErr)
|
||||||
|
m.Close(nodeID, compErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.pmut.Lock()
|
||||||
|
if config.ClientName == "syncthing" {
|
||||||
|
m.nodeVer[nodeID] = config.ClientVersion
|
||||||
|
} else {
|
||||||
|
m.nodeVer[nodeID] = config.ClientName + " " + config.ClientVersion
|
||||||
|
}
|
||||||
|
m.pmut.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// Close removes the peer from the model and closes the underlying connection if possible.
|
// Close removes the peer from the model and closes the underlying connection if possible.
|
||||||
// Implements the protocol.Model interface.
|
// Implements the protocol.Model interface.
|
||||||
func (m *Model) Close(node string, err error) {
|
func (m *Model) Close(node string, err error) {
|
||||||
if debugNet {
|
if debugNet {
|
||||||
dlog.Printf("%s: %v", node, err)
|
dlog.Printf("%s: %v", node, err)
|
||||||
}
|
}
|
||||||
if err == protocol.ErrClusterHash {
|
|
||||||
warnf("Connection to %s closed due to mismatched cluster hash. Ensure that the configured cluster members are identical on both nodes.", node)
|
if err != io.EOF {
|
||||||
} else if err != io.EOF {
|
warnf("Connection to %s closed: %v", node, err)
|
||||||
|
} else if _, ok := err.(ClusterConfigMismatch); ok {
|
||||||
warnf("Connection to %s closed: %v", node, err)
|
warnf("Connection to %s closed: %v", node, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,6 +294,7 @@ func (m *Model) Close(node string, err error) {
|
|||||||
}
|
}
|
||||||
delete(m.protoConn, node)
|
delete(m.protoConn, node)
|
||||||
delete(m.rawConn, node)
|
delete(m.rawConn, node)
|
||||||
|
delete(m.nodeVer, node)
|
||||||
m.pmut.Unlock()
|
m.pmut.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -386,6 +409,9 @@ func (m *Model) AddConnection(rawConn io.Closer, protoConn protocol.Connection)
|
|||||||
m.rawConn[nodeID] = rawConn
|
m.rawConn[nodeID] = rawConn
|
||||||
m.pmut.Unlock()
|
m.pmut.Unlock()
|
||||||
|
|
||||||
|
cm := m.clusterConfig(nodeID)
|
||||||
|
protoConn.ClusterConfig(cm)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
m.rmut.RLock()
|
m.rmut.RLock()
|
||||||
repos := m.nodeRepos[nodeID]
|
repos := m.nodeRepos[nodeID]
|
||||||
@ -596,46 +622,28 @@ func (m *Model) loadIndex(repo string, dir string) []protocol.FileInfo {
|
|||||||
return im.Files
|
return im.Files
|
||||||
}
|
}
|
||||||
|
|
||||||
func fileFromFileInfo(f protocol.FileInfo) scanner.File {
|
// clusterConfig returns a ClusterConfigMessage that is correct for the given peer node
|
||||||
var blocks = make([]scanner.Block, len(f.Blocks))
|
func (m *Model) clusterConfig(node string) protocol.ClusterConfigMessage {
|
||||||
var offset int64
|
cm := protocol.ClusterConfigMessage{
|
||||||
for i, b := range f.Blocks {
|
ClientName: "syncthing",
|
||||||
blocks[i] = scanner.Block{
|
ClientVersion: Version,
|
||||||
Offset: offset,
|
|
||||||
Size: b.Size,
|
|
||||||
Hash: b.Hash,
|
|
||||||
}
|
|
||||||
offset += int64(b.Size)
|
|
||||||
}
|
}
|
||||||
return scanner.File{
|
|
||||||
// Name is with native separator and normalization
|
|
||||||
Name: filepath.FromSlash(f.Name),
|
|
||||||
Size: offset,
|
|
||||||
Flags: f.Flags &^ protocol.FlagInvalid,
|
|
||||||
Modified: f.Modified,
|
|
||||||
Version: f.Version,
|
|
||||||
Blocks: blocks,
|
|
||||||
Suppressed: f.Flags&protocol.FlagInvalid != 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fileInfoFromFile(f scanner.File) protocol.FileInfo {
|
m.rmut.Lock()
|
||||||
var blocks = make([]protocol.BlockInfo, len(f.Blocks))
|
for _, repo := range m.nodeRepos[node] {
|
||||||
for i, b := range f.Blocks {
|
cr := protocol.Repository{
|
||||||
blocks[i] = protocol.BlockInfo{
|
ID: repo,
|
||||||
Size: b.Size,
|
|
||||||
Hash: b.Hash,
|
|
||||||
}
|
}
|
||||||
|
for _, node := range m.repoNodes[repo] {
|
||||||
|
// TODO: Set read only bit when relevant
|
||||||
|
cr.Nodes = append(cr.Nodes, protocol.Node{
|
||||||
|
ID: node,
|
||||||
|
Flags: protocol.FlagShareTrusted,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
cm.Repositories = append(cm.Repositories, cr)
|
||||||
}
|
}
|
||||||
pf := protocol.FileInfo{
|
m.rmut.Unlock()
|
||||||
Name: filepath.ToSlash(f.Name),
|
|
||||||
Flags: f.Flags,
|
return cm
|
||||||
Modified: f.Modified,
|
|
||||||
Version: f.Version,
|
|
||||||
Blocks: blocks,
|
|
||||||
}
|
|
||||||
if f.Suppressed {
|
|
||||||
pf.Flags |= protocol.FlagInvalid
|
|
||||||
}
|
|
||||||
return pf
|
|
||||||
}
|
}
|
||||||
|
@ -170,6 +170,8 @@ func (f FakeConnection) Request(repo, name string, offset int64, size int) ([]by
|
|||||||
return f.requestData, nil
|
return f.requestData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (FakeConnection) ClusterConfig(protocol.ClusterConfigMessage) {}
|
||||||
|
|
||||||
func (FakeConnection) Ping() bool {
|
func (FakeConnection) Ping() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,11 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/calmh/syncthing/protocol"
|
||||||
|
"github.com/calmh/syncthing/scanner"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MetricPrefix(n int64) string {
|
func MetricPrefix(n int64) string {
|
||||||
@ -41,3 +45,99 @@ func Rename(from, to string) error {
|
|||||||
}
|
}
|
||||||
return os.Rename(from, to)
|
return os.Rename(from, to)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fileFromFileInfo(f protocol.FileInfo) scanner.File {
|
||||||
|
var blocks = make([]scanner.Block, len(f.Blocks))
|
||||||
|
var offset int64
|
||||||
|
for i, b := range f.Blocks {
|
||||||
|
blocks[i] = scanner.Block{
|
||||||
|
Offset: offset,
|
||||||
|
Size: b.Size,
|
||||||
|
Hash: b.Hash,
|
||||||
|
}
|
||||||
|
offset += int64(b.Size)
|
||||||
|
}
|
||||||
|
return scanner.File{
|
||||||
|
// Name is with native separator and normalization
|
||||||
|
Name: filepath.FromSlash(f.Name),
|
||||||
|
Size: offset,
|
||||||
|
Flags: f.Flags &^ protocol.FlagInvalid,
|
||||||
|
Modified: f.Modified,
|
||||||
|
Version: f.Version,
|
||||||
|
Blocks: blocks,
|
||||||
|
Suppressed: f.Flags&protocol.FlagInvalid != 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileInfoFromFile(f scanner.File) protocol.FileInfo {
|
||||||
|
var blocks = make([]protocol.BlockInfo, len(f.Blocks))
|
||||||
|
for i, b := range f.Blocks {
|
||||||
|
blocks[i] = protocol.BlockInfo{
|
||||||
|
Size: b.Size,
|
||||||
|
Hash: b.Hash,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pf := protocol.FileInfo{
|
||||||
|
Name: filepath.ToSlash(f.Name),
|
||||||
|
Flags: f.Flags,
|
||||||
|
Modified: f.Modified,
|
||||||
|
Version: f.Version,
|
||||||
|
Blocks: blocks,
|
||||||
|
}
|
||||||
|
if f.Suppressed {
|
||||||
|
pf.Flags |= protocol.FlagInvalid
|
||||||
|
}
|
||||||
|
return pf
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmMap(cm protocol.ClusterConfigMessage) map[string]map[string]uint32 {
|
||||||
|
m := make(map[string]map[string]uint32)
|
||||||
|
for _, repo := range cm.Repositories {
|
||||||
|
m[repo.ID] = make(map[string]uint32)
|
||||||
|
for _, node := range repo.Nodes {
|
||||||
|
m[repo.ID][node.ID] = node.Flags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClusterConfigMismatch error
|
||||||
|
|
||||||
|
// compareClusterConfig returns nil for two equivalent configurations,
|
||||||
|
// otherwise a decriptive error
|
||||||
|
func compareClusterConfig(local, remote protocol.ClusterConfigMessage) error {
|
||||||
|
lm := cmMap(local)
|
||||||
|
rm := cmMap(remote)
|
||||||
|
|
||||||
|
for repo, lnodes := range lm {
|
||||||
|
_ = lnodes
|
||||||
|
if rnodes, ok := rm[repo]; ok {
|
||||||
|
for node, lflags := range lnodes {
|
||||||
|
if rflags, ok := rnodes[node]; ok {
|
||||||
|
if lflags&protocol.FlagShareBits != rflags&protocol.FlagShareBits {
|
||||||
|
return ClusterConfigMismatch(fmt.Errorf("remote has different sharing flags for node %q in repository %q", node, repo))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return ClusterConfigMismatch(fmt.Errorf("remote is missing node %q in repository %q", node, repo))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return ClusterConfigMismatch(fmt.Errorf("remote is missing repository %q", repo))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for repo, rnodes := range rm {
|
||||||
|
if lnodes, ok := lm[repo]; ok {
|
||||||
|
for node := range rnodes {
|
||||||
|
if _, ok := lnodes[node]; !ok {
|
||||||
|
return ClusterConfigMismatch(fmt.Errorf("remote has extra node %q in repository %q", node, repo))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return ClusterConfigMismatch(fmt.Errorf("remote has extra repository %q", repo))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
183
cmd/syncthing/util_test.go
Normal file
183
cmd/syncthing/util_test.go
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/calmh/syncthing/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testcases = []struct {
|
||||||
|
local, remote protocol.ClusterConfigMessage
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
local: protocol.ClusterConfigMessage{},
|
||||||
|
remote: protocol.ClusterConfigMessage{},
|
||||||
|
err: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
local: protocol.ClusterConfigMessage{ClientName: "a", ClientVersion: "b"},
|
||||||
|
remote: protocol.ClusterConfigMessage{ClientName: "c", ClientVersion: "d"},
|
||||||
|
err: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
local: protocol.ClusterConfigMessage{
|
||||||
|
Repositories: []protocol.Repository{
|
||||||
|
{ID: "foo"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
remote: protocol.ClusterConfigMessage{ClientName: "c", ClientVersion: "d"},
|
||||||
|
err: `remote is missing repository "foo"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
local: protocol.ClusterConfigMessage{ClientName: "c", ClientVersion: "d"},
|
||||||
|
remote: protocol.ClusterConfigMessage{
|
||||||
|
Repositories: []protocol.Repository{
|
||||||
|
{ID: "foo"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: `remote has extra repository "foo"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
local: protocol.ClusterConfigMessage{
|
||||||
|
Repositories: []protocol.Repository{
|
||||||
|
{ID: "foo"},
|
||||||
|
{ID: "bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
remote: protocol.ClusterConfigMessage{
|
||||||
|
Repositories: []protocol.Repository{
|
||||||
|
{ID: "foo"},
|
||||||
|
{ID: "bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
local: protocol.ClusterConfigMessage{
|
||||||
|
Repositories: []protocol.Repository{
|
||||||
|
{ID: "quux"},
|
||||||
|
{ID: "foo"},
|
||||||
|
{ID: "bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
remote: protocol.ClusterConfigMessage{
|
||||||
|
Repositories: []protocol.Repository{
|
||||||
|
{ID: "bar"},
|
||||||
|
{ID: "quux"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: `remote is missing repository "foo"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
local: protocol.ClusterConfigMessage{
|
||||||
|
Repositories: []protocol.Repository{
|
||||||
|
{ID: "quux"},
|
||||||
|
{ID: "bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
remote: protocol.ClusterConfigMessage{
|
||||||
|
Repositories: []protocol.Repository{
|
||||||
|
{ID: "bar"},
|
||||||
|
{ID: "foo"},
|
||||||
|
{ID: "quux"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: `remote has extra repository "foo"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
local: protocol.ClusterConfigMessage{
|
||||||
|
Repositories: []protocol.Repository{
|
||||||
|
{
|
||||||
|
ID: "foo",
|
||||||
|
Nodes: []protocol.Node{
|
||||||
|
{ID: "a"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ID: "bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
remote: protocol.ClusterConfigMessage{
|
||||||
|
Repositories: []protocol.Repository{
|
||||||
|
{ID: "foo"},
|
||||||
|
{ID: "bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: `remote is missing node "a" in repository "foo"`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
local: protocol.ClusterConfigMessage{
|
||||||
|
Repositories: []protocol.Repository{
|
||||||
|
{
|
||||||
|
ID: "foo",
|
||||||
|
Nodes: []protocol.Node{
|
||||||
|
{ID: "a"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ID: "bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
remote: protocol.ClusterConfigMessage{
|
||||||
|
Repositories: []protocol.Repository{
|
||||||
|
{
|
||||||
|
ID: "foo",
|
||||||
|
Nodes: []protocol.Node{
|
||||||
|
{ID: "a"},
|
||||||
|
{ID: "b"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ID: "bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: `remote has extra node "b" in repository "foo"`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
local: protocol.ClusterConfigMessage{
|
||||||
|
Repositories: []protocol.Repository{
|
||||||
|
{
|
||||||
|
ID: "foo",
|
||||||
|
Nodes: []protocol.Node{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
Flags: protocol.FlagShareReadOnly,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ID: "bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
remote: protocol.ClusterConfigMessage{
|
||||||
|
Repositories: []protocol.Repository{
|
||||||
|
{
|
||||||
|
ID: "foo",
|
||||||
|
Nodes: []protocol.Node{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
Flags: protocol.FlagShareTrusted,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ID: "bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: `remote has different sharing flags for node "a" in repository "foo"`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompareClusterConfig(t *testing.T) {
|
||||||
|
for i, tc := range testcases {
|
||||||
|
err := compareClusterConfig(tc.local, tc.remote)
|
||||||
|
switch {
|
||||||
|
case tc.err == "" && err != nil:
|
||||||
|
t.Errorf("#%d: unexpected error: %v", i, err)
|
||||||
|
|
||||||
|
case tc.err != "" && err == nil:
|
||||||
|
t.Errorf("#%d: unexpected nil error", i)
|
||||||
|
|
||||||
|
case tc.err != "" && err != nil && tc.err != err.Error():
|
||||||
|
t.Errorf("#%d: incorrect error: %q != %q", i, err, tc.err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -79,10 +79,14 @@ version, type and ID.
|
|||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
For BEP v1 the Version field is set to zero. Future versions with
|
For BEP v1 the Version field is set to zero. Future versions with
|
||||||
incompatible message formats will increment the Version field.
|
incompatible message formats will increment the Version field. A message
|
||||||
|
with an unknown version is a protocol error and MUST result in the
|
||||||
|
connection being terminated. A client supporting multiple versions MAY
|
||||||
|
retry with a different protcol version upon disconnection.
|
||||||
|
|
||||||
The Type field indicates the type of data following the message header
|
The Type field indicates the type of data following the message header
|
||||||
and is one of the integers defined below.
|
and is one of the integers defined below. A message of an unknown type
|
||||||
|
is a protocol error and MUST result in the connection being terminated.
|
||||||
|
|
||||||
The Message ID is set to a unique value for each transmitted message. In
|
The Message ID is set to a unique value for each transmitted message. In
|
||||||
request messages the Reply To is set to zero. In response messages it is
|
request messages the Reply To is set to zero. In response messages it is
|
||||||
@ -110,6 +114,183 @@ Opaque data should not be interpreted but can be compared bytewise to
|
|||||||
other opaque data. All strings MUST use the Unicode UTF-8 encoding,
|
other opaque data. All strings MUST use the Unicode UTF-8 encoding,
|
||||||
normalization form C.
|
normalization form C.
|
||||||
|
|
||||||
|
### Cluster Config (Type = 0)
|
||||||
|
|
||||||
|
This informational message provides information about the cluster
|
||||||
|
configuration, as it pertains to the current connection. It is sent by
|
||||||
|
both sides after connection establishment.
|
||||||
|
|
||||||
|
#### Graphical Representation
|
||||||
|
|
||||||
|
ClusterConfigMessage Structure:
|
||||||
|
|
||||||
|
0 1 2 3
|
||||||
|
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Length of ClientName |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/ /
|
||||||
|
\ ClientName (variable length) \
|
||||||
|
/ /
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Length of ClientVersion |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/ /
|
||||||
|
\ ClientVersion (variable length) \
|
||||||
|
/ /
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Number of Repositories |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/ /
|
||||||
|
\ Zero or more Repository Structures \
|
||||||
|
/ /
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Number of Options |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/ /
|
||||||
|
\ Zero or more Option Structures \
|
||||||
|
/ /
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
|
|
||||||
|
Repository Structure:
|
||||||
|
|
||||||
|
0 1 2 3
|
||||||
|
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Length of ID |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/ /
|
||||||
|
\ ID (variable length) \
|
||||||
|
/ /
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Number of Nodes |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/ /
|
||||||
|
\ Zero or more Node Structures \
|
||||||
|
/ /
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
|
|
||||||
|
Node Structure:
|
||||||
|
|
||||||
|
0 1 2 3
|
||||||
|
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Length of ID |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/ /
|
||||||
|
\ ID (variable length) \
|
||||||
|
/ /
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Flags |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
|
|
||||||
|
Option Structure:
|
||||||
|
|
||||||
|
0 1 2 3
|
||||||
|
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Length of Key |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/ /
|
||||||
|
\ Key (variable length) \
|
||||||
|
/ /
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Length of Value |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/ /
|
||||||
|
\ Value (variable length) \
|
||||||
|
/ /
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
|
#### Fields
|
||||||
|
|
||||||
|
The ClientName and ClientVersion fields identify the implementation. The
|
||||||
|
values SHOULD be simple strings identifying the implementation name, as
|
||||||
|
a user would expect to see it, and the version string in the same
|
||||||
|
manner. An example ClientName is "syncthing" and an example
|
||||||
|
ClientVersion is "v0.7.2". The ClientVersion field SHOULD follow the
|
||||||
|
patterns laid out in the [Semantic Versioning](http://semver.org/)
|
||||||
|
standard.
|
||||||
|
|
||||||
|
The Repositories field lists all repositories that will be synchronized
|
||||||
|
over the current connection. Each repository has a list of participating
|
||||||
|
Nodes. Each node has an associated Flags field to indicate the sharing
|
||||||
|
mode of that node for the repository in question. See the discussion on
|
||||||
|
Sharing Modes.
|
||||||
|
|
||||||
|
The Node Flags field contains the following single bit flags:
|
||||||
|
|
||||||
|
0 1 2 3
|
||||||
|
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Reserved |Pri| Reserved |R|T|
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
|
- Bit 31 ("T", Trusted) is set for nodes that participate in trusted
|
||||||
|
mode.
|
||||||
|
|
||||||
|
- Bit 30 ("R", Read Only) is set for nodes that participate in read
|
||||||
|
only mode.
|
||||||
|
|
||||||
|
- Bits 16 through 28 are reserved and MUST be set to zero.
|
||||||
|
|
||||||
|
- Bits 14-15 ("Pri) indicate the node's upload priority for this
|
||||||
|
repository. Possible values are:
|
||||||
|
|
||||||
|
- 00: The default. Normal priority.
|
||||||
|
|
||||||
|
- 01: High priority. Other nodes SHOULD favour requesting files from
|
||||||
|
this node over nodes with normal or low priority.
|
||||||
|
|
||||||
|
- 10: Low priority. Other nodes SHOULD avoid requesting files from
|
||||||
|
this node when they are available from other nodes.
|
||||||
|
|
||||||
|
- 11: Sharing disabled. Other nodes SHOULD NOT request files from
|
||||||
|
this node.
|
||||||
|
|
||||||
|
- Bits 0 through 14 are reserved and MUST be set to zero.
|
||||||
|
|
||||||
|
Exactly one of the T, R or S bits MUST be set.
|
||||||
|
|
||||||
|
The Options field contain option values to be used in an implementation
|
||||||
|
specific manner. The options list is conceptually a map of Key => Value
|
||||||
|
items, although it is transmitted in the form of a list of (Key, Value)
|
||||||
|
pairs, both of string type. Key ID:s are implementation specific. An
|
||||||
|
implementation MUST ignore unknown keys. An implementation MAY impose
|
||||||
|
limits on the length keys and values. The options list may be used to
|
||||||
|
inform nodes of relevant local configuration options such as rate
|
||||||
|
limiting or make recommendations about request parallellism, node
|
||||||
|
priorities, etc. An empty options list is valid for nodes not having any
|
||||||
|
such information to share. Nodes MAY NOT make any assumptions about
|
||||||
|
peers acting in a specific manner as a result of sent options.
|
||||||
|
|
||||||
|
#### XDR
|
||||||
|
|
||||||
|
struct ClusterConfigMessage {
|
||||||
|
string ClientName<>;
|
||||||
|
string ClientVersion<>;
|
||||||
|
Repository Repositories<>;
|
||||||
|
Option Options<>;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Repository {
|
||||||
|
string ID<>;
|
||||||
|
Node Nodes<>;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Node {
|
||||||
|
string ID<>;
|
||||||
|
unsigned int Flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Option {
|
||||||
|
string Key<>;
|
||||||
|
string Value<>;
|
||||||
|
}
|
||||||
|
|
||||||
### Index (Type = 1)
|
### Index (Type = 1)
|
||||||
|
|
||||||
The Index message defines the contents of the senders repository. An
|
The Index message defines the contents of the senders repository. An
|
||||||
@ -356,65 +537,32 @@ model, the Index Update merely amends it with new or updated file
|
|||||||
information. Any files not mentioned in an Index Update are left
|
information. Any files not mentioned in an Index Update are left
|
||||||
unchanged.
|
unchanged.
|
||||||
|
|
||||||
### Options (Type = 7)
|
Sharing Modes
|
||||||
|
-------------
|
||||||
|
|
||||||
This informational message provides information about the client
|
### Trusted
|
||||||
configuration, version, etc. It is sent at connection initiation and,
|
|
||||||
optionally, when any of the sent parameters have changed. The message is
|
|
||||||
in the form of a list of (key, value) pairs, both of string type.
|
|
||||||
|
|
||||||
Key ID:s apart from the well known ones are implementation specific. An
|
Trusted mode is the default sharing mode. Updates are exchanged in both
|
||||||
implementation is expected to ignore unknown keys. An implementation may
|
directions.
|
||||||
impose limits on key and value size.
|
|
||||||
|
|
||||||
Well known keys:
|
+------------+ Updates /---------\
|
||||||
|
| | -----------> / \
|
||||||
|
| Node | | Cluster |
|
||||||
|
| | <----------- \ /
|
||||||
|
+------------+ Updates \---------/
|
||||||
|
|
||||||
- "clientId" -- The name of the implementation. Example: "syncthing".
|
### Read Only
|
||||||
|
|
||||||
- "clientVersion" -- The version of the client. Example: "v1.0.33-47".
|
In read only mode a node does not synchronize the local repository to
|
||||||
The Following the SemVer 2.0 specification for version strings is
|
the cluster, but publishes changes to it's local repository contents as
|
||||||
encouraged but not enforced.
|
usual. The local repository can be seen as a "master copy" that is never
|
||||||
|
affected by the actions of other cluster nodes.
|
||||||
|
|
||||||
#### Graphical Representation
|
+------------+ Updates /---------\
|
||||||
|
| | -----------> / \
|
||||||
0 1 2 3
|
| Node | | Cluster |
|
||||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
| | \ /
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+------------+ \---------/
|
||||||
| Number of Options |
|
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
/ /
|
|
||||||
\ Zero or more KeyValue Structures \
|
|
||||||
/ /
|
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
|
|
||||||
KeyValue Structure:
|
|
||||||
|
|
||||||
0 1 2 3
|
|
||||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
| Length of Key |
|
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
/ /
|
|
||||||
\ Key (variable length) \
|
|
||||||
/ /
|
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
| Length of Value |
|
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
/ /
|
|
||||||
\ Value (variable length) \
|
|
||||||
/ /
|
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
|
|
||||||
#### XDR
|
|
||||||
|
|
||||||
struct OptionsMessage {
|
|
||||||
KeyValue Options<>;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct KeyValue {
|
|
||||||
string Key<>;
|
|
||||||
string Value<>;
|
|
||||||
}
|
|
||||||
|
|
||||||
Message Limits
|
Message Limits
|
||||||
--------------
|
--------------
|
||||||
|
@ -38,6 +38,9 @@ func (t *TestModel) Close(nodeID string, err error) {
|
|||||||
close(t.closedCh)
|
close(t.closedCh)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *TestModel) ClusterConfig(nodeID string, config ClusterConfigMessage) {
|
||||||
|
}
|
||||||
|
|
||||||
func (t *TestModel) isClosed() bool {
|
func (t *TestModel) isClosed() bool {
|
||||||
select {
|
select {
|
||||||
case <-t.closedCh:
|
case <-t.closedCh:
|
||||||
|
@ -25,8 +25,21 @@ type RequestMessage struct {
|
|||||||
Size uint32
|
Size uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
type OptionsMessage struct {
|
type ClusterConfigMessage struct {
|
||||||
Options []Option // max:64
|
ClientName string // max:64
|
||||||
|
ClientVersion string // max:64
|
||||||
|
Repositories []Repository // max:64
|
||||||
|
Options []Option // max:64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Repository struct {
|
||||||
|
ID string // max:64
|
||||||
|
Nodes []Node // max:64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Node struct {
|
||||||
|
ID string // max:64
|
||||||
|
Flags uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
type Option struct {
|
type Option struct {
|
||||||
|
@ -198,19 +198,34 @@ func (o *RequestMessage) decodeXDR(xr *xdr.Reader) error {
|
|||||||
return xr.Error()
|
return xr.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o OptionsMessage) EncodeXDR(w io.Writer) (int, error) {
|
func (o ClusterConfigMessage) EncodeXDR(w io.Writer) (int, error) {
|
||||||
var xw = xdr.NewWriter(w)
|
var xw = xdr.NewWriter(w)
|
||||||
return o.encodeXDR(xw)
|
return o.encodeXDR(xw)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o OptionsMessage) MarshalXDR() []byte {
|
func (o ClusterConfigMessage) MarshalXDR() []byte {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
var xw = xdr.NewWriter(&buf)
|
var xw = xdr.NewWriter(&buf)
|
||||||
o.encodeXDR(xw)
|
o.encodeXDR(xw)
|
||||||
return buf.Bytes()
|
return buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o OptionsMessage) encodeXDR(xw *xdr.Writer) (int, error) {
|
func (o ClusterConfigMessage) encodeXDR(xw *xdr.Writer) (int, error) {
|
||||||
|
if len(o.ClientName) > 64 {
|
||||||
|
return xw.Tot(), xdr.ErrElementSizeExceeded
|
||||||
|
}
|
||||||
|
xw.WriteString(o.ClientName)
|
||||||
|
if len(o.ClientVersion) > 64 {
|
||||||
|
return xw.Tot(), xdr.ErrElementSizeExceeded
|
||||||
|
}
|
||||||
|
xw.WriteString(o.ClientVersion)
|
||||||
|
if len(o.Repositories) > 64 {
|
||||||
|
return xw.Tot(), xdr.ErrElementSizeExceeded
|
||||||
|
}
|
||||||
|
xw.WriteUint32(uint32(len(o.Repositories)))
|
||||||
|
for i := range o.Repositories {
|
||||||
|
o.Repositories[i].encodeXDR(xw)
|
||||||
|
}
|
||||||
if len(o.Options) > 64 {
|
if len(o.Options) > 64 {
|
||||||
return xw.Tot(), xdr.ErrElementSizeExceeded
|
return xw.Tot(), xdr.ErrElementSizeExceeded
|
||||||
}
|
}
|
||||||
@ -221,18 +236,28 @@ func (o OptionsMessage) encodeXDR(xw *xdr.Writer) (int, error) {
|
|||||||
return xw.Tot(), xw.Error()
|
return xw.Tot(), xw.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OptionsMessage) DecodeXDR(r io.Reader) error {
|
func (o *ClusterConfigMessage) DecodeXDR(r io.Reader) error {
|
||||||
xr := xdr.NewReader(r)
|
xr := xdr.NewReader(r)
|
||||||
return o.decodeXDR(xr)
|
return o.decodeXDR(xr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OptionsMessage) UnmarshalXDR(bs []byte) error {
|
func (o *ClusterConfigMessage) UnmarshalXDR(bs []byte) error {
|
||||||
var buf = bytes.NewBuffer(bs)
|
var buf = bytes.NewBuffer(bs)
|
||||||
var xr = xdr.NewReader(buf)
|
var xr = xdr.NewReader(buf)
|
||||||
return o.decodeXDR(xr)
|
return o.decodeXDR(xr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OptionsMessage) decodeXDR(xr *xdr.Reader) error {
|
func (o *ClusterConfigMessage) decodeXDR(xr *xdr.Reader) error {
|
||||||
|
o.ClientName = xr.ReadStringMax(64)
|
||||||
|
o.ClientVersion = xr.ReadStringMax(64)
|
||||||
|
_RepositoriesSize := int(xr.ReadUint32())
|
||||||
|
if _RepositoriesSize > 64 {
|
||||||
|
return xdr.ErrElementSizeExceeded
|
||||||
|
}
|
||||||
|
o.Repositories = make([]Repository, _RepositoriesSize)
|
||||||
|
for i := range o.Repositories {
|
||||||
|
(&o.Repositories[i]).decodeXDR(xr)
|
||||||
|
}
|
||||||
_OptionsSize := int(xr.ReadUint32())
|
_OptionsSize := int(xr.ReadUint32())
|
||||||
if _OptionsSize > 64 {
|
if _OptionsSize > 64 {
|
||||||
return xdr.ErrElementSizeExceeded
|
return xdr.ErrElementSizeExceeded
|
||||||
@ -244,6 +269,95 @@ func (o *OptionsMessage) decodeXDR(xr *xdr.Reader) error {
|
|||||||
return xr.Error()
|
return xr.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o Repository) EncodeXDR(w io.Writer) (int, error) {
|
||||||
|
var xw = xdr.NewWriter(w)
|
||||||
|
return o.encodeXDR(xw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Repository) MarshalXDR() []byte {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
var xw = xdr.NewWriter(&buf)
|
||||||
|
o.encodeXDR(xw)
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Repository) encodeXDR(xw *xdr.Writer) (int, error) {
|
||||||
|
if len(o.ID) > 64 {
|
||||||
|
return xw.Tot(), xdr.ErrElementSizeExceeded
|
||||||
|
}
|
||||||
|
xw.WriteString(o.ID)
|
||||||
|
if len(o.Nodes) > 64 {
|
||||||
|
return xw.Tot(), xdr.ErrElementSizeExceeded
|
||||||
|
}
|
||||||
|
xw.WriteUint32(uint32(len(o.Nodes)))
|
||||||
|
for i := range o.Nodes {
|
||||||
|
o.Nodes[i].encodeXDR(xw)
|
||||||
|
}
|
||||||
|
return xw.Tot(), xw.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Repository) DecodeXDR(r io.Reader) error {
|
||||||
|
xr := xdr.NewReader(r)
|
||||||
|
return o.decodeXDR(xr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Repository) UnmarshalXDR(bs []byte) error {
|
||||||
|
var buf = bytes.NewBuffer(bs)
|
||||||
|
var xr = xdr.NewReader(buf)
|
||||||
|
return o.decodeXDR(xr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Repository) decodeXDR(xr *xdr.Reader) error {
|
||||||
|
o.ID = xr.ReadStringMax(64)
|
||||||
|
_NodesSize := int(xr.ReadUint32())
|
||||||
|
if _NodesSize > 64 {
|
||||||
|
return xdr.ErrElementSizeExceeded
|
||||||
|
}
|
||||||
|
o.Nodes = make([]Node, _NodesSize)
|
||||||
|
for i := range o.Nodes {
|
||||||
|
(&o.Nodes[i]).decodeXDR(xr)
|
||||||
|
}
|
||||||
|
return xr.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Node) EncodeXDR(w io.Writer) (int, error) {
|
||||||
|
var xw = xdr.NewWriter(w)
|
||||||
|
return o.encodeXDR(xw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Node) MarshalXDR() []byte {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
var xw = xdr.NewWriter(&buf)
|
||||||
|
o.encodeXDR(xw)
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Node) encodeXDR(xw *xdr.Writer) (int, error) {
|
||||||
|
if len(o.ID) > 64 {
|
||||||
|
return xw.Tot(), xdr.ErrElementSizeExceeded
|
||||||
|
}
|
||||||
|
xw.WriteString(o.ID)
|
||||||
|
xw.WriteUint32(o.Flags)
|
||||||
|
return xw.Tot(), xw.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Node) DecodeXDR(r io.Reader) error {
|
||||||
|
xr := xdr.NewReader(r)
|
||||||
|
return o.decodeXDR(xr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Node) UnmarshalXDR(bs []byte) error {
|
||||||
|
var buf = bytes.NewBuffer(bs)
|
||||||
|
var xr = xdr.NewReader(buf)
|
||||||
|
return o.decodeXDR(xr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Node) decodeXDR(xr *xdr.Reader) error {
|
||||||
|
o.ID = xr.ReadStringMax(64)
|
||||||
|
o.Flags = xr.ReadUint32()
|
||||||
|
return xr.Error()
|
||||||
|
}
|
||||||
|
|
||||||
func (o Option) EncodeXDR(w io.Writer) (int, error) {
|
func (o Option) EncodeXDR(w io.Writer) (int, error) {
|
||||||
var xw = xdr.NewWriter(w)
|
var xw = xdr.NewWriter(w)
|
||||||
return o.encodeXDR(xw)
|
return o.encodeXDR(xw)
|
||||||
|
@ -29,6 +29,10 @@ func (m nativeModel) Request(nodeID, repo string, name string, offset int64, siz
|
|||||||
return m.next.Request(nodeID, repo, name, offset, size)
|
return m.next.Request(nodeID, repo, name, offset, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m nativeModel) ClusterConfig(nodeID string, config ClusterConfigMessage) {
|
||||||
|
m.next.ClusterConfig(nodeID, config)
|
||||||
|
}
|
||||||
|
|
||||||
func (m nativeModel) Close(nodeID string, err error) {
|
func (m nativeModel) Close(nodeID string, err error) {
|
||||||
m.next.Close(nodeID, err)
|
m.next.Close(nodeID, err)
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,10 @@ func (m nativeModel) Request(nodeID, repo string, name string, offset int64, siz
|
|||||||
return m.next.Request(nodeID, repo, name, offset, size)
|
return m.next.Request(nodeID, repo, name, offset, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m nativeModel) ClusterConfig(nodeID string, config ClusterConfigMessage) {
|
||||||
|
m.next.ClusterConfig(nodeID, config)
|
||||||
|
}
|
||||||
|
|
||||||
func (m nativeModel) Close(nodeID string, err error) {
|
func (m nativeModel) Close(nodeID string, err error) {
|
||||||
m.next.Close(nodeID, err)
|
m.next.Close(nodeID, err)
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,10 @@ func (m nativeModel) Request(nodeID, repo string, name string, offset int64, siz
|
|||||||
return m.next.Request(nodeID, repo, name, offset, size)
|
return m.next.Request(nodeID, repo, name, offset, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m nativeModel) ClusterConfig(nodeID string, config ClusterConfigMessage) {
|
||||||
|
m.next.ClusterConfig(nodeID, config)
|
||||||
|
}
|
||||||
|
|
||||||
func (m nativeModel) Close(nodeID string, err error) {
|
func (m nativeModel) Close(nodeID string, err error) {
|
||||||
m.next.Close(nodeID, err)
|
m.next.Close(nodeID, err)
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -17,13 +16,13 @@ import (
|
|||||||
const BlockSize = 128 * 1024
|
const BlockSize = 128 * 1024
|
||||||
|
|
||||||
const (
|
const (
|
||||||
messageTypeIndex = 1
|
messageTypeClusterConfig = 0
|
||||||
messageTypeRequest = 2
|
messageTypeIndex = 1
|
||||||
messageTypeResponse = 3
|
messageTypeRequest = 2
|
||||||
messageTypePing = 4
|
messageTypeResponse = 3
|
||||||
messageTypePong = 5
|
messageTypePing = 4
|
||||||
messageTypeIndexUpdate = 6
|
messageTypePong = 5
|
||||||
messageTypeOptions = 7
|
messageTypeIndexUpdate = 6
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -32,6 +31,12 @@ const (
|
|||||||
FlagDirectory = 1 << 14
|
FlagDirectory = 1 << 14
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FlagShareTrusted uint32 = 1 << 0
|
||||||
|
FlagShareReadOnly = 1 << 1
|
||||||
|
FlagShareBits = 0x000000ff
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrClusterHash = fmt.Errorf("configuration error: mismatched cluster hash")
|
ErrClusterHash = fmt.Errorf("configuration error: mismatched cluster hash")
|
||||||
ErrClosed = errors.New("connection closed")
|
ErrClosed = errors.New("connection closed")
|
||||||
@ -44,6 +49,8 @@ type Model interface {
|
|||||||
IndexUpdate(nodeID string, repo string, files []FileInfo)
|
IndexUpdate(nodeID string, repo string, files []FileInfo)
|
||||||
// A request was made by the peer node
|
// A request was made by the peer node
|
||||||
Request(nodeID string, repo string, name string, offset int64, size int) ([]byte, error)
|
Request(nodeID string, repo string, name string, offset int64, size int) ([]byte, error)
|
||||||
|
// A cluster configuration message was received
|
||||||
|
ClusterConfig(nodeID string, config ClusterConfigMessage)
|
||||||
// The peer node closed the connection
|
// The peer node closed the connection
|
||||||
Close(nodeID string, err error)
|
Close(nodeID string, err error)
|
||||||
}
|
}
|
||||||
@ -52,27 +59,24 @@ type Connection interface {
|
|||||||
ID() string
|
ID() string
|
||||||
Index(repo string, files []FileInfo)
|
Index(repo string, files []FileInfo)
|
||||||
Request(repo string, name string, offset int64, size int) ([]byte, error)
|
Request(repo string, name string, offset int64, size int) ([]byte, error)
|
||||||
|
ClusterConfig(config ClusterConfigMessage)
|
||||||
Statistics() Statistics
|
Statistics() Statistics
|
||||||
Option(key string) string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type rawConnection struct {
|
type rawConnection struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
|
|
||||||
id string
|
id string
|
||||||
receiver Model
|
receiver Model
|
||||||
reader io.ReadCloser
|
reader io.ReadCloser
|
||||||
xr *xdr.Reader
|
xr *xdr.Reader
|
||||||
writer io.WriteCloser
|
writer io.WriteCloser
|
||||||
wb *bufio.Writer
|
wb *bufio.Writer
|
||||||
xw *xdr.Writer
|
xw *xdr.Writer
|
||||||
closed chan struct{}
|
closed chan struct{}
|
||||||
awaiting map[int]chan asyncResult
|
awaiting map[int]chan asyncResult
|
||||||
nextID int
|
nextID int
|
||||||
indexSent map[string]map[string][2]int64
|
indexSent map[string]map[string][2]int64
|
||||||
peerOptions map[string]string
|
|
||||||
myOptions map[string]string
|
|
||||||
optionsLock sync.Mutex
|
|
||||||
|
|
||||||
hasSentIndex bool
|
hasSentIndex bool
|
||||||
hasRecvdIndex bool
|
hasRecvdIndex bool
|
||||||
@ -88,7 +92,7 @@ const (
|
|||||||
pingIdleTime = 5 * time.Minute
|
pingIdleTime = 5 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewConnection(nodeID string, reader io.Reader, writer io.Writer, receiver Model, options map[string]string) Connection {
|
func NewConnection(nodeID string, reader io.Reader, writer io.Writer, receiver Model) Connection {
|
||||||
flrd := flate.NewReader(reader)
|
flrd := flate.NewReader(reader)
|
||||||
flwr, err := flate.NewWriter(writer, flate.BestSpeed)
|
flwr, err := flate.NewWriter(writer, flate.BestSpeed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -112,28 +116,6 @@ func NewConnection(nodeID string, reader io.Reader, writer io.Writer, receiver M
|
|||||||
go c.readerLoop()
|
go c.readerLoop()
|
||||||
go c.pingerLoop()
|
go c.pingerLoop()
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
c.myOptions = options
|
|
||||||
go func() {
|
|
||||||
c.Lock()
|
|
||||||
header{0, c.nextID, messageTypeOptions}.encodeXDR(c.xw)
|
|
||||||
var om OptionsMessage
|
|
||||||
for k, v := range options {
|
|
||||||
om.Options = append(om.Options, Option{k, v})
|
|
||||||
}
|
|
||||||
om.encodeXDR(c.xw)
|
|
||||||
err := c.xw.Error()
|
|
||||||
if err == nil {
|
|
||||||
err = c.flush()
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Warning: Write error during initial handshake:", err)
|
|
||||||
}
|
|
||||||
c.nextID++
|
|
||||||
c.Unlock()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
return wireFormatConnection{&c}
|
return wireFormatConnection{&c}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,6 +199,27 @@ func (c *rawConnection) Request(repo string, name string, offset int64, size int
|
|||||||
return res.val, res.err
|
return res.val, res.err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClusterConfig send the cluster configuration message to the peer and returns any error
|
||||||
|
func (c *rawConnection) ClusterConfig(config ClusterConfigMessage) {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
|
||||||
|
if c.isClosed() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
header{0, c.nextID, messageTypeClusterConfig}.encodeXDR(c.xw)
|
||||||
|
c.nextID = (c.nextID + 1) & 0xfff
|
||||||
|
|
||||||
|
_, err := config.encodeXDR(c.xw)
|
||||||
|
if err == nil {
|
||||||
|
err = c.flush()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
c.close(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *rawConnection) ping() bool {
|
func (c *rawConnection) ping() bool {
|
||||||
c.Lock()
|
c.Lock()
|
||||||
if c.isClosed() {
|
if c.isClosed() {
|
||||||
@ -386,24 +389,14 @@ loop:
|
|||||||
c.Unlock()
|
c.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
case messageTypeOptions:
|
case messageTypeClusterConfig:
|
||||||
var om OptionsMessage
|
var cm ClusterConfigMessage
|
||||||
om.decodeXDR(c.xr)
|
cm.decodeXDR(c.xr)
|
||||||
if c.xr.Error() != nil {
|
if c.xr.Error() != nil {
|
||||||
c.close(c.xr.Error())
|
c.close(c.xr.Error())
|
||||||
break loop
|
break loop
|
||||||
}
|
} else {
|
||||||
|
go c.receiver.ClusterConfig(c.id, cm)
|
||||||
c.optionsLock.Lock()
|
|
||||||
c.peerOptions = make(map[string]string, len(om.Options))
|
|
||||||
for _, opt := range om.Options {
|
|
||||||
c.peerOptions[opt.Key] = opt.Value
|
|
||||||
}
|
|
||||||
c.optionsLock.Unlock()
|
|
||||||
|
|
||||||
if mh, rh := c.myOptions["clusterHash"], c.peerOptions["clusterHash"]; len(mh) > 0 && len(rh) > 0 && mh != rh {
|
|
||||||
c.close(ErrClusterHash)
|
|
||||||
break loop
|
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -472,9 +465,3 @@ func (c *rawConnection) Statistics() Statistics {
|
|||||||
OutBytesTotal: int(c.xw.Tot()),
|
OutBytesTotal: int(c.xw.Tot()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *rawConnection) Option(key string) string {
|
|
||||||
c.optionsLock.Lock()
|
|
||||||
defer c.optionsLock.Unlock()
|
|
||||||
return c.peerOptions[key]
|
|
||||||
}
|
|
||||||
|
@ -25,8 +25,8 @@ func TestPing(t *testing.T) {
|
|||||||
ar, aw := io.Pipe()
|
ar, aw := io.Pipe()
|
||||||
br, bw := io.Pipe()
|
br, bw := io.Pipe()
|
||||||
|
|
||||||
c0 := NewConnection("c0", ar, bw, nil, nil).(wireFormatConnection).next.(*rawConnection)
|
c0 := NewConnection("c0", ar, bw, nil).(wireFormatConnection).next.(*rawConnection)
|
||||||
c1 := NewConnection("c1", br, aw, nil, nil).(wireFormatConnection).next.(*rawConnection)
|
c1 := NewConnection("c1", br, aw, nil).(wireFormatConnection).next.(*rawConnection)
|
||||||
|
|
||||||
if ok := c0.ping(); !ok {
|
if ok := c0.ping(); !ok {
|
||||||
t.Error("c0 ping failed")
|
t.Error("c0 ping failed")
|
||||||
@ -49,8 +49,8 @@ func TestPingErr(t *testing.T) {
|
|||||||
eaw := &ErrPipe{PipeWriter: *aw, max: i, err: e}
|
eaw := &ErrPipe{PipeWriter: *aw, max: i, err: e}
|
||||||
ebw := &ErrPipe{PipeWriter: *bw, max: j, err: e}
|
ebw := &ErrPipe{PipeWriter: *bw, max: j, err: e}
|
||||||
|
|
||||||
c0 := NewConnection("c0", ar, ebw, m0, nil).(wireFormatConnection).next.(*rawConnection)
|
c0 := NewConnection("c0", ar, ebw, m0).(wireFormatConnection).next.(*rawConnection)
|
||||||
NewConnection("c1", br, eaw, m1, nil)
|
NewConnection("c1", br, eaw, m1)
|
||||||
|
|
||||||
res := c0.ping()
|
res := c0.ping()
|
||||||
if (i < 4 || j < 4) && res {
|
if (i < 4 || j < 4) && res {
|
||||||
@ -125,8 +125,8 @@ func TestVersionErr(t *testing.T) {
|
|||||||
ar, aw := io.Pipe()
|
ar, aw := io.Pipe()
|
||||||
br, bw := io.Pipe()
|
br, bw := io.Pipe()
|
||||||
|
|
||||||
c0 := NewConnection("c0", ar, bw, m0, nil).(wireFormatConnection).next.(*rawConnection)
|
c0 := NewConnection("c0", ar, bw, m0).(wireFormatConnection).next.(*rawConnection)
|
||||||
NewConnection("c1", br, aw, m1, nil)
|
NewConnection("c1", br, aw, m1)
|
||||||
|
|
||||||
c0.xw.WriteUint32(encodeHeader(header{
|
c0.xw.WriteUint32(encodeHeader(header{
|
||||||
version: 2,
|
version: 2,
|
||||||
@ -147,8 +147,8 @@ func TestTypeErr(t *testing.T) {
|
|||||||
ar, aw := io.Pipe()
|
ar, aw := io.Pipe()
|
||||||
br, bw := io.Pipe()
|
br, bw := io.Pipe()
|
||||||
|
|
||||||
c0 := NewConnection("c0", ar, bw, m0, nil).(wireFormatConnection).next.(*rawConnection)
|
c0 := NewConnection("c0", ar, bw, m0).(wireFormatConnection).next.(*rawConnection)
|
||||||
NewConnection("c1", br, aw, m1, nil)
|
NewConnection("c1", br, aw, m1)
|
||||||
|
|
||||||
c0.xw.WriteUint32(encodeHeader(header{
|
c0.xw.WriteUint32(encodeHeader(header{
|
||||||
version: 0,
|
version: 0,
|
||||||
@ -169,8 +169,8 @@ func TestClose(t *testing.T) {
|
|||||||
ar, aw := io.Pipe()
|
ar, aw := io.Pipe()
|
||||||
br, bw := io.Pipe()
|
br, bw := io.Pipe()
|
||||||
|
|
||||||
c0 := NewConnection("c0", ar, bw, m0, nil).(wireFormatConnection).next.(*rawConnection)
|
c0 := NewConnection("c0", ar, bw, m0).(wireFormatConnection).next.(*rawConnection)
|
||||||
NewConnection("c1", br, aw, m1, nil)
|
NewConnection("c1", br, aw, m1)
|
||||||
|
|
||||||
c0.close(nil)
|
c0.close(nil)
|
||||||
|
|
||||||
|
@ -30,10 +30,10 @@ func (c wireFormatConnection) Request(repo, name string, offset int64, size int)
|
|||||||
return c.next.Request(repo, name, offset, size)
|
return c.next.Request(repo, name, offset, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c wireFormatConnection) ClusterConfig(config ClusterConfigMessage) {
|
||||||
|
c.next.ClusterConfig(config)
|
||||||
|
}
|
||||||
|
|
||||||
func (c wireFormatConnection) Statistics() Statistics {
|
func (c wireFormatConnection) Statistics() Statistics {
|
||||||
return c.next.Statistics()
|
return c.next.Statistics()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c wireFormatConnection) Option(key string) string {
|
|
||||||
return c.next.Option(key)
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user