pax_global_header00006660000000000000000000000064146502432370014520gustar00rootroot0000000000000052 comment=27bc0d6c39bb4e9d3c410057bf2c779f256ba15e golang-github-go-zookeeper-zk-1.0.4/000077500000000000000000000000001465024323700172775ustar00rootroot00000000000000golang-github-go-zookeeper-zk-1.0.4/.codecov.yaml000066400000000000000000000001651465024323700216650ustar00rootroot00000000000000coverage: status: patch: default: target: 80% project: default: threshold: 76% golang-github-go-zookeeper-zk-1.0.4/.devcontainer/000077500000000000000000000000001465024323700220365ustar00rootroot00000000000000golang-github-go-zookeeper-zk-1.0.4/.devcontainer/devcontainer.json000066400000000000000000000006311465024323700254120ustar00rootroot00000000000000// For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/go { "name": "Go", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile "image": "mcr.microsoft.com/devcontainers/go:0-1-bullseye", "features": { "ghcr.io/devcontainers/features/java:1": { } } } golang-github-go-zookeeper-zk-1.0.4/.gitattributes000066400000000000000000000000161465024323700221670ustar00rootroot00000000000000* text eol=lf golang-github-go-zookeeper-zk-1.0.4/.github/000077500000000000000000000000001465024323700206375ustar00rootroot00000000000000golang-github-go-zookeeper-zk-1.0.4/.github/workflows/000077500000000000000000000000001465024323700226745ustar00rootroot00000000000000golang-github-go-zookeeper-zk-1.0.4/.github/workflows/integration.yaml000066400000000000000000000017551465024323700261130ustar00rootroot00000000000000name: integration_test on: push: branches: - master pull_request: branches: - master jobs: integration_test: name: integration_test strategy: matrix: zk-version: [3.5.8, 3.6.1] go-version: ['oldstable', 'stable'] runs-on: ubuntu-latest steps: - name: Go ${{ matrix.go-version }} setup uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} - name: Setup Java 14 uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: 14 - name: Checkout code uses: actions/checkout@v3 - name: Test code run: make test ZK_VERSION=${{ matrix.zk-version }} - name: Upload code coverage # only upload one result from the matrix. if: ${{ strategy.job-index == 0 }} uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} # required file: ./profile.cov golang-github-go-zookeeper-zk-1.0.4/.github/workflows/lint.yaml000066400000000000000000000004531465024323700245300ustar00rootroot00000000000000 name: lint on: [pull_request] jobs: lint: name: lint runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v1 - name: Lint code uses: reviewdog/action-golangci-lint@v1 with: github_token: ${{ secrets.github_token }} golang-github-go-zookeeper-zk-1.0.4/.github/workflows/stale.yml000066400000000000000000000022571465024323700245350ustar00rootroot00000000000000# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. # # You can adjust the behavior by modifying this file. # For more information, see: # https://github.com/actions/stale name: Mark stale issues and pull requests on: schedule: - cron: '0 12 * * *' jobs: stale: runs-on: ubuntu-latest permissions: issues: write pull-requests: write steps: - uses: actions/stale@v9 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-label: 'no-issue-activity' stale-pr-label: 'no-pr-activity' days-before-stale: 180 days-before-issue-stale: 730 days-before-close: 30 days-before-pr-close: -1 stale-pr-message: 'Stale pull request detected at 180 days. Please update, comment, or rebase.' stale-issue-message: 'This issue is marked as stale. If you want to keep this issue open, please leave a comment, otherwise the issue will be closed due to inactivity.' close-issue-message: "This issue has been closed due to inactivity after being stale for 30 days." debug-only: true exempt-all-milestones: true golang-github-go-zookeeper-zk-1.0.4/.github/workflows/unittest.yaml000066400000000000000000000010331465024323700254340ustar00rootroot00000000000000name: unittest on: push: branches: - master pull_request: branches: - master jobs: unittest: name: unittest strategy: matrix: go-version: ['oldstable', 'stable'] runs-on: ubuntu-latest steps: - name: Go ${{ matrix.go-version }} setup uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v3 - name: Run unittest ${{ matrix.go-version }} run: make unittest golang-github-go-zookeeper-zk-1.0.4/.gitignore000066400000000000000000000001671465024323700212730ustar00rootroot00000000000000.vscode/ .DS_Store profile.cov zookeeper zookeeper-*/ zookeeper-*.tar.gz apache-zookeeper-*/ apache-zookeeper-*.tar.gz golang-github-go-zookeeper-zk-1.0.4/CONTRIBUTION.md000066400000000000000000000050171465024323700215430ustar00rootroot00000000000000# how to contribute to the go zookeeper library ## **Did you find a bug?** * **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/go-zookeeper/zk/issues). * If you're unable to find an open issue addressing the problem, open a new one. * Be sure to include a title and clear description. * Be sure to include the actual behavior vs the expected. * As much relevant information as possible, a code sample or an executable test case demonstrating the expected vs actual behavior. ## Did you write a patch that fixes a bug * Ensure that all bugs are first reported as an issue. This will help others in finding fixes through issues first. * Open a PR referencing the issue for the bug. ## Pull Requests We are open to all Pull Requests, its best to accompany the requests with an issue. * The PR requires the github actions to pass. * Requires at least one maintainer to approve the PR to merge to master. While the above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted. ## Versioned Releases Since this library is a core client for interacting with Zookeeper, we do [SemVer](https://semver.org/) releases to ensure predictable changes for users. Zookeeper itself maintains a compatibility check on the main codebase as well as maintaining backwards compatibility through all Major releases, this core library will try to uphold similar standards of releases. * Code that is merged into master should be ready for release at any given time. * This is to say, that code should not be merged into master if it is not complete and ready for production use. * If a fix needs to be released ahead of normal operations, file an issue explaining the urgency and impact of the bug. ## Coding guidelines Some good external resources for style: 1. [Effective Go](https://golang.org/doc/effective_go.html) 2. [The Go common mistakes guide](https://github.com/golang/go/wiki/CodeReviewComments) All code should be error-free when run through `golint` and `go vet`. We recommend setting up your editor to: * Run `goimports` on save * Run `golint` and `go vet` to check for errors You can find information in editor support for Go tools here: ## Addition information * We have zero external dependencies, and would like to maintain this. Use of any external go library should be limited to tests. golang-github-go-zookeeper-zk-1.0.4/LICENSE000066400000000000000000000027161465024323700203120ustar00rootroot00000000000000Copyright (c) 2013, Samuel Stauffer All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. golang-github-go-zookeeper-zk-1.0.4/Makefile000066400000000000000000000024311465024323700207370ustar00rootroot00000000000000# make file to hold the logic of build and test setup ZK_VERSION ?= 3.5.6 # Apache changed the name of the archive in version 3.5.x and seperated out # src and binary packages ZK_MINOR_VER=$(word 2, $(subst ., ,$(ZK_VERSION))) ifeq ($(shell test $(ZK_MINOR_VER) -le 4; echo $$?),0) ZK = zookeeper-$(ZK_VERSION) else ZK = apache-zookeeper-$(ZK_VERSION)-bin endif ZK_URL = "https://archive.apache.org/dist/zookeeper/zookeeper-$(ZK_VERSION)/$(ZK).tar.gz" PACKAGES := $(shell go list ./... | grep -v examples) .DEFAULT_GOAL := test $(ZK): wget $(ZK_URL) tar -zxf $(ZK).tar.gz rm $(ZK).tar.gz zookeeper: $(ZK) # we link to a standard directory path so then the tests dont need to find based on version # in the test code. this allows backward compatable testing. ln -s $(ZK) zookeeper .PHONY: setup setup: zookeeper .PHONY: lint lint: go fmt ./... go vet ./... .PHONY: build build: go build ./... .PHONY: unittest unittest: go test -timeout 500s -v -race -covermode atomic -skip=Integration ./... .PHONY: test test: build zookeeper go test -timeout 500s -v -race -covermode atomic -coverprofile=profile.cov $(PACKAGES) .PHONY: clean clean: rm -f apache-zookeeper-*.tar.gz rm -f zookeeper-*.tar.gz rm -rf apache-zookeeper-*/ rm -rf zookeeper-*/ rm -f zookeeper rm -f profile.cov golang-github-go-zookeeper-zk-1.0.4/README.md000066400000000000000000000010721465024323700205560ustar00rootroot00000000000000Native Go Zookeeper Client Library =================================== [![GoDoc](https://godoc.org/github.com/go-zookeeper/zk?status.svg)](https://godoc.org/github.com/go-zookeeper/zk) [![unittest](https://github.com/go-zookeeper/zk/actions/workflows/unittest.yaml/badge.svg?branch=master&event=push)](https://github.com/go-zookeeper/zk/actions/workflows/unittest.yaml) [![Coverage Status](https://img.shields.io/codecov/c/github/go-zookeeper/zk/master)](https://codecov.io/gh/go-zookeeper/zk/branch/master) License ------- 3-clause BSD. See [LICENSE](LICENSE) file. golang-github-go-zookeeper-zk-1.0.4/_examples/000077500000000000000000000000001465024323700212545ustar00rootroot00000000000000golang-github-go-zookeeper-zk-1.0.4/_examples/basic.go000066400000000000000000000005271465024323700226700ustar00rootroot00000000000000package main import ( "fmt" "time" "github.com/go-zookeeper/zk" ) func main() { c, _, err := zk.Connect([]string{"127.0.0.1"}, time.Second) //*10) if err != nil { panic(err) } children, stat, ch, err := c.ChildrenW("/") if err != nil { panic(err) } fmt.Printf("%+v %+v\n", children, stat) e := <-ch fmt.Printf("%+v\n", e) } golang-github-go-zookeeper-zk-1.0.4/cluster_test.go000066400000000000000000000206041465024323700223500ustar00rootroot00000000000000package zk import ( "sync" "testing" "time" ) type logWriter struct { t *testing.T p string } func (lw logWriter) Write(b []byte) (int, error) { lw.t.Logf("%s%s", lw.p, string(b)) return len(b), nil } func TestIntegration_BasicCluster(t *testing.T) { ts, err := StartTestCluster(t, 3, nil, logWriter{t: t, p: "[ZKERR] "}) if err != nil { t.Fatal(err) } defer ts.Stop() zk1, _, err := ts.Connect(0) if err != nil { t.Fatalf("Connect returned error: %+v", err) } defer zk1.Close() zk2, _, err := ts.Connect(1) if err != nil { t.Fatalf("Connect returned error: %+v", err) } defer zk2.Close() if _, err := zk1.Create("/gozk-test", []byte("foo-cluster"), 0, WorldACL(PermAll)); err != nil { t.Fatalf("Create failed on node 1: %+v", err) } if _, err := zk2.Sync("/gozk-test"); err != nil { t.Fatalf("Sync failed on node 2: %+v", err) } if by, _, err := zk2.Get("/gozk-test"); err != nil { t.Fatalf("Get failed on node 2: %+v", err) } else if string(by) != "foo-cluster" { t.Fatal("Wrong data for node 2") } } // If the current leader dies, then the session is reestablished with the new one. func TestIntegration_ClientClusterFailover(t *testing.T) { tc, err := StartTestCluster(t, 3, nil, logWriter{t: t, p: "[ZKERR] "}) if err != nil { t.Fatal(err) } defer tc.Stop() zk, evCh, err := tc.ConnectAll() if err != nil { t.Fatalf("Connect returned error: %+v", err) } defer zk.Close() sl := NewStateLogger(evCh) hasSessionEvent1 := sl.NewWatcher(sessionStateMatcher(StateHasSession)).Wait(8 * time.Second) if hasSessionEvent1 == nil { t.Fatalf("Failed to connect and get session") } if _, err := zk.Create("/gozk-test", []byte("foo-cluster"), 0, WorldACL(PermAll)); err != nil { t.Fatalf("Create failed on node 1: %+v", err) } hasSessionWatcher2 := sl.NewWatcher(sessionStateMatcher(StateHasSession)) // Kill the current leader tc.StopServer(hasSessionEvent1.Server) // Wait for the session to be reconnected with the new leader. if hasSessionWatcher2.Wait(8*time.Second) == nil { t.Fatalf("Failover failed") } if by, _, err := zk.Get("/gozk-test"); err != nil { t.Fatalf("Get failed on node 2: %+v", err) } else if string(by) != "foo-cluster" { t.Fatal("Wrong data for node 2") } } // If a ZooKeeper cluster looses quorum then a session is reconnected as soon // as the quorum is restored. func TestIntegration_NoQuorum(t *testing.T) { tc, err := StartTestCluster(t, 3, nil, logWriter{t: t, p: "[ZKERR] "}) if err != nil { t.Fatal(err) } defer tc.Stop() zk, evCh, err := tc.ConnectAllTimeout(4 * time.Second) if err != nil { t.Fatalf("Connect returned error: %+v", err) } defer zk.Close() sl := NewStateLogger(evCh) // Wait for initial session to be established hasSessionEvent1 := sl.NewWatcher(sessionStateMatcher(StateHasSession)).Wait(8 * time.Second) if hasSessionEvent1 == nil { t.Fatalf("Failed to connect and get session") } initialSessionID := zk.sessionID DefaultLogger.Printf(" Session established: id=%d, timeout=%d", zk.sessionID, zk.sessionTimeoutMs) // Kill the ZooKeeper leader and wait for the session to reconnect. DefaultLogger.Printf(" Kill the leader") disconnectWatcher1 := sl.NewWatcher(sessionStateMatcher(StateDisconnected)) hasSessionWatcher2 := sl.NewWatcher(sessionStateMatcher(StateHasSession)) tc.StopServer(hasSessionEvent1.Server) disconnectedEvent1 := disconnectWatcher1.Wait(8 * time.Second) if disconnectedEvent1 == nil { t.Fatalf("Failover failed, missed StateDisconnected event") } if disconnectedEvent1.Server != hasSessionEvent1.Server { t.Fatalf("Unexpected StateDisconnected event, expected=%s, actual=%s", hasSessionEvent1.Server, disconnectedEvent1.Server) } hasSessionEvent2 := hasSessionWatcher2.Wait(8 * time.Second) if hasSessionEvent2 == nil { t.Fatalf("Failover failed, missed StateHasSession event") } // Kill the ZooKeeper leader leaving the cluster without quorum. DefaultLogger.Printf(" Kill the leader") disconnectWatcher2 := sl.NewWatcher(sessionStateMatcher(StateDisconnected)) tc.StopServer(hasSessionEvent2.Server) disconnectedEvent2 := disconnectWatcher2.Wait(8 * time.Second) if disconnectedEvent2 == nil { t.Fatalf("Failover failed, missed StateDisconnected event") } if disconnectedEvent2.Server != hasSessionEvent2.Server { t.Fatalf("Unexpected StateDisconnected event, expected=%s, actual=%s", hasSessionEvent2.Server, disconnectedEvent2.Server) } // Make sure that we keep retrying connecting to the only remaining // ZooKeeper server, but the attempts are being dropped because there is // no quorum. DefaultLogger.Printf(" Retrying no luck...") var firstDisconnect *Event begin := time.Now() for time.Now().Sub(begin) < 6*time.Second { disconnectedEvent := sl.NewWatcher(sessionStateMatcher(StateDisconnected)).Wait(4 * time.Second) if disconnectedEvent == nil { t.Fatalf("Disconnected event expected") } if firstDisconnect == nil { firstDisconnect = disconnectedEvent continue } if disconnectedEvent.Server != firstDisconnect.Server { t.Fatalf("Disconnect from wrong server: expected=%s, actual=%s", firstDisconnect.Server, disconnectedEvent.Server) } } // Start a ZooKeeper node to restore quorum. hasSessionWatcher3 := sl.NewWatcher(sessionStateMatcher(StateHasSession)) tc.StartServer(hasSessionEvent1.Server) // Make sure that session is reconnected with the same ID. hasSessionEvent3 := hasSessionWatcher3.Wait(8 * time.Second) if hasSessionEvent3 == nil { t.Fatalf("Session has not been reconnected") } if zk.sessionID != initialSessionID { t.Fatalf("Wrong session ID: expected=%d, actual=%d", initialSessionID, zk.sessionID) } // Make sure that the session is not dropped soon after reconnect e := sl.NewWatcher(sessionStateMatcher(StateDisconnected)).Wait(6 * time.Second) if e != nil { t.Fatalf("Unexpected disconnect") } } func TestIntegration_WaitForClose(t *testing.T) { ts, err := StartTestCluster(t, 1, nil, logWriter{t: t, p: "[ZKERR] "}) if err != nil { t.Fatal(err) } defer ts.Stop() zk, _, err := ts.Connect(0) if err != nil { t.Fatalf("Connect returned error: %+v", err) } timeout := time.After(30 * time.Second) CONNECTED: for { select { case ev := <-zk.eventChan: if ev.State == StateConnected { break CONNECTED } case <-timeout: zk.Close() t.Fatal("Timeout") } } zk.Close() for { select { case _, ok := <-zk.eventChan: if !ok { return } case <-timeout: t.Fatal("Timeout") } } } func TestIntegration_BadSession(t *testing.T) { ts, err := StartTestCluster(t, 1, nil, logWriter{t: t, p: "[ZKERR] "}) if err != nil { t.Fatal(err) } defer ts.Stop() zk, _, err := ts.ConnectAll() if err != nil { t.Fatalf("Connect returned error: %+v", err) } defer zk.Close() if err := zk.Delete("/gozk-test", -1); err != nil && err != ErrNoNode { t.Fatalf("Delete returned error: %+v", err) } zk.conn.Close() time.Sleep(time.Millisecond * 100) if err := zk.Delete("/gozk-test", -1); err != nil && err != ErrNoNode { t.Fatalf("Delete returned error: %+v", err) } } type EventLogger struct { events []Event watchers []*EventWatcher lock sync.Mutex wg sync.WaitGroup } func NewStateLogger(eventCh <-chan Event) *EventLogger { el := &EventLogger{} el.wg.Add(1) go func() { defer el.wg.Done() for event := range eventCh { el.lock.Lock() for _, sw := range el.watchers { if !sw.triggered && sw.matcher(event) { sw.triggered = true sw.matchCh <- event } } DefaultLogger.Printf(" event received: %v\n", event) el.events = append(el.events, event) el.lock.Unlock() } }() return el } func (el *EventLogger) NewWatcher(matcher func(Event) bool) *EventWatcher { ew := &EventWatcher{matcher: matcher, matchCh: make(chan Event, 1)} el.lock.Lock() el.watchers = append(el.watchers, ew) el.lock.Unlock() return ew } func (el *EventLogger) Events() []Event { el.lock.Lock() transitions := make([]Event, len(el.events)) copy(transitions, el.events) el.lock.Unlock() return transitions } func (el *EventLogger) Wait4Stop() { el.wg.Wait() } type EventWatcher struct { matcher func(Event) bool matchCh chan Event triggered bool } func (ew *EventWatcher) Wait(timeout time.Duration) *Event { select { case event := <-ew.matchCh: return &event case <-time.After(timeout): return nil } } func sessionStateMatcher(s State) func(Event) bool { return func(e Event) bool { return e.Type == EventSession && e.State == s } } golang-github-go-zookeeper-zk-1.0.4/conn.go000066400000000000000000001143621465024323700205720ustar00rootroot00000000000000// Package zk is a native Go client library for the ZooKeeper orchestration service. package zk /* TODO: * make sure a ping response comes back in a reasonable time Possible watcher events: * Event{Type: EventNotWatching, State: StateDisconnected, Path: path, Err: err} */ import ( "context" "crypto/rand" "encoding/binary" "errors" "fmt" "io" "net" "strings" "sync" "sync/atomic" "time" ) // ErrNoServer indicates that an operation cannot be completed // because attempts to connect to all servers in the list failed. var ErrNoServer = errors.New("zk: could not connect to a server") // ErrInvalidPath indicates that an operation was being attempted on // an invalid path. (e.g. empty path). var ErrInvalidPath = errors.New("zk: invalid path") // DefaultLogger uses the stdlib log package for logging. var DefaultLogger Logger = defaultLogger{} const ( bufferSize = 1536 * 1024 eventChanSize = 6 sendChanSize = 16 protectedPrefix = "_c_" ) type watchType int const ( watchTypeData watchType = iota watchTypeExist watchTypeChild ) type watchPathType struct { path string wType watchType } // Dialer is a function to be used to establish a connection to a single host. type Dialer func(network, address string, timeout time.Duration) (net.Conn, error) // Logger is an interface that can be implemented to provide custom log output. type Logger interface { Printf(string, ...interface{}) } type authCreds struct { scheme string auth []byte } // Conn is the client connection and tracks all details for communication with the server. type Conn struct { lastZxid int64 sessionID int64 state State // must be 32-bit aligned xid uint32 sessionTimeoutMs int32 // session timeout in milliseconds passwd []byte dialer Dialer hostProvider HostProvider serverMu sync.Mutex // protects server server string // remember the address/port of the current server conn net.Conn eventChan chan Event eventCallback EventCallback // may be nil shouldQuit chan struct{} shouldQuitOnce sync.Once pingInterval time.Duration recvTimeout time.Duration connectTimeout time.Duration maxBufferSize int creds []authCreds credsMu sync.Mutex // protects server sendChan chan *request requests map[int32]*request // Xid -> pending request requestsLock sync.Mutex watchers map[watchPathType][]chan Event watchersLock sync.Mutex closeChan chan struct{} // channel to tell send loop stop // Debug (used by unit tests) reconnectLatch chan struct{} setWatchLimit int setWatchCallback func([]*setWatchesRequest) // Debug (for recurring re-auth hang) debugCloseRecvLoop bool resendZkAuthFn func(context.Context, *Conn) error logger Logger logInfo bool // true if information messages are logged; false if only errors are logged buf []byte } // connOption represents a connection option. type connOption func(c *Conn) type request struct { xid int32 opcode int32 pkt interface{} recvStruct interface{} recvChan chan response // Because sending and receiving happen in separate go routines, there's // a possible race condition when creating watches from outside the read // loop. We must ensure that a watcher gets added to the list synchronously // with the response from the server on any request that creates a watch. // In order to not hard code the watch logic for each opcode in the recv // loop the caller can use recvFunc to insert some synchronously code // after a response. recvFunc func(*request, *responseHeader, error) } type response struct { zxid int64 err error } // Event is an Znode event sent by the server. // Refer to EventType for more details. type Event struct { Type EventType State State Path string // For non-session events, the path of the watched node. Err error Server string // For connection events } // HostProvider is used to represent a set of hosts a ZooKeeper client should connect to. // It is an analog of the Java equivalent: // http://svn.apache.org/viewvc/zookeeper/trunk/src/java/main/org/apache/zookeeper/client/HostProvider.java?view=markup type HostProvider interface { // Init is called first, with the servers specified in the connection string. Init(servers []string) error // Len returns the number of servers. Len() int // Next returns the next server to connect to. retryStart will be true if we've looped through // all known servers without Connected() being called. Next() (server string, retryStart bool) // Notify the HostProvider of a successful connection. Connected() } // ConnectWithDialer establishes a new connection to a pool of zookeeper servers // using a custom Dialer. See Connect for further information about session timeout. // This method is deprecated and provided for compatibility: use the WithDialer option instead. func ConnectWithDialer(servers []string, sessionTimeout time.Duration, dialer Dialer) (*Conn, <-chan Event, error) { return Connect(servers, sessionTimeout, WithDialer(dialer)) } // Connect establishes a new connection to a pool of zookeeper // servers. The provided session timeout sets the amount of time for which // a session is considered valid after losing connection to a server. Within // the session timeout it's possible to reestablish a connection to a different // server and keep the same session. This is means any ephemeral nodes and // watches are maintained. func Connect(servers []string, sessionTimeout time.Duration, options ...connOption) (*Conn, <-chan Event, error) { if len(servers) == 0 { return nil, nil, errors.New("zk: server list must not be empty") } srvs := FormatServers(servers) // Randomize the order of the servers to avoid creating hotspots stringShuffle(srvs) ec := make(chan Event, eventChanSize) conn := &Conn{ dialer: net.DialTimeout, hostProvider: NewDNSHostProvider(), conn: nil, state: StateDisconnected, eventChan: ec, shouldQuit: make(chan struct{}), connectTimeout: 1 * time.Second, sendChan: make(chan *request, sendChanSize), requests: make(map[int32]*request), watchers: make(map[watchPathType][]chan Event), passwd: emptyPassword, logger: DefaultLogger, logInfo: true, // default is true for backwards compatability buf: make([]byte, bufferSize), resendZkAuthFn: resendZkAuth, } // Set provided options. for _, option := range options { option(conn) } if err := conn.hostProvider.Init(srvs); err != nil { return nil, nil, err } conn.setTimeouts(int32(sessionTimeout / time.Millisecond)) // TODO: This context should be passed in by the caller to be the connection lifecycle context. ctx := context.Background() go func() { conn.loop(ctx) conn.flushRequests(ErrClosing) conn.invalidateWatches(ErrClosing) close(conn.eventChan) }() return conn, ec, nil } // WithDialer returns a connection option specifying a non-default Dialer. func WithDialer(dialer Dialer) connOption { return func(c *Conn) { c.dialer = dialer } } // WithHostProvider returns a connection option specifying a non-default HostProvider. func WithHostProvider(hostProvider HostProvider) connOption { return func(c *Conn) { c.hostProvider = hostProvider } } // WithLogger returns a connection option specifying a non-default Logger. func WithLogger(logger Logger) connOption { return func(c *Conn) { c.logger = logger } } // WithLogInfo returns a connection option specifying whether or not information messages // should be logged. func WithLogInfo(logInfo bool) connOption { return func(c *Conn) { c.logInfo = logInfo } } // EventCallback is a function that is called when an Event occurs. type EventCallback func(Event) // WithEventCallback returns a connection option that specifies an event // callback. // The callback must not block - doing so would delay the ZK go routines. func WithEventCallback(cb EventCallback) connOption { return func(c *Conn) { c.eventCallback = cb } } // WithMaxBufferSize sets the maximum buffer size used to read and decode // packets received from the Zookeeper server. The standard Zookeeper client for // Java defaults to a limit of 1mb. For backwards compatibility, this Go client // defaults to unbounded unless overridden via this option. A value that is zero // or negative indicates that no limit is enforced. // // This is meant to prevent resource exhaustion in the face of potentially // malicious data in ZK. It should generally match the server setting (which // also defaults ot 1mb) so that clients and servers agree on the limits for // things like the size of data in an individual znode and the total size of a // transaction. // // For production systems, this should be set to a reasonable value (ideally // that matches the server configuration). For ops tooling, it is handy to use a // much larger limit, in order to do things like clean-up problematic state in // the ZK tree. For example, if a single znode has a huge number of children, it // is possible for the response to a "list children" operation to exceed this // buffer size and cause errors in clients. The only way to subsequently clean // up the tree (by removing superfluous children) is to use a client configured // with a larger buffer size that can successfully query for all of the child // names and then remove them. (Note there are other tools that can list all of // the child names without an increased buffer size in the client, but they work // by inspecting the servers' transaction logs to enumerate children instead of // sending an online request to a server. func WithMaxBufferSize(maxBufferSize int) connOption { return func(c *Conn) { c.maxBufferSize = maxBufferSize } } // WithMaxConnBufferSize sets maximum buffer size used to send and encode // packets to Zookeeper server. The standard Zookeeper client for java defaults // to a limit of 1mb. This option should be used for non-standard server setup // where znode is bigger than default 1mb. func WithMaxConnBufferSize(maxBufferSize int) connOption { return func(c *Conn) { c.buf = make([]byte, maxBufferSize) } } // Close will submit a close request with ZK and signal the connection to stop // sending and receiving packets. func (c *Conn) Close() { c.shouldQuitOnce.Do(func() { close(c.shouldQuit) select { case <-c.queueRequest(opClose, &closeRequest{}, &closeResponse{}, nil): case <-time.After(time.Second): } }) } // State returns the current state of the connection. func (c *Conn) State() State { return State(atomic.LoadInt32((*int32)(&c.state))) } // SessionID returns the current session id of the connection. func (c *Conn) SessionID() int64 { return atomic.LoadInt64(&c.sessionID) } // SetLogger sets the logger to be used for printing errors. // Logger is an interface provided by this package. func (c *Conn) SetLogger(l Logger) { c.logger = l } func (c *Conn) setTimeouts(sessionTimeoutMs int32) { c.sessionTimeoutMs = sessionTimeoutMs sessionTimeout := time.Duration(sessionTimeoutMs) * time.Millisecond c.recvTimeout = sessionTimeout * 2 / 3 c.pingInterval = c.recvTimeout / 2 } func (c *Conn) setState(state State) { atomic.StoreInt32((*int32)(&c.state), int32(state)) c.sendEvent(Event{Type: EventSession, State: state, Server: c.Server()}) } func (c *Conn) sendEvent(evt Event) { if c.eventCallback != nil { c.eventCallback(evt) } select { case c.eventChan <- evt: default: // panic("zk: event channel full - it must be monitored and never allowed to be full") } } func (c *Conn) connect() error { var retryStart bool for { c.serverMu.Lock() c.server, retryStart = c.hostProvider.Next() c.serverMu.Unlock() c.setState(StateConnecting) if retryStart { c.flushUnsentRequests(ErrNoServer) select { case <-time.After(time.Second): // pass case <-c.shouldQuit: c.setState(StateDisconnected) c.flushUnsentRequests(ErrClosing) return ErrClosing } } zkConn, err := c.dialer("tcp", c.Server(), c.connectTimeout) if err == nil { c.conn = zkConn c.setState(StateConnected) if c.logInfo { c.logger.Printf("connected to %s", c.Server()) } return nil } c.logger.Printf("failed to connect to %s: %v", c.Server(), err) } } func (c *Conn) sendRequest( opcode int32, req interface{}, res interface{}, recvFunc func(*request, *responseHeader, error), ) ( <-chan response, error, ) { rq := &request{ xid: c.nextXid(), opcode: opcode, pkt: req, recvStruct: res, recvChan: make(chan response, 1), recvFunc: recvFunc, } if err := c.sendData(rq); err != nil { return nil, err } return rq.recvChan, nil } func (c *Conn) loop(ctx context.Context) { for { if err := c.connect(); err != nil { // c.Close() was called return } err := c.authenticate() switch { case err == ErrSessionExpired: c.logger.Printf("authentication failed: %s", err) c.invalidateWatches(err) case err != nil && c.conn != nil: c.logger.Printf("authentication failed: %s", err) c.conn.Close() case err == nil: if c.logInfo { c.logger.Printf("authenticated: id=%d, timeout=%d", c.SessionID(), c.sessionTimeoutMs) } c.hostProvider.Connected() // mark success c.closeChan = make(chan struct{}) // channel to tell send loop stop var wg sync.WaitGroup wg.Add(1) go func() { defer c.conn.Close() // causes recv loop to EOF/exit defer wg.Done() if err := c.resendZkAuthFn(ctx, c); err != nil { c.logger.Printf("error in resending auth creds: %v", err) return } if err := c.sendLoop(); err != nil || c.logInfo { c.logger.Printf("send loop terminated: %v", err) } }() wg.Add(1) go func() { defer close(c.closeChan) // tell send loop to exit defer wg.Done() var err error if c.debugCloseRecvLoop { err = errors.New("DEBUG: close recv loop") } else { err = c.recvLoop(c.conn) } if err != io.EOF || c.logInfo { c.logger.Printf("recv loop terminated: %v", err) } if err == nil { panic("zk: recvLoop should never return nil error") } }() c.sendSetWatches() wg.Wait() } c.setState(StateDisconnected) select { case <-c.shouldQuit: c.flushRequests(ErrClosing) return default: } if err != ErrSessionExpired { err = ErrConnectionClosed } c.flushRequests(err) if c.reconnectLatch != nil { select { case <-c.shouldQuit: return case <-c.reconnectLatch: } } } } func (c *Conn) flushUnsentRequests(err error) { for { select { default: return case req := <-c.sendChan: req.recvChan <- response{-1, err} } } } // Send error to all pending requests and clear request map func (c *Conn) flushRequests(err error) { c.requestsLock.Lock() for _, req := range c.requests { req.recvChan <- response{-1, err} } c.requests = make(map[int32]*request) c.requestsLock.Unlock() } // Send event to all interested watchers func (c *Conn) notifyWatches(ev Event) { var wTypes []watchType switch ev.Type { case EventNodeCreated: wTypes = []watchType{watchTypeExist} case EventNodeDataChanged: wTypes = []watchType{watchTypeExist, watchTypeData} case EventNodeChildrenChanged: wTypes = []watchType{watchTypeChild} case EventNodeDeleted: wTypes = []watchType{watchTypeExist, watchTypeData, watchTypeChild} } c.watchersLock.Lock() defer c.watchersLock.Unlock() for _, t := range wTypes { wpt := watchPathType{ev.Path, t} if watchers := c.watchers[wpt]; len(watchers) > 0 { for _, ch := range watchers { ch <- ev close(ch) } delete(c.watchers, wpt) } } } // Send error to all watchers and clear watchers map func (c *Conn) invalidateWatches(err error) { c.watchersLock.Lock() defer c.watchersLock.Unlock() if len(c.watchers) >= 0 { for pathType, watchers := range c.watchers { ev := Event{Type: EventNotWatching, State: StateDisconnected, Path: pathType.path, Err: err} c.sendEvent(ev) // also publish globally for _, ch := range watchers { ch <- ev close(ch) } } c.watchers = make(map[watchPathType][]chan Event) } } func (c *Conn) sendSetWatches() { c.watchersLock.Lock() defer c.watchersLock.Unlock() if len(c.watchers) == 0 { return } // NB: A ZK server, by default, rejects packets >1mb. So, if we have too // many watches to reset, we need to break this up into multiple packets // to avoid hitting that limit. Mirroring the Java client behavior: we are // conservative in that we limit requests to 128kb (since server limit is // is actually configurable and could conceivably be configured smaller // than default of 1mb). limit := 128 * 1024 if c.setWatchLimit > 0 { limit = c.setWatchLimit } var reqs []*setWatchesRequest var req *setWatchesRequest var sizeSoFar int n := 0 for pathType, watchers := range c.watchers { if len(watchers) == 0 { continue } addlLen := 4 + len(pathType.path) if req == nil || sizeSoFar+addlLen > limit { if req != nil { // add to set of requests that we'll send reqs = append(reqs, req) } sizeSoFar = 28 // fixed overhead of a set-watches packet req = &setWatchesRequest{ RelativeZxid: c.lastZxid, DataWatches: make([]string, 0), ExistWatches: make([]string, 0), ChildWatches: make([]string, 0), } } sizeSoFar += addlLen switch pathType.wType { case watchTypeData: req.DataWatches = append(req.DataWatches, pathType.path) case watchTypeExist: req.ExistWatches = append(req.ExistWatches, pathType.path) case watchTypeChild: req.ChildWatches = append(req.ChildWatches, pathType.path) } n++ } if n == 0 { return } if req != nil { // don't forget any trailing packet we were building reqs = append(reqs, req) } if c.setWatchCallback != nil { c.setWatchCallback(reqs) } go func() { res := &setWatchesResponse{} // TODO: Pipeline these so queue all of them up before waiting on any // response. That will require some investigation to make sure there // aren't failure modes where a blocking write to the channel of requests // could hang indefinitely and cause this goroutine to leak... for _, req := range reqs { _, err := c.request(opSetWatches, req, res, nil) if err != nil { c.logger.Printf("Failed to set previous watches: %v", err) break } } }() } func (c *Conn) authenticate() error { buf := make([]byte, 256) // Encode and send a connect request. n, err := encodePacket(buf[4:], &connectRequest{ ProtocolVersion: protocolVersion, LastZxidSeen: c.lastZxid, TimeOut: c.sessionTimeoutMs, SessionID: c.SessionID(), Passwd: c.passwd, }) if err != nil { return err } binary.BigEndian.PutUint32(buf[:4], uint32(n)) c.conn.SetWriteDeadline(time.Now().Add(c.recvTimeout * 10)) _, err = c.conn.Write(buf[:n+4]) c.conn.SetWriteDeadline(time.Time{}) if err != nil { return err } // Receive and decode a connect response. c.conn.SetReadDeadline(time.Now().Add(c.recvTimeout * 10)) _, err = io.ReadFull(c.conn, buf[:4]) c.conn.SetReadDeadline(time.Time{}) if err != nil { return err } blen := int(binary.BigEndian.Uint32(buf[:4])) if cap(buf) < blen { buf = make([]byte, blen) } _, err = io.ReadFull(c.conn, buf[:blen]) if err != nil { return err } r := connectResponse{} _, err = decodePacket(buf[:blen], &r) if err != nil { return err } if r.SessionID == 0 { atomic.StoreInt64(&c.sessionID, int64(0)) c.passwd = emptyPassword c.lastZxid = 0 c.setState(StateExpired) return ErrSessionExpired } atomic.StoreInt64(&c.sessionID, r.SessionID) c.setTimeouts(r.TimeOut) c.passwd = r.Passwd c.setState(StateHasSession) return nil } func (c *Conn) sendData(req *request) error { header := &requestHeader{req.xid, req.opcode} n, err := encodePacket(c.buf[4:], header) if err != nil { req.recvChan <- response{-1, err} return nil } n2, err := encodePacket(c.buf[4+n:], req.pkt) if err != nil { req.recvChan <- response{-1, err} return nil } n += n2 binary.BigEndian.PutUint32(c.buf[:4], uint32(n)) c.requestsLock.Lock() select { case <-c.closeChan: req.recvChan <- response{-1, ErrConnectionClosed} c.requestsLock.Unlock() return ErrConnectionClosed default: } c.requests[req.xid] = req c.requestsLock.Unlock() c.conn.SetWriteDeadline(time.Now().Add(c.recvTimeout)) _, err = c.conn.Write(c.buf[:n+4]) c.conn.SetWriteDeadline(time.Time{}) if err != nil { req.recvChan <- response{-1, err} c.conn.Close() return err } return nil } func (c *Conn) sendLoop() error { pingTicker := time.NewTicker(c.pingInterval) defer pingTicker.Stop() for { select { case req := <-c.sendChan: if err := c.sendData(req); err != nil { return err } case <-pingTicker.C: n, err := encodePacket(c.buf[4:], &requestHeader{Xid: -2, Opcode: opPing}) if err != nil { panic("zk: opPing should never fail to serialize") } binary.BigEndian.PutUint32(c.buf[:4], uint32(n)) c.conn.SetWriteDeadline(time.Now().Add(c.recvTimeout)) _, err = c.conn.Write(c.buf[:n+4]) c.conn.SetWriteDeadline(time.Time{}) if err != nil { c.conn.Close() return err } case <-c.closeChan: return nil } } } func (c *Conn) recvLoop(conn net.Conn) error { sz := bufferSize if c.maxBufferSize > 0 && sz > c.maxBufferSize { sz = c.maxBufferSize } buf := make([]byte, sz) for { // package length if err := conn.SetReadDeadline(time.Now().Add(c.recvTimeout)); err != nil { c.logger.Printf("failed to set connection deadline: %v", err) } _, err := io.ReadFull(conn, buf[:4]) if err != nil { return fmt.Errorf("failed to read from connection: %v", err) } blen := int(binary.BigEndian.Uint32(buf[:4])) if cap(buf) < blen { if c.maxBufferSize > 0 && blen > c.maxBufferSize { return fmt.Errorf("received packet from server with length %d, which exceeds max buffer size %d", blen, c.maxBufferSize) } buf = make([]byte, blen) } _, err = io.ReadFull(conn, buf[:blen]) conn.SetReadDeadline(time.Time{}) if err != nil { return err } res := responseHeader{} _, err = decodePacket(buf[:16], &res) if err != nil { return err } if res.Xid == -1 { res := &watcherEvent{} _, err = decodePacket(buf[16:blen], res) if err != nil { return err } ev := Event{ Type: res.Type, State: res.State, Path: res.Path, Err: nil, } c.sendEvent(ev) c.notifyWatches(ev) } else if res.Xid == -2 { // Ping response. Ignore. } else if res.Xid < 0 { c.logger.Printf("Xid < 0 (%d) but not ping or watcher event", res.Xid) } else { if res.Zxid > 0 { c.lastZxid = res.Zxid } c.requestsLock.Lock() req, ok := c.requests[res.Xid] if ok { delete(c.requests, res.Xid) } c.requestsLock.Unlock() if !ok { c.logger.Printf("Response for unknown request with xid %d", res.Xid) } else { if res.Err != 0 { err = res.Err.toError() } else { _, err = decodePacket(buf[16:blen], req.recvStruct) } if req.recvFunc != nil { req.recvFunc(req, &res, err) } req.recvChan <- response{res.Zxid, err} if req.opcode == opClose { return io.EOF } } } } } func (c *Conn) nextXid() int32 { return int32(atomic.AddUint32(&c.xid, 1) & 0x7fffffff) } func (c *Conn) addWatcher(path string, watchType watchType) <-chan Event { c.watchersLock.Lock() defer c.watchersLock.Unlock() ch := make(chan Event, 1) wpt := watchPathType{path, watchType} c.watchers[wpt] = append(c.watchers[wpt], ch) return ch } func (c *Conn) queueRequest(opcode int32, req interface{}, res interface{}, recvFunc func(*request, *responseHeader, error)) <-chan response { rq := &request{ xid: c.nextXid(), opcode: opcode, pkt: req, recvStruct: res, recvChan: make(chan response, 2), recvFunc: recvFunc, } switch opcode { case opClose: // always attempt to send close ops. select { case c.sendChan <- rq: case <-time.After(c.connectTimeout * 2): c.logger.Printf("gave up trying to send opClose to server") rq.recvChan <- response{-1, ErrConnectionClosed} } default: // otherwise avoid deadlocks for dumb clients who aren't aware that // the ZK connection is closed yet. select { case <-c.shouldQuit: rq.recvChan <- response{-1, ErrConnectionClosed} case c.sendChan <- rq: // check for a tie select { case <-c.shouldQuit: // maybe the caller gets this, maybe not- we tried. rq.recvChan <- response{-1, ErrConnectionClosed} default: } } } return rq.recvChan } func (c *Conn) request(opcode int32, req interface{}, res interface{}, recvFunc func(*request, *responseHeader, error)) (int64, error) { recv := c.queueRequest(opcode, req, res, recvFunc) select { case r := <-recv: return r.zxid, r.err case <-c.shouldQuit: // queueRequest() can be racy, double-check for the race here and avoid // a potential data-race. otherwise the client of this func may try to // access `res` fields concurrently w/ the async response processor. // NOTE: callers of this func should check for (at least) ErrConnectionClosed // and avoid accessing fields of the response object if such error is present. return -1, ErrConnectionClosed } } // AddAuth adds an authentication config to the connection. func (c *Conn) AddAuth(scheme string, auth []byte) error { _, err := c.request(opSetAuth, &setAuthRequest{Type: 0, Scheme: scheme, Auth: auth}, &setAuthResponse{}, nil) if err != nil { return err } // Remember authdata so that it can be re-submitted on reconnect // // FIXME(prozlach): For now we treat "userfoo:passbar" and "userfoo:passbar2" // as two different entries, which will be re-submitted on reconnect. Some // research is needed on how ZK treats these cases and // then maybe switch to something like "map[username] = password" to allow // only single password for given user with users being unique. obj := authCreds{ scheme: scheme, auth: auth, } c.credsMu.Lock() c.creds = append(c.creds, obj) c.credsMu.Unlock() return nil } // Children returns the children of a znode. func (c *Conn) Children(path string) ([]string, *Stat, error) { if err := validatePath(path, false); err != nil { return nil, nil, err } res := &getChildren2Response{} _, err := c.request(opGetChildren2, &getChildren2Request{Path: path, Watch: false}, res, nil) if err == ErrConnectionClosed { return nil, nil, err } return res.Children, &res.Stat, err } // ChildrenW returns the children of a znode and sets a watch. func (c *Conn) ChildrenW(path string) ([]string, *Stat, <-chan Event, error) { if err := validatePath(path, false); err != nil { return nil, nil, nil, err } var ech <-chan Event res := &getChildren2Response{} _, err := c.request(opGetChildren2, &getChildren2Request{Path: path, Watch: true}, res, func(req *request, res *responseHeader, err error) { if err == nil { ech = c.addWatcher(path, watchTypeChild) } }) if err != nil { return nil, nil, nil, err } return res.Children, &res.Stat, ech, err } // Get gets the contents of a znode. func (c *Conn) Get(path string) ([]byte, *Stat, error) { if err := validatePath(path, false); err != nil { return nil, nil, err } res := &getDataResponse{} _, err := c.request(opGetData, &getDataRequest{Path: path, Watch: false}, res, nil) if err == ErrConnectionClosed { return nil, nil, err } return res.Data, &res.Stat, err } // GetW returns the contents of a znode and sets a watch func (c *Conn) GetW(path string) ([]byte, *Stat, <-chan Event, error) { if err := validatePath(path, false); err != nil { return nil, nil, nil, err } var ech <-chan Event res := &getDataResponse{} _, err := c.request(opGetData, &getDataRequest{Path: path, Watch: true}, res, func(req *request, res *responseHeader, err error) { if err == nil { ech = c.addWatcher(path, watchTypeData) } }) if err != nil { return nil, nil, nil, err } return res.Data, &res.Stat, ech, err } // Set updates the contents of a znode. func (c *Conn) Set(path string, data []byte, version int32) (*Stat, error) { if err := validatePath(path, false); err != nil { return nil, err } res := &setDataResponse{} _, err := c.request(opSetData, &SetDataRequest{path, data, version}, res, nil) if err == ErrConnectionClosed { return nil, err } return &res.Stat, err } // Create creates a znode. // The returned path is the new path assigned by the server, it may not be the // same as the input, for example when creating a sequence znode the returned path // will be the input path with a sequence number appended. func (c *Conn) Create(path string, data []byte, flags int32, acl []ACL) (string, error) { createMode, err := parseCreateMode(flags) if err != nil { return "", err } if err := validatePath(path, createMode.isSequential); err != nil { return "", err } if createMode.isTTL { return "", fmt.Errorf("Create with TTL flag disallowed: %w", ErrInvalidFlags) } res := &createResponse{} _, err = c.request(opCreate, &CreateRequest{path, data, acl, createMode.flag}, res, nil) if err == ErrConnectionClosed { return "", err } return res.Path, err } // CreateContainer creates a container znode and returns the path. // // Containers cannot be ephemeral or sequential, or have TTLs. // Ensure that we reject flags for TTL, Sequence, and Ephemeral. func (c *Conn) CreateContainer(path string, data []byte, flag int32, acl []ACL) (string, error) { createMode, err := parseCreateMode(flag) if err != nil { return "", err } if err := validatePath(path, createMode.isSequential); err != nil { return "", err } if !createMode.isContainer { return "", fmt.Errorf("CreateContainer requires container flag: %w", ErrInvalidFlags) } res := &createResponse{} _, err = c.request(opCreateContainer, &CreateRequest{path, data, acl, createMode.flag}, res, nil) return res.Path, err } // CreateTTL creates a TTL znode, which will be automatically deleted by server after the TTL. func (c *Conn) CreateTTL(path string, data []byte, flag int32, acl []ACL, ttl time.Duration) (string, error) { createMode, err := parseCreateMode(flag) if err != nil { return "", err } if err := validatePath(path, createMode.isSequential); err != nil { return "", err } if !createMode.isTTL { return "", fmt.Errorf("CreateTTL requires TTL flag: %w", ErrInvalidFlags) } res := &createResponse{} _, err = c.request(opCreateTTL, &CreateTTLRequest{path, data, acl, createMode.flag, ttl.Milliseconds()}, res, nil) return res.Path, err } // CreateProtectedEphemeralSequential fixes a race condition if the server crashes // after it creates the node. On reconnect the session may still be valid so the // ephemeral node still exists. Therefore, on reconnect we need to check if a node // with a GUID generated on create exists. func (c *Conn) CreateProtectedEphemeralSequential(path string, data []byte, acl []ACL) (string, error) { if err := validatePath(path, true); err != nil { return "", err } var guid [16]byte _, err := io.ReadFull(rand.Reader, guid[:16]) if err != nil { return "", err } guidStr := fmt.Sprintf("%x", guid) parts := strings.Split(path, "/") parts[len(parts)-1] = fmt.Sprintf("%s%s-%s", protectedPrefix, guidStr, parts[len(parts)-1]) rootPath := strings.Join(parts[:len(parts)-1], "/") protectedPath := strings.Join(parts, "/") var newPath string for i := 0; i < 3; i++ { newPath, err = c.Create(protectedPath, data, FlagEphemeral|FlagSequence, acl) switch err { case ErrSessionExpired: // No need to search for the node since it can't exist. Just try again. case ErrConnectionClosed: children, _, err := c.Children(rootPath) if err != nil { return "", err } for _, p := range children { parts := strings.Split(p, "/") if pth := parts[len(parts)-1]; strings.HasPrefix(pth, protectedPrefix) { if g := pth[len(protectedPrefix) : len(protectedPrefix)+32]; g == guidStr { return rootPath + "/" + p, nil } } } case nil: return newPath, nil default: return "", err } } return "", err } // Delete deletes a znode. func (c *Conn) Delete(path string, version int32) error { if err := validatePath(path, false); err != nil { return err } _, err := c.request(opDelete, &DeleteRequest{path, version}, &deleteResponse{}, nil) return err } // Exists tells the existence of a znode. func (c *Conn) Exists(path string) (bool, *Stat, error) { if err := validatePath(path, false); err != nil { return false, nil, err } res := &existsResponse{} _, err := c.request(opExists, &existsRequest{Path: path, Watch: false}, res, nil) if err == ErrConnectionClosed { return false, nil, err } exists := true if err == ErrNoNode { exists = false err = nil } return exists, &res.Stat, err } // ExistsW tells the existence of a znode and sets a watch. func (c *Conn) ExistsW(path string) (bool, *Stat, <-chan Event, error) { if err := validatePath(path, false); err != nil { return false, nil, nil, err } var ech <-chan Event res := &existsResponse{} _, err := c.request(opExists, &existsRequest{Path: path, Watch: true}, res, func(req *request, res *responseHeader, err error) { if err == nil { ech = c.addWatcher(path, watchTypeData) } else if err == ErrNoNode { ech = c.addWatcher(path, watchTypeExist) } }) exists := true if err == ErrNoNode { exists = false err = nil } if err != nil { return false, nil, nil, err } return exists, &res.Stat, ech, err } // GetACL gets the ACLs of a znode. func (c *Conn) GetACL(path string) ([]ACL, *Stat, error) { if err := validatePath(path, false); err != nil { return nil, nil, err } res := &getAclResponse{} _, err := c.request(opGetAcl, &getAclRequest{Path: path}, res, nil) if err == ErrConnectionClosed { return nil, nil, err } return res.Acl, &res.Stat, err } // SetACL updates the ACLs of a znode. func (c *Conn) SetACL(path string, acl []ACL, version int32) (*Stat, error) { if err := validatePath(path, false); err != nil { return nil, err } res := &setAclResponse{} _, err := c.request(opSetAcl, &setAclRequest{Path: path, Acl: acl, Version: version}, res, nil) if err == ErrConnectionClosed { return nil, err } return &res.Stat, err } // Sync flushes the channel between process and the leader of a given znode, // you may need it if you want identical views of ZooKeeper data for 2 client instances. // Please refer to the "Consistency Guarantees" section of ZK document for more details. func (c *Conn) Sync(path string) (string, error) { if err := validatePath(path, false); err != nil { return "", err } res := &syncResponse{} _, err := c.request(opSync, &syncRequest{Path: path}, res, nil) if err == ErrConnectionClosed { return "", err } return res.Path, err } // MultiResponse is the result of a Multi call. type MultiResponse struct { Stat *Stat String string Error error } // Multi executes multiple ZooKeeper operations or none of them. The provided // ops must be one of *CreateRequest, *DeleteRequest, *SetDataRequest, or // *CheckVersionRequest. func (c *Conn) Multi(ops ...interface{}) ([]MultiResponse, error) { req := &multiRequest{ Ops: make([]multiRequestOp, 0, len(ops)), DoneHeader: multiHeader{Type: -1, Done: true, Err: -1}, } for _, op := range ops { var opCode int32 switch op.(type) { case *CreateRequest: opCode = opCreate case *SetDataRequest: opCode = opSetData case *DeleteRequest: opCode = opDelete case *CheckVersionRequest: opCode = opCheck default: return nil, fmt.Errorf("unknown operation type %T", op) } req.Ops = append(req.Ops, multiRequestOp{multiHeader{opCode, false, -1}, op}) } res := &multiResponse{} _, err := c.request(opMulti, req, res, nil) if err == ErrConnectionClosed { return nil, err } mr := make([]MultiResponse, len(res.Ops)) for i, op := range res.Ops { mr[i] = MultiResponse{Stat: op.Stat, String: op.String, Error: op.Err.toError()} } return mr, err } // IncrementalReconfig is the zookeeper reconfiguration api that allows adding and removing servers // by lists of members. For more info refer to the ZK documentation. // // An optional version allows for conditional reconfigurations, -1 ignores the condition. // // Returns the new configuration znode stat. func (c *Conn) IncrementalReconfig(joining, leaving []string, version int64) (*Stat, error) { // TODO: validate the shape of the member string to give early feedback. request := &reconfigRequest{ JoiningServers: []byte(strings.Join(joining, ",")), LeavingServers: []byte(strings.Join(leaving, ",")), CurConfigId: version, } return c.internalReconfig(request) } // Reconfig is the non-incremental update functionality for Zookeeper where the list provided // is the entire new member list. For more info refer to the ZK documentation. // // An optional version allows for conditional reconfigurations, -1 ignores the condition. // // Returns the new configuration znode stat. func (c *Conn) Reconfig(members []string, version int64) (*Stat, error) { request := &reconfigRequest{ NewMembers: []byte(strings.Join(members, ",")), CurConfigId: version, } return c.internalReconfig(request) } func (c *Conn) internalReconfig(request *reconfigRequest) (*Stat, error) { response := &reconfigReponse{} _, err := c.request(opReconfig, request, response, nil) return &response.Stat, err } // Server returns the current or last-connected server name. func (c *Conn) Server() string { c.serverMu.Lock() defer c.serverMu.Unlock() return c.server } func resendZkAuth(ctx context.Context, c *Conn) error { shouldCancel := func() bool { select { case <-c.shouldQuit: return true case <-c.closeChan: return true default: return false } } c.credsMu.Lock() defer c.credsMu.Unlock() if c.logInfo { c.logger.Printf("re-submitting `%d` credentials after reconnect", len(c.creds)) } for _, cred := range c.creds { // return early before attempting to send request. if shouldCancel() { return nil } // do not use the public API for auth since it depends on the send/recv loops // that are waiting for this to return resChan, err := c.sendRequest( opSetAuth, &setAuthRequest{Type: 0, Scheme: cred.scheme, Auth: cred.auth, }, &setAuthResponse{}, nil, /* recvFunc*/ ) if err != nil { return fmt.Errorf("failed to send auth request: %v", err) } var res response select { case res = <-resChan: case <-c.closeChan: c.logger.Printf("recv closed, cancel re-submitting credentials") return nil case <-c.shouldQuit: c.logger.Printf("should quit, cancel re-submitting credentials") return nil case <-ctx.Done(): return ctx.Err() } if res.err != nil { return fmt.Errorf("failed connection setAuth request: %v", res.err) } } return nil } golang-github-go-zookeeper-zk-1.0.4/conn_test.go000066400000000000000000000102411465024323700216200ustar00rootroot00000000000000package zk import ( "context" "fmt" "io/ioutil" "sync" "testing" "time" ) func TestIntegration_RecurringReAuthHang(t *testing.T) { zkC, err := StartTestCluster(t, 3, ioutil.Discard, ioutil.Discard) if err != nil { panic(err) } defer zkC.Stop() conn, evtC, err := zkC.ConnectAll() if err != nil { panic(err) } defer conn.Close() ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() waitForSession(ctx, evtC) // Add auth. conn.AddAuth("digest", []byte("test:test")) var reauthCloseOnce sync.Once reauthSig := make(chan struct{}, 1) conn.resendZkAuthFn = func(ctx context.Context, c *Conn) error { // in current implimentation the reauth might be called more than once based on various conditions reauthCloseOnce.Do(func() { close(reauthSig) }) return resendZkAuth(ctx, c) } conn.debugCloseRecvLoop = true currentServer := conn.Server() zkC.StopServer(currentServer) // wait connect to new zookeeper. ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) defer cancel() waitForSession(ctx, evtC) select { case _, ok := <-reauthSig: if !ok { return // we closed the channel as expected } t.Fatal("reauth testing channel should have been closed") case <-ctx.Done(): t.Fatal(ctx.Err()) } } func TestConcurrentReadAndClose(t *testing.T) { WithListenServer(t, func(server string) { conn, _, err := Connect([]string{server}, 15*time.Second) if err != nil { t.Fatalf("Failed to create Connection %s", err) } okChan := make(chan struct{}) var setErr error go func() { _, setErr = conn.Create("/test-path", []byte("test data"), 0, WorldACL(PermAll)) close(okChan) }() go func() { time.Sleep(1 * time.Second) conn.Close() }() select { case <-okChan: if setErr != ErrConnectionClosed { t.Fatalf("unexpected error returned from Set %v", setErr) } case <-time.After(3 * time.Second): t.Fatal("apparent deadlock!") } }) } func TestDeadlockInClose(t *testing.T) { c := &Conn{ shouldQuit: make(chan struct{}), connectTimeout: 1 * time.Second, sendChan: make(chan *request, sendChanSize), logger: DefaultLogger, } for i := 0; i < sendChanSize; i++ { c.sendChan <- &request{} } okChan := make(chan struct{}) go func() { c.Close() close(okChan) }() select { case <-okChan: case <-time.After(3 * time.Second): t.Fatal("apparent deadlock!") } } func TestNotifyWatches(t *testing.T) { cases := []struct { eType EventType path string watches map[watchPathType]bool }{ { EventNodeCreated, "/", map[watchPathType]bool{ {"/", watchTypeExist}: true, {"/", watchTypeChild}: false, {"/", watchTypeData}: false, }, }, { EventNodeCreated, "/a", map[watchPathType]bool{ {"/b", watchTypeExist}: false, }, }, { EventNodeDataChanged, "/", map[watchPathType]bool{ {"/", watchTypeExist}: true, {"/", watchTypeData}: true, {"/", watchTypeChild}: false, }, }, { EventNodeChildrenChanged, "/", map[watchPathType]bool{ {"/", watchTypeExist}: false, {"/", watchTypeData}: false, {"/", watchTypeChild}: true, }, }, { EventNodeDeleted, "/", map[watchPathType]bool{ {"/", watchTypeExist}: true, {"/", watchTypeData}: true, {"/", watchTypeChild}: true, }, }, } conn := &Conn{watchers: make(map[watchPathType][]chan Event)} for idx, c := range cases { t.Run(fmt.Sprintf("#%d %s", idx, c.eType), func(t *testing.T) { c := c notifications := make([]struct { path string notify bool ch <-chan Event }, len(c.watches)) var idx int for wpt, expectEvent := range c.watches { ch := conn.addWatcher(wpt.path, wpt.wType) notifications[idx].path = wpt.path notifications[idx].notify = expectEvent notifications[idx].ch = ch idx++ } ev := Event{Type: c.eType, Path: c.path} conn.notifyWatches(ev) for _, res := range notifications { select { case e := <-res.ch: if !res.notify || e.Path != res.path { t.Fatal("unexpeted notification received") } default: if res.notify { t.Fatal("expected notification not received") } } } }) } } golang-github-go-zookeeper-zk-1.0.4/constants.go000066400000000000000000000171221465024323700216450ustar00rootroot00000000000000package zk import ( "errors" "fmt" ) const ( protocolVersion = 0 // DefaultPort is the default port listened by server. DefaultPort = 2181 ) const ( opNotify = 0 opCreate = 1 opDelete = 2 opExists = 3 opGetData = 4 opSetData = 5 opGetAcl = 6 opSetAcl = 7 opGetChildren = 8 opSync = 9 opPing = 11 opGetChildren2 = 12 opCheck = 13 opMulti = 14 opReconfig = 16 opCreateContainer = 19 opCreateTTL = 21 opClose = -11 opSetAuth = 100 opSetWatches = 101 opError = -1 // Not in protocol, used internally opWatcherEvent = -2 ) const ( // EventNodeCreated represents a node is created. EventNodeCreated EventType = 1 EventNodeDeleted EventType = 2 EventNodeDataChanged EventType = 3 EventNodeChildrenChanged EventType = 4 // EventSession represents a session event. EventSession EventType = -1 EventNotWatching EventType = -2 ) var ( eventNames = map[EventType]string{ EventNodeCreated: "EventNodeCreated", EventNodeDeleted: "EventNodeDeleted", EventNodeDataChanged: "EventNodeDataChanged", EventNodeChildrenChanged: "EventNodeChildrenChanged", EventSession: "EventSession", EventNotWatching: "EventNotWatching", } ) const ( // StateUnknown means the session state is unknown. StateUnknown State = -1 StateDisconnected State = 0 StateConnecting State = 1 StateSyncConnected State = 3 StateAuthFailed State = 4 StateConnectedReadOnly State = 5 StateSaslAuthenticated State = 6 StateExpired State = -112 StateConnected = State(100) StateHasSession = State(101) ) var ( stateNames = map[State]string{ StateUnknown: "StateUnknown", StateDisconnected: "StateDisconnected", StateConnectedReadOnly: "StateConnectedReadOnly", StateSaslAuthenticated: "StateSaslAuthenticated", StateExpired: "StateExpired", StateAuthFailed: "StateAuthFailed", StateConnecting: "StateConnecting", StateConnected: "StateConnected", StateHasSession: "StateHasSession", StateSyncConnected: "StateSyncConnected", } ) // State is the session state. type State int32 // String converts State to a readable string. func (s State) String() string { if name := stateNames[s]; name != "" { return name } return "Unknown" } // ErrCode is the error code defined by server. Refer to ZK documentations for more specifics. type ErrCode int32 var ( // ErrConnectionClosed means the connection has been closed. ErrConnectionClosed = errors.New("zk: connection closed") ErrUnknown = errors.New("zk: unknown error") ErrAPIError = errors.New("zk: api error") ErrNoNode = errors.New("zk: node does not exist") ErrNoAuth = errors.New("zk: not authenticated") ErrBadVersion = errors.New("zk: version conflict") ErrNoChildrenForEphemerals = errors.New("zk: ephemeral nodes may not have children") ErrNodeExists = errors.New("zk: node already exists") ErrNotEmpty = errors.New("zk: node has children") ErrSessionExpired = errors.New("zk: session has been expired by the server") ErrInvalidACL = errors.New("zk: invalid ACL specified") ErrInvalidFlags = errors.New("zk: invalid flags specified") ErrAuthFailed = errors.New("zk: client authentication failed") ErrClosing = errors.New("zk: zookeeper is closing") ErrNothing = errors.New("zk: no server responses to process") ErrSessionMoved = errors.New("zk: session moved to another server, so operation is ignored") ErrReconfigDisabled = errors.New("attempts to perform a reconfiguration operation when reconfiguration feature is disabled") ErrBadArguments = errors.New("invalid arguments") // ErrInvalidCallback = errors.New("zk: invalid callback specified") errCodeToError = map[ErrCode]error{ 0: nil, errAPIError: ErrAPIError, errNoNode: ErrNoNode, errNoAuth: ErrNoAuth, errBadVersion: ErrBadVersion, errNoChildrenForEphemerals: ErrNoChildrenForEphemerals, errNodeExists: ErrNodeExists, errNotEmpty: ErrNotEmpty, errSessionExpired: ErrSessionExpired, // errInvalidCallback: ErrInvalidCallback, errInvalidAcl: ErrInvalidACL, errAuthFailed: ErrAuthFailed, errClosing: ErrClosing, errNothing: ErrNothing, errSessionMoved: ErrSessionMoved, errZReconfigDisabled: ErrReconfigDisabled, errBadArguments: ErrBadArguments, } ) func (e ErrCode) toError() error { if err, ok := errCodeToError[e]; ok { return err } return fmt.Errorf("unknown error: %v", e) } const ( errOk = 0 // System and server-side errors errSystemError = -1 errRuntimeInconsistency = -2 errDataInconsistency = -3 errConnectionLoss = -4 errMarshallingError = -5 errUnimplemented = -6 errOperationTimeout = -7 errBadArguments = -8 errInvalidState = -9 // API errors errAPIError ErrCode = -100 errNoNode ErrCode = -101 // * errNoAuth ErrCode = -102 errBadVersion ErrCode = -103 // * errNoChildrenForEphemerals ErrCode = -108 errNodeExists ErrCode = -110 // * errNotEmpty ErrCode = -111 errSessionExpired ErrCode = -112 errInvalidCallback ErrCode = -113 errInvalidAcl ErrCode = -114 errAuthFailed ErrCode = -115 errClosing ErrCode = -116 errNothing ErrCode = -117 errSessionMoved ErrCode = -118 // Attempts to perform a reconfiguration operation when reconfiguration feature is disabled errZReconfigDisabled ErrCode = -123 ) // Constants for ACL permissions const ( // PermRead represents the permission needed to read a znode. PermRead = 1 << iota PermWrite PermCreate PermDelete PermAdmin PermAll = 0x1f ) var ( emptyPassword = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} opNames = map[int32]string{ opNotify: "notify", opCreate: "create", opCreateContainer: "createContainer", opCreateTTL: "createTTL", opDelete: "delete", opExists: "exists", opGetData: "getData", opSetData: "setData", opGetAcl: "getACL", opSetAcl: "setACL", opGetChildren: "getChildren", opSync: "sync", opPing: "ping", opGetChildren2: "getChildren2", opCheck: "check", opMulti: "multi", opReconfig: "reconfig", opClose: "close", opSetAuth: "setAuth", opSetWatches: "setWatches", opWatcherEvent: "watcherEvent", } ) // EventType represents the event type sent by server. type EventType int32 func (t EventType) String() string { if name := eventNames[t]; name != "" { return name } return "Unknown" } // Mode is used to build custom server modes (leader|follower|standalone). type Mode uint8 func (m Mode) String() string { if name := modeNames[m]; name != "" { return name } return "unknown" } const ( ModeUnknown Mode = iota ModeLeader Mode = iota ModeFollower Mode = iota ModeStandalone Mode = iota ) var ( modeNames = map[Mode]string{ ModeLeader: "leader", ModeFollower: "follower", ModeStandalone: "standalone", } ) golang-github-go-zookeeper-zk-1.0.4/constants_test.go000066400000000000000000000007621465024323700227060ustar00rootroot00000000000000package zk import ( "fmt" "testing" ) func TestModeString(t *testing.T) { if fmt.Sprintf("%v", ModeUnknown) != "unknown" { t.Errorf("unknown value should be 'unknown'") } if fmt.Sprintf("%v", ModeLeader) != "leader" { t.Errorf("leader value should be 'leader'") } if fmt.Sprintf("%v", ModeFollower) != "follower" { t.Errorf("follower value should be 'follower'") } if fmt.Sprintf("%v", ModeStandalone) != "standalone" { t.Errorf("standlone value should be 'standalone'") } } golang-github-go-zookeeper-zk-1.0.4/create_mode.go000066400000000000000000000027511465024323700221020ustar00rootroot00000000000000package zk import "fmt" // TODO: (v2) enum type for CreateMode API. const ( FlagPersistent = 0 FlagEphemeral = 1 FlagSequence = 2 FlagEphemeralSequential = 3 FlagContainer = 4 FlagTTL = 5 FlagPersistentSequentialWithTTL = 6 ) type createMode struct { flag int32 isEphemeral bool isSequential bool isContainer bool isTTL bool } // parsing a flag integer into the CreateMode needed to call the correct // Create RPC to Zookeeper. // // NOTE: This parse method is designed to be able to copy and paste the same // CreateMode ENUM constructors from Java: // https://github.com/apache/zookeeper/blob/master/zookeeper-server/src/main/java/org/apache/zookeeper/CreateMode.java func parseCreateMode(flag int32) (createMode, error) { switch flag { case FlagPersistent: return createMode{0, false, false, false, false}, nil case FlagEphemeral: return createMode{1, true, false, false, false}, nil case FlagSequence: return createMode{2, false, true, false, false}, nil case FlagEphemeralSequential: return createMode{3, true, true, false, false}, nil case FlagContainer: return createMode{4, false, false, true, false}, nil case FlagTTL: return createMode{5, false, false, false, true}, nil case FlagPersistentSequentialWithTTL: return createMode{6, false, true, false, true}, nil default: return createMode{}, fmt.Errorf("invalid flag value: [%v]", flag) } } golang-github-go-zookeeper-zk-1.0.4/create_mode_test.go000066400000000000000000000021131465024323700231310ustar00rootroot00000000000000package zk import ( "strings" "testing" ) func TestParseCreateMode(t *testing.T) { changeDetectorTests := []struct { name string flag int32 wantIntValue int32 }{ {"valid flag createmode 0 persistant", FlagPersistent, 0}, {"ephemeral", FlagEphemeral, 1}, {"sequential", FlagSequence, 2}, {"ephemeral sequential", FlagEphemeralSequential, 3}, {"container", FlagContainer, 4}, {"ttl", FlagTTL, 5}, {"persistentSequential w/TTL", FlagPersistentSequentialWithTTL, 6}, } for _, tt := range changeDetectorTests { t.Run(tt.name, func(t *testing.T) { cm, err := parseCreateMode(tt.flag) requireNoErrorf(t, err) if cm.flag != tt.wantIntValue { // change detector test for enum values. t.Fatalf("createmode value of flag; want: %v, got: %v", cm.flag, tt.wantIntValue) } }) } t.Run("failed to parse", func(t *testing.T) { cm, err := parseCreateMode(-123) if err == nil { t.Fatalf("error expected, got: %v", cm) } if !strings.Contains(err.Error(), "invalid flag value") { t.Fatalf("unexpected error value: %v", err) } }) } golang-github-go-zookeeper-zk-1.0.4/dnshostprovider.go000066400000000000000000000064531465024323700230730ustar00rootroot00000000000000package zk import ( "context" "fmt" "net" "sync" "time" ) const _defaultLookupTimeout = 3 * time.Second type lookupHostFn func(context.Context, string) ([]string, error) // DNSHostProviderOption is an option for the DNSHostProvider. type DNSHostProviderOption interface { apply(*DNSHostProvider) } type lookupTimeoutOption struct { timeout time.Duration } // WithLookupTimeout returns a DNSHostProviderOption that sets the lookup timeout. func WithLookupTimeout(timeout time.Duration) DNSHostProviderOption { return lookupTimeoutOption{ timeout: timeout, } } func (o lookupTimeoutOption) apply(provider *DNSHostProvider) { provider.lookupTimeout = o.timeout } // DNSHostProvider is the default HostProvider. It currently matches // the Java StaticHostProvider, resolving hosts from DNS once during // the call to Init. It could be easily extended to re-query DNS // periodically or if there is trouble connecting. type DNSHostProvider struct { mu sync.Mutex // Protects everything, so we can add asynchronous updates later. servers []string curr int last int lookupTimeout time.Duration lookupHost lookupHostFn // Override of net.LookupHost, for testing. } // NewDNSHostProvider creates a new DNSHostProvider with the given options. func NewDNSHostProvider(options ...DNSHostProviderOption) *DNSHostProvider { var provider DNSHostProvider for _, option := range options { option.apply(&provider) } return &provider } // Init is called first, with the servers specified in the connection // string. It uses DNS to look up addresses for each server, then // shuffles them all together. func (hp *DNSHostProvider) Init(servers []string) error { hp.mu.Lock() defer hp.mu.Unlock() lookupHost := hp.lookupHost if lookupHost == nil { var resolver net.Resolver lookupHost = resolver.LookupHost } timeout := hp.lookupTimeout if timeout == 0 { timeout = _defaultLookupTimeout } // TODO: consider using a context from the caller. ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() found := []string{} for _, server := range servers { host, port, err := net.SplitHostPort(server) if err != nil { return err } addrs, err := lookupHost(ctx, host) if err != nil { return err } for _, addr := range addrs { found = append(found, net.JoinHostPort(addr, port)) } } if len(found) == 0 { return fmt.Errorf("No hosts found for addresses %q", servers) } // Randomize the order of the servers to avoid creating hotspots stringShuffle(found) hp.servers = found hp.curr = -1 hp.last = -1 return nil } // Len returns the number of servers available func (hp *DNSHostProvider) Len() int { hp.mu.Lock() defer hp.mu.Unlock() return len(hp.servers) } // Next returns the next server to connect to. retryStart will be true // if we've looped through all known servers without Connected() being // called. func (hp *DNSHostProvider) Next() (server string, retryStart bool) { hp.mu.Lock() defer hp.mu.Unlock() hp.curr = (hp.curr + 1) % len(hp.servers) retryStart = hp.curr == hp.last if hp.last == -1 { hp.last = 0 } return hp.servers[hp.curr], retryStart } // Connected notifies the HostProvider of a successful connection. func (hp *DNSHostProvider) Connected() { hp.mu.Lock() defer hp.mu.Unlock() hp.last = hp.curr } golang-github-go-zookeeper-zk-1.0.4/dnshostprovider_test.go000066400000000000000000000161241465024323700241260ustar00rootroot00000000000000package zk import ( "context" "fmt" "log" "testing" "time" ) type lookupHostOption struct { lookupFn lookupHostFn } func withLookupHost(lookupFn lookupHostFn) DNSHostProviderOption { return lookupHostOption{ lookupFn: lookupFn, } } func (o lookupHostOption) apply(provider *DNSHostProvider) { provider.lookupHost = o.lookupFn } // TestDNSHostProviderCreate is just like TestCreate, but with an // overridden HostProvider that ignores the provided hostname. func TestIntegration_DNSHostProviderCreate(t *testing.T) { ts, err := StartTestCluster(t, 1, nil, logWriter{t: t, p: "[ZKERR] "}) if err != nil { t.Fatal(err) } defer ts.Stop() port := ts.Servers[0].Port server := fmt.Sprintf("foo.example.com:%d", port) hostProvider := NewDNSHostProvider( withLookupHost(func(ctx context.Context, host string) ([]string, error) { if _, ok := ctx.Deadline(); !ok { t.Fatal("No lookup context deadline set") } return []string{"127.0.0.1"}, nil }), ) zk, _, err := Connect([]string{server}, time.Second*15, WithHostProvider(hostProvider)) if err != nil { t.Fatalf("Connect returned error: %+v", err) } defer zk.Close() path := "/gozk-test" if err := zk.Delete(path, -1); err != nil && err != ErrNoNode { t.Fatalf("Delete returned error: %+v", err) } if p, err := zk.Create(path, []byte{1, 2, 3, 4}, 0, WorldACL(PermAll)); err != nil { t.Fatalf("Create returned error: %+v", err) } else if p != path { t.Fatalf("Create returned different path '%s' != '%s'", p, path) } if data, stat, err := zk.Get(path); err != nil { t.Fatalf("Get returned error: %+v", err) } else if stat == nil { t.Fatal("Get returned nil stat") } else if len(data) < 4 { t.Fatal("Get returned wrong size data") } } // localHostPortsFacade wraps a HostProvider, remapping the // address/port combinations it returns to "localhost:$PORT" where // $PORT is chosen from the provided ports. type localHostPortsFacade struct { inner HostProvider // The wrapped HostProvider ports []int // The provided list of ports nextPort int // The next port to use mapped map[string]string // Already-mapped address/port combinations } func newLocalHostPortsFacade(inner HostProvider, ports []int) *localHostPortsFacade { return &localHostPortsFacade{ inner: inner, ports: ports, mapped: make(map[string]string), } } func (lhpf *localHostPortsFacade) Len() int { return lhpf.inner.Len() } func (lhpf *localHostPortsFacade) Connected() { lhpf.inner.Connected() } func (lhpf *localHostPortsFacade) Init(servers []string) error { return lhpf.inner.Init(servers) } func (lhpf *localHostPortsFacade) Next() (string, bool) { server, retryStart := lhpf.inner.Next() // If we've already set up a mapping for that server, just return it. if localMapping := lhpf.mapped[server]; localMapping != "" { return localMapping, retryStart } if lhpf.nextPort == len(lhpf.ports) { log.Fatalf("localHostPortsFacade out of ports to assign to %q; current config: %q", server, lhpf.mapped) } localMapping := fmt.Sprintf("localhost:%d", lhpf.ports[lhpf.nextPort]) lhpf.mapped[server] = localMapping lhpf.nextPort++ return localMapping, retryStart } var _ HostProvider = &localHostPortsFacade{} // TestDNSHostProviderReconnect tests that the zk.Conn correctly // reconnects when the Zookeeper instance it's connected to // restarts. It wraps the DNSHostProvider in a lightweight facade that // remaps addresses to localhost:$PORT combinations corresponding to // the test ZooKeeper instances. func TestIntegration_DNSHostProviderReconnect(t *testing.T) { ts, err := StartTestCluster(t, 3, nil, logWriter{t: t, p: "[ZKERR] "}) if err != nil { t.Fatal(err) } defer ts.Stop() innerHp := NewDNSHostProvider( withLookupHost(func(_ context.Context, host string) ([]string, error) { return []string{"192.0.2.1", "192.0.2.2", "192.0.2.3"}, nil }), ) ports := make([]int, 0, len(ts.Servers)) for _, server := range ts.Servers { ports = append(ports, server.Port) } hp := newLocalHostPortsFacade(innerHp, ports) zk, _, err := Connect([]string{"foo.example.com:12345"}, time.Second, WithHostProvider(hp)) if err != nil { t.Fatalf("Connect returned error: %+v", err) } defer zk.Close() path := "/gozk-test" // Initial operation to force connection. if err := zk.Delete(path, -1); err != nil && err != ErrNoNode { t.Fatalf("Delete returned error: %+v", err) } // Figure out which server we're connected to. currentServer := zk.Server() t.Logf("Connected to %q. Finding test server index…", currentServer) serverIndex := -1 for i, server := range ts.Servers { server := fmt.Sprintf("localhost:%d", server.Port) t.Logf("…trying %q", server) if currentServer == server { serverIndex = i t.Logf("…found at index %d", i) break } } if serverIndex == -1 { t.Fatalf("Cannot determine test server index.") } // Restart the connected server. ts.Servers[serverIndex].Srv.Stop() ts.Servers[serverIndex].Srv.Start() // Continue with the basic TestCreate tests. if p, err := zk.Create(path, []byte{1, 2, 3, 4}, 0, WorldACL(PermAll)); err != nil { t.Fatalf("Create returned error: %+v", err) } else if p != path { t.Fatalf("Create returned different path '%s' != '%s'", p, path) } if data, stat, err := zk.Get(path); err != nil { t.Fatalf("Get returned error: %+v", err) } else if stat == nil { t.Fatal("Get returned nil stat") } else if len(data) < 4 { t.Fatal("Get returned wrong size data") } if zk.Server() == currentServer { t.Errorf("Still connected to %q after restart.", currentServer) } } // TestDNSHostProviderRetryStart tests the `retryStart` functionality // of DNSHostProvider. // It's also probably the clearest visual explanation of exactly how // it works. func TestDNSHostProviderRetryStart(t *testing.T) { t.Parallel() hp := NewDNSHostProvider( withLookupHost(func(_ context.Context, host string) ([]string, error) { return []string{"192.0.2.1", "192.0.2.2", "192.0.2.3"}, nil }), ) if err := hp.Init([]string{"foo.example.com:12345"}); err != nil { t.Fatal(err) } testdata := []struct { retryStartWant bool callConnected bool }{ // Repeated failures. {false, false}, {false, false}, {false, false}, {true, false}, {false, false}, {false, false}, {true, true}, // One success offsets things. {false, false}, {false, true}, {false, true}, // Repeated successes. {false, true}, {false, true}, {false, true}, {false, true}, {false, true}, // And some more failures. {false, false}, {false, false}, {true, false}, // Looped back to last known good server: all alternates failed. {false, false}, } for i, td := range testdata { _, retryStartGot := hp.Next() if retryStartGot != td.retryStartWant { t.Errorf("%d: retryStart=%v; want %v", i, retryStartGot, td.retryStartWant) } if td.callConnected { hp.Connected() } } } func TestNewDNSHostProvider(t *testing.T) { want := 5 * time.Second provider := NewDNSHostProvider(WithLookupTimeout(want)) if provider.lookupTimeout != want { t.Fatalf("expected lookup timeout to be %v, got %v", want, provider.lookupTimeout) } } golang-github-go-zookeeper-zk-1.0.4/flw.go000066400000000000000000000171071465024323700204240ustar00rootroot00000000000000package zk import ( "bufio" "bytes" "fmt" "io/ioutil" "net" "regexp" "strconv" "strings" "time" ) // FLWSrvr is a FourLetterWord helper function. In particular, this function pulls the srvr output // from the zookeeper instances and parses the output. A slice of *ServerStats structs are returned // as well as a boolean value to indicate whether this function processed successfully. // // If the boolean value is false there was a problem. If the *ServerStats slice is empty or nil, // then the error happened before we started to obtain 'srvr' values. Otherwise, one of the // servers had an issue and the "Error" value in the struct should be inspected to determine // which server had the issue. func FLWSrvr(servers []string, timeout time.Duration) ([]*ServerStats, bool) { // different parts of the regular expression that are required to parse the srvr output const ( zrVer = `^Zookeeper version: ([A-Za-z0-9\.\-]+), built on (\d\d/\d\d/\d\d\d\d \d\d:\d\d [A-Za-z0-9:\+\-]+)` zrLat = `^Latency min/avg/max: (\d+)/([0-9.]+)/(\d+)` zrNet = `^Received: (\d+).*\n^Sent: (\d+).*\n^Connections: (\d+).*\n^Outstanding: (\d+)` zrState = `^Zxid: (0x[A-Za-z0-9]+).*\n^Mode: (\w+).*\n^Node count: (\d+)` ) // build the regex from the pieces above re, err := regexp.Compile(fmt.Sprintf(`(?m:\A%v.*\n%v.*\n%v.*\n%v)`, zrVer, zrLat, zrNet, zrState)) if err != nil { return nil, false } imOk := true servers = FormatServers(servers) ss := make([]*ServerStats, len(servers)) for i := range ss { response, err := fourLetterWord(servers[i], "srvr", timeout) if err != nil { ss[i] = &ServerStats{Server: servers[i], Error: err} imOk = false continue } matches := re.FindAllStringSubmatch(string(response), -1) if matches == nil { err := fmt.Errorf("unable to parse fields from zookeeper response (no regex matches)") ss[i] = &ServerStats{Server: servers[i], Error: err} imOk = false continue } match := matches[0][1:] // determine current server var srvrMode Mode switch match[10] { case "leader": srvrMode = ModeLeader case "follower": srvrMode = ModeFollower case "standalone": srvrMode = ModeStandalone default: srvrMode = ModeUnknown } buildTime, err := time.Parse("01/02/2006 15:04 MST", match[1]) if err != nil { ss[i] = &ServerStats{Server: servers[i], Error: err} imOk = false continue } parsedInt, err := strconv.ParseInt(match[9], 0, 64) if err != nil { ss[i] = &ServerStats{Server: servers[i], Error: err} imOk = false continue } // the ZxID value is an int64 with two int32s packed inside // the high int32 is the epoch (i.e., number of leader elections) // the low int32 is the counter epoch := int32(parsedInt >> 32) counter := int32(parsedInt & 0xFFFFFFFF) // within the regex above, these values must be numerical // so we can avoid useless checking of the error return value minLatency, _ := strconv.ParseInt(match[2], 0, 64) avgLatency, _ := strconv.ParseFloat(match[3], 64) maxLatency, _ := strconv.ParseInt(match[4], 0, 64) recv, _ := strconv.ParseInt(match[5], 0, 64) sent, _ := strconv.ParseInt(match[6], 0, 64) cons, _ := strconv.ParseInt(match[7], 0, 64) outs, _ := strconv.ParseInt(match[8], 0, 64) ncnt, _ := strconv.ParseInt(match[11], 0, 64) ss[i] = &ServerStats{ Server: servers[i], Sent: sent, Received: recv, NodeCount: ncnt, MinLatency: minLatency, AvgLatency: avgLatency, MaxLatency: maxLatency, Connections: cons, Outstanding: outs, Epoch: epoch, Counter: counter, BuildTime: buildTime, Mode: srvrMode, Version: match[0], } } return ss, imOk } // FLWRuok is a FourLetterWord helper function. In particular, this function // pulls the ruok output from each server. func FLWRuok(servers []string, timeout time.Duration) []bool { servers = FormatServers(servers) oks := make([]bool, len(servers)) for i := range oks { response, err := fourLetterWord(servers[i], "ruok", timeout) if err != nil { continue } if string(response[:4]) == "imok" { oks[i] = true } } return oks } // FLWCons is a FourLetterWord helper function. In particular, this function // pulls the ruok output from each server. // // As with FLWSrvr, the boolean value indicates whether one of the requests had // an issue. The Clients struct has an Error value that can be checked. func FLWCons(servers []string, timeout time.Duration) ([]*ServerClients, bool) { const ( zrAddr = `^ /((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?):(?:\d+))\[\d+\]` zrPac = `\(queued=(\d+),recved=(\d+),sent=(\d+),sid=(0x[A-Za-z0-9]+),lop=(\w+),est=(\d+),to=(\d+),` zrSesh = `lcxid=(0x[A-Za-z0-9]+),lzxid=(0x[A-Za-z0-9]+),lresp=(\d+),llat=(\d+),minlat=(\d+),avglat=(\d+),maxlat=(\d+)\)` ) re, err := regexp.Compile(fmt.Sprintf("%v%v%v", zrAddr, zrPac, zrSesh)) if err != nil { return nil, false } servers = FormatServers(servers) sc := make([]*ServerClients, len(servers)) imOk := true for i := range sc { response, err := fourLetterWord(servers[i], "cons", timeout) if err != nil { sc[i] = &ServerClients{Error: err} imOk = false continue } scan := bufio.NewScanner(bytes.NewReader(response)) var clients []*ServerClient for scan.Scan() { line := scan.Bytes() if len(line) == 0 { continue } m := re.FindAllStringSubmatch(string(line), -1) if m == nil { err := fmt.Errorf("unable to parse fields from zookeeper response (no regex matches)") sc[i] = &ServerClients{Error: err} imOk = false continue } match := m[0][1:] queued, _ := strconv.ParseInt(match[1], 0, 64) recvd, _ := strconv.ParseInt(match[2], 0, 64) sent, _ := strconv.ParseInt(match[3], 0, 64) sid, _ := strconv.ParseInt(match[4], 0, 64) est, _ := strconv.ParseInt(match[6], 0, 64) timeout, _ := strconv.ParseInt(match[7], 0, 32) lcxid, _ := parseInt64(match[8]) lzxid, _ := parseInt64(match[9]) lresp, _ := strconv.ParseInt(match[10], 0, 64) llat, _ := strconv.ParseInt(match[11], 0, 32) minlat, _ := strconv.ParseInt(match[12], 0, 32) avglat, _ := strconv.ParseInt(match[13], 0, 32) maxlat, _ := strconv.ParseInt(match[14], 0, 32) clients = append(clients, &ServerClient{ Queued: queued, Received: recvd, Sent: sent, SessionID: sid, Lcxid: int64(lcxid), Lzxid: int64(lzxid), Timeout: int32(timeout), LastLatency: int32(llat), MinLatency: int32(minlat), AvgLatency: int32(avglat), MaxLatency: int32(maxlat), Established: time.Unix(est, 0), LastResponse: time.Unix(lresp, 0), Addr: match[0], LastOperation: match[5], }) } sc[i] = &ServerClients{Clients: clients} } return sc, imOk } // parseInt64 is similar to strconv.ParseInt, but it also handles hex values that represent negative numbers func parseInt64(s string) (int64, error) { if strings.HasPrefix(s, "0x") { i, err := strconv.ParseUint(s, 0, 64) return int64(i), err } return strconv.ParseInt(s, 0, 64) } func fourLetterWord(server, command string, timeout time.Duration) ([]byte, error) { conn, err := net.DialTimeout("tcp", server, timeout) if err != nil { return nil, err } // the zookeeper server should automatically close this socket // once the command has been processed, but better safe than sorry defer conn.Close() conn.SetWriteDeadline(time.Now().Add(timeout)) _, err = conn.Write([]byte(command)) if err != nil { return nil, err } conn.SetReadDeadline(time.Now().Add(timeout)) return ioutil.ReadAll(conn) } golang-github-go-zookeeper-zk-1.0.4/flw_test.go000066400000000000000000000162541465024323700214650ustar00rootroot00000000000000package zk import ( "net" "testing" "time" ) var ( zkSrvrOut = `Zookeeper version: 3.4.6-1569965, built on 02/20/2014 09:09 GMT Latency min/avg/max: 0/1/10 Received: 4207 Sent: 4220 Connections: 81 Outstanding: 1 Zxid: 0x110a7a8f37 Mode: leader Node count: 306 ` zkConsOut = ` /10.42.45.231:45361[1](queued=0,recved=9435,sent=9457,sid=0x94c2989e04716b5,lop=PING,est=1427238717217,to=20001,lcxid=0x55120915,lzxid=0xffffffffffffffff,lresp=1427259255908,llat=0,minlat=0,avglat=1,maxlat=17) /10.55.33.98:34342[1](queued=0,recved=9338,sent=9350,sid=0x94c2989e0471731,lop=PING,est=1427238849319,to=20001,lcxid=0x55120944,lzxid=0xffffffffffffffff,lresp=1427259252294,llat=0,minlat=0,avglat=1,maxlat=18) /10.44.145.114:46556[1](queued=0,recved=109253,sent=109617,sid=0x94c2989e0471709,lop=DELE,est=1427238791305,to=20001,lcxid=0x55139618,lzxid=0x110a7b187d,lresp=1427259257423,llat=2,minlat=0,avglat=1,maxlat=23) ` ) func TestFLWRuok(t *testing.T) { t.Parallel() l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer l.Close() go tcpServer(l, "") oks := FLWRuok([]string{l.Addr().String()}, time.Second*10) if len(oks) == 0 { t.Errorf("no values returned") } if !oks[0] { t.Errorf("instance should be marked as OK") } // // Confirm that it also returns false for dead instances // l, err = net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer l.Close() go tcpServer(l, "dead") oks = FLWRuok([]string{l.Addr().String()}, time.Second*10) if len(oks) == 0 { t.Errorf("no values returned") } if oks[0] { t.Errorf("instance should be marked as not OK") } } func TestFLWSrvr(t *testing.T) { t.Parallel() l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer l.Close() go tcpServer(l, "") statsSlice, ok := FLWSrvr([]string{l.Addr().String()}, time.Second*10) if !ok { t.Errorf("failure indicated on 'srvr' parsing") } if len(statsSlice) == 0 { t.Errorf("no *ServerStats instances returned") } stats := statsSlice[0] if stats.Error != nil { t.Fatalf("error seen in stats: %v", err.Error()) } if stats.Sent != 4220 { t.Errorf("Sent != 4220") } if stats.Received != 4207 { t.Errorf("Received != 4207") } if stats.NodeCount != 306 { t.Errorf("NodeCount != 306") } if stats.MinLatency != 0 { t.Errorf("MinLatency != 0") } if stats.AvgLatency != 1 { t.Errorf("AvgLatency != 1") } if stats.MaxLatency != 10 { t.Errorf("MaxLatency != 10") } if stats.Connections != 81 { t.Errorf("Connection != 81") } if stats.Outstanding != 1 { t.Errorf("Outstanding != 1") } if stats.Epoch != 17 { t.Errorf("Epoch != 17") } if stats.Counter != 175804215 { t.Errorf("Counter != 175804215") } if stats.Mode != ModeLeader { t.Errorf("Mode != ModeLeader") } if stats.Version != "3.4.6-1569965" { t.Errorf("Version expected: 3.4.6-1569965") } } func TestFLWCons(t *testing.T) { t.Parallel() l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer l.Close() go tcpServer(l, "") clients, ok := FLWCons([]string{l.Addr().String()}, time.Second*10) if !ok { t.Errorf("failure indicated on 'cons' parsing") } if len(clients) == 0 { t.Errorf("no *ServerClients instances returned") } results := []*ServerClient{ { Queued: 0, Received: 9435, Sent: 9457, SessionID: 669956116721374901, LastOperation: "PING", Established: time.Unix(1427238717217, 0), Timeout: 20001, Lcxid: 1427245333, Lzxid: -1, LastResponse: time.Unix(1427259255908, 0), LastLatency: 0, MinLatency: 0, AvgLatency: 1, MaxLatency: 17, Addr: "10.42.45.231:45361", }, { Queued: 0, Received: 9338, Sent: 9350, SessionID: 669956116721375025, LastOperation: "PING", Established: time.Unix(1427238849319, 0), Timeout: 20001, Lcxid: 1427245380, Lzxid: -1, LastResponse: time.Unix(1427259252294, 0), LastLatency: 0, MinLatency: 0, AvgLatency: 1, MaxLatency: 18, Addr: "10.55.33.98:34342", }, { Queued: 0, Received: 109253, Sent: 109617, SessionID: 669956116721374985, LastOperation: "DELE", Established: time.Unix(1427238791305, 0), Timeout: 20001, Lcxid: 1427346968, Lzxid: 73190283389, LastResponse: time.Unix(1427259257423, 0), LastLatency: 2, MinLatency: 0, AvgLatency: 1, MaxLatency: 23, Addr: "10.44.145.114:46556", }, } for _, z := range clients { if z.Error != nil { t.Errorf("error seen: %v", err.Error()) } for i, v := range z.Clients { c := results[i] if v.Error != nil { t.Errorf("client error seen: %v", err.Error()) } if v.Queued != c.Queued { t.Errorf("Queued value mismatch (%d/%d)", v.Queued, c.Queued) } if v.Received != c.Received { t.Errorf("Received value mismatch (%d/%d)", v.Received, c.Received) } if v.Sent != c.Sent { t.Errorf("Sent value mismatch (%d/%d)", v.Sent, c.Sent) } if v.SessionID != c.SessionID { t.Errorf("SessionID value mismatch (%d/%d)", v.SessionID, c.SessionID) } if v.LastOperation != c.LastOperation { t.Errorf("LastOperation value mismatch ('%v'/'%v')", v.LastOperation, c.LastOperation) } if v.Timeout != c.Timeout { t.Errorf("Timeout value mismatch (%d/%d)", v.Timeout, c.Timeout) } if v.Lcxid != c.Lcxid { t.Errorf("Lcxid value mismatch (%d/%d)", v.Lcxid, c.Lcxid) } if v.Lzxid != c.Lzxid { t.Errorf("Lzxid value mismatch (%d/%d)", v.Lzxid, c.Lzxid) } if v.LastLatency != c.LastLatency { t.Errorf("LastLatency value mismatch (%d/%d)", v.LastLatency, c.LastLatency) } if v.MinLatency != c.MinLatency { t.Errorf("MinLatency value mismatch (%d/%d)", v.MinLatency, c.MinLatency) } if v.AvgLatency != c.AvgLatency { t.Errorf("AvgLatency value mismatch (%d/%d)", v.AvgLatency, c.AvgLatency) } if v.MaxLatency != c.MaxLatency { t.Errorf("MaxLatency value mismatch (%d/%d)", v.MaxLatency, c.MaxLatency) } if v.Addr != c.Addr { t.Errorf("Addr value mismatch ('%v'/'%v')", v.Addr, c.Addr) } if !c.Established.Equal(v.Established) { t.Errorf("Established value mismatch (%v/%v)", c.Established, v.Established) } if !c.LastResponse.Equal(v.LastResponse) { t.Errorf("Established value mismatch (%v/%v)", c.LastResponse, v.LastResponse) } } } } func tcpServer(listener net.Listener, thing string) { for { conn, err := listener.Accept() if err != nil { return } go connHandler(conn, thing) } } func connHandler(conn net.Conn, thing string) { defer conn.Close() data := make([]byte, 4) _, err := conn.Read(data) if err != nil { return } switch string(data) { case "ruok": switch thing { case "dead": return default: conn.Write([]byte("imok")) } case "srvr": switch thing { case "dead": return default: conn.Write([]byte(zkSrvrOut)) } case "cons": switch thing { case "dead": return default: conn.Write([]byte(zkConsOut)) } default: conn.Write([]byte("This ZooKeeper instance is not currently serving requests.")) } } golang-github-go-zookeeper-zk-1.0.4/go.mod000066400000000000000000000000531465024323700204030ustar00rootroot00000000000000module github.com/go-zookeeper/zk go 1.13 golang-github-go-zookeeper-zk-1.0.4/lock.go000066400000000000000000000064721465024323700205670ustar00rootroot00000000000000package zk import ( "errors" "fmt" "strconv" "strings" ) var ( // ErrDeadlock is returned by Lock when trying to lock twice without unlocking first ErrDeadlock = errors.New("zk: trying to acquire a lock twice") // ErrNotLocked is returned by Unlock when trying to release a lock that has not first be acquired. ErrNotLocked = errors.New("zk: not locked") ) // Lock is a mutual exclusion lock. type Lock struct { c *Conn path string acl []ACL lockPath string seq int } // NewLock creates a new lock instance using the provided connection, path, and acl. // The path must be a node that is only used by this lock. A lock instances starts // unlocked until Lock() is called. func NewLock(c *Conn, path string, acl []ACL) *Lock { return &Lock{ c: c, path: path, acl: acl, } } func parseSeq(path string) (int, error) { parts := strings.Split(path, "lock-") // python client uses a __LOCK__ prefix if len(parts) == 1 { parts = strings.Split(path, "__") } return strconv.Atoi(parts[len(parts)-1]) } // Lock attempts to acquire the lock. It works like LockWithData, but it doesn't // write any data to the lock node. func (l *Lock) Lock() error { return l.LockWithData([]byte{}) } // LockWithData attempts to acquire the lock, writing data into the lock node. // It will wait to return until the lock is acquired or an error occurs. If // this instance already has the lock then ErrDeadlock is returned. func (l *Lock) LockWithData(data []byte) error { if l.lockPath != "" { return ErrDeadlock } prefix := fmt.Sprintf("%s/lock-", l.path) path := "" var err error for i := 0; i < 3; i++ { path, err = l.c.CreateProtectedEphemeralSequential(prefix, data, l.acl) if err == ErrNoNode { // Create parent node. parts := strings.Split(l.path, "/") pth := "" for _, p := range parts[1:] { var exists bool pth += "/" + p exists, _, err = l.c.Exists(pth) if err != nil { return err } if exists == true { continue } _, err = l.c.Create(pth, []byte{}, 0, l.acl) if err != nil && err != ErrNodeExists { return err } } } else if err == nil { break } else { return err } } if err != nil { return err } seq, err := parseSeq(path) if err != nil { return err } for { children, _, err := l.c.Children(l.path) if err != nil { return err } lowestSeq := seq prevSeq := -1 prevSeqPath := "" for _, p := range children { s, err := parseSeq(p) if err != nil { return err } if s < lowestSeq { lowestSeq = s } if s < seq && s > prevSeq { prevSeq = s prevSeqPath = p } } if seq == lowestSeq { // Acquired the lock break } // Wait on the node next in line for the lock _, _, ch, err := l.c.GetW(l.path + "/" + prevSeqPath) if err != nil && err != ErrNoNode { return err } else if err != nil && err == ErrNoNode { // try again continue } ev := <-ch if ev.Err != nil { return ev.Err } } l.seq = seq l.lockPath = path return nil } // Unlock releases an acquired lock. If the lock is not currently acquired by // this Lock instance than ErrNotLocked is returned. func (l *Lock) Unlock() error { if l.lockPath == "" { return ErrNotLocked } if err := l.c.Delete(l.lockPath, -1); err != nil { return err } l.lockPath = "" l.seq = 0 return nil } golang-github-go-zookeeper-zk-1.0.4/lock_test.go000066400000000000000000000053701465024323700216220ustar00rootroot00000000000000package zk import ( "testing" "time" ) func TestIntegration_Lock(t *testing.T) { ts, err := StartTestCluster(t, 1, nil, logWriter{t: t, p: "[ZKERR] "}) if err != nil { t.Fatal(err) } defer ts.Stop() zk, _, err := ts.ConnectAll() if err != nil { t.Fatalf("Connect returned error: %+v", err) } defer zk.Close() acls := WorldACL(PermAll) l := NewLock(zk, "/test", acls) if err := l.Lock(); err != nil { t.Fatal(err) } if err := l.Unlock(); err != nil { t.Fatal(err) } val := make(chan int, 3) if err := l.Lock(); err != nil { t.Fatal(err) } l2 := NewLock(zk, "/test", acls) go func() { if err := l2.Lock(); err != nil { t.Fatal(err) } val <- 2 if err := l2.Unlock(); err != nil { t.Fatal(err) } val <- 3 }() time.Sleep(time.Millisecond * 100) val <- 1 if err := l.Unlock(); err != nil { t.Fatal(err) } if x := <-val; x != 1 { t.Fatalf("Expected 1 instead of %d", x) } if x := <-val; x != 2 { t.Fatalf("Expected 2 instead of %d", x) } if x := <-val; x != 3 { t.Fatalf("Expected 3 instead of %d", x) } } // This tests creating a lock with a path that's more than 1 node deep (e.g. "/test-multi-level/lock"), // when a part of that path already exists (i.e. "/test-multi-level" node already exists). func TestIntegration_MultiLevelLock(t *testing.T) { ts, err := StartTestCluster(t, 1, nil, logWriter{t: t, p: "[ZKERR] "}) if err != nil { t.Fatal(err) } defer ts.Stop() zk, _, err := ts.ConnectAll() if err != nil { t.Fatalf("Connect returned error: %+v", err) } defer zk.Close() acls := WorldACL(PermAll) path := "/test-multi-level" if p, err := zk.Create(path, []byte{1, 2, 3, 4}, 0, WorldACL(PermAll)); err != nil { t.Fatalf("Create returned error: %+v", err) } else if p != path { t.Fatalf("Create returned different path '%s' != '%s'", p, path) } l := NewLock(zk, "/test-multi-level/lock", acls) defer zk.Delete("/test-multi-level", -1) // Clean up what we've created for this test defer zk.Delete("/test-multi-level/lock", -1) if err := l.Lock(); err != nil { t.Fatal(err) } if err := l.Unlock(); err != nil { t.Fatal(err) } } func TestParseSeq(t *testing.T) { const ( goLock = "_c_38553bd6d1d57f710ae70ddcc3d24715-lock-0000000000" negativeLock = "_c_38553bd6d1d57f710ae70ddcc3d24715-lock--2147483648" pyLock = "da5719988c244fc793f49ec3aa29b566__lock__0000000003" ) seq, err := parseSeq(goLock) if err != nil { t.Fatal(err) } if seq != 0 { t.Fatalf("Expected 0 instead of %d", seq) } seq, err = parseSeq(negativeLock) if err != nil { t.Fatal(err) } if seq != -2147483648 { t.Fatalf("Expected -2147483648 instead of %d", seq) } seq, err = parseSeq(pyLock) if err != nil { t.Fatal(err) } if seq != 3 { t.Fatalf("Expected 3 instead of %d", seq) } } golang-github-go-zookeeper-zk-1.0.4/server_help_test.go000066400000000000000000000137551465024323700232160ustar00rootroot00000000000000package zk import ( "fmt" "io" "math/rand" "os" "path/filepath" "strings" "testing" "time" ) const ( _testConfigName = "zoo.cfg" _testMyIDFileName = "myid" ) type TestServer struct { Port int Path string Srv *server Config ServerConfigServer } type TestCluster struct { Path string Config ServerConfig Servers []TestServer } // TODO: pull this into its own package to allow for better isolation of integration tests vs. unit // testing. This should be used on CI systems and local only when needed whereas unit tests should remain // fast and not rely on external dependencies. func StartTestCluster(t *testing.T, size int, stdout, stderr io.Writer) (*TestCluster, error) { t.Helper() if testing.Short() { t.Skip("ZK cluster tests skipped in short case.") } if testing.Verbose() { // if testing verbose we just spam the server logs as many issues with tests will have the ZK server // logs have the cause of the failure in it. if stdout == nil { stdout = os.Stderr } else { stdout = io.MultiWriter(stdout, os.Stderr) } } tmpPath := t.TempDir() success := false startPort := int(rand.Int31n(6000) + 10000) cluster := &TestCluster{Path: tmpPath} defer func() { if !success { cluster.Stop() } }() for serverN := 0; serverN < size; serverN++ { srvPath := filepath.Join(tmpPath, fmt.Sprintf("srv%d", serverN+1)) requireNoErrorf(t, os.Mkdir(srvPath, 0700), "failed to make server path") port := startPort + serverN*3 cfg := ServerConfig{ ClientPort: port, DataDir: srvPath, } for i := 0; i < size; i++ { serverNConfig := ServerConfigServer{ ID: i + 1, Host: "127.0.0.1", PeerPort: startPort + i*3 + 1, LeaderElectionPort: startPort + i*3 + 2, } cfg.Servers = append(cfg.Servers, serverNConfig) } cfgPath := filepath.Join(srvPath, _testConfigName) fi, err := os.Create(cfgPath) requireNoErrorf(t, err) requireNoErrorf(t, cfg.Marshall(fi)) fi.Close() fi, err = os.Create(filepath.Join(srvPath, _testMyIDFileName)) requireNoErrorf(t, err) _, err = fmt.Fprintf(fi, "%d\n", serverN+1) fi.Close() requireNoErrorf(t, err) srv, err := NewIntegrationTestServer(t, cfgPath, stdout, stderr) requireNoErrorf(t, err) if err := srv.Start(); err != nil { return nil, err } cluster.Servers = append(cluster.Servers, TestServer{ Path: srvPath, Port: cfg.ClientPort, Srv: srv, Config: cfg.Servers[serverN], }) cluster.Config = cfg } if err := cluster.waitForStart(30, time.Second); err != nil { return nil, err } success = true return cluster, nil } func (tc *TestCluster) Connect(idx int) (*Conn, <-chan Event, error) { return Connect([]string{fmt.Sprintf("127.0.0.1:%d", tc.Servers[idx].Port)}, time.Second*15) } func (tc *TestCluster) ConnectAll() (*Conn, <-chan Event, error) { return tc.ConnectAllTimeout(time.Second * 15) } func (tc *TestCluster) ConnectAllTimeout(sessionTimeout time.Duration) (*Conn, <-chan Event, error) { return tc.ConnectWithOptions(sessionTimeout) } func (tc *TestCluster) ConnectWithOptions(sessionTimeout time.Duration, options ...connOption) (*Conn, <-chan Event, error) { hosts := make([]string, len(tc.Servers)) for i, srv := range tc.Servers { hosts[i] = fmt.Sprintf("127.0.0.1:%d", srv.Port) } zk, ch, err := Connect(hosts, sessionTimeout, options...) return zk, ch, err } func (tc *TestCluster) Stop() error { for _, srv := range tc.Servers { srv.Srv.Stop() } return tc.waitForStop(5, time.Second) } // waitForStart blocks until the cluster is up func (tc *TestCluster) waitForStart(maxRetry int, interval time.Duration) error { // verify that the servers are up with SRVR serverAddrs := make([]string, len(tc.Servers)) for i, s := range tc.Servers { serverAddrs[i] = fmt.Sprintf("127.0.0.1:%d", s.Port) } for i := 0; i < maxRetry; i++ { _, ok := FLWSrvr(serverAddrs, time.Second) if ok { return nil } time.Sleep(interval) } return fmt.Errorf("unable to verify health of servers") } // waitForStop blocks until the cluster is down func (tc *TestCluster) waitForStop(maxRetry int, interval time.Duration) error { // verify that the servers are up with RUOK serverAddrs := make([]string, len(tc.Servers)) for i, s := range tc.Servers { serverAddrs[i] = fmt.Sprintf("127.0.0.1:%d", s.Port) } var success bool for i := 0; i < maxRetry && !success; i++ { success = true for _, ok := range FLWRuok(serverAddrs, time.Second) { if ok { success = false } } if !success { time.Sleep(interval) } } if !success { return fmt.Errorf("unable to verify servers are down") } return nil } func (tc *TestCluster) StartServer(server string) { for _, s := range tc.Servers { if strings.HasSuffix(server, fmt.Sprintf(":%d", s.Port)) { s.Srv.Start() return } } panic(fmt.Sprintf("unknown server: %s", server)) } func (tc *TestCluster) StopServer(server string) { for _, s := range tc.Servers { if strings.HasSuffix(server, fmt.Sprintf(":%d", s.Port)) { s.Srv.Stop() return } } panic(fmt.Sprintf("unknown server: %s", server)) } func (tc *TestCluster) StartAllServers() error { for _, s := range tc.Servers { if err := s.Srv.Start(); err != nil { return fmt.Errorf("failed to start server listening on port `%d` : %+v", s.Port, err) } } if err := tc.waitForStart(10, time.Second*2); err != nil { return fmt.Errorf("failed to wait to startup zk servers: %v", err) } return nil } func (tc *TestCluster) StopAllServers() error { var err error for _, s := range tc.Servers { if err := s.Srv.Stop(); err != nil { err = fmt.Errorf("failed to stop server listening on port `%d` : %v", s.Port, err) } } if err != nil { return err } if err := tc.waitForStop(5, time.Second); err != nil { return fmt.Errorf("failed to wait to startup zk servers: %v", err) } return nil } func requireNoErrorf(t *testing.T, err error, msgAndArgs ...interface{}) { t.Helper() if err != nil { t.Logf("received unexpected error: %v", err) t.Fatal(msgAndArgs...) } } golang-github-go-zookeeper-zk-1.0.4/server_java_test.go000066400000000000000000000124101465024323700231720ustar00rootroot00000000000000package zk import ( "context" "fmt" "io" "os" "os/exec" "path/filepath" "testing" ) type ErrMissingServerConfigField string func (e ErrMissingServerConfigField) Error() string { return fmt.Sprintf("zk: missing server config field '%s'", string(e)) } const ( DefaultServerTickTime = 500 DefaultServerInitLimit = 10 DefaultServerSyncLimit = 5 DefaultServerAutoPurgeSnapRetainCount = 3 DefaultPeerPort = 2888 DefaultLeaderElectionPort = 3888 ) type server struct { stdout, stderr io.Writer cmdString string cmdArgs []string cmdEnv []string cmd *exec.Cmd // this cancel will kill the command being run in this case the server itself. cancelFunc context.CancelFunc } func NewIntegrationTestServer(t *testing.T, configPath string, stdout, stderr io.Writer) (*server, error) { // allow external systems to configure this zk server bin path. zkPath := os.Getenv("ZOOKEEPER_BIN_PATH") if zkPath == "" { // default to a static reletive path that can be setup with a build system zkPath = "zookeeper/bin" } if _, err := os.Stat(zkPath); err != nil { if os.IsNotExist(err) { return nil, fmt.Errorf("zk: could not find testing zookeeper bin path at %q: %v ", zkPath, err) } } // password is 'test' superString := `SERVER_JVMFLAGS=-Dzookeeper.DigestAuthenticationProvider.superDigest=super:D/InIHSb7yEEbrWz8b9l71RjZJU=` // enable TTL superString += ` -Dzookeeper.extendedTypesEnabled=true -Dzookeeper.emulate353TTLNodes=true` return &server{ cmdString: filepath.Join(zkPath, "zkServer.sh"), cmdArgs: []string{"start-foreground", configPath}, cmdEnv: []string{superString}, stdout: stdout, stderr: stderr, }, nil } func (srv *server) Start() error { ctx, cancel := context.WithCancel(context.Background()) srv.cancelFunc = cancel srv.cmd = exec.CommandContext(ctx, srv.cmdString, srv.cmdArgs...) srv.cmd.Stdout = srv.stdout srv.cmd.Stderr = srv.stderr srv.cmd.Env = append(os.Environ(), srv.cmdEnv...) return srv.cmd.Start() } func (srv *server) Stop() error { srv.cancelFunc() return srv.cmd.Wait() } type ServerConfigServer struct { ID int Host string PeerPort int LeaderElectionPort int } type ServerConfig struct { TickTime int // Number of milliseconds of each tick InitLimit int // Number of ticks that the initial synchronization phase can take SyncLimit int // Number of ticks that can pass between sending a request and getting an acknowledgement DataDir string // Direcrory where the snapshot is stored ClientPort int // Port at which clients will connect AutoPurgeSnapRetainCount int // Number of snapshots to retain in dataDir AutoPurgePurgeInterval int // Purge task internal in hours (0 to disable auto purge) Servers []ServerConfigServer } func (sc ServerConfig) Marshall(w io.Writer) error { // the admin server is not wanted in test cases as it slows the startup process and is // of little unit test value. fmt.Fprintln(w, "admin.enableServer=false") if sc.DataDir == "" { return ErrMissingServerConfigField("dataDir") } fmt.Fprintf(w, "dataDir=%s\n", sc.DataDir) if sc.TickTime <= 0 { sc.TickTime = DefaultServerTickTime } fmt.Fprintf(w, "tickTime=%d\n", sc.TickTime) if sc.InitLimit <= 0 { sc.InitLimit = DefaultServerInitLimit } fmt.Fprintf(w, "initLimit=%d\n", sc.InitLimit) if sc.SyncLimit <= 0 { sc.SyncLimit = DefaultServerSyncLimit } fmt.Fprintf(w, "syncLimit=%d\n", sc.SyncLimit) if sc.ClientPort <= 0 { sc.ClientPort = DefaultPort } fmt.Fprintf(w, "clientPort=%d\n", sc.ClientPort) if sc.AutoPurgePurgeInterval > 0 { if sc.AutoPurgeSnapRetainCount <= 0 { sc.AutoPurgeSnapRetainCount = DefaultServerAutoPurgeSnapRetainCount } fmt.Fprintf(w, "autopurge.snapRetainCount=%d\n", sc.AutoPurgeSnapRetainCount) fmt.Fprintf(w, "autopurge.purgeInterval=%d\n", sc.AutoPurgePurgeInterval) } // enable reconfig. // TODO: allow setting this fmt.Fprintln(w, "reconfigEnabled=true") fmt.Fprintln(w, "4lw.commands.whitelist=*") if len(sc.Servers) < 2 { // if we dont have more than 2 servers we just dont specify server list to start in standalone mode // see https://zookeeper.apache.org/doc/current/zookeeperStarted.html#sc_InstallingSingleMode for more details. return nil } // if we then have more than one server force it to be distributed fmt.Fprintln(w, "standaloneEnabled=false") for _, srv := range sc.Servers { if srv.PeerPort <= 0 { srv.PeerPort = DefaultPeerPort } if srv.LeaderElectionPort <= 0 { srv.LeaderElectionPort = DefaultLeaderElectionPort } fmt.Fprintf(w, "server.%d=%s:%d:%d\n", srv.ID, srv.Host, srv.PeerPort, srv.LeaderElectionPort) } return nil } // this is a helper to wait for the zk connection to at least get to the HasSession state func waitForSession(ctx context.Context, eventChan <-chan Event) error { select { case event, ok := <-eventChan: // The eventChan is used solely to determine when the ZK conn has // stopped. if !ok { return fmt.Errorf("connection closed before state reached") } if event.State == StateHasSession { return nil } case <-ctx.Done(): return ctx.Err() } return nil } golang-github-go-zookeeper-zk-1.0.4/structs.go000066400000000000000000000315731465024323700213460ustar00rootroot00000000000000package zk import ( "encoding/binary" "errors" "log" "reflect" "runtime" "strings" "time" ) var ( ErrUnhandledFieldType = errors.New("zk: unhandled field type") ErrPtrExpected = errors.New("zk: encode/decode expect a non-nil pointer to struct") ErrShortBuffer = errors.New("zk: buffer too small") ) type defaultLogger struct{} func (defaultLogger) Printf(format string, a ...interface{}) { log.Printf(format, a...) } type ACL struct { Perms int32 Scheme string ID string } type Stat struct { Czxid int64 // The zxid of the change that caused this znode to be created. Mzxid int64 // The zxid of the change that last modified this znode. Ctime int64 // The time in milliseconds from epoch when this znode was created. Mtime int64 // The time in milliseconds from epoch when this znode was last modified. Version int32 // The number of changes to the data of this znode. Cversion int32 // The number of changes to the children of this znode. Aversion int32 // The number of changes to the ACL of this znode. EphemeralOwner int64 // The session id of the owner of this znode if the znode is an ephemeral node. If it is not an ephemeral node, it will be zero. DataLength int32 // The length of the data field of this znode. NumChildren int32 // The number of children of this znode. Pzxid int64 // last modified children } // ServerClient is the information for a single Zookeeper client and its session. // This is used to parse/extract the output fo the `cons` command. type ServerClient struct { Queued int64 Received int64 Sent int64 SessionID int64 Lcxid int64 Lzxid int64 Timeout int32 LastLatency int32 MinLatency int32 AvgLatency int32 MaxLatency int32 Established time.Time LastResponse time.Time Addr string LastOperation string // maybe? Error error } // ServerClients is a struct for the FLWCons() function. It's used to provide // the list of Clients. // // This is needed because FLWCons() takes multiple servers. type ServerClients struct { Clients []*ServerClient Error error } // ServerStats is the information pulled from the Zookeeper `stat` command. type ServerStats struct { Server string Sent int64 Received int64 NodeCount int64 MinLatency int64 AvgLatency float64 MaxLatency int64 Connections int64 Outstanding int64 Epoch int32 Counter int32 BuildTime time.Time Mode Mode Version string Error error } type requestHeader struct { Xid int32 Opcode int32 } type responseHeader struct { Xid int32 Zxid int64 Err ErrCode } type multiHeader struct { Type int32 Done bool Err ErrCode } type auth struct { Type int32 Scheme string Auth []byte } // Generic request structs type pathRequest struct { Path string } type PathVersionRequest struct { Path string Version int32 } type pathWatchRequest struct { Path string Watch bool } type pathResponse struct { Path string } type statResponse struct { Stat Stat } // type CheckVersionRequest PathVersionRequest type closeRequest struct{} type closeResponse struct{} type connectRequest struct { ProtocolVersion int32 LastZxidSeen int64 TimeOut int32 SessionID int64 Passwd []byte } type connectResponse struct { ProtocolVersion int32 TimeOut int32 SessionID int64 Passwd []byte } type CreateRequest struct { Path string Data []byte Acl []ACL Flags int32 } type CreateTTLRequest struct { Path string Data []byte Acl []ACL Flags int32 Ttl int64 // ms } type createResponse pathResponse type DeleteRequest PathVersionRequest type deleteResponse struct{} type errorResponse struct { Err int32 } type existsRequest pathWatchRequest type existsResponse statResponse type getAclRequest pathRequest type getAclResponse struct { Acl []ACL Stat Stat } type getChildrenRequest pathRequest type getChildrenResponse struct { Children []string } type getChildren2Request pathWatchRequest type getChildren2Response struct { Children []string Stat Stat } type getDataRequest pathWatchRequest type getDataResponse struct { Data []byte Stat Stat } type getMaxChildrenRequest pathRequest type getMaxChildrenResponse struct { Max int32 } type getSaslRequest struct { Token []byte } type pingRequest struct{} type pingResponse struct{} type setAclRequest struct { Path string Acl []ACL Version int32 } type setAclResponse statResponse type SetDataRequest struct { Path string Data []byte Version int32 } type setDataResponse statResponse type setMaxChildren struct { Path string Max int32 } type setSaslRequest struct { Token string } type setSaslResponse struct { Token string } type setWatchesRequest struct { RelativeZxid int64 DataWatches []string ExistWatches []string ChildWatches []string } type setWatchesResponse struct{} type syncRequest pathRequest type syncResponse pathResponse type setAuthRequest auth type setAuthResponse struct{} type multiRequestOp struct { Header multiHeader Op interface{} } type multiRequest struct { Ops []multiRequestOp DoneHeader multiHeader } type multiResponseOp struct { Header multiHeader String string Stat *Stat Err ErrCode } type multiResponse struct { Ops []multiResponseOp DoneHeader multiHeader } // zk version 3.5 reconfig API type reconfigRequest struct { JoiningServers []byte LeavingServers []byte NewMembers []byte // curConfigId version of the current configuration // optional - causes reconfiguration to return an error if configuration is no longer current CurConfigId int64 } type reconfigReponse getDataResponse func (r *multiRequest) Encode(buf []byte) (int, error) { total := 0 for _, op := range r.Ops { op.Header.Done = false n, err := encodePacketValue(buf[total:], reflect.ValueOf(op)) if err != nil { return total, err } total += n } r.DoneHeader.Done = true n, err := encodePacketValue(buf[total:], reflect.ValueOf(r.DoneHeader)) if err != nil { return total, err } total += n return total, nil } func (r *multiRequest) Decode(buf []byte) (int, error) { r.Ops = make([]multiRequestOp, 0) r.DoneHeader = multiHeader{-1, true, -1} total := 0 for { header := &multiHeader{} n, err := decodePacketValue(buf[total:], reflect.ValueOf(header)) if err != nil { return total, err } total += n if header.Done { r.DoneHeader = *header break } req := requestStructForOp(header.Type) if req == nil { return total, ErrAPIError } n, err = decodePacketValue(buf[total:], reflect.ValueOf(req)) if err != nil { return total, err } total += n r.Ops = append(r.Ops, multiRequestOp{*header, req}) } return total, nil } func (r *multiResponse) Decode(buf []byte) (int, error) { var multiErr error r.Ops = make([]multiResponseOp, 0) r.DoneHeader = multiHeader{-1, true, -1} total := 0 for { header := &multiHeader{} n, err := decodePacketValue(buf[total:], reflect.ValueOf(header)) if err != nil { return total, err } total += n if header.Done { r.DoneHeader = *header break } res := multiResponseOp{Header: *header} var w reflect.Value switch header.Type { default: return total, ErrAPIError case opError: w = reflect.ValueOf(&res.Err) case opCreate: w = reflect.ValueOf(&res.String) case opSetData: res.Stat = new(Stat) w = reflect.ValueOf(res.Stat) case opCheck, opDelete: } if w.IsValid() { n, err := decodePacketValue(buf[total:], w) if err != nil { return total, err } total += n } r.Ops = append(r.Ops, res) if multiErr == nil && res.Err != errOk { // Use the first error as the error returned from Multi(). multiErr = res.Err.toError() } } return total, multiErr } type watcherEvent struct { Type EventType State State Path string } type decoder interface { Decode(buf []byte) (int, error) } type encoder interface { Encode(buf []byte) (int, error) } func decodePacket(buf []byte, st interface{}) (n int, err error) { defer func() { if r := recover(); r != nil { if e, ok := r.(runtime.Error); ok && strings.HasPrefix(e.Error(), "runtime error: slice bounds out of range") { err = ErrShortBuffer } else { panic(r) } } }() v := reflect.ValueOf(st) if v.Kind() != reflect.Ptr || v.IsNil() { return 0, ErrPtrExpected } return decodePacketValue(buf, v) } func decodePacketValue(buf []byte, v reflect.Value) (int, error) { rv := v kind := v.Kind() if kind == reflect.Ptr { if v.IsNil() { v.Set(reflect.New(v.Type().Elem())) } v = v.Elem() kind = v.Kind() } n := 0 switch kind { default: return n, ErrUnhandledFieldType case reflect.Struct: if de, ok := rv.Interface().(decoder); ok { return de.Decode(buf) } else if de, ok := v.Interface().(decoder); ok { return de.Decode(buf) } else { for i := 0; i < v.NumField(); i++ { field := v.Field(i) n2, err := decodePacketValue(buf[n:], field) n += n2 if err != nil { return n, err } } } case reflect.Bool: v.SetBool(buf[n] != 0) n++ case reflect.Int32: v.SetInt(int64(binary.BigEndian.Uint32(buf[n : n+4]))) n += 4 case reflect.Int64: v.SetInt(int64(binary.BigEndian.Uint64(buf[n : n+8]))) n += 8 case reflect.String: ln := int(binary.BigEndian.Uint32(buf[n : n+4])) v.SetString(string(buf[n+4 : n+4+ln])) n += 4 + ln case reflect.Slice: switch v.Type().Elem().Kind() { default: count := int(binary.BigEndian.Uint32(buf[n : n+4])) n += 4 values := reflect.MakeSlice(v.Type(), count, count) v.Set(values) for i := 0; i < count; i++ { n2, err := decodePacketValue(buf[n:], values.Index(i)) n += n2 if err != nil { return n, err } } case reflect.Uint8: ln := int(int32(binary.BigEndian.Uint32(buf[n : n+4]))) if ln < 0 { n += 4 v.SetBytes(nil) } else { bytes := make([]byte, ln) copy(bytes, buf[n+4:n+4+ln]) v.SetBytes(bytes) n += 4 + ln } } } return n, nil } func encodePacket(buf []byte, st interface{}) (n int, err error) { defer func() { if r := recover(); r != nil { if e, ok := r.(runtime.Error); ok && strings.HasPrefix(e.Error(), "runtime error: slice bounds out of range") { err = ErrShortBuffer } else { panic(r) } } }() v := reflect.ValueOf(st) if v.Kind() != reflect.Ptr || v.IsNil() { return 0, ErrPtrExpected } return encodePacketValue(buf, v) } func encodePacketValue(buf []byte, v reflect.Value) (int, error) { rv := v for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { v = v.Elem() } n := 0 switch v.Kind() { default: return n, ErrUnhandledFieldType case reflect.Struct: if en, ok := rv.Interface().(encoder); ok { return en.Encode(buf) } else if en, ok := v.Interface().(encoder); ok { return en.Encode(buf) } else { for i := 0; i < v.NumField(); i++ { field := v.Field(i) n2, err := encodePacketValue(buf[n:], field) n += n2 if err != nil { return n, err } } } case reflect.Bool: if v.Bool() { buf[n] = 1 } else { buf[n] = 0 } n++ case reflect.Int32: binary.BigEndian.PutUint32(buf[n:n+4], uint32(v.Int())) n += 4 case reflect.Int64: binary.BigEndian.PutUint64(buf[n:n+8], uint64(v.Int())) n += 8 case reflect.String: str := v.String() binary.BigEndian.PutUint32(buf[n:n+4], uint32(len(str))) copy(buf[n+4:n+4+len(str)], []byte(str)) n += 4 + len(str) case reflect.Slice: switch v.Type().Elem().Kind() { default: count := v.Len() startN := n n += 4 for i := 0; i < count; i++ { n2, err := encodePacketValue(buf[n:], v.Index(i)) n += n2 if err != nil { return n, err } } binary.BigEndian.PutUint32(buf[startN:startN+4], uint32(count)) case reflect.Uint8: if v.IsNil() { binary.BigEndian.PutUint32(buf[n:n+4], uint32(0xffffffff)) n += 4 } else { bytes := v.Bytes() binary.BigEndian.PutUint32(buf[n:n+4], uint32(len(bytes))) copy(buf[n+4:n+4+len(bytes)], bytes) n += 4 + len(bytes) } } } return n, nil } func requestStructForOp(op int32) interface{} { switch op { case opClose: return &closeRequest{} case opCreate, opCreateContainer: return &CreateRequest{} case opCreateTTL: return &CreateTTLRequest{} case opDelete: return &DeleteRequest{} case opExists: return &existsRequest{} case opGetAcl: return &getAclRequest{} case opGetChildren: return &getChildrenRequest{} case opGetChildren2: return &getChildren2Request{} case opGetData: return &getDataRequest{} case opPing: return &pingRequest{} case opSetAcl: return &setAclRequest{} case opSetData: return &SetDataRequest{} case opSetWatches: return &setWatchesRequest{} case opSync: return &syncRequest{} case opSetAuth: return &setAuthRequest{} case opCheck: return &CheckVersionRequest{} case opMulti: return &multiRequest{} case opReconfig: return &reconfigRequest{} } return nil } golang-github-go-zookeeper-zk-1.0.4/structs_test.go000066400000000000000000000044401465024323700223760ustar00rootroot00000000000000package zk import ( "reflect" "testing" ) func TestEncodeDecodePacket(t *testing.T) { t.Parallel() encodeDecodeTest(t, &requestHeader{-2, 5}) encodeDecodeTest(t, &connectResponse{1, 2, 3, nil}) encodeDecodeTest(t, &connectResponse{1, 2, 3, []byte{4, 5, 6}}) encodeDecodeTest(t, &getAclResponse{[]ACL{{12, "s", "anyone"}}, Stat{}}) encodeDecodeTest(t, &getChildrenResponse{[]string{"foo", "bar"}}) encodeDecodeTest(t, &pathWatchRequest{"path", true}) encodeDecodeTest(t, &pathWatchRequest{"path", false}) encodeDecodeTest(t, &CheckVersionRequest{"/", -1}) encodeDecodeTest(t, &reconfigRequest{nil, nil, nil, -1}) encodeDecodeTest(t, &multiRequest{Ops: []multiRequestOp{{multiHeader{opCheck, false, -1}, &CheckVersionRequest{"/", -1}}}}) } func TestRequestStructForOp(t *testing.T) { for op, name := range opNames { if op != opNotify && op != opWatcherEvent { if s := requestStructForOp(op); s == nil { t.Errorf("No struct for op %s", name) } } } } func encodeDecodeTest(t *testing.T, r interface{}) { buf := make([]byte, 1024) n, err := encodePacket(buf, r) if err != nil { t.Errorf("encodePacket returned non-nil error %+v\n", err) return } t.Logf("%+v %x", r, buf[:n]) r2 := reflect.New(reflect.ValueOf(r).Elem().Type()).Interface() n2, err := decodePacket(buf[:n], r2) if err != nil { t.Errorf("decodePacket returned non-nil error %+v\n", err) return } if n != n2 { t.Errorf("sizes don't match: %d != %d", n, n2) return } if !reflect.DeepEqual(r, r2) { t.Errorf("results don't match: %+v != %+v", r, r2) return } } func TestEncodeShortBuffer(t *testing.T) { t.Parallel() _, err := encodePacket([]byte{}, &requestHeader{1, 2}) if err != ErrShortBuffer { t.Errorf("encodePacket should return ErrShortBuffer on a short buffer instead of '%+v'", err) return } } func TestDecodeShortBuffer(t *testing.T) { t.Parallel() _, err := decodePacket([]byte{}, &responseHeader{}) if err != ErrShortBuffer { t.Errorf("decodePacket should return ErrShortBuffer on a short buffer instead of '%+v'", err) return } } func BenchmarkEncode(b *testing.B) { buf := make([]byte, 4096) st := &connectRequest{Passwd: []byte("1234567890")} b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { if _, err := encodePacket(buf, st); err != nil { b.Fatal(err) } } } golang-github-go-zookeeper-zk-1.0.4/tcp_server_test.go000066400000000000000000000011711465024323700230410ustar00rootroot00000000000000package zk import ( "fmt" "math/rand" "net" "testing" "time" ) func WithListenServer(t *testing.T, test func(server string)) { startPort := int(rand.Int31n(6000) + 10000) server := fmt.Sprintf("localhost:%d", startPort) l, err := net.Listen("tcp", server) if err != nil { t.Fatalf("Failed to start listen server: %v", err) } defer l.Close() go func() { conn, err := l.Accept() if err != nil { t.Logf("Failed to accept connection: %s", err.Error()) } handleRequest(conn) }() test(server) } // Handles incoming requests. func handleRequest(conn net.Conn) { time.Sleep(5 * time.Second) conn.Close() } golang-github-go-zookeeper-zk-1.0.4/throttle_test.go000066400000000000000000000052141465024323700225340ustar00rootroot00000000000000/* Copyright 2012 Google Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Vendored from go4.org/net/throttle package zk import ( "fmt" "net" "sync" "time" ) const unitSize = 1400 // read/write chunk size. ~MTU size. type Rate struct { KBps int // or 0, to not rate-limit bandwidth Latency time.Duration } // byteTime returns the time required for n bytes. func (r Rate) byteTime(n int) time.Duration { if r.KBps == 0 { return 0 } return time.Duration(float64(n)/1024/float64(r.KBps)) * time.Second } type Listener struct { net.Listener Down Rate // server Writes to Client Up Rate // server Reads from client } func (ln *Listener) Accept() (net.Conn, error) { c, err := ln.Listener.Accept() time.Sleep(ln.Up.Latency) if err != nil { return nil, err } tc := &conn{Conn: c, Down: ln.Down, Up: ln.Up} tc.start() return tc, nil } type nErr struct { n int err error } type writeReq struct { writeAt time.Time p []byte resc chan nErr } type conn struct { net.Conn Down Rate // for reads Up Rate // for writes wchan chan writeReq closeOnce sync.Once closeErr error } func (c *conn) start() { c.wchan = make(chan writeReq, 1024) go c.writeLoop() } func (c *conn) writeLoop() { for req := range c.wchan { time.Sleep(req.writeAt.Sub(time.Now())) var res nErr for len(req.p) > 0 && res.err == nil { writep := req.p if len(writep) > unitSize { writep = writep[:unitSize] } n, err := c.Conn.Write(writep) time.Sleep(c.Up.byteTime(len(writep))) res.n += n res.err = err req.p = req.p[n:] } req.resc <- res } } func (c *conn) Close() error { c.closeOnce.Do(func() { err := c.Conn.Close() close(c.wchan) c.closeErr = err }) return c.closeErr } func (c *conn) Write(p []byte) (n int, err error) { defer func() { if e := recover(); e != nil { n = 0 err = fmt.Errorf("%v", err) return } }() resc := make(chan nErr, 1) c.wchan <- writeReq{time.Now().Add(c.Up.Latency), p, resc} res := <-resc return res.n, res.err } func (c *conn) Read(p []byte) (n int, err error) { const max = 1024 if len(p) > max { p = p[:max] } n, err = c.Conn.Read(p) time.Sleep(c.Down.byteTime(n)) return } golang-github-go-zookeeper-zk-1.0.4/util.go000066400000000000000000000056161465024323700206130ustar00rootroot00000000000000package zk import ( "crypto/sha1" "encoding/base64" "fmt" "math/rand" "strconv" "strings" "unicode/utf8" ) // AuthACL produces an ACL list containing a single ACL which uses the // provided permissions, with the scheme "auth", and ID "", which is used // by ZooKeeper to represent any authenticated user. func AuthACL(perms int32) []ACL { return []ACL{{perms, "auth", ""}} } // WorldACL produces an ACL list containing a single ACL which uses the // provided permissions, with the scheme "world", and ID "anyone", which // is used by ZooKeeper to represent any user at all. func WorldACL(perms int32) []ACL { return []ACL{{perms, "world", "anyone"}} } func DigestACL(perms int32, user, password string) []ACL { userPass := []byte(fmt.Sprintf("%s:%s", user, password)) h := sha1.New() if n, err := h.Write(userPass); err != nil || n != len(userPass) { panic("SHA1 failed") } digest := base64.StdEncoding.EncodeToString(h.Sum(nil)) return []ACL{{perms, "digest", fmt.Sprintf("%s:%s", user, digest)}} } // FormatServers takes a slice of addresses, and makes sure they are in a format // that resembles :. If the server has no port provided, the // DefaultPort constant is added to the end. func FormatServers(servers []string) []string { srvs := make([]string, len(servers)) for i, addr := range servers { if strings.Contains(addr, ":") { srvs[i] = addr } else { srvs[i] = addr + ":" + strconv.Itoa(DefaultPort) } } return srvs } // stringShuffle performs a Fisher-Yates shuffle on a slice of strings func stringShuffle(s []string) { for i := len(s) - 1; i > 0; i-- { j := rand.Intn(i + 1) s[i], s[j] = s[j], s[i] } } // validatePath will make sure a path is valid before sending the request func validatePath(path string, isSequential bool) error { if path == "" { return ErrInvalidPath } if path[0] != '/' { return ErrInvalidPath } n := len(path) if n == 1 { // path is just the root return nil } if !isSequential && path[n-1] == '/' { return ErrInvalidPath } // Start at rune 1 since we already know that the first character is // a '/'. for i, w := 1, 0; i < n; i += w { r, width := utf8.DecodeRuneInString(path[i:]) switch { case r == '\u0000': return ErrInvalidPath case r == '/': last, _ := utf8.DecodeLastRuneInString(path[:i]) if last == '/' { return ErrInvalidPath } case r == '.': last, lastWidth := utf8.DecodeLastRuneInString(path[:i]) // Check for double dot if last == '.' { last, _ = utf8.DecodeLastRuneInString(path[:i-lastWidth]) } if last == '/' { if i+1 == n { return ErrInvalidPath } next, _ := utf8.DecodeRuneInString(path[i+w:]) if next == '/' { return ErrInvalidPath } } case r >= '\u0000' && r <= '\u001f', r >= '\u007f' && r <= '\u009f', r >= '\uf000' && r <= '\uf8ff', r >= '\ufff0' && r < '\uffff': return ErrInvalidPath } w = width } return nil } golang-github-go-zookeeper-zk-1.0.4/util_test.go000066400000000000000000000026361465024323700216510ustar00rootroot00000000000000package zk import "testing" func TestFormatServers(t *testing.T) { t.Parallel() servers := []string{"127.0.0.1:2181", "127.0.0.42", "127.0.42.1:8811"} r := []string{"127.0.0.1:2181", "127.0.0.42:2181", "127.0.42.1:8811"} for i, s := range FormatServers(servers) { if s != r[i] { t.Errorf("%v should equal %v", s, r[i]) } } } func TestValidatePath(t *testing.T) { tt := []struct { path string seq bool valid bool }{ {"/this is / a valid/path", false, true}, {"/", false, true}, {"", false, false}, {"not/valid", false, false}, {"/ends/with/slash/", false, false}, {"/sequential/", true, true}, {"/test\u0000", false, false}, {"/double//slash", false, false}, {"/single/./period", false, false}, {"/double/../period", false, false}, {"/double/..ok/period", false, true}, {"/double/alsook../period", false, true}, {"/double/period/at/end/..", false, false}, {"/name/with.period", false, true}, {"/test\u0001", false, false}, {"/test\u001f", false, false}, {"/test\u0020", false, true}, // first allowable {"/test\u007e", false, true}, // last valid ascii {"/test\u007f", false, false}, {"/test\u009f", false, false}, {"/test\uf8ff", false, false}, {"/test\uffef", false, true}, {"/test\ufff0", false, false}, } for _, tc := range tt { err := validatePath(tc.path, tc.seq) if (err != nil) == tc.valid { t.Errorf("failed to validate path %q", tc.path) } } } golang-github-go-zookeeper-zk-1.0.4/zk_test.go000066400000000000000000001122061465024323700213130ustar00rootroot00000000000000package zk import ( "context" "encoding/hex" "errors" "fmt" "io" "math/rand" "net" "os" "path/filepath" "reflect" "regexp" "sort" "strconv" "strings" "sync" "sync/atomic" "testing" "time" ) func TestIntegration_StateChanges(t *testing.T) { ts, err := StartTestCluster(t, 1, nil, logWriter{t: t, p: "[ZKERR] "}) if err != nil { t.Fatal(err) } defer ts.Stop() callbackChan := make(chan Event) f := func(event Event) { callbackChan <- event } zk, eventChan, err := ts.ConnectWithOptions(15*time.Second, WithEventCallback(f)) if err != nil { t.Fatalf("Connect returned error: %+v", err) } verifyEventOrder := func(c <-chan Event, expectedStates []State, source string) { for _, state := range expectedStates { for { event, ok := <-c if !ok { t.Fatalf("unexpected channel close for %s", source) } if event.Type != EventSession { continue } if event.State != state { t.Fatalf("mismatched state order from %s, expected %v, received %v", source, state, event.State) } break } } } states := []State{StateConnecting, StateConnected, StateHasSession} verifyEventOrder(callbackChan, states, "callback") verifyEventOrder(eventChan, states, "event channel") zk.Close() verifyEventOrder(callbackChan, []State{StateDisconnected}, "callback") verifyEventOrder(eventChan, []State{StateDisconnected}, "event channel") } func TestIntegration_Create(t *testing.T) { ts, err := StartTestCluster(t, 1, nil, logWriter{t: t, p: "[ZKERR] "}) requireNoErrorf(t, err) defer ts.Stop() zk, _, err := ts.ConnectAll() requireNoErrorf(t, err, "ConnectAll()") defer zk.Close() tests := []struct { name string createFlags int32 specifiedPath string wantErr string }{ { name: "valid create persistant", createFlags: FlagPersistent, }, { name: "valid container from Create", createFlags: FlagContainer, // NOTE for v2: we dont need CreateContainer method. }, { name: "invalid path", specifiedPath: "not/valid", wantErr: "zk: invalid path", }, { name: "invalid create ttl", createFlags: FlagTTL, wantErr: "zk: invalid flags specified", }, { name: "invalid flag for create mode", createFlags: 999, wantErr: "invalid flag value: [999]", }, } const testPath = "/ttl_znode_tests" // create sub node to create per test in avoiding using the root path. _, err = zk.Create(testPath, nil /* data */, FlagPersistent, WorldACL(PermAll)) requireNoErrorf(t, err) for idx, tt := range tests { t.Run(tt.name, func(t *testing.T) { path := filepath.Join(testPath, fmt.Sprint(idx)) if tt.specifiedPath != "" { path = tt.specifiedPath } _, err := zk.Create(path, []byte{12}, tt.createFlags, WorldACL(PermAll)) if tt.wantErr == "" { requireNoErrorf(t, err, fmt.Sprintf("error not expected: path; %q; flags %v", path, tt.createFlags)) return } // want an error if err == nil { t.Fatalf("did not get expected error: %q", tt.wantErr) } if !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("wanted error not found: %v; got: %v", tt.wantErr, err.Error()) } }) } } func TestIntegration_CreateTTL(t *testing.T) { ts, err := StartTestCluster(t, 1, nil, logWriter{t: t, p: "[ZKERR] "}) requireNoErrorf(t, err) defer ts.Stop() zk, _, err := ts.ConnectAll() requireNoErrorf(t, err, "ConnectAll()") defer zk.Close() tests := []struct { name string createFlags int32 specifiedPath string giveDuration time.Duration wantErr string wantErrIs error }{ { name: "valid create ttl", specifiedPath: "/test-valid-create-ttl", createFlags: FlagTTL, giveDuration: time.Minute, }, { name: "valid change detector", specifiedPath: "/test-valid-change", createFlags: 5, giveDuration: time.Minute, }, { name: "invalid path", createFlags: FlagTTL, specifiedPath: "not/valid", wantErr: "zk: invalid path", wantErrIs: ErrInvalidPath, }, { name: "invalid container with ttl", specifiedPath: "/test-invalid-flags", createFlags: FlagContainer, wantErr: "zk: invalid flags specified", wantErrIs: ErrInvalidFlags, }, { name: "invalid flag for create mode", specifiedPath: "/test-invalid-mode", createFlags: 999, giveDuration: time.Minute, wantErr: "invalid flag value: [999]", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.specifiedPath == "" { t.Fatalf("path for test required: %v", tt.name) } _, err := zk.CreateTTL(tt.specifiedPath, []byte{12}, tt.createFlags, WorldACL(PermAll), tt.giveDuration) if tt.wantErr == "" { requireNoErrorf(t, err, fmt.Sprintf("error not expected: path; %q; flags %v", tt.specifiedPath, tt.createFlags)) return } // want an error if tt.wantErrIs != nil { if !errors.Is(err, tt.wantErrIs) { t.Errorf("error expected Is: %q", tt.wantErr) } } if err == nil { t.Errorf("did not get expected error: %q", tt.wantErr) } if !strings.Contains(err.Error(), tt.wantErr) { t.Errorf("wanted error not found: %v; got: %v", tt.wantErr, err.Error()) } }) } } // NOTE: Currently there is not a way to get the znode after creating and // asserting it is once mode or another. This means these tests are only testing the // path of creation, but is not asserting that the resulting znode is the // mode we set with flags. func TestIntegration_CreateContainer(t *testing.T) { ts, err := StartTestCluster(t, 1, nil, logWriter{t: t, p: "[ZKERR] "}) if err != nil { t.Fatal(err) } defer ts.Stop() zk, _, err := ts.ConnectAll() if err != nil { t.Fatalf("Connect returned error: %+v", err) } defer zk.Close() tests := []struct { name string createFlags int32 specifiedPath string wantErr string }{ { name: "valid create container", createFlags: FlagContainer, }, { name: "valid create container hard coded flag int", createFlags: 4, // container flag, ensure matches ZK Create Mode (change detector test) }, { name: "invalid path", createFlags: FlagContainer, specifiedPath: "not/valid", wantErr: "zk: invalid path", }, { name: "invalid create mode", createFlags: 999, wantErr: "invalid flag value: [999]", }, { name: "invalid containers cannot be persistant", createFlags: FlagPersistent, wantErr: ErrInvalidFlags.Error(), }, { name: "invalid containers cannot be ephemeral", createFlags: FlagEphemeral, wantErr: ErrInvalidFlags.Error(), }, { name: "invalid containers cannot be sequential", createFlags: FlagSequence, wantErr: ErrInvalidFlags.Error(), }, { name: "invalid container and sequential", createFlags: FlagContainer | FlagSequence, wantErr: ErrInvalidFlags.Error(), }, { name: "invliad TTLs cannot be used with Container znodes", createFlags: FlagTTL, wantErr: ErrInvalidFlags.Error(), }, } const testPath = "/container_test_znode" // create sub node to create per test in avoiding using the root path. _, err = zk.Create(testPath, nil /* data */, FlagPersistent, WorldACL(PermAll)) requireNoErrorf(t, err) for idx, tt := range tests { t.Run(tt.name, func(t *testing.T) { path := filepath.Join(testPath, fmt.Sprint(idx)) if tt.specifiedPath != "" { path = tt.specifiedPath } _, err := zk.CreateContainer(path, []byte{12}, tt.createFlags, WorldACL(PermAll)) if tt.wantErr == "" { requireNoErrorf(t, err, fmt.Sprintf("error not expected: path; %q; flags %v", path, tt.createFlags)) return } // want an error if err == nil { t.Fatalf("did not get expected error: %q", tt.wantErr) } if !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("wanted error not found: %v; got: %v", tt.wantErr, err.Error()) } }) } } func TestIntegration_IncrementalReconfig(t *testing.T) { requireMinimumZkVersion(t, "3.5") ts, err := StartTestCluster(t, 3, nil, logWriter{t: t, p: "[ZKERR] "}) requireNoErrorf(t, err, "failed to setup test cluster") defer ts.Stop() // start and add a new server. tmpPath := t.TempDir() startPort := int(rand.Int31n(6000) + 10000) srvPath := filepath.Join(tmpPath, fmt.Sprintf("srv4")) if err := os.Mkdir(srvPath, 0700); err != nil { requireNoErrorf(t, err, "failed to make server path") } testSrvConfig := ServerConfigServer{ ID: 4, Host: "127.0.0.1", PeerPort: startPort + 1, LeaderElectionPort: startPort + 2, } cfg := ServerConfig{ ClientPort: startPort, DataDir: srvPath, Servers: []ServerConfigServer{testSrvConfig}, } // TODO: clean all this server creating up to a better helper method cfgPath := filepath.Join(srvPath, _testConfigName) fi, err := os.Create(cfgPath) requireNoErrorf(t, err) requireNoErrorf(t, cfg.Marshall(fi)) fi.Close() fi, err = os.Create(filepath.Join(srvPath, _testMyIDFileName)) requireNoErrorf(t, err) _, err = fmt.Fprintln(fi, "4") fi.Close() requireNoErrorf(t, err) testServer, err := NewIntegrationTestServer(t, cfgPath, nil, nil) requireNoErrorf(t, err) requireNoErrorf(t, testServer.Start()) defer testServer.Stop() zk, events, err := ts.ConnectAll() requireNoErrorf(t, err, "failed to connect to cluster") defer zk.Close() err = zk.AddAuth("digest", []byte("super:test")) requireNoErrorf(t, err, "failed to auth to cluster") waitCtx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() err = waitForSession(waitCtx, events) requireNoErrorf(t, err, "failed to wail for session") _, _, err = zk.Get("/zookeeper/config") if err != nil { t.Fatalf("get config returned error: %+v", err) } // initially should be 1<<32, which is 0x100000000. This is the zxid // of the first NEWLEADER message, used as the inital version // reflect.DeepEqual(bytes.Split(data, []byte("\n")), []byte("version=100000000")) // remove node 3. _, err = zk.IncrementalReconfig(nil, []string{"3"}, -1) if err != nil && err == ErrConnectionClosed { t.Log("conneciton closed is fine since the cluster re-elects and we dont reconnect") } else { requireNoErrorf(t, err, "failed to remove node from cluster") } // add node a new 4th node server := fmt.Sprintf("server.%d=%s:%d:%d;%d", testSrvConfig.ID, testSrvConfig.Host, testSrvConfig.PeerPort, testSrvConfig.LeaderElectionPort, cfg.ClientPort) _, err = zk.IncrementalReconfig([]string{server}, nil, -1) if err != nil && err == ErrConnectionClosed { t.Log("conneciton closed is fine since the cluster re-elects and we dont reconnect") } else { requireNoErrorf(t, err, "failed to add new server to cluster") } } func TestIntegration_Reconfig(t *testing.T) { requireMinimumZkVersion(t, "3.5") // This test enures we can do an non-incremental reconfig ts, err := StartTestCluster(t, 3, nil, logWriter{t: t, p: "[ZKERR] "}) requireNoErrorf(t, err, "failed to setup test cluster") defer ts.Stop() zk, events, err := ts.ConnectAll() requireNoErrorf(t, err, "failed to connect to cluster") defer zk.Close() err = zk.AddAuth("digest", []byte("super:test")) requireNoErrorf(t, err, "failed to auth to cluster") waitCtx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() err = waitForSession(waitCtx, events) requireNoErrorf(t, err, "failed to wail for session") _, _, err = zk.Get("/zookeeper/config") if err != nil { t.Fatalf("get config returned error: %+v", err) } // essentially remove the first node var s []string for _, host := range ts.Config.Servers[1:] { s = append(s, fmt.Sprintf("server.%d=%s:%d:%d;%d\n", host.ID, host.Host, host.PeerPort, host.LeaderElectionPort, ts.Config.ClientPort)) } _, err = zk.Reconfig(s, -1) requireNoErrorf(t, err, "failed to reconfig cluster") // reconfig to all the hosts again s = []string{} for _, host := range ts.Config.Servers { s = append(s, fmt.Sprintf("server.%d=%s:%d:%d;%d\n", host.ID, host.Host, host.PeerPort, host.LeaderElectionPort, ts.Config.ClientPort)) } _, err = zk.Reconfig(s, -1) requireNoErrorf(t, err, "failed to reconfig cluster") } func TestIntegration_OpsAfterCloseDontDeadlock(t *testing.T) { ts, err := StartTestCluster(t, 1, nil, logWriter{t: t, p: "[ZKERR] "}) if err != nil { t.Fatal(err) } defer ts.Stop() zk, _, err := ts.ConnectAll() if err != nil { t.Fatalf("Connect returned error: %+v", err) } zk.Close() path := "/gozk-test" ch := make(chan struct{}) go func() { defer close(ch) for range make([]struct{}, 30) { if _, err := zk.Create(path, []byte{1, 2, 3, 4}, 0, WorldACL(PermAll)); err == nil { t.Fatal("Create did not return error") } } }() select { case <-ch: // expected case <-time.After(10 * time.Second): t.Fatal("ZK connection deadlocked when executing ops after a Close operation") } } func TestIntegration_Multi(t *testing.T) { ts, err := StartTestCluster(t, 1, nil, logWriter{t: t, p: "[ZKERR] "}) if err != nil { t.Fatal(err) } defer ts.Stop() zk, _, err := ts.ConnectAll() if err != nil { t.Fatalf("Connect returned error: %+v", err) } defer zk.Close() path := "/gozk-test" if err := zk.Delete(path, -1); err != nil && err != ErrNoNode { t.Fatalf("Delete returned error: %+v", err) } ops := []interface{}{ &CreateRequest{Path: path, Data: []byte{1, 2, 3, 4}, Acl: WorldACL(PermAll)}, &SetDataRequest{Path: path, Data: []byte{1, 2, 3, 4}, Version: -1}, } if res, err := zk.Multi(ops...); err != nil { t.Fatalf("Multi returned error: %+v", err) } else if len(res) != 2 { t.Fatalf("Expected 2 responses got %d", len(res)) } else { t.Logf("%+v", res) } if data, stat, err := zk.Get(path); err != nil { t.Fatalf("Get returned error: %+v", err) } else if stat == nil { t.Fatal("Get returned nil stat") } else if len(data) < 4 { t.Fatal("Get returned wrong size data") } } func TestIntegration_IfAuthdataSurvivesReconnect(t *testing.T) { // This test case ensures authentication data is being resubmited after // reconnect. testNode := "/auth-testnode" ts, err := StartTestCluster(t, 1, nil, logWriter{t: t, p: "[ZKERR] "}) if err != nil { t.Fatal(err) } defer ts.Stop() zk, _, err := ts.ConnectAll() if err != nil { t.Fatalf("Connect returned error: %+v", err) } defer zk.Close() acl := DigestACL(PermAll, "userfoo", "passbar") _, err = zk.Create(testNode, []byte("Some very secret content"), 0, acl) if err != nil && err != ErrNodeExists { t.Fatalf("Failed to create test node : %+v", err) } _, _, err = zk.Get(testNode) if err == nil || err != ErrNoAuth { var msg string if err == nil { msg = "Fetching data without auth should have resulted in an error" } else { msg = fmt.Sprintf("Expecting ErrNoAuth, got `%+v` instead", err) } t.Fatalf(msg) } zk.AddAuth("digest", []byte("userfoo:passbar")) _, _, err = zk.Get(testNode) if err != nil { t.Fatalf("Fetching data with auth failed: %+v", err) } ts.StopAllServers() ts.StartAllServers() _, _, err = zk.Get(testNode) if err != nil { t.Fatalf("Fetching data after reconnect failed: %+v", err) } } func TestIntegration_MultiFailures(t *testing.T) { // This test case ensures that we return the errors associated with each // opeThis in the event a call to Multi() fails. const firstPath = "/gozk-test-first" const secondPath = "/gozk-test-second" ts, err := StartTestCluster(t, 1, nil, logWriter{t: t, p: "[ZKERR] "}) if err != nil { t.Fatal(err) } defer ts.Stop() zk, _, err := ts.ConnectAll() if err != nil { t.Fatalf("Connect returned error: %+v", err) } defer zk.Close() // Ensure firstPath doesn't exist and secondPath does. This will cause the // 2nd operation in the Multi() to fail. if err := zk.Delete(firstPath, -1); err != nil && err != ErrNoNode { t.Fatalf("Delete returned error: %+v", err) } if _, err := zk.Create(secondPath, nil /* data */, 0, WorldACL(PermAll)); err != nil { t.Fatalf("Create returned error: %+v", err) } ops := []interface{}{ &CreateRequest{Path: firstPath, Data: []byte{1, 2}, Acl: WorldACL(PermAll)}, &CreateRequest{Path: secondPath, Data: []byte{3, 4}, Acl: WorldACL(PermAll)}, } res, err := zk.Multi(ops...) if err != ErrNodeExists { t.Fatalf("Multi() didn't return correct error: %+v", err) } if len(res) != 2 { t.Fatalf("Expected 2 responses received %d", len(res)) } if res[0].Error != nil { t.Fatalf("First operation returned an unexpected error %+v", res[0].Error) } if res[1].Error != ErrNodeExists { t.Fatalf("Second operation returned incorrect error %+v", res[1].Error) } if _, _, err := zk.Get(firstPath); err != ErrNoNode { t.Fatalf("Node %s was incorrectly created: %+v", firstPath, err) } } func TestIntegration_GetSetACL(t *testing.T) { ts, err := StartTestCluster(t, 1, nil, logWriter{t: t, p: "[ZKERR] "}) if err != nil { t.Fatal(err) } defer ts.Stop() zk, _, err := ts.ConnectAll() if err != nil { t.Fatalf("Connect returned error: %+v", err) } defer zk.Close() if err := zk.AddAuth("digest", []byte("blah")); err != nil { t.Fatalf("AddAuth returned error %+v", err) } path := "/gozk-test" if err := zk.Delete(path, -1); err != nil && err != ErrNoNode { t.Fatalf("Delete returned error: %+v", err) } if path, err := zk.Create(path, []byte{1, 2, 3, 4}, 0, WorldACL(PermAll)); err != nil { t.Fatalf("Create returned error: %+v", err) } else if path != "/gozk-test" { t.Fatalf("Create returned different path '%s' != '/gozk-test'", path) } expected := WorldACL(PermAll) if acl, stat, err := zk.GetACL(path); err != nil { t.Fatalf("GetACL returned error %+v", err) } else if stat == nil { t.Fatalf("GetACL returned nil Stat") } else if len(acl) != 1 || expected[0] != acl[0] { t.Fatalf("GetACL mismatch expected %+v instead of %+v", expected, acl) } expected = []ACL{{PermAll, "ip", "127.0.0.1"}} if stat, err := zk.SetACL(path, expected, -1); err != nil { t.Fatalf("SetACL returned error %+v", err) } else if stat == nil { t.Fatalf("SetACL returned nil Stat") } if acl, stat, err := zk.GetACL(path); err != nil { t.Fatalf("GetACL returned error %+v", err) } else if stat == nil { t.Fatalf("GetACL returned nil Stat") } else if len(acl) != 1 || expected[0] != acl[0] { t.Fatalf("GetACL mismatch expected %+v instead of %+v", expected, acl) } } func TestIntegration_Auth(t *testing.T) { ts, err := StartTestCluster(t, 1, nil, logWriter{t: t, p: "[ZKERR] "}) if err != nil { t.Fatal(err) } defer ts.Stop() zk, _, err := ts.ConnectAll() if err != nil { t.Fatalf("Connect returned error: %+v", err) } defer zk.Close() path := "/gozk-digest-test" if err := zk.Delete(path, -1); err != nil && err != ErrNoNode { t.Fatalf("Delete returned error: %+v", err) } acl := DigestACL(PermAll, "user", "password") if p, err := zk.Create(path, []byte{1, 2, 3, 4}, 0, acl); err != nil { t.Fatalf("Create returned error: %+v", err) } else if p != path { t.Fatalf("Create returned different path '%s' != '%s'", p, path) } if _, _, err := zk.Get(path); err != ErrNoAuth { t.Fatalf("Get returned error %+v instead of ErrNoAuth", err) } if err := zk.AddAuth("digest", []byte("user:password")); err != nil { t.Fatalf("AddAuth returned error %+v", err) } if a, stat, err := zk.GetACL(path); err != nil { t.Fatalf("GetACL returned error %+v", err) } else if stat == nil { t.Fatalf("GetACL returned nil Stat") } else if len(a) != 1 || acl[0] != a[0] { t.Fatalf("GetACL mismatch expected %+v instead of %+v", acl, a) } if data, stat, err := zk.Get(path); err != nil { t.Fatalf("Get returned error %+v", err) } else if stat == nil { t.Fatalf("Get returned nil Stat") } else if len(data) != 4 { t.Fatalf("Get returned wrong data length") } } func TestIntegration_Children(t *testing.T) { ts, err := StartTestCluster(t, 1, nil, logWriter{t: t, p: "[ZKERR] "}) if err != nil { t.Fatal(err) } defer ts.Stop() zk, _, err := ts.ConnectAll() if err != nil { t.Fatalf("Connect returned error: %+v", err) } defer zk.Close() deleteNode := func(node string) { if err := zk.Delete(node, -1); err != nil && err != ErrNoNode { t.Fatalf("Delete returned error: %+v", err) } } deleteNode("/gozk-test-big") if path, err := zk.Create("/gozk-test-big", []byte{1, 2, 3, 4}, 0, WorldACL(PermAll)); err != nil { t.Fatalf("Create returned error: %+v", err) } else if path != "/gozk-test-big" { t.Fatalf("Create returned different path '%s' != '/gozk-test-big'", path) } rb := make([]byte, 1000) hb := make([]byte, 2000) prefix := []byte("/gozk-test-big/") for i := 0; i < 10000; i++ { _, err := rand.Read(rb) if err != nil { t.Fatal("Cannot create random znode name") } hex.Encode(hb, rb) expect := string(append(prefix, hb...)) if path, err := zk.Create(expect, []byte{1, 2, 3, 4}, 0, WorldACL(PermAll)); err != nil { t.Fatalf("Create returned error: %+v", err) } else if path != expect { t.Fatalf("Create returned different path '%s' != '%s'", path, expect) } defer deleteNode(string(expect)) } children, _, err := zk.Children("/gozk-test-big") if err != nil { t.Fatalf("Children returned error: %+v", err) } else if len(children) != 10000 { t.Fatal("Children returned wrong number of nodes") } } func TestIntegration_ChildWatch(t *testing.T) { ts, err := StartTestCluster(t, 1, nil, logWriter{t: t, p: "[ZKERR] "}) if err != nil { t.Fatal(err) } defer ts.Stop() zk, _, err := ts.ConnectAll() if err != nil { t.Fatalf("Connect returned error: %+v", err) } defer zk.Close() if err := zk.Delete("/gozk-test", -1); err != nil && err != ErrNoNode { t.Fatalf("Delete returned error: %+v", err) } children, stat, childCh, err := zk.ChildrenW("/") if err != nil { t.Fatalf("Children returned error: %+v", err) } else if stat == nil { t.Fatal("Children returned nil stat") } else if len(children) < 1 { t.Fatal("Children should return at least 1 child") } if path, err := zk.Create("/gozk-test", []byte{1, 2, 3, 4}, 0, WorldACL(PermAll)); err != nil { t.Fatalf("Create returned error: %+v", err) } else if path != "/gozk-test" { t.Fatalf("Create returned different path '%s' != '/gozk-test'", path) } select { case ev := <-childCh: if ev.Err != nil { t.Fatalf("Child watcher error %+v", ev.Err) } if ev.Path != "/" { t.Fatalf("Child watcher wrong path %s instead of %s", ev.Path, "/") } case _ = <-time.After(time.Second * 2): t.Fatal("Child watcher timed out") } // Delete of the watched node should trigger the watch children, stat, childCh, err = zk.ChildrenW("/gozk-test") if err != nil { t.Fatalf("Children returned error: %+v", err) } else if stat == nil { t.Fatal("Children returned nil stat") } else if len(children) != 0 { t.Fatal("Children should return 0 children") } if err := zk.Delete("/gozk-test", -1); err != nil && err != ErrNoNode { t.Fatalf("Delete returned error: %+v", err) } select { case ev := <-childCh: if ev.Err != nil { t.Fatalf("Child watcher error %+v", ev.Err) } if ev.Path != "/gozk-test" { t.Fatalf("Child watcher wrong path %s instead of %s", ev.Path, "/") } case _ = <-time.After(time.Second * 2): t.Fatal("Child watcher timed out") } } func TestIntegration_SetWatchers(t *testing.T) { ts, err := StartTestCluster(t, 1, nil, logWriter{t: t, p: "[ZKERR] "}) if err != nil { t.Fatal(err) } defer ts.Stop() zk, _, err := ts.ConnectAll() if err != nil { t.Fatalf("Connect returned error: %+v", err) } defer zk.Close() zk.reconnectLatch = make(chan struct{}) zk.setWatchLimit = 1024 // break up set-watch step into 1k requests var setWatchReqs atomic.Value zk.setWatchCallback = func(reqs []*setWatchesRequest) { setWatchReqs.Store(reqs) } zk2, _, err := ts.ConnectAll() if err != nil { t.Fatalf("Connect returned error: %+v", err) } defer zk2.Close() if err := zk.Delete("/gozk-test", -1); err != nil && err != ErrNoNode { t.Fatalf("Delete returned error: %+v", err) } testPaths := map[string]<-chan Event{} defer func() { // clean up all of the test paths we create for p := range testPaths { zk2.Delete(p, -1) } }() // we create lots of paths to watch, to make sure a "set watches" request // on re-create will be too big and be required to span multiple packets for i := 0; i < 1000; i++ { testPath, err := zk.Create(fmt.Sprintf("/gozk-test-%d", i), []byte{}, 0, WorldACL(PermAll)) if err != nil { t.Fatalf("Create returned: %+v", err) } testPaths[testPath] = nil _, _, testEvCh, err := zk.GetW(testPath) if err != nil { t.Fatalf("GetW returned: %+v", err) } testPaths[testPath] = testEvCh } children, stat, childCh, err := zk.ChildrenW("/") if err != nil { t.Fatalf("Children returned error: %+v", err) } else if stat == nil { t.Fatal("Children returned nil stat") } else if len(children) < 1 { t.Fatal("Children should return at least 1 child") } // Simulate network error by brutally closing the network connection. zk.conn.Close() for p := range testPaths { if err := zk2.Delete(p, -1); err != nil && err != ErrNoNode { t.Fatalf("Delete returned error: %+v", err) } } if path, err := zk2.Create("/gozk-test", []byte{1, 2, 3, 4}, 0, WorldACL(PermAll)); err != nil { t.Fatalf("Create returned error: %+v", err) } else if path != "/gozk-test" { t.Fatalf("Create returned different path '%s' != '/gozk-test'", path) } time.Sleep(100 * time.Millisecond) // zk should still be waiting to reconnect, so none of the watches should have been triggered for p, ch := range testPaths { select { case <-ch: t.Fatalf("GetW watcher for %q should not have triggered yet", p) default: } } select { case <-childCh: t.Fatalf("ChildrenW watcher should not have triggered yet") default: } // now we let the reconnect occur and make sure it resets watches close(zk.reconnectLatch) for p, ch := range testPaths { select { case ev := <-ch: if ev.Err != nil { t.Fatalf("GetW watcher error %+v", ev.Err) } if ev.Path != p { t.Fatalf("GetW watcher wrong path %s instead of %s", ev.Path, p) } case <-time.After(2 * time.Second): t.Fatal("GetW watcher timed out") } } select { case ev := <-childCh: if ev.Err != nil { t.Fatalf("Child watcher error %+v", ev.Err) } if ev.Path != "/" { t.Fatalf("Child watcher wrong path %s instead of %s", ev.Path, "/") } case <-time.After(2 * time.Second): t.Fatal("Child watcher timed out") } // Yay! All watches fired correctly. Now we also inspect the actual set-watch request objects // to ensure they didn't exceed the expected packet set. buf := make([]byte, bufferSize) totalWatches := 0 actualReqs := setWatchReqs.Load().([]*setWatchesRequest) if len(actualReqs) < 12 { // sanity check: we should have generated *at least* 12 requests to reset watches t.Fatalf("too few setWatchesRequest messages: %d", len(actualReqs)) } for _, r := range actualReqs { totalWatches += len(r.ChildWatches) + len(r.DataWatches) + len(r.ExistWatches) n, err := encodePacket(buf, r) if err != nil { t.Fatalf("encodePacket failed: %v! request:\n%+v", err, r) } else if n > 1024 { t.Fatalf("setWatchesRequest exceeded allowed size (%d > 1024)! request:\n%+v", n, r) } } if totalWatches != len(testPaths)+1 { t.Fatalf("setWatchesRequests did not include all expected watches; expecting %d, got %d", len(testPaths)+1, totalWatches) } } func TestIntegration_ExpiringWatch(t *testing.T) { ts, err := StartTestCluster(t, 1, nil, logWriter{t: t, p: "[ZKERR] "}) if err != nil { t.Fatal(err) } defer ts.Stop() zk, _, err := ts.ConnectAll() if err != nil { t.Fatalf("Connect returned error: %+v", err) } defer zk.Close() if err := zk.Delete("/gozk-test", -1); err != nil && err != ErrNoNode { t.Fatalf("Delete returned error: %+v", err) } children, stat, childCh, err := zk.ChildrenW("/") if err != nil { t.Fatalf("Children returned error: %+v", err) } else if stat == nil { t.Fatal("Children returned nil stat") } else if len(children) < 1 { t.Fatal("Children should return at least 1 child") } zk.sessionID = 99999 zk.conn.Close() select { case ev := <-childCh: if ev.Err != ErrSessionExpired { t.Fatalf("Child watcher error %+v instead of expected ErrSessionExpired", ev.Err) } if ev.Path != "/" { t.Fatalf("Child watcher wrong path %s instead of %s", ev.Path, "/") } case <-time.After(2 * time.Second): t.Fatal("Child watcher timed out") } } func TestRequestFail(t *testing.T) { // If connecting fails to all servers in the list then pending requests // should be errored out so they don't hang forever. zk, _, err := Connect([]string{"127.0.0.1:32444"}, time.Second*15) if err != nil { t.Fatal(err) } defer zk.Close() ch := make(chan error) go func() { _, _, err := zk.Get("/blah") ch <- err }() select { case err := <-ch: if err == nil { t.Fatal("Expected non-nil error on failed request due to connection failure") } case <-time.After(time.Second * 2): t.Fatal("Get hung when connection could not be made") } } func TestIdempotentClose(t *testing.T) { zk, _, err := Connect([]string{"127.0.0.1:32444"}, time.Second*15) if err != nil { t.Fatal(err) } // multiple calls to Close() should not panic zk.Close() zk.Close() } func TestIntegration_SlowServer(t *testing.T) { ts, err := StartTestCluster(t, 1, nil, logWriter{t: t, p: "[ZKERR] "}) if err != nil { t.Fatal(err) } defer ts.Stop() realAddr := fmt.Sprintf("127.0.0.1:%d", ts.Servers[0].Port) proxyAddr, stopCh, err := startSlowProxy(t, Rate{}, Rate{}, realAddr, func(ln *Listener) { if ln.Up.Latency == 0 { ln.Up.Latency = time.Millisecond * 2000 ln.Down.Latency = time.Millisecond * 2000 } else { ln.Up.Latency = 0 ln.Down.Latency = 0 } }) if err != nil { t.Fatal(err) } defer close(stopCh) zk, _, err := Connect([]string{proxyAddr}, time.Millisecond*500) if err != nil { t.Fatal(err) } defer zk.Close() _, _, wch, err := zk.ChildrenW("/") if err != nil { t.Fatal(err) } // Force a reconnect to get a throttled connection zk.conn.Close() time.Sleep(time.Millisecond * 100) if err := zk.Delete("/gozk-test", -1); err == nil { t.Fatal("Delete should have failed") } // The previous request should have timed out causing the server to be disconnected and reconnected if _, err := zk.Create("/gozk-test", []byte{1, 2, 3, 4}, 0, WorldACL(PermAll)); err != nil { t.Fatal(err) } // Make sure event is still returned because the session should not have been affected select { case ev := <-wch: t.Logf("Received event: %+v", ev) case <-time.After(time.Second): t.Fatal("Expected to receive a watch event") } } func TestIntegration_MaxBufferSize(t *testing.T) { ts, err := StartTestCluster(t, 1, nil, logWriter{t: t, p: "[ZKERR] "}) if err != nil { t.Fatal(err) } defer ts.Stop() // no buffer size zk, _, err := ts.ConnectWithOptions(15 * time.Second) var l testLogger if err != nil { t.Fatalf("Connect returned error: %+v", err) } defer zk.Close() // 1k buffer size, logs to custom test logger zkLimited, _, err := ts.ConnectWithOptions(15*time.Second, WithMaxBufferSize(1024), func(conn *Conn) { conn.SetLogger(&l) }) if err != nil { t.Fatalf("Connect returned error: %+v", err) } defer zkLimited.Close() // With small node with small number of children data := []byte{101, 102, 103, 103} _, err = zk.Create("/foo", data, 0, WorldACL(PermAll)) if err != nil { t.Fatalf("Create returned error: %+v", err) } var children []string for i := 0; i < 4; i++ { childName, err := zk.Create("/foo/child", nil, FlagEphemeral|FlagSequence, WorldACL(PermAll)) if err != nil { t.Fatalf("Create returned error: %+v", err) } children = append(children, childName[len("/foo/"):]) // strip parent prefix from name } sort.Strings(children) // Limited client works fine resultData, _, err := zkLimited.Get("/foo") if err != nil { t.Fatalf("Get returned error: %+v", err) } if !reflect.DeepEqual(resultData, data) { t.Fatalf("Get returned unexpected data; expecting %+v, got %+v", data, resultData) } resultChildren, _, err := zkLimited.Children("/foo") if err != nil { t.Fatalf("Children returned error: %+v", err) } sort.Strings(resultChildren) if !reflect.DeepEqual(resultChildren, children) { t.Fatalf("Children returned unexpected names; expecting %+v, got %+v", children, resultChildren) } // With large node though... data = make([]byte, 1024) for i := 0; i < 1024; i++ { data[i] = byte(i) } _, err = zk.Create("/bar", data, 0, WorldACL(PermAll)) if err != nil { t.Fatalf("Create returned error: %+v", err) } _, _, err = zkLimited.Get("/bar") // NB: Sadly, without actually de-serializing the too-large response packet, we can't send the // right error to the corresponding outstanding request. So the request just sees ErrConnectionClosed // while the log will see the actual reason the connection was closed. expectErr(t, err, ErrConnectionClosed) expectLogMessage(t, &l, "received packet from server with length .*, which exceeds max buffer size 1024") // Or with large number of children... totalLen := 0 children = nil for totalLen < 1024 { childName, err := zk.Create("/bar/child", nil, FlagEphemeral|FlagSequence, WorldACL(PermAll)) if err != nil { t.Fatalf("Create returned error: %+v", err) } n := childName[len("/bar/"):] // strip parent prefix from name children = append(children, n) totalLen += len(n) } sort.Strings(children) _, _, err = zkLimited.Children("/bar") expectErr(t, err, ErrConnectionClosed) expectLogMessage(t, &l, "received packet from server with length .*, which exceeds max buffer size 1024") // Other client (without buffer size limit) can successfully query the node and its children, of course resultData, _, err = zk.Get("/bar") if err != nil { t.Fatalf("Get returned error: %+v", err) } if !reflect.DeepEqual(resultData, data) { t.Fatalf("Get returned unexpected data; expecting %+v, got %+v", data, resultData) } resultChildren, _, err = zk.Children("/bar") if err != nil { t.Fatalf("Children returned error: %+v", err) } sort.Strings(resultChildren) if !reflect.DeepEqual(resultChildren, children) { t.Fatalf("Children returned unexpected names; expecting %+v, got %+v", children, resultChildren) } } func startSlowProxy(t *testing.T, up, down Rate, upstream string, adj func(ln *Listener)) (string, chan bool, error) { ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return "", nil, err } tln := &Listener{ Listener: ln, Up: up, Down: down, } stopCh := make(chan bool) go func() { <-stopCh tln.Close() }() go func() { for { cn, err := tln.Accept() if err != nil { if !strings.Contains(err.Error(), "use of closed network connection") { t.Fatalf("Accept failed: %s", err.Error()) } return } if adj != nil { adj(tln) } go func(cn net.Conn) { defer cn.Close() upcn, err := net.Dial("tcp", upstream) if err != nil { t.Log(err) return } // This will leave hanging goroutines util stopCh is closed // but it doesn't matter in the context of running tests. go func() { <-stopCh upcn.Close() }() go func() { if _, err := io.Copy(upcn, cn); err != nil { if !strings.Contains(err.Error(), "use of closed network connection") { // log.Printf("Upstream write failed: %s", err.Error()) } } }() if _, err := io.Copy(cn, upcn); err != nil { if !strings.Contains(err.Error(), "use of closed network connection") { // log.Printf("Upstream read failed: %s", err.Error()) } } }(cn) } }() return ln.Addr().String(), stopCh, nil } func expectErr(t *testing.T, err error, expected error) { if err == nil { t.Fatalf("Get for node that is too large should have returned error!") } if err != expected { t.Fatalf("Get returned wrong error; expecting ErrClosing, got %+v", err) } } func expectLogMessage(t *testing.T, logger *testLogger, pattern string) { re := regexp.MustCompile(pattern) events := logger.Reset() if len(events) == 0 { t.Fatalf("Failed to log error; expecting message that matches pattern: %s", pattern) } var found []string for _, e := range events { if re.Match([]byte(e)) { found = append(found, e) } } if len(found) == 0 { t.Fatalf("Failed to log error; expecting message that matches pattern: %s", pattern) } else if len(found) > 1 { t.Fatalf("Logged error redundantly %d times:\n%+v", len(found), found) } } func requireMinimumZkVersion(t *testing.T, minimum string) { t.Helper() actualVersionStr, ok := os.LookupEnv("ZK_VERSION") if !ok { t.Skip("did not detect ZK_VERSION from env. skipping test") } parseFn := func(v string) (parts []int) { for _, s := range strings.Split(v, ".") { i, err := strconv.Atoi(s) if err != nil { t.Fatalf("invalid version segment: %q", s) } parts = append(parts, i) } return parts } minimumV, actualV := parseFn(minimum), parseFn(actualVersionStr) for i, p := range minimumV { if len(actualV) <= i { break } if actualV[i] < p { t.Skipf("zookeeper required min version not met; requires: %q, actual: %q", minimum, actualVersionStr) } } } type testLogger struct { mu sync.Mutex events []string } func (l *testLogger) Printf(msgFormat string, args ...interface{}) { msg := fmt.Sprintf(msgFormat, args...) fmt.Println(msg) l.mu.Lock() defer l.mu.Unlock() l.events = append(l.events, msg) } func (l *testLogger) Reset() []string { l.mu.Lock() defer l.mu.Unlock() ret := l.events l.events = nil return ret }