From c7e0eb318d9afd1030a891126fcc32eda1de2156 Mon Sep 17 00:00:00 2001 From: Ewout Prangsma Date: Fri, 15 Jun 2018 08:16:52 +0200 Subject: [PATCH] Added duration test app --- Makefile | 28 + tests/duration/Dockerfile | 5 + tests/duration/README.md | 32 + tests/duration/main.go | 132 ++++ tests/duration/simple/check.go | 50 ++ tests/duration/simple/error.go | 31 + tests/duration/simple/simple.go | 689 +++++++++++++++++++ tests/duration/simple/simple_collection.go | 94 +++ tests/duration/simple/simple_create.go | 50 ++ tests/duration/simple/simple_import.go | 79 +++ tests/duration/simple/simple_query.go | 66 ++ tests/duration/simple/simple_query_update.go | 115 ++++ tests/duration/simple/simple_read.go | 88 +++ tests/duration/simple/simple_rebalance.go | 40 ++ tests/duration/simple/simple_remove.go | 71 ++ tests/duration/simple/simple_replace.go | 92 +++ tests/duration/simple/simple_update.go | 87 +++ tests/duration/test/shuffle.go | 43 ++ tests/duration/test/test.go | 66 ++ tests/duration/test_listener.go | 88 +++ tests/duration/test_loop.go | 124 ++++ 21 files changed, 2070 insertions(+) create mode 100644 tests/duration/Dockerfile create mode 100644 tests/duration/README.md create mode 100644 tests/duration/main.go create mode 100644 tests/duration/simple/check.go create mode 100644 tests/duration/simple/error.go create mode 100644 tests/duration/simple/simple.go create mode 100644 tests/duration/simple/simple_collection.go create mode 100644 tests/duration/simple/simple_create.go create mode 100644 tests/duration/simple/simple_import.go create mode 100644 tests/duration/simple/simple_query.go create mode 100644 tests/duration/simple/simple_query_update.go create mode 100644 tests/duration/simple/simple_read.go create mode 100644 tests/duration/simple/simple_rebalance.go create mode 100644 tests/duration/simple/simple_remove.go create mode 100644 tests/duration/simple/simple_replace.go create mode 100644 tests/duration/simple/simple_update.go create mode 100644 tests/duration/test/shuffle.go create mode 100644 tests/duration/test/test.go create mode 100644 tests/duration/test_listener.go create mode 100644 tests/duration/test_loop.go diff --git a/Makefile b/Makefile index bc39e4611..dbcb53c82 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,7 @@ PULSAR := $(GOBUILDDIR)/bin/pulsar$(shell go env GOEXE) DOCKERFILE := Dockerfile DOCKERTESTFILE := Dockerfile.test +DOCKERDURATIONTESTFILE := tests/duration/Dockerfile ifndef LOCALONLY PUSHIMAGES := 1 @@ -63,6 +64,9 @@ endif ifndef TESTIMAGE TESTIMAGE := $(DOCKERNAMESPACE)/kube-arangodb-test$(IMAGESUFFIX) endif +ifndef DURATIONTESTIMAGE + DURATIONTESTIMAGE := $(DOCKERNAMESPACE)/kube-arangodb-durationtest$(IMAGESUFFIX) +endif ifndef ENTERPRISEIMAGE ENTERPRISEIMAGE := $(DEFAULTENTERPRISEIMAGE) endif @@ -75,6 +79,8 @@ BINNAME := $(PROJECT) BIN := $(BINDIR)/$(BINNAME) TESTBINNAME := $(PROJECT)_test TESTBIN := $(BINDIR)/$(TESTBINNAME) +DURATIONTESTBINNAME := $(PROJECT)_duration_test +DURATIONTESTBIN := $(BINDIR)/$(DURATIONTESTBINNAME) RELEASE := $(GOBUILDDIR)/bin/release GHRELEASE := $(GOBUILDDIR)/bin/github-release @@ -278,6 +284,28 @@ endif $(ROOTDIR)/scripts/kube_create_storage.sh $(DEPLOYMENTNAMESPACE) $(ROOTDIR)/scripts/kube_run_tests.sh $(DEPLOYMENTNAMESPACE) $(TESTIMAGE) "$(ENTERPRISEIMAGE)" $(TESTTIMEOUT) $(TESTLENGTHOPTIONS) +$(DURATIONTESTBIN): $(GOBUILDDIR) $(SOURCES) + @mkdir -p $(BINDIR) + docker run \ + --rm \ + -v $(SRCDIR):/usr/code \ + -v $(CACHEVOL):/usr/gocache \ + -e GOCACHE=/usr/gocache \ + -e GOPATH=/usr/code/.gobuild \ + -e GOOS=linux \ + -e GOARCH=amd64 \ + -e CGO_ENABLED=0 \ + -w /usr/code/ \ + golang:$(GOVERSION) \ + go build -installsuffix cgo -ldflags "-X main.projectVersion=$(VERSION) -X main.projectBuild=$(COMMIT)" -o /usr/code/bin/$(DURATIONTESTBINNAME) $(REPOPATH)/tests/duration + +.PHONY: docker-duration-test +docker-duration-test: $(DURATIONTESTBIN) + docker build --quiet -f $(DOCKERDURATIONTESTFILE) -t $(DURATIONTESTIMAGE) . +ifdef PUSHIMAGES + docker push $(DURATIONTESTIMAGE) +endif + .PHONY: cleanup-tests cleanup-tests: ifneq ($(DEPLOYMENTNAMESPACE), default) diff --git a/tests/duration/Dockerfile b/tests/duration/Dockerfile new file mode 100644 index 000000000..b00043c4f --- /dev/null +++ b/tests/duration/Dockerfile @@ -0,0 +1,5 @@ +FROM scratch + +ADD bin/arangodb_operator_duration_test /usr/bin/ + +ENTRYPOINT [ "/usr/bin/arangodb_operator_duration_test" ] \ No newline at end of file diff --git a/tests/duration/README.md b/tests/duration/README.md new file mode 100644 index 000000000..79f9e1024 --- /dev/null +++ b/tests/duration/README.md @@ -0,0 +1,32 @@ +# Kube-ArangoDB duration test + +This test is a simple application that keeps accessing the database with various requests. + +## Building + +In root of kube-arangodb repository, run: + +```bash +make docker-duration-test +``` + +## Running + +Start an ArangoDB `Cluster` deployment. + +Run: + +```bash +kubectl run \ + --image=${DOCKERNAMESPACE}/kube-arangodb-durationtest:dev \ + --image-pull-policy=Always duration-test \ + -- \ + --cluster=https://..svc:8529 \ + --username=root +``` + +To remove the test, run: + +```bash +kubectl delete -n deployment/duration-test +``` diff --git a/tests/duration/main.go b/tests/duration/main.go new file mode 100644 index 000000000..2ddfc0b6f --- /dev/null +++ b/tests/duration/main.go @@ -0,0 +1,132 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package main + +import ( + "context" + "crypto/tls" + "flag" + "fmt" + "log" + "os" + "os/signal" + "strings" + "syscall" + "time" + + driver "github.com/arangodb/go-driver" + "github.com/arangodb/go-driver/http" + "github.com/pkg/errors" + + "github.com/arangodb/kube-arangodb/pkg/util/retry" +) + +const ( + defaultTestDuration = time.Hour * 24 * 7 // 7 days +) + +var ( + maskAny = errors.WithStack + userName string + password string + clusterEndpoints string + testDuration time.Duration +) + +func init() { + flag.StringVar(&userName, "username", "", "Authenticating username") + flag.StringVar(&password, "password", "", "Authenticating password") + flag.StringVar(&clusterEndpoints, "cluster", "", "Endpoints for database cluster") + flag.DurationVar(&testDuration, "duration", defaultTestDuration, "Duration of the test") +} + +func main() { + flag.Parse() + + // Create clients & wait for cluster available + client, err := createClusterClient(clusterEndpoints, userName, password) + if err != nil { + log.Fatalf("Failed to create cluster client: %#v\n", err) + } + if err := waitUntilClusterUp(client); err != nil { + log.Fatalf("Failed to reach cluster: %#v\n", err) + } + + // Start running tests + ctx, cancel := context.WithCancel(context.Background()) + sigChannel := make(chan os.Signal) + signal.Notify(sigChannel, os.Interrupt, syscall.SIGTERM) + go handleSignal(sigChannel, cancel) + runTestLoop(ctx, client, testDuration) +} + +// createClusterClient creates a configuration, connection and client for +// one of the two ArangoDB clusters in the test. It uses the go-driver. +// It needs a list of endpoints. +func createClusterClient(endpoints string, user string, password string) (driver.Client, error) { + // This will always use HTTP, and user and password authentication + config := http.ConnectionConfig{ + Endpoints: strings.Split(endpoints, ","), + TLSConfig: &tls.Config{InsecureSkipVerify: true}, + } + connection, err := http.NewConnection(config) + if err != nil { + return nil, maskAny(err) + } + clientCfg := driver.ClientConfig{ + Connection: connection, + Authentication: driver.BasicAuthentication(user, password), + } + client, err := driver.NewClient(clientCfg) + if err != nil { + return nil, maskAny(err) + } + return client, nil +} + +func waitUntilClusterUp(c driver.Client) error { + op := func() error { + ctx := context.Background() + if _, err := c.Version(ctx); err != nil { + return maskAny(err) + } + return nil + } + if err := retry.Retry(op, time.Minute); err != nil { + return maskAny(err) + } + return nil +} + +// handleSignal listens for termination signals and stops this process on termination. +func handleSignal(sigChannel chan os.Signal, cancel context.CancelFunc) { + signalCount := 0 + for s := range sigChannel { + signalCount++ + fmt.Println("Received signal:", s) + if signalCount > 1 { + os.Exit(1) + } + cancel() + } +} diff --git a/tests/duration/simple/check.go b/tests/duration/simple/check.go new file mode 100644 index 000000000..ea769eeef --- /dev/null +++ b/tests/duration/simple/check.go @@ -0,0 +1,50 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package simple + +import "context" + +// isDocumentEqualTo reads an existing document and checks that it is equal to the given document. +// Returns: (isEqual,currentRevision,error) +func (t *simpleTest) isDocumentEqualTo(c *collection, key string, expected UserDocument) (bool, string, error) { + ctx := context.Background() + var result UserDocument + t.log.Info().Msgf("Checking existing document '%s' from '%s'...", key, c.name) + col, err := t.db.Collection(ctx, c.name) + if err != nil { + return false, "", maskAny(err) + } + m, err := col.ReadDocument(ctx, key, &result) + if err != nil { + // This is a failure + t.log.Error().Msgf("Failed to read document '%s' from '%s': %v", key, c.name, err) + return false, "", maskAny(err) + } + // Compare document against expected document + if result.Equals(expected) { + // Found an exact match + return true, m.Rev, nil + } + t.log.Info().Msgf("Document '%s' in '%s' returned different values: got %q expected %q", key, c.name, result, expected) + return false, m.Rev, nil +} diff --git a/tests/duration/simple/error.go b/tests/duration/simple/error.go new file mode 100644 index 000000000..cf34f2807 --- /dev/null +++ b/tests/duration/simple/error.go @@ -0,0 +1,31 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package simple + +import ( + "github.com/pkg/errors" +) + +var ( + maskAny = errors.WithStack +) diff --git a/tests/duration/simple/simple.go b/tests/duration/simple/simple.go new file mode 100644 index 000000000..053ac6301 --- /dev/null +++ b/tests/duration/simple/simple.go @@ -0,0 +1,689 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package simple + +import ( + "context" + "fmt" + "io" + "math/rand" + "os" + "sort" + "sync" + "sync/atomic" + "time" + + driver "github.com/arangodb/go-driver" + "github.com/pkg/errors" + "github.com/rs/zerolog" + + "github.com/arangodb/kube-arangodb/tests/duration/test" +) + +type SimpleConfig struct { + MaxDocuments int + MaxCollections int +} + +const ( + initialDocumentCount = 999 +) + +type simpleTest struct { + SimpleConfig + activeMutex sync.Mutex + logPath string + reportDir string + log zerolog.Logger + listener test.TestListener + stop chan struct{} + active bool + pauseRequested bool + paused bool + client driver.Client + db driver.Database + failures int + actions int + collections map[string]*collection + collectionsMutex sync.Mutex + lastCollectionIndex int32 + readExistingCounter counter + readExistingWrongRevisionCounter counter + readNonExistingCounter counter + createCounter counter + createCollectionCounter counter + removeExistingCollectionCounter counter + updateExistingCounter counter + updateExistingWrongRevisionCounter counter + updateNonExistingCounter counter + replaceExistingCounter counter + replaceExistingWrongRevisionCounter counter + replaceNonExistingCounter counter + deleteExistingCounter counter + deleteExistingWrongRevisionCounter counter + deleteNonExistingCounter counter + importCounter counter + queryCreateCursorCounter counter + queryNextBatchCounter counter + queryNextBatchNewCoordinatorCounter counter + queryLongRunningCounter counter + rebalanceShardsCounter counter + queryUpdateCounter counter + queryUpdateLongRunningCounter counter +} + +type counter struct { + succeeded int + failed int +} + +type collection struct { + name string + existingDocs map[string]UserDocument +} + +// NewSimpleTest creates a simple test +func NewSimpleTest(log zerolog.Logger, reportDir string, config SimpleConfig) test.TestScript { + return &simpleTest{ + SimpleConfig: config, + reportDir: reportDir, + log: log, + collections: make(map[string]*collection), + } +} + +// Name returns the name of the script +func (t *simpleTest) Name() string { + return "simple" +} + +// Start triggers the test script to start. +// It should spwan actions in a go routine. +func (t *simpleTest) Start(client driver.Client, listener test.TestListener) error { + t.activeMutex.Lock() + defer t.activeMutex.Unlock() + + if t.active { + // No restart unless needed + return nil + } + + t.listener = listener + t.client = client + ctx := context.Background() + db, err := client.Database(ctx, "_system") + if err != nil { + return maskAny(err) + } + t.db = db + + // Cleanup of old data + for i := 1; i <= t.MaxCollections; i++ { + col, err := db.Collection(ctx, t.getCollectionName(i)) + if err == nil { + if err := col.Remove(ctx); err != nil { + return errors.Wrapf(err, "Failed to remove collection %s", col.Name()) + } + } else if !driver.IsNotFound(err) { + return maskAny(err) + } + } + + t.active = true + go t.testLoop() + return nil +} + +// Stop any running test. This should not return until tests are actually stopped. +func (t *simpleTest) Stop() error { + t.activeMutex.Lock() + defer t.activeMutex.Unlock() + + if !t.active { + // No active, nothing to stop + return nil + } + + stop := make(chan struct{}) + t.stop = stop + <-stop + return nil +} + +// Interrupt the tests, but be prepared to continue. +func (t *simpleTest) Pause() error { + t.pauseRequested = true + return nil +} + +// Resume running the tests, where Pause interrupted it. +func (t *simpleTest) Resume() error { + t.pauseRequested = false + return nil +} + +// Status returns the current status of the test +func (t *simpleTest) Status() test.TestStatus { + cc := func(name string, c counter) test.Counter { + return test.Counter{ + Name: name, + Succeeded: c.succeeded, + Failed: c.failed, + } + } + + status := test.TestStatus{ + Active: t.active && !t.paused, + Pausing: t.pauseRequested && t.paused, + Failures: t.failures, + Actions: t.actions, + Counters: []test.Counter{ + cc("#collections created", t.createCollectionCounter), + cc("#collections removed", t.removeExistingCollectionCounter), + cc("#documents created", t.createCounter), + cc("#existing documents read", t.readExistingCounter), + cc("#existing documents updated", t.updateExistingCounter), + cc("#existing documents replaced", t.replaceExistingCounter), + cc("#existing documents removed", t.deleteExistingCounter), + cc("#existing documents wrong revision read", t.readExistingWrongRevisionCounter), + cc("#existing documents wrong revision updated", t.updateExistingWrongRevisionCounter), + cc("#existing documents wrong revision replaced", t.replaceExistingWrongRevisionCounter), + cc("#existing documents wrong revision removed", t.deleteExistingWrongRevisionCounter), + cc("#non-existing documents read", t.readNonExistingCounter), + cc("#non-existing documents updated", t.updateNonExistingCounter), + cc("#non-existing documents replaced", t.replaceNonExistingCounter), + cc("#non-existing documents removed", t.deleteNonExistingCounter), + cc("#import operations", t.importCounter), + cc("#create AQL cursor operations", t.queryCreateCursorCounter), + cc("#fetch next AQL cursor batch operations", t.queryNextBatchCounter), + cc("#fetch next AQL cursor batch after coordinator change operations", t.queryNextBatchNewCoordinatorCounter), + cc("#long running AQL query operations", t.queryLongRunningCounter), + cc("#rebalance shards operations", t.rebalanceShardsCounter), + cc("#update AQL query operations", t.queryUpdateCounter), + cc("#long running update AQL query operations", t.queryUpdateLongRunningCounter), + }, + } + + t.collectionsMutex.Lock() + for _, c := range t.collections { + status.Messages = append(status.Messages, + fmt.Sprintf("Current #documents in %s: %d", c.name, len(c.existingDocs)), + ) + } + t.collectionsMutex.Unlock() + + return status +} + +// CollectLogs copies all logging info to the given writer. +func (t *simpleTest) CollectLogs(w io.Writer) error { + if logPath := t.logPath; logPath == "" { + // Nothing to log yet + return nil + } else { + rd, err := os.Open(logPath) + if err != nil { + return maskAny(err) + } + defer rd.Close() + if _, err := io.Copy(w, rd); err != nil { + return maskAny(err) + } + return nil + } +} + +func (t *simpleTest) shouldStop() bool { + // Should we stop? + if stop := t.stop; stop != nil { + stop <- struct{}{} + return true + } + return false +} + +type UserDocument struct { + Key string `json:"_key"` + rev string // Note that we do not export this field! + Value int `json:"value"` + Name string `json:"name"` + Odd bool `json:"odd"` +} + +// Equals returns true when the value fields of `d` and `other` are the equal. +func (d UserDocument) Equals(other UserDocument) bool { + return d.Value == other.Value && + d.Name == other.Name && + d.Odd == other.Odd +} + +func (t *simpleTest) reportFailure(f test.Failure) { + t.failures++ + t.listener.ReportFailure(f) +} + +func (t *simpleTest) testLoop() { + t.active = true + t.actions = 0 + defer func() { t.active = false }() + + if err := t.createAndInitCollection(); err != nil { + t.log.Error().Msgf("Failed to create&init first collection: %v. Giving up", err) + return + } + + var plan []int + planIndex := 0 + for { + // Should we stop + if t.shouldStop() { + return + } + if t.pauseRequested { + t.paused = true + time.Sleep(time.Second * 2) + continue + } + t.paused = false + t.actions++ + if plan == nil || planIndex >= len(plan) { + plan = createTestPlan(20) // Update when more tests are added + planIndex = 0 + } + + switch plan[planIndex] { + case 0: + // Create collection with initial data + if len(t.collections) < t.MaxCollections && rand.Intn(100)%2 == 0 { + if err := t.createAndInitCollection(); err != nil { + t.log.Error().Msgf("Failed to create&init collection: %v", err) + } + } + planIndex++ + + case 1: + // Remove an existing collection + if len(t.collections) > 1 && rand.Intn(100)%2 == 0 { + c := t.selectRandomCollection() + if err := t.removeExistingCollection(c); err != nil { + t.log.Error().Msgf("Failed to remove existing collection: %#v", err) + } + } + planIndex++ + + case 2: + // Create a random document + if len(t.collections) > 0 { + c := t.selectRandomCollection() + if len(c.existingDocs) < t.MaxDocuments { + userDoc := UserDocument{ + Key: c.createNewKey(true), + Value: rand.Int(), + Name: fmt.Sprintf("User %d", time.Now().Nanosecond()), + Odd: time.Now().Nanosecond()%2 == 1, + } + if rev, err := t.createDocument(c, userDoc, userDoc.Key); err != nil { + t.log.Error().Msgf("Failed to create document: %#v", err) + } else { + userDoc.rev = rev + c.existingDocs[userDoc.Key] = userDoc + + // Now try to read it, it must exist + //t.client.SetCoordinator("") + if _, err := t.readExistingDocument(c, userDoc.Key, rev, false, false); err != nil { + t.log.Error().Msgf("Failed to read just-created document '%s': %#v", userDoc.Key, err) + } + } + } + } + planIndex++ + + case 3: + // Read a random existing document + if len(t.collections) > 0 { + c := t.selectRandomCollection() + if len(c.existingDocs) > 0 { + randomKey, rev := c.selectRandomKey() + if _, err := t.readExistingDocument(c, randomKey, rev, false, false); err != nil { + t.log.Error().Msgf("Failed to read existing document '%s': %#v", randomKey, err) + } + } + } + planIndex++ + + case 4: + // Read a random existing document but with wrong revision + planIndex++ + + case 5: + // Read a random non-existing document + if len(t.collections) > 0 { + c := t.selectRandomCollection() + randomKey := c.createNewKey(false) + if err := t.readNonExistingDocument(c.name, randomKey); err != nil { + t.log.Error().Msgf("Failed to read non-existing document '%s': %#v", randomKey, err) + } + } + planIndex++ + + case 6: + // Remove a random existing document + if len(t.collections) > 0 { + c := t.selectRandomCollection() + if len(c.existingDocs) > 0 { + randomKey, rev := c.selectRandomKey() + if err := t.removeExistingDocument(c.name, randomKey, rev); err != nil { + t.log.Error().Msgf("Failed to remove existing document '%s': %#v", randomKey, err) + } else { + // Remove succeeded, key should no longer exist + c.removeExistingKey(randomKey) + + // Now try to read it, it should not exist + //t.client.SetCoordinator("") + if err := t.readNonExistingDocument(c.name, randomKey); err != nil { + t.log.Error().Msgf("Failed to read just-removed document '%s': %#v", randomKey, err) + } + } + } + } + planIndex++ + + case 7: + // Remove a random existing document but with wrong revision + planIndex++ + + case 8: + // Remove a random non-existing document + if len(t.collections) > 0 { + c := t.selectRandomCollection() + randomKey := c.createNewKey(false) + if err := t.removeNonExistingDocument(c.name, randomKey); err != nil { + t.log.Error().Msgf("Failed to remove non-existing document '%s': %#v", randomKey, err) + } + } + planIndex++ + + case 9: + // Update a random existing document + if len(t.collections) > 0 { + c := t.selectRandomCollection() + if len(c.existingDocs) > 0 { + randomKey, rev := c.selectRandomKey() + if newRev, err := t.updateExistingDocument(c, randomKey, rev); err != nil { + t.log.Error().Msgf("Failed to update existing document '%s': %#v", randomKey, err) + } else { + // Updated succeeded, now try to read it, it should exist and be updated + //t.client.SetCoordinator("") + if _, err := t.readExistingDocument(c, randomKey, newRev, false, false); err != nil { + t.log.Error().Msgf("Failed to read just-updated document '%s': %#v", randomKey, err) + } + } + } + } + planIndex++ + + case 10: + // Update a random existing document but with wrong revision + planIndex++ + + case 11: + // Update a random non-existing document + if len(t.collections) > 0 { + c := t.selectRandomCollection() + randomKey := c.createNewKey(false) + if err := t.updateNonExistingDocument(c.name, randomKey); err != nil { + t.log.Error().Msgf("Failed to update non-existing document '%s': %#v", randomKey, err) + } + } + planIndex++ + + case 12: + // Replace a random existing document + if len(t.collections) > 0 { + c := t.selectRandomCollection() + if len(c.existingDocs) > 0 { + randomKey, rev := c.selectRandomKey() + if newRev, err := t.replaceExistingDocument(c, randomKey, rev); err != nil { + t.log.Error().Msgf("Failed to replace existing document '%s': %#v", randomKey, err) + } else { + // Replace succeeded, now try to read it, it should exist and be replaced + //t.client.SetCoordinator("") + if _, err := t.readExistingDocument(c, randomKey, newRev, false, false); err != nil { + t.log.Error().Msgf("Failed to read just-replaced document '%s': %#v", randomKey, err) + } + } + } + } + planIndex++ + + case 13: + // Replace a random existing document but with wrong revision + planIndex++ + + case 14: + // Replace a random non-existing document + if len(t.collections) > 0 { + c := t.selectRandomCollection() + randomKey := c.createNewKey(false) + if err := t.replaceNonExistingDocument(c.name, randomKey); err != nil { + t.log.Error().Msgf("Failed to replace non-existing document '%s': %#v", randomKey, err) + } + } + planIndex++ + + case 15: + // Query documents + planIndex++ + + case 16: + // Query documents (long running) + if len(t.collections) > 0 { + c := t.selectRandomCollection() + if err := t.queryDocumentsLongRunning(c); err != nil { + t.log.Error().Msgf("Failed to query (long running) documents: %#v", err) + } + } + planIndex++ + + case 17: + // Rebalance shards + if err := t.rebalanceShards(); err != nil { + t.log.Error().Msgf("Failed to rebalance shards: %#v", err) + } + planIndex++ + + case 18: + // AQL update query + if len(t.collections) > 0 { + c := t.selectRandomCollection() + if len(c.existingDocs) > 0 { + randomKey, _ := c.selectRandomKey() + if newRev, err := t.queryUpdateDocuments(c, randomKey); err != nil { + t.log.Error().Msgf("Failed to update document using AQL query: %#v", err) + } else { + // Updated succeeded, now try to read it (anywhere), it should exist and be updated + //t.client.SetCoordinator("") + if _, err := t.readExistingDocument(c, randomKey, newRev, false, false); err != nil { + t.log.Error().Msgf("Failed to read just-updated document '%s': %#v", randomKey, err) + } + } + } + } + planIndex++ + + case 19: + // Long running AQL update query + if len(t.collections) > 0 { + c := t.selectRandomCollection() + if len(c.existingDocs) > 0 { + randomKey, _ := c.selectRandomKey() + if newRev, err := t.queryUpdateDocumentsLongRunning(c, randomKey); err != nil { + t.log.Error().Msgf("Failed to update document using long running AQL query: %#v", err) + } else { + // Updated succeeded, now try to read it (anywhere), it should exist and be updated + //t.client.SetCoordinator("") + if _, err := t.readExistingDocument(c, randomKey, newRev, false, false); err != nil { + t.log.Error().Msgf("Failed to read just-updated document '%s': %#v", randomKey, err) + } + } + } + } + planIndex++ + } + time.Sleep(time.Second * 2) + } +} + +// createTestPlan creates an int-array of 'steps' long with all values from 0..steps-1 in random order. +func createTestPlan(steps int) []int { + plan := make([]int, steps) + for i := 0; i < steps; i++ { + plan[i] = i + } + test.Shuffle(sort.IntSlice(plan)) + return plan +} + +// createNewCollectionName returns a new (unique) collection name +func (t *simpleTest) createNewCollectionName() string { + index := atomic.AddInt32(&t.lastCollectionIndex, 1) + return t.getCollectionName(int(index)) +} + +// getCollectionName returns a collection name with given index +func (t *simpleTest) getCollectionName(index int) string { + return fmt.Sprintf("simple_user_%d", index) +} + +func (t *simpleTest) selectRandomCollection() *collection { + index := rand.Intn(len(t.collections)) + for _, c := range t.collections { + if index == 0 { + return c + } + index-- + } + return nil // This should never be reached when len(t.collections) > 0 +} + +func (t *simpleTest) registerCollection(c *collection) { + t.collectionsMutex.Lock() + defer t.collectionsMutex.Unlock() + t.collections[c.name] = c +} + +func (t *simpleTest) unregisterCollection(c *collection) { + t.collectionsMutex.Lock() + defer t.collectionsMutex.Unlock() + delete(t.collections, c.name) +} + +func (t *simpleTest) createAndInitCollection() error { + c := &collection{ + name: t.createNewCollectionName(), + existingDocs: make(map[string]UserDocument), + } + if err := t.createCollection(c, 9, 2); err != nil { + t.reportFailure(test.NewFailure("Creating collection '%s' failed: %v", c.name, err)) + return maskAny(err) + } + t.registerCollection(c) + t.createCollectionCounter.succeeded++ + t.actions++ + + // Import documents + if err := t.importDocuments(c); err != nil { + t.reportFailure(test.NewFailure("Failed to import documents: %#v", err)) + } + t.actions++ + + // Check imported documents + for k := range c.existingDocs { + if t.shouldStop() || t.pauseRequested { + return nil + } + if _, err := t.readExistingDocument(c, k, "", true, false); err != nil { + t.reportFailure(test.NewFailure("Failed to read existing document '%s': %#v", k, err)) + } + t.actions++ + } + + // Create sample users + for i := 0; i < initialDocumentCount; i++ { + if t.shouldStop() || t.pauseRequested { + return nil + } + userDoc := UserDocument{ + Key: fmt.Sprintf("doc%05d", i), + Value: i, + Name: fmt.Sprintf("User %d", i), + Odd: i%2 == 1, + } + if rev, err := t.createDocument(c, userDoc, userDoc.Key); err != nil { + t.reportFailure(test.NewFailure("Failed to create document: %#v", err)) + } else { + userDoc.rev = rev + c.existingDocs[userDoc.Key] = userDoc + } + t.actions++ + } + return nil +} + +func (c *collection) createNewKey(record bool) string { + for { + key := fmt.Sprintf("newkey%07d", rand.Int31n(100*1000)) + _, found := c.existingDocs[key] + if !found { + if record { + c.existingDocs[key] = UserDocument{} + } + return key + } + } +} + +func (c *collection) removeExistingKey(key string) { + delete(c.existingDocs, key) +} + +func (c *collection) selectRandomKey() (string, string) { + index := rand.Intn(len(c.existingDocs)) + for k, v := range c.existingDocs { + if index == 0 { + return k, v.rev + } + index-- + } + return "", "" // This should never be reached when len(t.existingDocs) > 0 +} + +func (c *collection) selectWrongRevision(key string) (string, bool) { + correctRev := c.existingDocs[key].rev + for _, v := range c.existingDocs { + if v.rev != correctRev && v.rev != "" { + return v.rev, true + } + } + return "", false // This should never be reached when len(t.existingDocs) > 1 +} diff --git a/tests/duration/simple/simple_collection.go b/tests/duration/simple/simple_collection.go new file mode 100644 index 000000000..71ddaab0c --- /dev/null +++ b/tests/duration/simple/simple_collection.go @@ -0,0 +1,94 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package simple + +import ( + "context" + "fmt" + + driver "github.com/arangodb/go-driver" + + "github.com/arangodb/kube-arangodb/tests/duration/test" +) + +// createCollection creates a new collection. +// The operation is expected to succeed. +func (t *simpleTest) createCollection(c *collection, numberOfShards, replicationFactor int) error { + ctx := context.Background() + opts := &driver.CreateCollectionOptions{ + NumberOfShards: numberOfShards, + ReplicationFactor: replicationFactor, + } + t.log.Info().Msgf("Creating collection '%s' with numberOfShards=%d, replicationFactor=%d...", c.name, numberOfShards, replicationFactor) + if _, err := t.db.CreateCollection(ctx, c.name, opts); err != nil { + // This is a failure + t.reportFailure(test.NewFailure("Failed to create collection '%s': %v", c.name, err)) + return maskAny(err) + } else if driver.IsConflict(err) { + // Duplicate name, check if that is correct + if exists, checkErr := t.collectionExists(c); checkErr != nil { + t.log.Error().Msgf("Failed to check if collection exists: %v", checkErr) + t.reportFailure(test.NewFailure("Failed to create collection '%s': %v and cannot check existance: %v", c.name, err, checkErr)) + return maskAny(err) + } else if !exists { + // Collection has not been created, so 409 status is really wrong + t.reportFailure(test.NewFailure("Failed to create collection '%s': 409 reported but collection does not exist", c.name)) + return maskAny(fmt.Errorf("Create collection reported 409, but collection does not exist")) + } + } + t.log.Info().Msgf("Creating collection '%s' with numberOfShards=%d, replicationFactor=%d succeeded", c.name, numberOfShards, replicationFactor) + return nil +} + +// removeCollection remove an existing collection. +// The operation is expected to succeed. +func (t *simpleTest) removeExistingCollection(c *collection) error { + ctx := context.Background() + t.log.Info().Msgf("Removing collection '%s'...", c.name) + col, err := t.db.Collection(ctx, c.name) + if err != nil { + return maskAny(err) + } + if err := col.Remove(ctx); err != nil { + // This is a failure + t.removeExistingCollectionCounter.failed++ + t.reportFailure(test.NewFailure("Failed to remove collection '%s': %v", c.name, err)) + return maskAny(err) + } + t.removeExistingCollectionCounter.succeeded++ + t.log.Info().Msgf("Removing collection '%s' succeeded", c.name) + t.unregisterCollection(c) + return nil +} + +// collectionExists tries to fetch information about the collection to see if it exists. +func (t *simpleTest) collectionExists(c *collection) (bool, error) { + ctx := context.Background() + t.log.Info().Msgf("Checking collection '%s'...", c.name) + if found, err := t.db.CollectionExists(ctx, c.name); err != nil { + // This is a failure + return false, maskAny(err) + } else { + return found, nil + } +} diff --git a/tests/duration/simple/simple_create.go b/tests/duration/simple/simple_create.go new file mode 100644 index 000000000..af5ce6bb5 --- /dev/null +++ b/tests/duration/simple/simple_create.go @@ -0,0 +1,50 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package simple + +import ( + "context" + + "github.com/arangodb/kube-arangodb/tests/duration/test" +) + +// createDocument creates a new document. +// The operation is expected to succeed. +func (t *simpleTest) createDocument(c *collection, document interface{}, key string) (string, error) { + ctx := context.Background() + col, err := t.db.Collection(ctx, c.name) + if err != nil { + return "", maskAny(err) + } + t.log.Info().Msgf("Creating document '%s' in '%s'...", key, c.name) + m, err := col.CreateDocument(ctx, document) + if err != nil { + // This is a failure + t.createCounter.failed++ + t.reportFailure(test.NewFailure("Failed to create document with key '%s' in collection '%s': %v", key, c.name, err)) + return "", maskAny(err) + } + t.createCounter.succeeded++ + t.log.Info().Msgf("Creating document '%s' in '%s' succeeded", key, c.name) + return m.Rev, nil +} diff --git a/tests/duration/simple/simple_import.go b/tests/duration/simple/simple_import.go new file mode 100644 index 000000000..02dcaeb8d --- /dev/null +++ b/tests/duration/simple/simple_import.go @@ -0,0 +1,79 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package simple + +import ( + "bytes" + "context" + "fmt" + + "github.com/arangodb/kube-arangodb/tests/duration/test" +) + +// createImportDocument creates a #document based import file. +func (t *simpleTest) createImportDocument() ([]byte, []UserDocument) { + buf := &bytes.Buffer{} + docs := make([]UserDocument, 0, 10000) + fmt.Fprintf(buf, `[ "_key", "value", "name", "odd" ]`) + fmt.Fprintln(buf) + for i := 0; i < 10000; i++ { + key := fmt.Sprintf("docimp%05d", i) + userDoc := UserDocument{ + Key: key, + Value: i, + Name: fmt.Sprintf("Imported %d", i), + Odd: i%2 == 0, + } + docs = append(docs, userDoc) + fmt.Fprintf(buf, `[ "%s", %d, "%s", %v ]`, userDoc.Key, userDoc.Value, userDoc.Name, userDoc.Odd) + fmt.Fprintln(buf) + } + return buf.Bytes(), docs +} + +// importDocuments imports a bulk set of documents. +// The operation is expected to succeed. +func (t *simpleTest) importDocuments(c *collection) error { + ctx := context.Background() + col, err := t.db.Collection(ctx, c.name) + if err != nil { + return maskAny(err) + } + _, docs := t.createImportDocument() + t.log.Info().Msgf("Importing %d documents ('%s' - '%s') into '%s'...", len(docs), docs[0].Key, docs[len(docs)-1].Key, c.name) + _, errs, err := col.CreateDocuments(ctx, docs) + if err != nil { + // This is a failure + t.importCounter.failed++ + t.reportFailure(test.NewFailure("Failed to import documents in collection '%s': %v", c.name, err)) + return maskAny(err) + } + for i, d := range docs { + if errs[i] == nil { + c.existingDocs[d.Key] = d + } + } + t.importCounter.succeeded++ + t.log.Info().Msgf("Importing %d documents ('%s' - '%s') into '%s' succeeded", len(docs), docs[0].Key, docs[len(docs)-1].Key, c.name) + return nil +} diff --git a/tests/duration/simple/simple_query.go b/tests/duration/simple/simple_query.go new file mode 100644 index 000000000..9757bb93e --- /dev/null +++ b/tests/duration/simple/simple_query.go @@ -0,0 +1,66 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package simple + +import ( + "context" + "fmt" + + driver "github.com/arangodb/go-driver" + + "github.com/arangodb/kube-arangodb/tests/duration/test" +) + +// queryDocumentsLongRunning runs a long running AQL query. +// The operation is expected to succeed. +func (t *simpleTest) queryDocumentsLongRunning(c *collection) error { + if len(c.existingDocs) < 10 { + t.log.Info().Msgf("Skipping query test, we need 10 or more documents") + return nil + } + + ctx := context.Background() + ctx = driver.WithQueryCount(ctx) + + t.log.Info().Msgf("Creating long running AQL query for '%s'...", c.name) + query := fmt.Sprintf("FOR d IN %s LIMIT 10 RETURN {d:d, s:SLEEP(2)}", c.name) + cursor, err := t.db.Query(ctx, query, nil) + if err != nil { + // This is a failure + t.queryLongRunningCounter.failed++ + t.reportFailure(test.NewFailure("Failed to create long running AQL cursor in collection '%s': %v", c.name, err)) + return maskAny(err) + } + cursor.Close() + resultCount := cursor.Count() + t.queryLongRunningCounter.succeeded++ + t.log.Info().Msgf("Creating long running AQL query for collection '%s' succeeded", c.name) + + // We should've fetched all documents, check result count + if resultCount != 10 { + t.reportFailure(test.NewFailure("Number of documents was %d, expected 10", resultCount)) + return maskAny(fmt.Errorf("Number of documents was %d, expected 10", resultCount)) + } + + return nil +} diff --git a/tests/duration/simple/simple_query_update.go b/tests/duration/simple/simple_query_update.go new file mode 100644 index 000000000..059d83001 --- /dev/null +++ b/tests/duration/simple/simple_query_update.go @@ -0,0 +1,115 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package simple + +import ( + "context" + "fmt" + "time" + + driver "github.com/arangodb/go-driver" + + "github.com/arangodb/kube-arangodb/tests/duration/test" +) + +// queryUpdateDocuments runs an AQL update query. +// The operation is expected to succeed. +func (t *simpleTest) queryUpdateDocuments(c *collection, key string) (string, error) { + ctx := context.Background() + ctx = driver.WithQueryCount(ctx) + + t.log.Info().Msgf("Creating update AQL query for collection '%s'...", c.name) + newName := fmt.Sprintf("AQLUpdate name %s", time.Now()) + query := fmt.Sprintf("UPDATE \"%s\" WITH { name: \"%s\" } IN %s RETURN NEW", key, newName, c.name) + cursor, err := t.db.Query(ctx, query, nil) + if err != nil { + // This is a failure + t.queryUpdateCounter.failed++ + t.reportFailure(test.NewFailure("Failed to create update AQL cursor in collection '%s': %v", c.name, err)) + return "", maskAny(err) + } + var resultDocument UserDocument + m, err := cursor.ReadDocument(ctx, &resultDocument) + if err != nil { + // This is a failure + t.queryUpdateCounter.failed++ + t.reportFailure(test.NewFailure("Failed to read document from cursor in collection '%s': %v", c.name, err)) + return "", maskAny(err) + } + resultCount := cursor.Count() + cursor.Close() + if resultCount != 1 { + // This is a failure + t.queryUpdateCounter.failed++ + t.reportFailure(test.NewFailure("Failed to create update AQL cursor in collection '%s': expected 1 result, got %d", c.name, resultCount)) + return "", maskAny(fmt.Errorf("Number of documents was %d, expected 1", resultCount)) + } + + // Update document + c.existingDocs[key] = resultDocument + t.queryUpdateCounter.succeeded++ + t.log.Info().Msgf("Creating update AQL query for collection '%s' succeeded", c.name) + + return m.Rev, nil +} + +// queryUpdateDocumentsLongRunning runs a long running AQL update query. +// The operation is expected to succeed. +func (t *simpleTest) queryUpdateDocumentsLongRunning(c *collection, key string) (string, error) { + ctx := context.Background() + ctx = driver.WithQueryCount(ctx) + + t.log.Info().Msgf("Creating long running update AQL query for collection '%s'...", c.name) + newName := fmt.Sprintf("AQLLongRunningUpdate name %s", time.Now()) + query := fmt.Sprintf("UPDATE \"%s\" WITH { name: \"%s\", unknown: SLEEP(15) } IN %s RETURN NEW", key, newName, c.name) + cursor, err := t.db.Query(ctx, query, nil) + if err != nil { + // This is a failure + t.queryUpdateLongRunningCounter.failed++ + t.reportFailure(test.NewFailure("Failed to create long running update AQL cursor in collection '%s': %v", c.name, err)) + return "", maskAny(err) + } + var resultDocument UserDocument + m, err := cursor.ReadDocument(ctx, &resultDocument) + if err != nil { + // This is a failure + t.queryUpdateCounter.failed++ + t.reportFailure(test.NewFailure("Failed to read document from cursor in collection '%s': %v", c.name, err)) + return "", maskAny(err) + } + resultCount := cursor.Count() + cursor.Close() + if resultCount != 1 { + // This is a failure + t.queryUpdateLongRunningCounter.failed++ + t.reportFailure(test.NewFailure("Failed to create long running update AQL cursor in collection '%s': expected 1 result, got %d", c.name, resultCount)) + return "", maskAny(fmt.Errorf("Number of documents was %d, expected 1", resultCount)) + } + + // Update document + c.existingDocs[key] = resultDocument + t.queryUpdateLongRunningCounter.succeeded++ + t.log.Info().Msgf("Creating long running update AQL query for collection '%s' succeeded", c.name) + + return m.Rev, nil +} diff --git a/tests/duration/simple/simple_read.go b/tests/duration/simple/simple_read.go new file mode 100644 index 000000000..131d0b15f --- /dev/null +++ b/tests/duration/simple/simple_read.go @@ -0,0 +1,88 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package simple + +import ( + "context" + "fmt" + + driver "github.com/arangodb/go-driver" + + "github.com/arangodb/kube-arangodb/tests/duration/test" +) + +// readExistingDocument reads an existing document with an optional explicit revision. +// The operation is expected to succeed. +func (t *simpleTest) readExistingDocument(c *collection, key, rev string, updateRevision, skipExpectedValueCheck bool) (string, error) { + ctx := context.Background() + var result UserDocument + col, err := t.db.Collection(ctx, c.name) + if err != nil { + return "", maskAny(err) + } + m, err := col.ReadDocument(ctx, key, &result) + if err != nil { + // This is a failure + t.readExistingCounter.failed++ + t.reportFailure(test.NewFailure("Failed to read existing document '%s' in collection '%s': %v", key, c.name, err)) + return "", maskAny(err) + } + // Compare document against expected document + if !skipExpectedValueCheck { + expected := c.existingDocs[key] + if result.Value != expected.Value || result.Name != expected.Name || result.Odd != expected.Odd { + // This is a failure + t.readExistingCounter.failed++ + t.reportFailure(test.NewFailure("Read existing document '%s' returned different values '%s': got %q expected %q", key, c.name, result, expected)) + return "", maskAny(fmt.Errorf("Read returned invalid values")) + } + } + if updateRevision { + // Store read document so we have the last revision + c.existingDocs[key] = result + } + t.readExistingCounter.succeeded++ + t.log.Info().Msgf("Reading existing document '%s' from '%s' succeeded", key, c.name) + return m.Rev, nil +} + +// readNonExistingDocument reads a non-existing document. +// The operation is expected to fail. +func (t *simpleTest) readNonExistingDocument(collectionName string, key string) error { + ctx := context.Background() + var result UserDocument + t.log.Info().Msgf("Reading non-existing document '%s' from '%s'...", key, collectionName) + col, err := t.db.Collection(ctx, collectionName) + if err != nil { + return maskAny(err) + } + if _, err := col.ReadDocument(ctx, key, &result); !driver.IsNotFound(err) { + // This is a failure + t.readNonExistingCounter.failed++ + t.reportFailure(test.NewFailure("Failed to read non-existing document '%s' in collection '%s': %v", key, collectionName, err)) + return maskAny(err) + } + t.readNonExistingCounter.succeeded++ + t.log.Info().Msgf("Reading non-existing document '%s' from '%s' succeeded", key, collectionName) + return nil +} diff --git a/tests/duration/simple/simple_rebalance.go b/tests/duration/simple/simple_rebalance.go new file mode 100644 index 000000000..330a32afa --- /dev/null +++ b/tests/duration/simple/simple_rebalance.go @@ -0,0 +1,40 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package simple + +// rebalanceShards attempts to rebalance shards over the existing servers. +// The operation is expected to succeed. +func (t *simpleTest) rebalanceShards() error { + /*opts := struct{}{} + operationTimeout, retryTimeout := t.OperationTimeout, t.RetryTimeout + t.log.Info().Msgf("Rebalancing shards...") + if _, err := t.client.Post("/_admin/cluster/rebalanceShards", nil, nil, opts, "", nil, []int{202}, []int{400, 403, 503}, operationTimeout, retryTimeout); err != nil { + // This is a failure + t.rebalanceShardsCounter.failed++ + t.reportFailure(test.NewFailure("Failed to rebalance shards: %v", err)) + return maskAny(err) + } + t.rebalanceShardsCounter.succeeded++ + t.log.Info().Msgf("Rebalancing shards succeeded")*/ + return nil +} diff --git a/tests/duration/simple/simple_remove.go b/tests/duration/simple/simple_remove.go new file mode 100644 index 000000000..17cd5e518 --- /dev/null +++ b/tests/duration/simple/simple_remove.go @@ -0,0 +1,71 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package simple + +import ( + "context" + + driver "github.com/arangodb/go-driver" + + "github.com/arangodb/kube-arangodb/tests/duration/test" +) + +// removeExistingDocument removes an existing document with an optional explicit revision. +// The operation is expected to succeed. +func (t *simpleTest) removeExistingDocument(collectionName string, key, rev string) error { + ctx := context.Background() + col, err := t.db.Collection(ctx, collectionName) + if err != nil { + return maskAny(err) + } + t.log.Info().Msgf("Removing existing document '%s' from '%s'...", key, collectionName) + if _, err := col.RemoveDocument(ctx, key); err != nil { + // This is a failure + t.deleteExistingCounter.failed++ + t.reportFailure(test.NewFailure("Failed to delete existing document '%s' in collection '%s': %v", key, collectionName, err)) + return maskAny(err) + } + t.deleteExistingCounter.succeeded++ + t.log.Info().Msgf("Removing existing document '%s' from '%s' succeeded", key, collectionName) + return nil +} + +// removeNonExistingDocument removes a non-existing document. +// The operation is expected to fail. +func (t *simpleTest) removeNonExistingDocument(collectionName string, key string) error { + ctx := context.Background() + col, err := t.db.Collection(ctx, collectionName) + if err != nil { + return maskAny(err) + } + t.log.Info().Msgf("Removing non-existing document '%s' from '%s'...", key, collectionName) + if _, err := col.RemoveDocument(ctx, key); !driver.IsNotFound(err) { + // This is a failure + t.deleteNonExistingCounter.failed++ + t.reportFailure(test.NewFailure("Failed to delete non-existing document '%s' in collection '%s': %v", key, collectionName, err)) + return maskAny(err) + } + t.deleteNonExistingCounter.succeeded++ + t.log.Info().Msgf("Removing non-existing document '%s' from '%s' succeeded", key, collectionName) + return nil +} diff --git a/tests/duration/simple/simple_replace.go b/tests/duration/simple/simple_replace.go new file mode 100644 index 000000000..852968d6f --- /dev/null +++ b/tests/duration/simple/simple_replace.go @@ -0,0 +1,92 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package simple + +import ( + "context" + "fmt" + "math/rand" + "time" + + driver "github.com/arangodb/go-driver" + + "github.com/arangodb/kube-arangodb/tests/duration/test" +) + +// replaceExistingDocument replaces an existing document with an optional explicit revision. +// The operation is expected to succeed. +func (t *simpleTest) replaceExistingDocument(c *collection, key, rev string) (string, error) { + ctx := context.Background() + col, err := t.db.Collection(ctx, c.name) + if err != nil { + return "", maskAny(err) + } + newName := fmt.Sprintf("Updated name %s", time.Now()) + t.log.Info().Msgf("Replacing existing document '%s' in '%s' (name -> '%s')...", key, c.name, newName) + newDoc := UserDocument{ + Key: key, + Name: fmt.Sprintf("Replaced named %s", key), + Value: rand.Int(), + Odd: rand.Int()%2 == 0, + } + m, err := col.ReplaceDocument(ctx, key, newDoc) + if err != nil { + // This is a failure + t.replaceExistingCounter.failed++ + t.reportFailure(test.NewFailure("Failed to replace existing document '%s' in collection '%s': %v", key, c.name, err)) + return "", maskAny(err) + } + // Update internal doc + newDoc.rev = m.Rev + c.existingDocs[key] = newDoc + t.replaceExistingCounter.succeeded++ + t.log.Info().Msgf("Replacing existing document '%s' in '%s' (name -> '%s') succeeded", key, c.name, newName) + return m.Rev, nil +} + +// replaceNonExistingDocument replaces a non-existing document. +// The operation is expected to fail. +func (t *simpleTest) replaceNonExistingDocument(collectionName string, key string) error { + ctx := context.Background() + col, err := t.db.Collection(ctx, collectionName) + if err != nil { + return maskAny(err) + } + newName := fmt.Sprintf("Updated non-existing name %s", time.Now()) + t.log.Info().Msgf("Replacing non-existing document '%s' in '%s' (name -> '%s')...", key, collectionName, newName) + newDoc := UserDocument{ + Key: key, + Name: fmt.Sprintf("Replaced named %s", key), + Value: rand.Int(), + Odd: rand.Int()%2 == 0, + } + if _, err := col.ReplaceDocument(ctx, key, newDoc); !driver.IsNotFound(err) { + // This is a failure + t.replaceNonExistingCounter.failed++ + t.reportFailure(test.NewFailure("Failed to replace non-existing document '%s' in collection '%s': %v", key, collectionName, err)) + return maskAny(err) + } + t.replaceNonExistingCounter.succeeded++ + t.log.Info().Msgf("Replacing non-existing document '%s' in '%s' (name -> '%s') succeeded", key, collectionName, newName) + return nil +} diff --git a/tests/duration/simple/simple_update.go b/tests/duration/simple/simple_update.go new file mode 100644 index 000000000..b8f3f9d63 --- /dev/null +++ b/tests/duration/simple/simple_update.go @@ -0,0 +1,87 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package simple + +import ( + "context" + "fmt" + "time" + + driver "github.com/arangodb/go-driver" + + "github.com/arangodb/kube-arangodb/tests/duration/test" +) + +// updateExistingDocument updates an existing document with an optional explicit revision. +// The operation is expected to succeed. +func (t *simpleTest) updateExistingDocument(c *collection, key, rev string) (string, error) { + ctx := context.Background() + col, err := t.db.Collection(ctx, c.name) + if err != nil { + return "", maskAny(err) + } + newName := fmt.Sprintf("Updated name %s", time.Now()) + t.log.Info().Msgf("Updating existing document '%s' in '%s' (name -> '%s')...", key, c.name, newName) + delta := map[string]interface{}{ + "name": newName, + } + doc := c.existingDocs[key] + m, err := col.UpdateDocument(ctx, key, delta) + if err != nil { + // This is a failure + t.updateExistingCounter.failed++ + t.reportFailure(test.NewFailure("Failed to update existing document '%s' in collection '%s': %v", key, c.name, err)) + return "", maskAny(err) + } + // Update internal doc + doc.Name = newName + doc.rev = m.Rev + c.existingDocs[key] = doc + t.updateExistingCounter.succeeded++ + t.log.Info().Msgf("Updating existing document '%s' in '%s' (name -> '%s') succeeded", key, c.name, newName) + return m.Rev, nil +} + +// updateNonExistingDocument updates a non-existing document. +// The operation is expected to fail. +func (t *simpleTest) updateNonExistingDocument(collectionName string, key string) error { + ctx := context.Background() + col, err := t.db.Collection(ctx, collectionName) + if err != nil { + return maskAny(err) + } + newName := fmt.Sprintf("Updated non-existing name %s", time.Now()) + t.log.Info().Msgf("Updating non-existing document '%s' in '%s' (name -> '%s')...", key, collectionName, newName) + delta := map[string]interface{}{ + "name": newName, + } + if _, err := col.UpdateDocument(ctx, key, delta); !driver.IsNotFound(err) { + // This is a failure + t.updateNonExistingCounter.failed++ + t.reportFailure(test.NewFailure("Failed to update non-existing document '%s' in collection '%s': %v", key, collectionName, err)) + return maskAny(err) + } + t.updateNonExistingCounter.succeeded++ + t.log.Info().Msgf("Updating non-existing document '%s' in '%s' (name -> '%s') succeeded", key, collectionName, newName) + return nil +} diff --git a/tests/duration/test/shuffle.go b/tests/duration/test/shuffle.go new file mode 100644 index 000000000..98892f167 --- /dev/null +++ b/tests/duration/test/shuffle.go @@ -0,0 +1,43 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package test + +import "math/rand" + +// A type, typically a collection, that satisfies shuffle.Interface can be +// shuffled by the routines in this package. +type Interface interface { + // Len is the number of elements in the collection. + Len() int + // Swap swaps the elements with indexes i and j. + Swap(i, j int) +} + +// Shuffle shuffles Data. +func Shuffle(data Interface) { + n := data.Len() + for i := n - 1; i >= 0; i-- { + j := rand.Intn(i + 1) + data.Swap(i, j) + } +} diff --git a/tests/duration/test/test.go b/tests/duration/test/test.go new file mode 100644 index 000000000..b99c5639e --- /dev/null +++ b/tests/duration/test/test.go @@ -0,0 +1,66 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package test + +import ( + "fmt" + + driver "github.com/arangodb/go-driver" +) + +type TestScript interface { + Start(client driver.Client, listener TestListener) error + Stop() error + Pause() error + Resume() error + Status() TestStatus +} + +type TestListener interface { + ReportFailure(Failure) +} + +type Counter struct { + Name string + Succeeded int + Failed int +} + +type TestStatus struct { + Active bool + Pausing bool + Failures int + Actions int + Counters []Counter + Messages []string +} + +type Failure struct { + Message string +} + +func NewFailure(msg string, args ...interface{}) Failure { + return Failure{ + Message: fmt.Sprintf(msg, args...), + } +} diff --git a/tests/duration/test_listener.go b/tests/duration/test_listener.go new file mode 100644 index 000000000..8e141fd26 --- /dev/null +++ b/tests/duration/test_listener.go @@ -0,0 +1,88 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package main + +import ( + "sync" + "time" + + "github.com/rs/zerolog" + + "github.com/arangodb/kube-arangodb/tests/duration/test" +) + +const ( + recentFailureTimeout = time.Hour // Disregard failures old than this timeout + requiredRecentFailureSpread = time.Minute * 5 // How far apart the first and last recent failure must be + requiredRecentFailures = 30 // At least so many recent failures are needed to fail the test +) + +type testListener struct { + mutex sync.Mutex + Log zerolog.Logger + FailedCallback func() + recentFailures []time.Time + failed bool +} + +var _ test.TestListener = &testListener{} + +// ReportFailure logs the given failure and keeps track of recent failure timestamps. +func (l *testListener) ReportFailure(f test.Failure) { + l.Log.Error().Msg(f.Message) + + // Remove all old recent failures + l.mutex.Lock() + defer l.mutex.Unlock() + for { + if len(l.recentFailures) == 0 { + break + } + isOld := l.recentFailures[0].Add(recentFailureTimeout).Before(time.Now()) + if isOld { + // Remove first entry + l.recentFailures = l.recentFailures[1:] + } else { + // First failure is not old, keep the list as is + break + } + } + l.recentFailures = append(l.recentFailures, time.Now()) + + // Detect failed state + if len(l.recentFailures) > requiredRecentFailures { + spread := l.recentFailures[len(l.recentFailures)-1].Sub(l.recentFailures[0]) + if spread > requiredRecentFailureSpread { + l.failed = true + if l.FailedCallback != nil { + l.FailedCallback() + } + } + } +} + +// IsFailed returns true when the number of recent failures +// has gone above the set maximum, false otherwise. +func (l *testListener) IsFailed() bool { + return l.failed +} diff --git a/tests/duration/test_loop.go b/tests/duration/test_loop.go new file mode 100644 index 000000000..acdbaaa9f --- /dev/null +++ b/tests/duration/test_loop.go @@ -0,0 +1,124 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package main + +import ( + "context" + "os" + "time" + + driver "github.com/arangodb/go-driver" + "github.com/rs/zerolog" + + "github.com/arangodb/kube-arangodb/tests/duration/simple" + t "github.com/arangodb/kube-arangodb/tests/duration/test" +) + +var ( + delayBeforeCompare = time.Minute + testPeriod = time.Minute * 2 + systemCollectionsToIgnore = map[string]bool{ + "_appbundles": true, + "_apps": true, + "_jobs": true, + "_queues": true, + "_routing": true, + "_statistics": true, + "_statisticsRaw": true, + "_statistics15": true, + } +) + +// runTestLoop keeps running tests until the given context is canceled. +func runTestLoop(ctx context.Context, client driver.Client, duration time.Duration) { + log := zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).With().Timestamp().Logger() + endTime := time.Now().Add(duration) + reportDir := "." + tests := []t.TestScript{} + tests = append(tests, simple.NewSimpleTest(log, reportDir, simple.SimpleConfig{ + MaxDocuments: 500, + MaxCollections: 50, + })) + + log.Info().Msg("Starting tests") + listener := &testListener{ + Log: log, + FailedCallback: func() { + log.Fatal().Msg("Too many recent failures. Aborting test") + }, + } + for _, tst := range tests { + if err := tst.Start(client, listener); err != nil { + log.Fatal().Err(err).Msg("Failed to start test") + } + } + for { + if err := ctx.Err(); err != nil { + return + } + + // Check end time + if time.Now().After(endTime) { + log.Info().Msgf("Test has run for %s. We're done", duration) + return + } + + // Run tests + log.Info().Msg("Running tests...") + select { + case <-time.After(testPeriod): + // Continue + case <-ctx.Done(): + return + } + + // Pause tests + log.Info().Msg("Pause tests") + for _, tst := range tests { + if err := tst.Pause(); err != nil { + log.Fatal().Err(err).Msg("Failed to pause test") + } + } + + // Wait for tests to really pause + log.Info().Msg("Waiting for tests to reach pausing state") + for _, tst := range tests { + for !tst.Status().Pausing { + select { + case <-time.After(time.Second): + // Continue + case <-ctx.Done(): + return + } + } + } + + // Resume tests + log.Info().Msg("Resuming tests") + for _, tst := range tests { + if err := tst.Resume(); err != nil { + log.Fatal().Err(err).Msg("Failed to resume test") + } + } + } +}