├── .envrc ├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── TODO ├── VERSION ├── main.go ├── service ├── arangodb.go ├── backoff.go ├── docker.go ├── error.go ├── master.go ├── runner.go ├── runner_docker.go ├── runner_process.go ├── server.go ├── setup_config.go └── slave.go ├── test.sh ├── testdocker.sh └── tools └── release.go /.envrc: -------------------------------------------------------------------------------- 1 | GOPATH=$(pwd)/.gobuild:${GOPATH} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gobuild 2 | arangodb 3 | bin -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM arangodb:latest 2 | MAINTAINER Max Neunhoeffer 3 | 4 | COPY bin/arangodb-linux-amd64 /app/ 5 | 6 | EXPOSE 4000 7 | 8 | VOLUME /data 9 | 10 | ENV DATA_DIR=/data 11 | ENV DOCKER_IMAGE=arangodb/arangodb:3.1.9 12 | 13 | ENTRYPOINT ["/app/arangodb-linux-amd64"] 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT := ArangoDBStarter 2 | SCRIPTDIR := $(shell pwd) 3 | ROOTDIR := $(shell cd $(SCRIPTDIR) && pwd) 4 | VERSION:= $(shell cat $(ROOTDIR)/VERSION) 5 | COMMIT := $(shell git rev-parse --short HEAD) 6 | 7 | GOBUILDDIR := $(SCRIPTDIR)/.gobuild 8 | SRCDIR := $(SCRIPTDIR) 9 | BINDIR := $(ROOTDIR)/bin 10 | 11 | ORGPATH := github.com/neunhoef 12 | ORGDIR := $(GOBUILDDIR)/src/$(ORGPATH) 13 | REPONAME := $(PROJECT) 14 | REPODIR := $(ORGDIR)/$(REPONAME) 15 | REPOPATH := $(ORGPATH)/$(REPONAME) 16 | 17 | GOPATH := $(GOBUILDDIR) 18 | GOVERSION := 1.7.4-alpine 19 | 20 | ifndef GOOS 21 | GOOS := linux 22 | endif 23 | ifndef GOARCH 24 | GOARCH := amd64 25 | endif 26 | 27 | ifndef DOCKERNAMESPACE 28 | DOCKERNAMESPACE := arangodb 29 | endif 30 | 31 | BINNAME := arangodb-$(GOOS)-$(GOARCH) 32 | BIN := $(BINDIR)/$(BINNAME) 33 | 34 | SOURCES := $(shell find $(SRCDIR) -name '*.go') 35 | 36 | .PHONY: all clean deps docker build build-local 37 | 38 | all: build 39 | 40 | clean: 41 | rm -Rf $(BIN) $(GOBUILDDIR) 42 | 43 | local: 44 | @${MAKE} -B GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) build-local 45 | 46 | build: $(BIN) 47 | 48 | build-local: build 49 | @ln -sf $(BIN) $(ROOTDIR)/arangodb 50 | 51 | deps: 52 | @${MAKE} -B -s $(GOBUILDDIR) 53 | 54 | $(GOBUILDDIR): 55 | @mkdir -p $(ORGDIR) 56 | @rm -f $(REPODIR) && ln -s ../../../.. $(REPODIR) 57 | GOPATH=$(GOBUILDDIR) go get github.com/cenkalti/backoff 58 | GOPATH=$(GOBUILDDIR) go get github.com/fsouza/go-dockerclient 59 | GOPATH=$(GOBUILDDIR) go get github.com/juju/errgo 60 | GOPATH=$(GOBUILDDIR) go get github.com/op/go-logging 61 | GOPATH=$(GOBUILDDIR) go get github.com/spf13/cobra 62 | GOPATH=$(GOBUILDDIR) go get github.com/coreos/go-semver/semver 63 | 64 | $(BIN): $(GOBUILDDIR) $(SOURCES) 65 | @mkdir -p $(BINDIR) 66 | docker run \ 67 | --rm \ 68 | -v $(SRCDIR):/usr/code \ 69 | -e GOPATH=/usr/code/.gobuild \ 70 | -e GOOS=$(GOOS) \ 71 | -e GOARCH=$(GOARCH) \ 72 | -e CGO_ENABLED=0 \ 73 | -w /usr/code/ \ 74 | golang:$(GOVERSION) \ 75 | go build -a -installsuffix netgo -tags netgo -ldflags "-X main.projectVersion=$(VERSION) -X main.projectBuild=$(COMMIT)" -o /usr/code/bin/$(BINNAME) $(REPOPATH) 76 | 77 | docker: build 78 | docker build -t arangodb/arangodb-starter . 79 | 80 | docker-push: docker 81 | ifneq ($(DOCKERNAMESPACE), arangodb) 82 | docker tag arangodb/arangodb-starter $(DOCKERNAMESPACE)/arangodb-starter 83 | endif 84 | docker push $(DOCKERNAMESPACE)/arangodb-starter 85 | 86 | docker-push-version: docker 87 | docker tag arangodb/arangodb-starter arangodb/arangodb-starter:$(VERSION) 88 | docker push arangodb/arangodb-starter:$(VERSION) 89 | 90 | release-patch: $(GOBUILDDIR) 91 | go run ./tools/release.go -type=patch 92 | 93 | release-minor: $(GOBUILDDIR) 94 | go run ./tools/release.go -type=minor 95 | 96 | release-major: $(GOBUILDDIR) 97 | go run ./tools/release.go -type=major 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Starting an ArangoDB cluster the easy way 2 | ========================================= 3 | 4 | THIS REPO HAS BEEN MOVED TO 5 | 6 | [https://github.com/arangodb-helper/ArangoDBStarter](https://github.com/arangodb-helper/ArangoDBStarter) 7 | 8 | Please use that one only from now on. 9 | 10 | Building 11 | -------- 12 | 13 | Just do 14 | 15 | ``` 16 | make local 17 | ``` 18 | 19 | and the executable is in `./bin` named after the current OS & architecture (e.g. `arangodb-linux-amd64`). 20 | You can copy the binary anywhere in your PATH. 21 | A link to the binary for the local OS & architecture is made to `./arangodb`. 22 | This program will run on Linux, OSX or Windows. 23 | 24 | Starting a cluster 25 | ------------------ 26 | 27 | Install ArangoDB in the usual way as binary package. Then: 28 | 29 | On host A: 30 | 31 | ``` 32 | arangodb 33 | ``` 34 | 35 | This will use port 4000 to wait for colleagues (3 are needed for a 36 | resilient agency). On host B: (can be the same as A): 37 | 38 | ``` 39 | arangodb --join A 40 | ``` 41 | 42 | This will contact A on port 4000 and register. On host C: (can be same 43 | as A or B): 44 | 45 | ``` 46 | arangodb --join A 47 | ``` 48 | 49 | This will contact A on port 4000 and register. 50 | 51 | From the moment on when 3 have joined, each will fire up an agent, a 52 | coordinator and a dbserver and the cluster is up. Ports are shown on 53 | the console. 54 | 55 | Additional servers can be added in the same way. 56 | 57 | If two or more of the `arangodb` instances run on the same machine, 58 | one has to use the `--dataDir` option to let each use a different 59 | directory. 60 | 61 | The `arangodb` program will find the ArangoDB executable and the 62 | other installation files automatically. If this fails, use the 63 | `--arangod` and `--jsdir` options described below. 64 | 65 | Running in Docker 66 | ----------------- 67 | 68 | The executable can be run inside Docker. In that case it will also run all 69 | servers in a Docker container. 70 | 71 | First make sure the docker images are build using: 72 | 73 | ``` 74 | make docker 75 | ``` 76 | 77 | When running in Docker it is important to care about the volume mappings on 78 | the container. Typically you will start the executable in docker with the following 79 | commands. 80 | 81 | ``` 82 | export IP= 83 | docker volume create arangodb1 84 | docker run -it --name=adb1 --rm -p 4000:4000 \ 85 | -v arangodb1:/data \ 86 | -v /var/run/docker.sock:/var/run/docker.sock \ 87 | arangodb/arangodb-starter \ 88 | --dockerContainer=adb1 --ownAddress=$IP 89 | ``` 90 | 91 | The executable will show the commands needed to run the other instances. 92 | 93 | Note that the commands above create a docker volume. If you're running on Linux 94 | it is also possible to use a host mapped volume. Make sure to map it on `/data`. 95 | 96 | Common options 97 | -------------- 98 | 99 | * `--dataDir path` 100 | 101 | `path` is the directory in which all data is stored. (default "./") 102 | 103 | In the directory, there will be a single file `setup.json` used for 104 | restarts and a directory for each instances that runs on this machine. 105 | Different instances of `arangodb` must use different data directories. 106 | 107 | * `--join addr` 108 | 109 | join a cluster with master at address `addr` (default "") 110 | 111 | * `--agencySize int` 112 | 113 | number of agents in agency (default 3). 114 | 115 | This number has to be positive and odd, and anything beyond 5 probably 116 | does not make sense. The default 3 allows for the failure of one agent. 117 | 118 | * `--ownAddress addr` 119 | 120 | `addr` is the address under which this server is reachable from the 121 | outside. 122 | 123 | Usually, this option does not have to be specified. Only in the case 124 | that `--agencySize` is set to 1 (see below), the master has to know 125 | under which address it can be reached from the outside. If you specify 126 | `localhost` here, then all instances must run on the local machine. 127 | 128 | * `--docker image` 129 | 130 | `image` is the name of a Docker image to run instead of the normal 131 | executable. For each started instance a Docker container is launched. 132 | Usually one would use the Docker image `arangodb/arangodb`. 133 | 134 | * `--dockerContainer containerName` 135 | 136 | `containerName` is the name of a Docker container that is used to run the 137 | executable. This argument is required when running the executable in docker. 138 | 139 | Esoteric options 140 | ---------------- 141 | 142 | * `--masterPort int` 143 | 144 | port for arangodb master (default 4000). 145 | 146 | This is the port used for communication of the `arangodb` instances 147 | amongst each other. 148 | 149 | * `--arangod path` 150 | 151 | path to the `arangod` executable (default varies from platform to 152 | platform, an executable is searched in various places). 153 | 154 | This option only has to be specified if the standard search fails. 155 | 156 | * `--jsDir path` 157 | 158 | path to JS library directory (default varies from platform to platform, 159 | this is coupled to the search for the executable). 160 | 161 | This option only has to be specified if the standard search fails. 162 | 163 | * `--startCoordinator bool` 164 | 165 | This indicates whether or not a coordinator instance should be started 166 | (default true). 167 | 168 | * `--startDBserver bool` 169 | 170 | This indicates whether or not a DBserver instance should be started 171 | (default true). 172 | 173 | * `--rr path` 174 | 175 | path to rr executable to use if non-empty (default ""). Expert and 176 | debugging only. 177 | 178 | * `--verbose bool` 179 | 180 | show more information (default false). 181 | 182 | * `--dockerUser user` 183 | 184 | `user` is an expression to be used for `docker run` with the `--user` 185 | option. One can give a user id or a user id and a group id, separated 186 | by a colon. The purpose of this option is to limit the access rights 187 | of the process in the Docker container. 188 | 189 | * `--dockerEndpoint endpoint` 190 | 191 | `endpoint` is the URL used to reach the docker host. This is needed to run 192 | the executable in docker. The default value is "unix:///var/run/docker.sock". 193 | 194 | * `--dockerNetHost bool` 195 | 196 | If `dockerNetHost` is set, all docker container will be started 197 | with the `--net=host` option. 198 | 199 | * `--dockerPrivileged bool` 200 | 201 | If `dockerPrivileged` is set, all docker container will be started 202 | with the `--privileged` option turned on. 203 | 204 | Future plans 205 | ------------ 206 | 207 | * deploy this program as a Docker image 208 | * bundle this program with the usual distribution 209 | * make port usage configurable 210 | * support SSL 211 | * support authentication 212 | 213 | Technical explanation as to what happens 214 | ---------------------------------------- 215 | 216 | The procedure is essentially that the first instance of `arangodb` (aka 217 | the "master") offers an HTTP service on port 4000 for peers to register. 218 | Every instance that registers becomes a slave. As soon as there are 219 | `agencySize` peers, every instance of `arangodb` starts up an agent (if 220 | it is one of the first 3), a DBserver, and a coordinator. The necessary 221 | command line options to link the `arangod` instances up are generated 222 | automatically. The cluster bootstraps and can be used. 223 | 224 | Whenever an `arangodb` instance shuts down, it shuts down the `arangod` 225 | instances under its control as well. When the `arangodb` is started 226 | again, it recalls the old configuration from the `setup.json` file in 227 | its data directory, starts up its `arangod` instances again (with their 228 | data) and they join the cluster. 229 | 230 | All network addresses are discovered from the HTTP communication between 231 | the `arangodb` instances. The ports used 4001(/4006/4011) for the agent, 232 | 4002(/4007/4012) for the coordinator, 4003(/4008/4013) for the DBserver) 233 | need to be free. If more than one instance of an `arangodb` are started 234 | on the same machine, the second will increase all these port numbers by 5 and so on. 235 | 236 | In case the executable is running in Docker, it will use the Docker 237 | API to retrieve the port number of the Docker host to which the 4000 port 238 | number is mapped. The containers started by the executable will all 239 | map the port they use to the exact same host port. 240 | 241 | Feedback 242 | -------- 243 | 244 | Feedback is very welcome in the form of github issues, pull requests 245 | or simply emails to me: 246 | 247 | `Max Neunhöffer ` 248 | 249 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | Docker: 2 | some progress has been made, challenges remaining: 3 | EXPOSE does not really open the ports, -p seems to still be needed 4 | at runtime or at least --net=host 5 | address recognition does not work, since we get the IP addresses of the 6 | Docker container 7 | data volumes not yet sorted 8 | in short: it does not work yet 9 | 10 | 11 | future: 12 | shutdown via API instead of Kill 13 | observe subprocesses and potentially restart them as long as we are running 14 | configurable base ports 15 | free port testing? 16 | SSL 17 | authentication 18 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.1+git -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "path/filepath" 8 | "runtime" 9 | "sort" 10 | "strings" 11 | "syscall" 12 | "time" 13 | 14 | service "github.com/neunhoef/ArangoDBStarter/service" 15 | logging "github.com/op/go-logging" 16 | "github.com/spf13/cobra" 17 | ) 18 | 19 | // Configuration data with defaults: 20 | 21 | const ( 22 | projectName = "arangodb" 23 | defaultDockerGCDelay = time.Minute * 10 24 | ) 25 | 26 | var ( 27 | projectVersion = "dev" 28 | projectBuild = "dev" 29 | cmdMain = cobra.Command{ 30 | Use: projectName, 31 | Short: "Start ArangoDB clusters with ease", 32 | Run: cmdMainRun, 33 | } 34 | log = logging.MustGetLogger(projectName) 35 | agencySize int 36 | arangodExecutable string 37 | arangodJSstartup string 38 | masterPort int 39 | rrPath string 40 | startCoordinator bool 41 | startDBserver bool 42 | dataDir string 43 | ownAddress string 44 | masterAddress string 45 | verbose bool 46 | serverThreads int 47 | dockerEndpoint string 48 | dockerImage string 49 | dockerUser string 50 | dockerContainer string 51 | dockerGCDelay time.Duration 52 | dockerNetHost bool 53 | dockerPrivileged bool 54 | ) 55 | 56 | func init() { 57 | f := cmdMain.Flags() 58 | f.IntVar(&agencySize, "agencySize", 3, "Number of agents in the cluster") 59 | f.StringVar(&arangodExecutable, "arangod", "/usr/sbin/arangod", "Path of arangod") 60 | f.StringVar(&arangodJSstartup, "jsDir", "/usr/share/arangodb3/js", "Path of arango JS") 61 | f.IntVar(&masterPort, "masterPort", 4000, "Port to listen on for other arangodb's to join") 62 | f.StringVar(&rrPath, "rr", "", "Path of rr") 63 | f.BoolVar(&startCoordinator, "startCoordinator", true, "should a coordinator instance be started") 64 | f.BoolVar(&startDBserver, "startDBserver", true, "should a dbserver instance be started") 65 | f.StringVar(&dataDir, "dataDir", getEnvVar("DATA_DIR", "."), "directory to store all data") 66 | f.StringVar(&ownAddress, "ownAddress", "", "address under which this server is reachable, needed for running arangodb in docker or the case of --agencySize 1 in the master") 67 | f.StringVar(&masterAddress, "join", "", "join a cluster with master at address addr") 68 | f.BoolVar(&verbose, "verbose", false, "Turn on debug logging") 69 | f.IntVar(&serverThreads, "server.threads", 0, "Adjust server.threads of each server") 70 | f.StringVar(&dockerEndpoint, "dockerEndpoint", "unix:///var/run/docker.sock", "Endpoint used to reach the docker daemon") 71 | f.StringVar(&dockerImage, "docker", getEnvVar("DOCKER_IMAGE", ""), "name of the Docker image to use to launch arangod instances (leave empty to avoid using docker)") 72 | f.StringVar(&dockerUser, "dockerUser", "", "use the given name as user to run the Docker container") 73 | f.StringVar(&dockerContainer, "dockerContainer", "", "name of the docker container that is running this process") 74 | f.DurationVar(&dockerGCDelay, "dockerGCDelay", defaultDockerGCDelay, "Delay before stopped containers are garbage collected") 75 | f.BoolVar(&dockerNetHost, "dockerNetHost", false, "Run containers with --net=host") 76 | f.BoolVar(&dockerPrivileged, "dockerPrivileged", false, "Run containers with --privileged") 77 | } 78 | 79 | // handleSignal listens for termination signals and stops this process onup termination. 80 | func handleSignal(sigChannel chan os.Signal, stopChan chan bool) { 81 | signalCount := 0 82 | for s := range sigChannel { 83 | signalCount++ 84 | fmt.Println("Received signal:", s) 85 | if signalCount > 1 { 86 | os.Exit(1) 87 | } 88 | stopChan <- true 89 | } 90 | } 91 | 92 | // For Windows we need to change backslashes to slashes, strangely enough: 93 | func slasher(s string) string { 94 | return strings.Replace(s, "\\", "/", -1) 95 | } 96 | 97 | func findExecutable() { 98 | var pathList = make([]string, 0, 10) 99 | pathList = append(pathList, "build/bin/arangod") 100 | switch runtime.GOOS { 101 | case "windows": 102 | // Look in the default installation location: 103 | foundPaths := make([]string, 0, 20) 104 | basePath := "C:/Program Files" 105 | d, e := os.Open(basePath) 106 | if e == nil { 107 | l, e := d.Readdir(1024) 108 | if e == nil { 109 | for _, n := range l { 110 | if n.IsDir() { 111 | name := n.Name() 112 | if strings.HasPrefix(name, "ArangoDB3 ") || 113 | strings.HasPrefix(name, "ArangoDB3e ") { 114 | foundPaths = append(foundPaths, basePath+"/"+name+ 115 | "/usr/bin/arangod.exe") 116 | } 117 | } 118 | } 119 | } else { 120 | log.Errorf("Could not read directory %s to look for executable.", basePath) 121 | } 122 | d.Close() 123 | } else { 124 | log.Errorf("Could not open directory %s to look for executable.", basePath) 125 | } 126 | sort.Sort(sort.Reverse(sort.StringSlice(foundPaths))) 127 | pathList = append(pathList, foundPaths...) 128 | case "darwin": 129 | pathList = append(pathList, 130 | "/Applications/ArangoDB3-CLI.app/Contents/MacOS/usr/sbin/arangod", 131 | "/usr/local/opt/arangodb/sbin/arangod", 132 | ) 133 | case "linux": 134 | pathList = append(pathList, 135 | "/usr/sbin/arangod", 136 | ) 137 | } 138 | for _, p := range pathList { 139 | if _, e := os.Stat(filepath.Clean(filepath.FromSlash(p))); e == nil || !os.IsNotExist(e) { 140 | arangodExecutable, _ = filepath.Abs(filepath.FromSlash(p)) 141 | if p == "build/bin/arangod" { 142 | arangodJSstartup, _ = filepath.Abs("js") 143 | } else { 144 | arangodJSstartup, _ = filepath.Abs( 145 | filepath.FromSlash(filepath.Dir(p) + "/../share/arangodb3/js")) 146 | } 147 | return 148 | } 149 | } 150 | } 151 | 152 | func main() { 153 | // Find executable and jsdir default in a platform dependent way: 154 | findExecutable() 155 | 156 | cmdMain.Execute() 157 | } 158 | 159 | func cmdMainRun(cmd *cobra.Command, args []string) { 160 | log.Infof("Starting %s version %s, build %s", projectName, projectVersion, projectBuild) 161 | 162 | if verbose { 163 | logging.SetLevel(logging.DEBUG, projectName) 164 | } else { 165 | logging.SetLevel(logging.INFO, projectName) 166 | } 167 | // Some plausibility checks: 168 | if agencySize%2 == 0 || agencySize <= 0 { 169 | log.Fatal("Error: agencySize needs to be a positive, odd number.") 170 | } 171 | if agencySize == 1 && ownAddress == "" { 172 | log.Fatal("Error: if agencySize==1, ownAddress must be given.") 173 | } 174 | if dockerImage != "" && rrPath != "" { 175 | log.Fatal("Error: using --dockerImage and --rr is not possible.") 176 | } 177 | log.Debugf("Using %s as default arangod executable.", arangodExecutable) 178 | log.Debugf("Using %s as default JS dir.", arangodJSstartup) 179 | 180 | // Sort out work directory: 181 | if len(dataDir) == 0 { 182 | dataDir = "." 183 | } 184 | dataDir, _ = filepath.Abs(dataDir) 185 | if err := os.MkdirAll(dataDir, 0755); err != nil { 186 | log.Fatalf("Cannot create data directory %s because %v, giving up.", dataDir, err) 187 | } 188 | 189 | // Interrupt signal: 190 | sigChannel := make(chan os.Signal) 191 | stopChan := make(chan bool) 192 | signal.Notify(sigChannel, os.Interrupt, syscall.SIGTERM) 193 | go handleSignal(sigChannel, stopChan) 194 | 195 | // Create service 196 | service, err := service.NewService(log, service.ServiceConfig{ 197 | AgencySize: agencySize, 198 | ArangodExecutable: arangodExecutable, 199 | ArangodJSstartup: arangodJSstartup, 200 | MasterPort: masterPort, 201 | RrPath: rrPath, 202 | StartCoordinator: startCoordinator, 203 | StartDBserver: startDBserver, 204 | DataDir: dataDir, 205 | OwnAddress: ownAddress, 206 | MasterAddress: masterAddress, 207 | Verbose: verbose, 208 | ServerThreads: serverThreads, 209 | DockerContainer: dockerContainer, 210 | DockerEndpoint: dockerEndpoint, 211 | DockerImage: dockerImage, 212 | DockerUser: dockerUser, 213 | DockerGCDelay: dockerGCDelay, 214 | DockerNetHost: dockerNetHost, 215 | DockerPrivileged: dockerPrivileged, 216 | }) 217 | if err != nil { 218 | log.Fatalf("Failed to create service: %#v", err) 219 | } 220 | 221 | // Run the service 222 | service.Run(stopChan) 223 | } 224 | 225 | // getEnvVar returns the value of the environment variable with given key of the given default 226 | // value of no such variable exist or is empty. 227 | func getEnvVar(key, defaultValue string) string { 228 | value := os.Getenv(key) 229 | if value != "" { 230 | return value 231 | } 232 | return defaultValue 233 | } 234 | -------------------------------------------------------------------------------- /service/arangodb.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | "runtime" 11 | "strconv" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | logging "github.com/op/go-logging" 17 | ) 18 | 19 | type ServiceConfig struct { 20 | AgencySize int 21 | ArangodExecutable string 22 | ArangodJSstartup string 23 | MasterPort int 24 | RrPath string 25 | StartCoordinator bool 26 | StartDBserver bool 27 | DataDir string 28 | OwnAddress string // IP address of used to reach this process 29 | MasterAddress string 30 | Verbose bool 31 | ServerThreads int // If set to something other than 0, this will be added to the commandline of each server with `--server.threads`... 32 | 33 | DockerContainer string // Name of the container running this process 34 | DockerEndpoint string // Where to reach the docker daemon 35 | DockerImage string // Name of Arangodb docker image 36 | DockerUser string 37 | DockerGCDelay time.Duration 38 | DockerNetHost bool 39 | DockerPrivileged bool 40 | } 41 | 42 | type Service struct { 43 | ServiceConfig 44 | log *logging.Logger 45 | state State 46 | starter chan bool 47 | myPeers peers 48 | announcePort int // Port I can be reached on from the outside 49 | mutex sync.Mutex // Mutex used to protect access to this datastructure 50 | allowSameDataDir bool // If set, multiple arangdb instances are allowed to have the same dataDir (docker case) 51 | servers struct { 52 | agentProc Process 53 | dbserverProc Process 54 | coordinatorProc Process 55 | } 56 | stop bool 57 | } 58 | 59 | // NewService creates a new Service instance from the given config. 60 | func NewService(log *logging.Logger, config ServiceConfig) (*Service, error) { 61 | return &Service{ 62 | ServiceConfig: config, 63 | log: log, 64 | state: stateStart, 65 | starter: make(chan bool), 66 | }, nil 67 | } 68 | 69 | // Configuration data with defaults: 70 | 71 | // Overall state: 72 | 73 | type State int 74 | 75 | const ( 76 | stateStart State = iota // initial state after start 77 | stateMaster // finding phase, first instance 78 | stateSlave // finding phase, further instances 79 | stateRunning // running phase 80 | ) 81 | 82 | // State of peers: 83 | 84 | type Peer struct { 85 | Address string // IP address of arangodb peer server 86 | Port int // Port number of arangodb peer server 87 | PortOffset int // Offset to add to base ports for the various servers (agent, coordinator, dbserver) 88 | DataDir string // Directory holding my data 89 | } 90 | 91 | // Peer information. 92 | // When this type (or any of the types used in here) is changed, increase `SetupConfigVersion`. 93 | type peers struct { 94 | Peers []Peer // All peers (index 0 is reserver for the master) 95 | MyIndex int // Index into Peers for myself 96 | AgencySize int // Number of agents 97 | } 98 | 99 | const ( 100 | portOffsetAgent = 1 101 | portOffsetCoordinator = 2 102 | portOffsetDBServer = 3 103 | portOffsetIncrement = 5 // {our http server, agent, coordinator, dbserver, reserved} 104 | ) 105 | 106 | // A helper function: 107 | 108 | func findHost(a string) string { 109 | pos := strings.LastIndex(a, ":") 110 | var host string 111 | if pos > 0 { 112 | host = a[:pos] 113 | } else { 114 | host = a 115 | } 116 | if host == "127.0.0.1" || host == "[::1]" { 117 | host = "localhost" 118 | } 119 | return host 120 | } 121 | 122 | // For Windows we need to change backslashes to slashes, strangely enough: 123 | func slasher(s string) string { 124 | return strings.Replace(s, "\\", "/", -1) 125 | } 126 | 127 | func testInstance(ctx context.Context, address string, port int) (up, cancelled bool) { 128 | instanceUp := make(chan bool) 129 | go func() { 130 | client := &http.Client{Timeout: time.Second * 10} 131 | for i := 0; i < 300; i++ { 132 | url := fmt.Sprintf("http://%s:%d/_api/version", address, port) 133 | r, e := client.Get(url) 134 | if e == nil && r != nil && r.StatusCode == 200 { 135 | instanceUp <- true 136 | break 137 | } 138 | time.Sleep(time.Millisecond * 500) 139 | } 140 | instanceUp <- false 141 | }() 142 | select { 143 | case up := <-instanceUp: 144 | return up, false 145 | case <-ctx.Done(): 146 | return false, true 147 | } 148 | } 149 | 150 | var confFileTemplate = `# ArangoDB configuration file 151 | # 152 | # Documentation: 153 | # https://docs.arangodb.com/Manual/Administration/Configuration/ 154 | # 155 | 156 | [server] 157 | endpoint = tcp://0.0.0.0:%s 158 | threads = %d 159 | 160 | [log] 161 | level = %s 162 | 163 | [javascript] 164 | v8-contexts = %d 165 | ` 166 | 167 | func (s *Service) makeBaseArgs(myHostDir, myContainerDir string, myAddress string, myPort string, mode string) (args []string, configVolumes []Volume) { 168 | hostConfFileName := filepath.Join(myHostDir, "arangod.conf") 169 | containerConfFileName := filepath.Join(myContainerDir, "arangod.conf") 170 | 171 | if runtime.GOOS != "linux" { 172 | configVolumes = append(configVolumes, Volume{ 173 | HostPath: hostConfFileName, 174 | ContainerPath: containerConfFileName, 175 | ReadOnly: true, 176 | }) 177 | } 178 | 179 | if _, err := os.Stat(hostConfFileName); os.IsNotExist(err) { 180 | out, e := os.Create(hostConfFileName) 181 | if e != nil { 182 | s.log.Fatalf("Could not create configuration file %s, error: %#v", hostConfFileName, e) 183 | } 184 | switch mode { 185 | // Parameters are: port, server threads, log level, v8-contexts 186 | case "agent": 187 | fmt.Fprintf(out, confFileTemplate, myPort, 8, "INFO", 1) 188 | case "dbserver": 189 | fmt.Fprintf(out, confFileTemplate, myPort, 4, "INFO", 4) 190 | case "coordinator": 191 | fmt.Fprintf(out, confFileTemplate, myPort, 16, "INFO", 4) 192 | } 193 | out.Close() 194 | } 195 | args = make([]string, 0, 40) 196 | executable := s.ArangodExecutable 197 | jsStartup := s.ArangodJSstartup 198 | if s.RrPath != "" { 199 | args = append(args, s.RrPath) 200 | } 201 | args = append(args, 202 | executable, 203 | "-c", slasher(containerConfFileName), 204 | "--database.directory", slasher(filepath.Join(myContainerDir, "data")), 205 | "--javascript.startup-directory", slasher(jsStartup), 206 | "--javascript.app-path", slasher(filepath.Join(myContainerDir, "apps")), 207 | "--log.file", slasher(filepath.Join(myContainerDir, "arangod.log")), 208 | "--log.force-direct", "false", 209 | "--server.authentication", "false", 210 | ) 211 | if s.ServerThreads != 0 { 212 | args = append(args, "--server.threads", strconv.Itoa(s.ServerThreads)) 213 | } 214 | switch mode { 215 | case "agent": 216 | args = append(args, 217 | "--agency.activate", "true", 218 | "--agency.my-address", fmt.Sprintf("tcp://%s:%s", myAddress, myPort), 219 | "--agency.size", strconv.Itoa(s.AgencySize), 220 | "--agency.supervision", "true", 221 | "--foxx.queues", "false", 222 | "--server.statistics", "false", 223 | ) 224 | for i := 0; i < s.AgencySize; i++ { 225 | if i != s.myPeers.MyIndex { 226 | p := s.myPeers.Peers[i] 227 | args = append(args, 228 | "--agency.endpoint", 229 | fmt.Sprintf("tcp://%s:%d", p.Address, s.MasterPort+p.PortOffset+portOffsetAgent), 230 | ) 231 | } 232 | } 233 | case "dbserver": 234 | args = append(args, 235 | "--cluster.my-address", fmt.Sprintf("tcp://%s:%s", myAddress, myPort), 236 | "--cluster.my-role", "PRIMARY", 237 | "--cluster.my-local-info", fmt.Sprintf("tcp://%s:%s", myAddress, myPort), 238 | "--foxx.queues", "false", 239 | "--server.statistics", "true", 240 | ) 241 | case "coordinator": 242 | args = append(args, 243 | "--cluster.my-address", fmt.Sprintf("tcp://%s:%s", myAddress, myPort), 244 | "--cluster.my-role", "COORDINATOR", 245 | "--cluster.my-local-info", fmt.Sprintf("tcp://%s:%s", myAddress, myPort), 246 | "--foxx.queues", "true", 247 | "--server.statistics", "true", 248 | ) 249 | } 250 | if mode != "agent" { 251 | for i := 0; i < s.AgencySize; i++ { 252 | p := s.myPeers.Peers[i] 253 | args = append(args, 254 | "--cluster.agency-endpoint", 255 | fmt.Sprintf("tcp://%s:%d", p.Address, s.MasterPort+p.PortOffset+portOffsetAgent), 256 | ) 257 | } 258 | } 259 | return 260 | } 261 | 262 | func (s *Service) writeCommand(filename string, executable string, args []string) { 263 | content := strings.Join(args, " \\\n") + "\n" 264 | if _, err := os.Stat(filename); os.IsNotExist(err) { 265 | if err := ioutil.WriteFile(filename, []byte(content), 0755); err != nil { 266 | s.log.Errorf("Failed to write command to %s: %#v", filename, err) 267 | } 268 | } 269 | } 270 | 271 | func (s *Service) startRunning(runner Runner) { 272 | s.state = stateRunning 273 | portOffset := s.myPeers.Peers[s.myPeers.MyIndex].PortOffset 274 | myHost := s.myPeers.Peers[s.myPeers.MyIndex].Address 275 | 276 | var executable string 277 | if s.RrPath != "" { 278 | executable = s.RrPath 279 | } else { 280 | executable = s.ArangodExecutable 281 | } 282 | 283 | addDataVolumes := func(configVolumes []Volume, hostPath, containerPath string) []Volume { 284 | if runtime.GOOS == "linux" { 285 | return []Volume{ 286 | Volume{ 287 | HostPath: hostPath, 288 | ContainerPath: containerPath, 289 | ReadOnly: false, 290 | }, 291 | } 292 | } 293 | return configVolumes 294 | } 295 | 296 | startArangod := func(serverPortOffset int, mode string, restart int) (Process, error) { 297 | myPort := s.MasterPort + portOffset + serverPortOffset 298 | s.log.Infof("Starting %s on port %d", mode, myPort) 299 | myHostDir := filepath.Join(s.DataDir, fmt.Sprintf("%s%d", mode, myPort)) 300 | os.MkdirAll(filepath.Join(myHostDir, "data"), 0755) 301 | os.MkdirAll(filepath.Join(myHostDir, "apps"), 0755) 302 | myContainerDir := runner.GetContainerDir(myHostDir) 303 | args, vols := s.makeBaseArgs(myHostDir, myContainerDir, myHost, strconv.Itoa(myPort), mode) 304 | vols = addDataVolumes(vols, myHostDir, myContainerDir) 305 | s.writeCommand(filepath.Join(myHostDir, "arangod_command.txt"), executable, args) 306 | containerNamePrefix := "" 307 | if s.DockerContainer != "" { 308 | containerNamePrefix = fmt.Sprintf("%s-", s.DockerContainer) 309 | } 310 | containerName := fmt.Sprintf("%s%s-%d-%d-%s-%d", containerNamePrefix, mode, s.myPeers.MyIndex, restart, myHost, myPort) 311 | ports := []int{myPort} 312 | if p, err := runner.Start(args[0], args[1:], vols, ports, containerName); err != nil { 313 | return nil, maskAny(err) 314 | } else { 315 | return p, nil 316 | } 317 | } 318 | 319 | runArangod := func(serverPortOffset int, mode string, processVar *Process, runProcess *bool) { 320 | restart := 0 321 | for { 322 | p, err := startArangod(serverPortOffset, mode, restart) 323 | if err != nil { 324 | s.log.Errorf("Error while starting %s: %#v", mode, err) 325 | break 326 | } 327 | *processVar = p 328 | ctx, cancel := context.WithCancel(context.Background()) 329 | go func() { 330 | if up, cancelled := testInstance(ctx, myHost, s.MasterPort+portOffset+serverPortOffset); !cancelled { 331 | if up { 332 | s.log.Infof("%s up and running.", mode) 333 | } else { 334 | s.log.Warningf("%s not ready after 5min!", mode) 335 | } 336 | } 337 | }() 338 | p.Wait() 339 | cancel() 340 | 341 | s.log.Infof("%s has terminated", mode) 342 | if s.stop { 343 | break 344 | } 345 | s.log.Infof("restarting %s", mode) 346 | restart++ 347 | } 348 | } 349 | 350 | // Start agent: 351 | if s.needsAgent() { 352 | runAlways := true 353 | go runArangod(portOffsetAgent, "agent", &s.servers.agentProc, &runAlways) 354 | } 355 | time.Sleep(time.Second) 356 | 357 | // Start DBserver: 358 | if s.StartDBserver { 359 | go runArangod(portOffsetDBServer, "dbserver", &s.servers.dbserverProc, &s.StartDBserver) 360 | } 361 | 362 | time.Sleep(time.Second) 363 | 364 | // Start Coordinator: 365 | if s.StartCoordinator { 366 | go runArangod(portOffsetCoordinator, "coordinator", &s.servers.coordinatorProc, &s.StartCoordinator) 367 | } 368 | 369 | for { 370 | time.Sleep(time.Second) 371 | if s.stop { 372 | break 373 | } 374 | } 375 | 376 | s.log.Info("Shutting down services...") 377 | if p := s.servers.coordinatorProc; p != nil { 378 | if err := p.Terminate(); err != nil { 379 | s.log.Warningf("Failed to terminate coordinator: %v", err) 380 | } 381 | } 382 | if p := s.servers.dbserverProc; p != nil { 383 | if err := p.Terminate(); err != nil { 384 | s.log.Warningf("Failed to terminate dbserver: %v", err) 385 | } 386 | } 387 | time.Sleep(3 * time.Second) 388 | if p := s.servers.agentProc; p != nil { 389 | if err := p.Terminate(); err != nil { 390 | s.log.Warningf("Failed to terminate agent: %v", err) 391 | } 392 | } 393 | 394 | // Cleanup containers 395 | if p := s.servers.coordinatorProc; p != nil { 396 | if err := p.Cleanup(); err != nil { 397 | s.log.Warningf("Failed to cleanup coordinator: %v", err) 398 | } 399 | } 400 | if p := s.servers.dbserverProc; p != nil { 401 | if err := p.Cleanup(); err != nil { 402 | s.log.Warningf("Failed to cleanup dbserver: %v", err) 403 | } 404 | } 405 | time.Sleep(3 * time.Second) 406 | if p := s.servers.agentProc; p != nil { 407 | if err := p.Cleanup(); err != nil { 408 | s.log.Warningf("Failed to cleanup agent: %v", err) 409 | } 410 | } 411 | 412 | // Cleanup runner 413 | if err := runner.Cleanup(); err != nil { 414 | s.log.Warningf("Failed to cleanup runner: %v", err) 415 | } 416 | } 417 | 418 | // Run runs the service in either master or slave mode. 419 | func (s *Service) Run(stopChan chan bool) { 420 | go func() { 421 | select { 422 | case <-stopChan: 423 | s.stop = true 424 | } 425 | }() 426 | 427 | // Find the port mapping if running in a docker container 428 | if s.DockerContainer != "" { 429 | if s.OwnAddress == "" { 430 | s.log.Fatal("OwnAddress must be specified") 431 | } 432 | hostPort, err := findDockerExposedAddress(s.DockerEndpoint, s.DockerContainer, s.MasterPort) 433 | if err != nil { 434 | if s.DockerNetHost { 435 | s.log.Warningf("Cannot detect port mapping, assuming host mapping") 436 | s.announcePort = s.MasterPort 437 | } else { 438 | s.log.Fatalf("Failed to detect port mapping: %#v", err) 439 | return 440 | } 441 | } else { 442 | s.announcePort = hostPort 443 | } 444 | } else { 445 | s.announcePort = s.MasterPort 446 | } 447 | 448 | // Create a runner 449 | var runner Runner 450 | if s.DockerEndpoint != "" && s.DockerImage != "" { 451 | var err error 452 | runner, err = NewDockerRunner(s.log, s.DockerEndpoint, s.DockerImage, s.DockerUser, s.DockerContainer, s.DockerGCDelay, s.DockerNetHost, s.DockerPrivileged) 453 | if err != nil { 454 | s.log.Fatalf("Failed to create docker runner: %#v", err) 455 | } 456 | s.log.Debug("Using docker runner") 457 | // Set executables to their image path's 458 | s.ArangodExecutable = "/usr/sbin/arangod" 459 | s.ArangodJSstartup = "/usr/share/arangodb3/js" 460 | // Docker setup uses different volumes with same dataDir, allow that 461 | s.allowSameDataDir = true 462 | } else { 463 | runner = NewProcessRunner() 464 | s.log.Debug("Using process runner") 465 | } 466 | 467 | // Is this a new start or a restart? 468 | if s.relaunch(runner) { 469 | return 470 | } 471 | 472 | // Do we have to register? 473 | if s.MasterAddress != "" { 474 | s.state = stateSlave 475 | s.startSlave(s.MasterAddress, runner) 476 | } else { 477 | s.state = stateMaster 478 | s.startMaster(runner) 479 | } 480 | } 481 | 482 | // needsAgent returns true if the agent should run in this instance 483 | func (s *Service) needsAgent() bool { 484 | return s.myPeers.MyIndex < s.AgencySize 485 | } 486 | -------------------------------------------------------------------------------- /service/backoff.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/cenkalti/backoff" 7 | "github.com/juju/errgo" 8 | ) 9 | 10 | type PermanentError struct { 11 | Err error 12 | } 13 | 14 | func (e *PermanentError) Error() string { 15 | return e.Err.Error() 16 | } 17 | 18 | func retry(op func() error, timeout time.Duration) error { 19 | var failure error 20 | wrappedOp := func() error { 21 | if err := op(); err == nil { 22 | return nil 23 | } else { 24 | if pe, ok := errgo.Cause(err).(*PermanentError); ok { 25 | // Detected permanent error 26 | failure = pe.Err 27 | return nil 28 | } else { 29 | return err 30 | } 31 | } 32 | } 33 | b := backoff.NewExponentialBackOff() 34 | b.MaxElapsedTime = timeout 35 | b.MaxInterval = timeout / 3 36 | if err := backoff.Retry(wrappedOp, b); err != nil { 37 | return maskAny(err) 38 | } 39 | if failure != nil { 40 | return maskAny(failure) 41 | } 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /service/docker.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | docker "github.com/fsouza/go-dockerclient" 8 | ) 9 | 10 | // findDockerExposedAddress looks up the external port number to which the given 11 | // port is mapped onto for the given container. 12 | func findDockerExposedAddress(dockerEndpoint, containerName string, port int) (int, error) { 13 | client, err := docker.NewClient(dockerEndpoint) 14 | if err != nil { 15 | return 0, maskAny(err) 16 | } 17 | container, err := client.InspectContainer(containerName) 18 | if err != nil { 19 | return 0, maskAny(err) 20 | } 21 | dockerPort := docker.Port(fmt.Sprintf("%d/tcp", port)) 22 | bindings, ok := container.NetworkSettings.Ports[dockerPort] 23 | if !ok || len(bindings) == 0 { 24 | return 0, maskAny(fmt.Errorf("Cannot find port binding for TCP port %d", port)) 25 | } 26 | hostPort, err := strconv.Atoi(bindings[0].HostPort) 27 | if err != nil { 28 | return 0, maskAny(err) 29 | } 30 | return hostPort, nil 31 | } 32 | -------------------------------------------------------------------------------- /service/error.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/juju/errgo" 5 | ) 6 | 7 | var ( 8 | maskAny = errgo.MaskFunc(errgo.Any) 9 | ) 10 | 11 | type ErrorResponse struct { 12 | Error string 13 | } 14 | -------------------------------------------------------------------------------- /service/master.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | func (s *Service) startMaster(runner Runner) { 10 | // Start HTTP listener 11 | s.startHTTPServer() 12 | 13 | // Permanent loop: 14 | s.log.Infof("Serving as master on %s:%d...", s.OwnAddress, s.announcePort) 15 | 16 | if s.AgencySize == 1 { 17 | s.myPeers.Peers = []Peer{ 18 | Peer{ 19 | Address: s.OwnAddress, 20 | Port: s.announcePort, 21 | PortOffset: 0, 22 | DataDir: s.DataDir, 23 | }, 24 | } 25 | s.myPeers.AgencySize = s.AgencySize 26 | s.myPeers.MyIndex = 0 27 | s.saveSetup() 28 | s.log.Info("Starting service...") 29 | s.startRunning(runner) 30 | return 31 | } 32 | s.log.Infof("Waiting for %d servers to show up.\n", s.AgencySize) 33 | s.log.Infof("Use the following commands to start other servers:") 34 | fmt.Println() 35 | for index := 2; index <= s.AgencySize; index++ { 36 | port := "" 37 | if s.announcePort != s.MasterPort { 38 | port = strconv.Itoa(s.announcePort) 39 | } 40 | fmt.Println(runner.CreateStartArangodbCommand(index, s.OwnAddress, port)) 41 | fmt.Println() 42 | } 43 | for { 44 | time.Sleep(time.Second) 45 | select { 46 | case <-s.starter: 47 | s.saveSetup() 48 | s.log.Info("Starting service...") 49 | s.startRunning(runner) 50 | return 51 | default: 52 | } 53 | if s.stop { 54 | break 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /service/runner.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | type Volume struct { 4 | HostPath string 5 | ContainerPath string 6 | ReadOnly bool 7 | } 8 | 9 | type Runner interface { 10 | GetContainerDir(hostDir string) string 11 | Start(command string, args []string, volumes []Volume, ports []int, containerName string) (Process, error) 12 | CreateStartArangodbCommand(index int, masterIP string, masterPort string) string 13 | 14 | // Cleanup after all processes are dead and have been cleaned themselves 15 | Cleanup() error 16 | } 17 | 18 | type Process interface { 19 | // ProcessID returns the pid of the process (if not running in docker) 20 | ProcessID() int 21 | // ContainerID returns the ID of the docker container that runs the process. 22 | ContainerID() string 23 | 24 | // Wait until the process has terminated 25 | Wait() 26 | // Terminate performs a graceful termination of the process 27 | Terminate() error 28 | // Kill performs a hard termination of the process 29 | Kill() error 30 | 31 | // Remove all traces of this process 32 | Cleanup() error 33 | } 34 | -------------------------------------------------------------------------------- /service/runner_docker.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "sync" 8 | "time" 9 | 10 | docker "github.com/fsouza/go-dockerclient" 11 | "github.com/juju/errgo" 12 | logging "github.com/op/go-logging" 13 | ) 14 | 15 | const ( 16 | stopContainerTimeout = 60 // Seconds before a container is killed (after graceful stop) 17 | ) 18 | 19 | // NewDockerRunner creates a runner that starts processes on the local OS. 20 | func NewDockerRunner(log *logging.Logger, endpoint, image, user, volumesFrom string, gcDelay time.Duration, netHost, privileged bool) (Runner, error) { 21 | client, err := docker.NewClient(endpoint) 22 | if err != nil { 23 | return nil, maskAny(err) 24 | } 25 | return &dockerRunner{ 26 | log: log, 27 | client: client, 28 | image: image, 29 | user: user, 30 | volumesFrom: volumesFrom, 31 | containerIDs: make(map[string]time.Time), 32 | gcDelay: gcDelay, 33 | netHost: netHost, 34 | privileged: privileged, 35 | }, nil 36 | } 37 | 38 | // dockerRunner implements a Runner that starts processes in a docker container. 39 | type dockerRunner struct { 40 | log *logging.Logger 41 | client *docker.Client 42 | image string 43 | user string 44 | volumesFrom string 45 | mutex sync.Mutex 46 | containerIDs map[string]time.Time 47 | gcOnce sync.Once 48 | gcDelay time.Duration 49 | netHost bool 50 | privileged bool 51 | } 52 | 53 | type dockerContainer struct { 54 | client *docker.Client 55 | container *docker.Container 56 | } 57 | 58 | func (r *dockerRunner) GetContainerDir(hostDir string) string { 59 | if r.volumesFrom != "" { 60 | return hostDir 61 | } 62 | return "/data" 63 | } 64 | 65 | func (r *dockerRunner) Start(command string, args []string, volumes []Volume, ports []int, containerName string) (Process, error) { 66 | // Start gc (once) 67 | r.gcOnce.Do(func() { go r.gc() }) 68 | 69 | // Pull docker image 70 | if err := r.pullImage(r.image); err != nil { 71 | return nil, maskAny(err) 72 | } 73 | 74 | // Ensure container name is valid 75 | containerName = strings.Replace(containerName, ":", "", -1) 76 | 77 | var result Process 78 | op := func() error { 79 | // Make sure the container is really gone 80 | r.log.Debugf("Removing container '%s' (if it exists)", containerName) 81 | if err := r.client.RemoveContainer(docker.RemoveContainerOptions{ 82 | ID: containerName, 83 | Force: true, 84 | }); err != nil && !isNoSuchContainer(err) { 85 | r.log.Errorf("Failed to remove container '%s': %v", containerName, err) 86 | } 87 | // Try starting it now 88 | p, err := r.start(command, args, volumes, ports, containerName) 89 | if err != nil { 90 | return maskAny(err) 91 | } 92 | result = p 93 | return nil 94 | } 95 | 96 | if err := retry(op, time.Minute*2); err != nil { 97 | return nil, maskAny(err) 98 | } 99 | return result, nil 100 | } 101 | 102 | // Try to start a command with given arguments 103 | func (r *dockerRunner) start(command string, args []string, volumes []Volume, ports []int, containerName string) (Process, error) { 104 | opts := docker.CreateContainerOptions{ 105 | Name: containerName, 106 | Config: &docker.Config{ 107 | Image: r.image, 108 | Entrypoint: []string{command}, 109 | Cmd: args, 110 | Tty: true, 111 | User: r.user, 112 | ExposedPorts: make(map[docker.Port]struct{}), 113 | }, 114 | HostConfig: &docker.HostConfig{ 115 | PortBindings: make(map[docker.Port][]docker.PortBinding), 116 | PublishAllPorts: false, 117 | AutoRemove: false, 118 | Privileged: r.privileged, 119 | }, 120 | } 121 | if r.volumesFrom != "" { 122 | opts.HostConfig.VolumesFrom = []string{r.volumesFrom} 123 | } else { 124 | for _, v := range volumes { 125 | bind := fmt.Sprintf("%s:%s", v.HostPath, v.ContainerPath) 126 | if v.ReadOnly { 127 | bind = bind + ":ro" 128 | } 129 | opts.HostConfig.Binds = append(opts.HostConfig.Binds, bind) 130 | } 131 | } 132 | if r.netHost { 133 | opts.HostConfig.NetworkMode = "host" 134 | } else { 135 | for _, p := range ports { 136 | dockerPort := docker.Port(fmt.Sprintf("%d/tcp", p)) 137 | opts.Config.ExposedPorts[dockerPort] = struct{}{} 138 | opts.HostConfig.PortBindings[dockerPort] = []docker.PortBinding{ 139 | docker.PortBinding{ 140 | HostIP: "0.0.0.0", 141 | HostPort: strconv.Itoa(p), 142 | }, 143 | } 144 | } 145 | } 146 | r.log.Debugf("Creating container %s", containerName) 147 | c, err := r.client.CreateContainer(opts) 148 | if err != nil { 149 | return nil, maskAny(err) 150 | } 151 | r.recordContainerID(c.ID) // Record ID so we can clean it up later 152 | r.log.Debugf("Starting container %s", containerName) 153 | if err := r.client.StartContainer(c.ID, opts.HostConfig); err != nil { 154 | return nil, maskAny(err) 155 | } 156 | r.log.Debugf("Started container %s", containerName) 157 | return &dockerContainer{ 158 | client: r.client, 159 | container: c, 160 | }, nil 161 | } 162 | 163 | // pullImage tries to pull the given image. 164 | // It retries several times upon failure. 165 | func (r *dockerRunner) pullImage(image string) error { 166 | // Pull docker image 167 | repo, tag := docker.ParseRepositoryTag(r.image) 168 | 169 | op := func() error { 170 | r.log.Debugf("Pulling image %s:%s", repo, tag) 171 | if err := r.client.PullImage(docker.PullImageOptions{ 172 | Repository: repo, 173 | Tag: tag, 174 | }, docker.AuthConfiguration{}); err != nil { 175 | if isNotFound(err) { 176 | return maskAny(&PermanentError{err}) 177 | } 178 | return maskAny(err) 179 | } 180 | return nil 181 | } 182 | 183 | if err := retry(op, time.Minute*2); err != nil { 184 | return maskAny(err) 185 | } 186 | return nil 187 | } 188 | 189 | func (r *dockerRunner) CreateStartArangodbCommand(index int, masterIP string, masterPort string) string { 190 | addr := masterIP 191 | hostPort := 4000 + (portOffsetIncrement * (index - 1)) 192 | if masterPort != "" { 193 | addr = addr + ":" + masterPort 194 | masterPortI, _ := strconv.Atoi(masterPort) 195 | hostPort = masterPortI + (portOffsetIncrement * (index - 1)) 196 | } 197 | lines := []string{ 198 | fmt.Sprintf("docker volume create arangodb%d &&", index), 199 | fmt.Sprintf("docker run -it --name=adb%d --rm -p %d:4000 -v arangodb%d:/data -v /var/run/docker.sock:/var/run/docker.sock arangodb/arangodb-starter", index, hostPort, index), 200 | fmt.Sprintf("--dockerContainer=adb%d --ownAddress=%s --join=%s", index, masterIP, addr), 201 | } 202 | return strings.Join(lines, " \\\n ") 203 | } 204 | 205 | // Cleanup after all processes are dead and have been cleaned themselves 206 | func (r *dockerRunner) Cleanup() error { 207 | r.mutex.Lock() 208 | defer r.mutex.Unlock() 209 | 210 | for id := range r.containerIDs { 211 | r.log.Infof("Removing container %s", id) 212 | if err := r.client.RemoveContainer(docker.RemoveContainerOptions{ 213 | ID: id, 214 | Force: true, 215 | }); err != nil && !isNoSuchContainer(err) { 216 | r.log.Warningf("Failed to remove container %s: %#v", id, err) 217 | } 218 | } 219 | r.containerIDs = nil 220 | 221 | return nil 222 | } 223 | 224 | // recordContainerID records an ID of a created container 225 | func (r *dockerRunner) recordContainerID(id string) { 226 | r.mutex.Lock() 227 | defer r.mutex.Unlock() 228 | r.containerIDs[id] = time.Now() 229 | } 230 | 231 | // unrecordContainerID removes an ID from the list of created containers 232 | func (r *dockerRunner) unrecordContainerID(id string) { 233 | r.mutex.Lock() 234 | defer r.mutex.Unlock() 235 | delete(r.containerIDs, id) 236 | } 237 | 238 | // gc performs continues garbage collection of stopped old containers 239 | func (r *dockerRunner) gc() { 240 | canGC := func(c *docker.Container) bool { 241 | gcBoundary := time.Now().UTC().Add(-r.gcDelay) 242 | switch c.State.StateString() { 243 | case "dead", "exited": 244 | if c.State.FinishedAt.Before(gcBoundary) { 245 | // Dead or exited long enough 246 | return true 247 | } 248 | case "created": 249 | if c.Created.Before(gcBoundary) { 250 | // Created but not running long enough 251 | return true 252 | } 253 | } 254 | return false 255 | } 256 | for { 257 | ids := r.gatherCollectableContainerIDs() 258 | for _, id := range ids { 259 | c, err := r.client.InspectContainer(id) 260 | if err != nil { 261 | if isNoSuchContainer(err) { 262 | // container no longer exists 263 | r.unrecordContainerID(id) 264 | } else { 265 | r.log.Warningf("Failed to inspect container %s: %#v", id, err) 266 | } 267 | } else if canGC(c) { 268 | // Container is dead for more than 10 minutes, gc it. 269 | r.log.Infof("Removing old container %s", id) 270 | if err := r.client.RemoveContainer(docker.RemoveContainerOptions{ 271 | ID: id, 272 | }); err != nil { 273 | r.log.Warningf("Failed to remove container %s: %#v", id, err) 274 | } else { 275 | // Remove succeeded 276 | r.unrecordContainerID(id) 277 | } 278 | } 279 | } 280 | time.Sleep(time.Minute) 281 | } 282 | } 283 | 284 | // gatherCollectableContainerIDs returns all container ID's that are old enough to be consider for garbage collection. 285 | func (r *dockerRunner) gatherCollectableContainerIDs() []string { 286 | r.mutex.Lock() 287 | defer r.mutex.Unlock() 288 | 289 | var result []string 290 | gcBoundary := time.Now().Add(-r.gcDelay) 291 | for id, ts := range r.containerIDs { 292 | if ts.Before(gcBoundary) { 293 | result = append(result, id) 294 | } 295 | } 296 | return result 297 | } 298 | 299 | // ProcessID returns the pid of the process (if not running in docker) 300 | func (p *dockerContainer) ProcessID() int { 301 | return 0 302 | } 303 | 304 | // ContainerID returns the ID of the docker container that runs the process. 305 | func (p *dockerContainer) ContainerID() string { 306 | return p.container.ID 307 | } 308 | 309 | func (p *dockerContainer) Wait() { 310 | p.client.WaitContainer(p.container.ID) 311 | } 312 | 313 | func (p *dockerContainer) Terminate() error { 314 | if err := p.client.StopContainer(p.container.ID, stopContainerTimeout); err != nil { 315 | return maskAny(err) 316 | } 317 | return nil 318 | } 319 | 320 | func (p *dockerContainer) Kill() error { 321 | if err := p.client.KillContainer(docker.KillContainerOptions{ 322 | ID: p.container.ID, 323 | }); err != nil { 324 | return maskAny(err) 325 | } 326 | return nil 327 | } 328 | 329 | func (p *dockerContainer) Cleanup() error { 330 | opts := docker.RemoveContainerOptions{ 331 | ID: p.container.ID, 332 | Force: true, 333 | } 334 | if err := p.client.RemoveContainer(opts); err != nil { 335 | return maskAny(err) 336 | } 337 | return nil 338 | } 339 | 340 | // isNoSuchContainer returns true if the given error is (or is caused by) a NoSuchContainer error. 341 | func isNoSuchContainer(err error) bool { 342 | if _, ok := err.(*docker.NoSuchContainer); ok { 343 | return true 344 | } 345 | if _, ok := errgo.Cause(err).(*docker.NoSuchContainer); ok { 346 | return true 347 | } 348 | return false 349 | } 350 | 351 | // isNotFound returns true if the given error is (or is caused by) a 404 response error. 352 | func isNotFound(err error) bool { 353 | if err, ok := errgo.Cause(err).(*docker.Error); ok { 354 | return err.Status == 404 355 | } 356 | return false 357 | } 358 | -------------------------------------------------------------------------------- /service/runner_process.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "syscall" 7 | ) 8 | 9 | // NewProcessRunner creates a runner that starts processes on the local OS. 10 | func NewProcessRunner() Runner { 11 | return &processRunner{} 12 | } 13 | 14 | // processRunner implements a ProcessRunner that starts processes on the local OS. 15 | type processRunner struct { 16 | } 17 | 18 | type process struct { 19 | cmd *exec.Cmd 20 | } 21 | 22 | func (r *processRunner) GetContainerDir(hostDir string) string { 23 | return hostDir 24 | } 25 | 26 | func (r *processRunner) Start(command string, args []string, volumes []Volume, ports []int, containerName string) (Process, error) { 27 | c := exec.Command(command, args...) 28 | if err := c.Start(); err != nil { 29 | return nil, maskAny(err) 30 | } 31 | return &process{c}, nil 32 | } 33 | 34 | func (r *processRunner) CreateStartArangodbCommand(index int, masterIP string, masterPort string) string { 35 | if masterIP == "" { 36 | masterIP = "127.0.0.1" 37 | } 38 | addr := masterIP 39 | if masterPort != "" { 40 | addr = addr + ":" + masterPort 41 | } 42 | return fmt.Sprintf("arangodb --dataDir=./db%d --join %s", index, addr) 43 | } 44 | 45 | // Cleanup after all processes are dead and have been cleaned themselves 46 | func (r *processRunner) Cleanup() error { 47 | // Nothing here 48 | return nil 49 | } 50 | 51 | // ProcessID returns the pid of the process (if not running in docker) 52 | func (p *process) ProcessID() int { 53 | proc := p.cmd.Process 54 | if proc != nil { 55 | return proc.Pid 56 | } 57 | return 0 58 | } 59 | 60 | // ContainerID returns the ID of the docker container that runs the process. 61 | func (p *process) ContainerID() string { 62 | return "" 63 | } 64 | 65 | func (p *process) Wait() { 66 | proc := p.cmd.Process 67 | if proc != nil { 68 | proc.Wait() 69 | } 70 | } 71 | 72 | func (p *process) Terminate() error { 73 | proc := p.cmd.Process 74 | if proc != nil { 75 | if err := proc.Signal(syscall.SIGTERM); err != nil { 76 | return maskAny(err) 77 | } 78 | } 79 | return nil 80 | } 81 | 82 | func (p *process) Kill() error { 83 | proc := p.cmd.Process 84 | if proc != nil { 85 | if err := proc.Kill(); err != nil { 86 | return maskAny(err) 87 | } 88 | } 89 | return nil 90 | } 91 | 92 | // Remove all traces of this process 93 | func (p *process) Cleanup() error { 94 | // Nothing todo here 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /service/server.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | ) 9 | 10 | type SlaveRequest struct { 11 | SlaveAddress string // Address used to reach the slave (if empty, this will be derived from the request) 12 | SlavePort int // Port used to reach the slave 13 | DataDir string // Directory used for data by this slave 14 | } 15 | 16 | type ProcessListResponse struct { 17 | ServersStarted bool `json:"servers-started,omitempty"` // True if the server have all been started 18 | Servers []ServerProcess `json:"servers,omitempty"` // List of servers started by ArangoDB 19 | } 20 | 21 | type ServerProcess struct { 22 | Type string `json:"type"` // agent | coordinator | dbserver 23 | IP string `json:"ip"` // IP address needed to reach the server 24 | Port int `json:"port"` // Port needed to reach the server 25 | ProcessID int `json:"pid,omitempty"` // PID of the process (0 when running in docker) 26 | ContainerID string `json:"container-id,omitempty"` // ID of docker container running the server 27 | } 28 | 29 | // startHTTPServer initializes and runs the HTTP server. 30 | // If will return directly after starting it. 31 | func (s *Service) startHTTPServer() { 32 | http.HandleFunc("/hello", s.helloHandler) 33 | http.HandleFunc("/process", s.processListHandler) 34 | 35 | go func() { 36 | containerPort, _ := s.getHTTPServerPort() 37 | addr := fmt.Sprintf("0.0.0.0:%d", containerPort) 38 | s.log.Infof("Listening on %s", addr) 39 | if err := http.ListenAndServe(addr, nil); err != nil { 40 | s.log.Errorf("Failed to listen on %s: %v", addr, err) 41 | } 42 | }() 43 | } 44 | 45 | // HTTP service function: 46 | 47 | func (s *Service) helloHandler(w http.ResponseWriter, r *http.Request) { 48 | // Claim exclusive access to our data structures 49 | s.mutex.Lock() 50 | defer s.mutex.Unlock() 51 | 52 | s.log.Debugf("Received request from %s", r.RemoteAddr) 53 | if s.state == stateSlave { 54 | header := w.Header() 55 | if len(s.myPeers.Peers) > 0 { 56 | master := s.myPeers.Peers[0] 57 | header.Add("Location", fmt.Sprintf("http://%s:%d/hello", master.Address, master.Port)) 58 | w.WriteHeader(http.StatusTemporaryRedirect) 59 | } else { 60 | writeError(w, http.StatusBadRequest, "No master known.") 61 | } 62 | return 63 | } 64 | 65 | // Learn my own address (if needed) 66 | if len(s.myPeers.Peers) == 0 { 67 | myself := findHost(r.Host) 68 | _, hostPort := s.getHTTPServerPort() 69 | s.myPeers.Peers = []Peer{ 70 | Peer{ 71 | Address: myself, 72 | Port: hostPort, 73 | PortOffset: 0, 74 | DataDir: s.DataDir, 75 | }, 76 | } 77 | s.myPeers.AgencySize = s.AgencySize 78 | s.myPeers.MyIndex = 0 79 | } 80 | 81 | if r.Method == "POST" { 82 | var req SlaveRequest 83 | defer r.Body.Close() 84 | body, _ := ioutil.ReadAll(r.Body) 85 | json.Unmarshal(body, &req) 86 | 87 | slaveAddr := req.SlaveAddress 88 | if slaveAddr == "" { 89 | slaveAddr = findHost(r.RemoteAddr) 90 | } 91 | slavePort := req.SlavePort 92 | 93 | if !s.allowSameDataDir { 94 | for _, p := range s.myPeers.Peers { 95 | if p.Address == slaveAddr && p.DataDir == req.DataDir { 96 | writeError(w, http.StatusBadRequest, "Cannot use same directory as peer.") 97 | return 98 | } 99 | } 100 | } 101 | 102 | newPeer := Peer{ 103 | Address: slaveAddr, 104 | Port: slavePort, 105 | PortOffset: len(s.myPeers.Peers) * portOffsetIncrement, 106 | DataDir: req.DataDir, 107 | } 108 | s.myPeers.Peers = append(s.myPeers.Peers, newPeer) 109 | s.log.Infof("New peer: %s, portOffset: %d", newPeer.Address, newPeer.PortOffset) 110 | if len(s.myPeers.Peers) == s.AgencySize { 111 | s.starter <- true 112 | } 113 | } 114 | b, err := json.Marshal(s.myPeers) 115 | if err != nil { 116 | writeError(w, http.StatusInternalServerError, err.Error()) 117 | } else { 118 | w.Write(b) 119 | } 120 | } 121 | 122 | func (s *Service) processListHandler(w http.ResponseWriter, r *http.Request) { 123 | // Gather processes 124 | resp := ProcessListResponse{} 125 | peers := s.myPeers.Peers 126 | index := s.myPeers.MyIndex 127 | if index < len(peers) { 128 | p := peers[index] 129 | portOffset := p.PortOffset 130 | ip := p.Address 131 | if p := s.servers.agentProc; p != nil { 132 | resp.Servers = append(resp.Servers, ServerProcess{ 133 | Type: "agent", 134 | IP: ip, 135 | Port: s.MasterPort + portOffset + portOffsetAgent, 136 | ProcessID: p.ProcessID(), 137 | ContainerID: p.ContainerID(), 138 | }) 139 | } 140 | if p := s.servers.coordinatorProc; p != nil { 141 | resp.Servers = append(resp.Servers, ServerProcess{ 142 | Type: "coordinator", 143 | IP: ip, 144 | Port: s.MasterPort + portOffset + portOffsetCoordinator, 145 | ProcessID: p.ProcessID(), 146 | ContainerID: p.ContainerID(), 147 | }) 148 | } 149 | if p := s.servers.dbserverProc; p != nil { 150 | resp.Servers = append(resp.Servers, ServerProcess{ 151 | Type: "dbserver", 152 | IP: ip, 153 | Port: s.MasterPort + portOffset + portOffsetDBServer, 154 | ProcessID: p.ProcessID(), 155 | ContainerID: p.ContainerID(), 156 | }) 157 | } 158 | } 159 | expectedServers := 2 160 | if s.needsAgent() { 161 | expectedServers = 3 162 | } 163 | resp.ServersStarted = len(resp.Servers) == expectedServers 164 | b, err := json.Marshal(resp) 165 | if err != nil { 166 | writeError(w, http.StatusInternalServerError, err.Error()) 167 | } else { 168 | w.Write(b) 169 | } 170 | } 171 | 172 | func writeError(w http.ResponseWriter, status int, message string) { 173 | if message == "" { 174 | message = "Unknown error" 175 | } 176 | resp := ErrorResponse{Error: message} 177 | b, _ := json.Marshal(resp) 178 | w.WriteHeader(status) 179 | w.Write(b) 180 | } 181 | 182 | func (s *Service) getHTTPServerPort() (containerPort, hostPort int) { 183 | port := s.MasterPort 184 | if s.announcePort == s.MasterPort && len(s.myPeers.Peers) > 0 { 185 | port += s.myPeers.Peers[s.myPeers.MyIndex].PortOffset 186 | } 187 | return port, s.announcePort 188 | } 189 | -------------------------------------------------------------------------------- /service/setup_config.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "path/filepath" 7 | ) 8 | 9 | const ( 10 | // Version of the process that created this. If the structure or semantics changed, you must increase this version. 11 | SetupConfigVersion = "0.1.0" 12 | setupFileName = "setup.json" 13 | ) 14 | 15 | // SetupConfigFile is the JSON structure stored in the setup file of this process. 16 | type SetupConfigFile struct { 17 | Version string `json:"version"` // Version of the process that created this. If the structure or semantics changed, you must increase this version. 18 | Peers peers `json:"peers"` 19 | } 20 | 21 | // saveSetup saves the current peer configuration to disk. 22 | func (s *Service) saveSetup() error { 23 | cfg := SetupConfigFile{ 24 | Version: SetupConfigVersion, 25 | Peers: s.myPeers, 26 | } 27 | b, err := json.Marshal(cfg) 28 | if err != nil { 29 | s.log.Errorf("Cannot serialize config: %#v", err) 30 | return maskAny(err) 31 | } 32 | if err := ioutil.WriteFile(filepath.Join(s.DataDir, setupFileName), b, 0644); err != nil { 33 | s.log.Errorf("Error writing setup: %#v", err) 34 | return maskAny(err) 35 | } 36 | return nil 37 | } 38 | 39 | // relaunch tries to read a setup.json config file and relaunch when that file exists and is valid. 40 | // Returns true on relaunch or false to continue with a fresh start. 41 | func (s *Service) relaunch(runner Runner) bool { 42 | // Is this a new start or a restart? 43 | if setupContent, err := ioutil.ReadFile(filepath.Join(s.DataDir, setupFileName)); err == nil { 44 | // Could read file 45 | var cfg SetupConfigFile 46 | if err := json.Unmarshal(setupContent, &cfg); err == nil { 47 | if cfg.Version == SetupConfigVersion { 48 | s.myPeers = cfg.Peers 49 | s.AgencySize = s.myPeers.AgencySize 50 | s.log.Infof("Relaunching service on %s:%d...", s.OwnAddress, s.announcePort) 51 | s.startHTTPServer() 52 | s.startRunning(runner) 53 | return true 54 | } 55 | s.log.Warningf("%s is outdated. Starting fresh...", setupFileName) 56 | } else { 57 | s.log.Warningf("Failed to unmarshal existing %s: %#v", setupFileName, err) 58 | } 59 | } 60 | return false 61 | } 62 | -------------------------------------------------------------------------------- /service/slave.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net" 9 | "net/http" 10 | "strconv" 11 | "time" 12 | ) 13 | 14 | func (s *Service) startSlave(peerAddress string, runner Runner) { 15 | masterPort := s.MasterPort 16 | if host, port, err := net.SplitHostPort(peerAddress); err == nil { 17 | peerAddress = host 18 | masterPort, _ = strconv.Atoi(port) 19 | } 20 | for { 21 | s.log.Infof("Contacting master %s:%d...", peerAddress, masterPort) 22 | _, hostPort := s.getHTTPServerPort() 23 | b, _ := json.Marshal(SlaveRequest{ 24 | DataDir: s.DataDir, 25 | SlaveAddress: s.OwnAddress, 26 | SlavePort: hostPort, 27 | }) 28 | buf := bytes.Buffer{} 29 | buf.Write(b) 30 | r, e := http.Post(fmt.Sprintf("http://%s:%d/hello", peerAddress, masterPort), "application/json", &buf) 31 | if e != nil { 32 | s.log.Infof("Cannot start because of error from master: %v", e) 33 | time.Sleep(time.Second) 34 | continue 35 | } 36 | 37 | body, e := ioutil.ReadAll(r.Body) 38 | defer r.Body.Close() 39 | if e != nil { 40 | s.log.Infof("Cannot start because HTTP response from master was bad: %v", e) 41 | time.Sleep(time.Second) 42 | continue 43 | } 44 | 45 | if r.StatusCode != http.StatusOK { 46 | var errResp ErrorResponse 47 | json.Unmarshal(body, &errResp) 48 | s.log.Fatalf("Cannot start because of HTTP error from master: code=%d, message=%s\n", r.StatusCode, errResp.Error) 49 | } 50 | e = json.Unmarshal(body, &s.myPeers) 51 | if e != nil { 52 | s.log.Warningf("Cannot parse body from master: %v", e) 53 | return 54 | } 55 | s.myPeers.MyIndex = len(s.myPeers.Peers) - 1 56 | s.AgencySize = s.myPeers.AgencySize 57 | break 58 | } 59 | 60 | // Run the HTTP service so we can forward other clients 61 | s.startHTTPServer() 62 | 63 | // Wait until we can start: 64 | if s.AgencySize > 1 { 65 | s.log.Infof("Waiting for %d servers to show up...", s.AgencySize) 66 | } 67 | for { 68 | if len(s.myPeers.Peers) >= s.AgencySize { 69 | s.log.Info("Starting service...") 70 | s.saveSetup() 71 | s.startRunning(runner) 72 | return 73 | } 74 | time.Sleep(time.Second) 75 | master := s.myPeers.Peers[0] 76 | r, err := http.Get(fmt.Sprintf("http://%s:%d/hello", master.Address, master.Port)) 77 | if err != nil { 78 | s.log.Errorf("Failed to connect to master: %v", err) 79 | time.Sleep(time.Second * 2) 80 | } else { 81 | defer r.Body.Close() 82 | body, _ := ioutil.ReadAll(r.Body) 83 | var newPeers peers 84 | json.Unmarshal(body, &newPeers) 85 | s.myPeers.Peers = newPeers.Peers 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CURL="curl --insecure -s -f -X" 4 | testServer() { 5 | PORT=$1 6 | while true ; do 7 | $CURL GET http://127.0.0.1:$PORT/_api/version > /dev/null 2>&1 8 | if [ "$?" != "0" ] ; then 9 | echo Server on port $PORT does not answer yet. 10 | else 11 | echo Server on port $PORT is ready for business. 12 | break 13 | fi 14 | sleep 1 15 | done 16 | } 17 | 18 | rm -rf a b c a.log b.log c.log 19 | mkdir a b c 20 | arangodb/arangodb --dataDir a >a.log & 21 | PID1=$! 22 | sleep 1 23 | arangodb/arangodb --dataDir b --join localhost >b.log & 24 | PID2=$! 25 | sleep 1 26 | arangodb/arangodb --dataDir c --join localhost >c.log & 27 | PID3=$! 28 | 29 | testServer 4001 30 | testServer 4002 31 | testServer 4003 32 | testServer 8629 33 | testServer 8630 34 | testServer 8631 35 | testServer 8530 36 | testServer 8531 37 | testServer 8532 38 | 39 | $CURL POST http://localhost:8530/_api/collection -d '{"name":"c","replicationFactor":2,"numberOfShards":3}' >/dev/null 2>&1 40 | if [ "$?" != "0" ] ; then 41 | echo Could not create collection! 42 | kill ${PID1} ${PID2} ${PID3} 43 | wait 44 | exit 1 45 | fi 46 | 47 | $CURL POST http://localhost:8530/_api/collection/c -d '{"name":"Max"}' >/dev/null 2>&1 48 | if [ "$?" != "0" ] ; then 49 | echo Could not create document! 50 | kill ${PID1} ${PID2} ${PID3} 51 | wait 52 | exit 1 53 | fi 54 | 55 | $CURL DELETE http://localhost:8530/_api/collection/c >/dev/null 2>&1 56 | if [ "$?" != "0" ] ; then 57 | echo Could not drop collection! 58 | kill ${PID1} ${PID2} ${PID3} 59 | wait 60 | exit 1 61 | fi 62 | 63 | echo Cluster seems to work... Sleeping for 15s... 64 | sleep 15 65 | 66 | kill ${PID1} ${PID2} ${PID3} 67 | wait 68 | 69 | echo Test successful 70 | rm -rf a b c a.log b.log c.log 71 | -------------------------------------------------------------------------------- /testdocker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CURL="curl --insecure -s -f -X" 4 | testServer() { 5 | PORT=$1 6 | while true ; do 7 | $CURL GET http://127.0.0.1:$PORT/_api/version > /dev/null 2>&1 8 | if [ "$?" != "0" ] ; then 9 | echo Server on port $PORT does not answer yet. 10 | else 11 | echo Server on port $PORT is ready for business. 12 | break 13 | fi 14 | sleep 1 15 | done 16 | } 17 | 18 | rm -rf a b c a.log b.log c.log 19 | mkdir a b c 20 | arangodb/arangodb --dataDir a --docker arangodb/arangodb --dockerUser `id -u`:`id -g` >a.log & 21 | PID1=$! 22 | sleep 1 23 | arangodb/arangodb --dataDir b --docker arangodb/arangodb --dockerUser `id -u`:`id -g` --join localhost >b.log & 24 | PID2=$! 25 | sleep 1 26 | arangodb/arangodb --dataDir c --docker arangodb/arangodb --dockerUser `id -u`:`id -g` --join localhost >c.log & 27 | PID3=$! 28 | 29 | testServer 4001 30 | testServer 4002 31 | testServer 4003 32 | testServer 8629 33 | testServer 8630 34 | testServer 8631 35 | testServer 8530 36 | testServer 8531 37 | testServer 8532 38 | 39 | $CURL POST http://localhost:8530/_api/collection -d '{"name":"c","replicationFactor":2,"numberOfShards":3}' >/dev/null 2>&1 40 | if [ "$?" != "0" ] ; then 41 | echo Could not create collection! 42 | kill ${PID1} ${PID2} ${PID3} 43 | wait 44 | exit 1 45 | fi 46 | 47 | $CURL POST http://localhost:8530/_api/collection/c -d '{"name":"Max"}' >/dev/null 2>&1 48 | if [ "$?" != "0" ] ; then 49 | echo Could not create document! 50 | kill ${PID1} ${PID2} ${PID3} 51 | wait 52 | exit 1 53 | fi 54 | 55 | $CURL DELETE http://localhost:8530/_api/collection/c >/dev/null 2>&1 56 | if [ "$?" != "0" ] ; then 57 | echo Could not drop collection! 58 | kill ${PID1} ${PID2} ${PID3} 59 | wait 60 | exit 1 61 | fi 62 | 63 | echo Cluster seems to work... Sleeping for 15s... 64 | sleep 15 65 | 66 | kill ${PID1} ${PID2} ${PID3} 67 | wait 68 | 69 | echo Test successful 70 | rm -rf a b c a.log b.log c.log 71 | -------------------------------------------------------------------------------- /tools/release.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | 12 | "github.com/coreos/go-semver/semver" 13 | ) 14 | 15 | var ( 16 | versionFile string 17 | releaseType string 18 | ) 19 | 20 | func init() { 21 | flag.StringVar(&versionFile, "versionfile", "./VERSION", "Path of the VERSION file") 22 | flag.StringVar(&releaseType, "type", "patch", "Type of release to build (major|minor|patch)") 23 | } 24 | 25 | func main() { 26 | checkCleanRepo() 27 | version := bumpVersion(releaseType) 28 | make("clean") 29 | make("docker-push-version") 30 | gitTag(version) 31 | bumpVersion("devel") 32 | } 33 | 34 | func checkCleanRepo() { 35 | output, err := exec.Command("git", "status", "--porcelain").Output() 36 | if err != nil { 37 | log.Fatalf("Failed to check git status: %v\n", err) 38 | } 39 | if strings.TrimSpace(string(output)) != "" { 40 | log.Fatal("Repository has uncommitted changes\n") 41 | } 42 | } 43 | 44 | func make(target string) { 45 | if err := run("make", target); err != nil { 46 | log.Fatalf("Failed to make %s: %v\n", target, err) 47 | } 48 | } 49 | 50 | func bumpVersion(action string) string { 51 | contents, err := ioutil.ReadFile(versionFile) 52 | if err != nil { 53 | log.Fatalf("Cannot read '%s': %v\n", versionFile, err) 54 | } 55 | version := semver.New(strings.TrimSpace(string(contents))) 56 | 57 | switch action { 58 | case "patch": 59 | version.BumpPatch() 60 | case "minor": 61 | version.BumpMinor() 62 | case "major": 63 | version.BumpMajor() 64 | case "devel": 65 | version.Metadata = "git" 66 | } 67 | contents = []byte(version.String()) 68 | 69 | if err := ioutil.WriteFile(versionFile, contents, 0755); err != nil { 70 | log.Fatalf("Cannot write '%s': %v\n", versionFile, err) 71 | } 72 | 73 | gitCommitAll(fmt.Sprintf("Updated to %s", version)) 74 | log.Printf("Updated '%s' to '%s'\n", versionFile, string(contents)) 75 | 76 | return version.String() 77 | } 78 | 79 | func gitCommitAll(message string) { 80 | args := []string{ 81 | "commit", 82 | "--all", 83 | "-m", message, 84 | } 85 | if err := run("git", args...); err != nil { 86 | log.Fatalf("Failed to commit: %v\n", err) 87 | } 88 | if err := run("git", "push"); err != nil { 89 | log.Fatalf("Failed to push commit: %v\n", err) 90 | } 91 | } 92 | 93 | func gitTag(version string) { 94 | if err := run("git", "tag", version); err != nil { 95 | log.Fatalf("Failed to tag: %v\n", err) 96 | } 97 | if err := run("git", "push", "--tags"); err != nil { 98 | log.Fatalf("Failed to push tags: %v\n", err) 99 | } 100 | } 101 | 102 | func run(cmd string, args ...string) error { 103 | c := exec.Command(cmd, args...) 104 | c.Stdout = os.Stdout 105 | c.Stderr = os.Stderr 106 | return c.Run() 107 | } 108 | --------------------------------------------------------------------------------