From a8bcc5107113d89c98afbea5f83396e95aeca5c0 Mon Sep 17 00:00:00 2001 From: Cameron Sparr Date: Tue, 10 Nov 2015 14:21:02 -0700 Subject: [PATCH] Godep update: influxdb --- Godeps/Godeps.json | 4 +- .../github.com/influxdb/influxdb/CHANGELOG.md | 36 + .../influxdb/influxdb/CONTRIBUTING.md | 16 +- .../influxdb/LICENSE_OF_DEPENDENCIES.md | 2 +- .../github.com/influxdb/influxdb/QUERIES.md | 2 +- .../influxdb/influxdb/circle-test.sh | 6 +- .../influxdb/influxdb/client/README.md | 34 +- .../influxdb/influxdb/client/influxdb.go | 33 +- .../influxdb/influxdb/client/influxdb_test.go | 11 + .../influxdb/influxdb/client/v2/client.go | 128 +- .../influxdb/client/v2/client_test.go | 85 +- .../influxdb/client/v2/example/example.go | 129 -- .../influxdb/client/v2/example_test.go | 248 ++++ .../influxdb/{ => cluster}/balancer.go | 2 +- .../influxdb/{ => cluster}/balancer_test.go | 12 +- .../influxdb/cluster/points_writer.go | 53 +- .../influxdb/cluster/points_writer_test.go | 3 + .../influxdb/influxdb/cluster/rpc.go | 16 +- .../influxdb/influxdb/cluster/service.go | 12 +- .../influxdb/cluster/shard_writer_test.go | 10 +- .../influxdb/influxdb/cmd/influx/main.go | 124 +- .../influxdb/influxdb/cmd/influx/main_test.go | 56 + .../influxdb/cmd/influx_inspect/tsm.go | 3 +- .../cmd/influx_stress/influx_stress.go | 4 - .../influxdb/cmd/influxd/restore/restore.go | 11 +- .../influxdb/cmd/influxd/run/config.go | 10 +- .../influxdb/cmd/influxd/run/server.go | 50 +- .../cmd/influxd/run/server_helpers_test.go | 25 +- .../influxdb/cmd/influxd/run/server_test.go | 706 +++++++--- .../influxdb/influxdb/etc/config.sample.toml | 68 +- .../influxdb/influxdb/importer/v8/importer.go | 34 +- .../influxdb/influxdb/influxql/INFLUXQL.md | 14 +- .../influxdb/influxdb/influxql/ast.go | 7 + .../influxdb/influxdb/influxql/parser.go | 10 + .../influxdb/influxdb/influxql/parser_test.go | 16 +- .../influxdb/influxdb/meta/errors.go | 5 +- .../influxdb/meta/internal/meta.pb.go | 37 + .../influxdb/meta/internal/meta.proto | 9 + .../github.com/influxdb/influxdb/meta/rpc.go | 4 +- .../influxdb/influxdb/meta/state.go | 27 +- .../influxdb/meta/statement_executor.go | 20 +- .../influxdb/meta/statement_executor_test.go | 16 +- .../influxdb/influxdb/meta/store.go | 115 +- .../influxdb/influxdb/meta/store_test.go | 26 +- .../influxdb/influxdb/models/points.go | 75 +- .../influxdb/influxdb/models/points_test.go | 220 ++-- .../influxdb/influxdb/monitor/service.go | 7 +- .../github.com/influxdb/influxdb/package.sh | 2 +- .../influxdb/influxdb/pkg/slices/strings.go | 3 + .../influxdb/services/admin/config.go | 8 +- .../influxdb/services/admin/config_test.go | 8 +- .../influxdb/services/admin/service.go | 4 +- .../influxdb/services/collectd/README.md | 20 + .../influxdb/services/collectd/config.go | 25 +- .../influxdb/services/collectd/service.go | 46 +- .../services/continuous_querier/service.go | 6 +- .../influxdb/services/graphite/README.md | 33 +- .../influxdb/services/graphite/config.go | 40 +- .../influxdb/services/graphite/parser.go | 8 +- .../influxdb/services/graphite/parser_test.go | 46 +- .../influxdb/services/graphite/service.go | 41 +- .../services/graphite/service_test.go | 25 +- .../influxdb/influxdb/services/hh/config.go | 6 + .../influxdb/services/hh/config_test.go | 5 + .../influxdb/services/hh/node_processor.go | 293 +++++ .../services/hh/node_processor_test.go | 155 +++ .../influxdb/services/hh/processor.go | 341 ----- .../influxdb/services/hh/processor_test.go | 143 -- .../influxdb/influxdb/services/hh/queue.go | 22 +- .../influxdb/influxdb/services/hh/service.go | 258 ++-- .../influxdb/services/httpd/config.go | 8 +- .../influxdb/services/httpd/config_test.go | 8 +- .../influxdb/services/httpd/handler.go | 40 +- .../influxdb/services/httpd/handler_test.go | 94 +- .../influxdb/services/httpd/service.go | 24 +- .../influxdb/services/opentsdb/handler.go | 7 +- .../influxdb/services/opentsdb/service.go | 41 +- .../services/opentsdb/service_test.go | 4 +- .../influxdb/services/registration/service.go | 78 +- .../influxdb/services/retention/service.go | 16 +- .../influxdb/services/subscriber/service.go | 18 +- .../services/subscriber/service_test.go | 59 + .../influxdb/influxdb/services/udp/README.md | 118 +- .../influxdb/influxdb/services/udp/config.go | 38 +- .../influxdb/influxdb/services/udp/service.go | 94 +- .../influxdb/influxdb/stress/runner.go | 1 - .../github.com/influxdb/influxdb/tcp/mux.go | 2 +- .../influxdb/influxdb/toml/toml_test.go | 19 +- .../influxdb/influxdb/tsdb/aggregate.go | 892 +++++++++++++ .../influxdb/influxdb/tsdb/config.go | 33 +- .../influxdb/influxdb/tsdb/cursor.go | 3 +- .../influxdb/influxdb/tsdb/engine.go | 10 + .../influxdb/influxdb/tsdb/engine/bz1/bz1.go | 12 +- .../influxdb/tsdb/engine/bz1/bz1_test.go | 8 +- .../influxdb/tsdb/engine/tsm1/cursor.go | 6 +- .../influxdb/tsdb/engine/tsm1/encoding.go | 71 +- .../tsdb/engine/tsm1/encoding_test.go | 61 +- .../influxdb/tsdb/engine/tsm1/float.go | 13 +- .../influxdb/tsdb/engine/tsm1/float_test.go | 44 +- .../influxdb/tsdb/engine/tsm1/tsm1.go | 895 +++++++++---- .../influxdb/tsdb/engine/tsm1/tsm1_test.go | 349 ++++- .../influxdb/influxdb/tsdb/engine/tsm1/wal.go | 79 +- .../influxdb/tsdb/engine/tsm1/wal_test.go | 134 +- .../influxdb/influxdb/tsdb/engine/wal/wal.go | 24 +- .../influxdb/influxdb/tsdb/executor.go | 1152 +---------------- .../influxdb/influxdb/tsdb/functions.go | 221 ++-- .../influxdb/influxdb/tsdb/functions_test.go | 20 +- .../github.com/influxdb/influxdb/tsdb/into.go | 9 +- .../influxdb/influxdb/tsdb/mapper.go | 515 -------- .../influxdb/influxdb/tsdb/mapper_test.go | 38 +- .../influxdb/influxdb/tsdb/query_executor.go | 41 +- .../influxdb/tsdb/query_executor_test.go | 16 +- .../github.com/influxdb/influxdb/tsdb/raw.go | 950 ++++++++++++++ .../tsdb/{executor_test.go => raw_test.go} | 912 +++++-------- .../influxdb/influxdb/tsdb/shard.go | 12 +- .../influxdb/influxdb/tsdb/shard_test.go | 14 +- .../influxdb/influxdb/tsdb/store.go | 2 + .../influxdb/influxdb/tsdb/store_test.go | 2 +- 118 files changed, 6999 insertions(+), 4287 deletions(-) delete mode 100644 Godeps/_workspace/src/github.com/influxdb/influxdb/client/v2/example/example.go create mode 100644 Godeps/_workspace/src/github.com/influxdb/influxdb/client/v2/example_test.go rename Godeps/_workspace/src/github.com/influxdb/influxdb/{ => cluster}/balancer.go (99%) rename Godeps/_workspace/src/github.com/influxdb/influxdb/{ => cluster}/balancer_test.go (90%) create mode 100644 Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/node_processor.go create mode 100644 Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/node_processor_test.go delete mode 100644 Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/processor.go delete mode 100644 Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/processor_test.go create mode 100644 Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/aggregate.go create mode 100644 Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/raw.go rename Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/{executor_test.go => raw_test.go} (61%) diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index a3a594e11..c7bd8027c 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -111,8 +111,8 @@ }, { "ImportPath": "github.com/influxdb/influxdb", - "Comment": "v0.9.4-rc1-703-g956efae", - "Rev": "956efaeb94ee57ecd8dc23e2f654b5231204e28f" + "Comment": "v0.9.4-rc1-884-g9625953", + "Rev": "9625953d3e06bd41b18c9d05aa1feccf353e20c8" }, { "ImportPath": "github.com/lib/pq", diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/CHANGELOG.md b/Godeps/_workspace/src/github.com/influxdb/influxdb/CHANGELOG.md index d73148615..b2c878738 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/CHANGELOG.md +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/CHANGELOG.md @@ -1,6 +1,10 @@ ## v0.9.5 [unreleased] +### Release Notes +- Field names for the internal stats have been changed to be more inline with Go style. + ### Features +- [#4098](https://github.com/influxdb/influxdb/pull/4702): Support 'history' command at CLI - [#4098](https://github.com/influxdb/influxdb/issues/4098): Enable `golint` on the code base - uuid subpackage - [#4141](https://github.com/influxdb/influxdb/pull/4141): Control whether each query should be logged - [#4065](https://github.com/influxdb/influxdb/pull/4065): Added precision support in cmd client. Thanks @sbouchex @@ -20,12 +24,26 @@ - [#4379](https://github.com/influxdb/influxdb/pull/4379): Auto-create database for UDP input. - [#4375](https://github.com/influxdb/influxdb/pull/4375): Add Subscriptions so data can be 'forked' out of InfluxDB to another third party. - [#4506](https://github.com/influxdb/influxdb/pull/4506): Register with Enterprise service and upload stats, if token is available. +- [#4516](https://github.com/influxdb/influxdb/pull/4516): Hinted-handoff refactor, with new statistics and diagnostics - [#4501](https://github.com/influxdb/influxdb/pull/4501): Allow filtering SHOW MEASUREMENTS by regex. +- [#4547](https://github.com/influxdb/influxdb/pull/4547): Allow any node to be dropped, even a raft node (even the leader). +- [#4600](https://github.com/influxdb/influxdb/pull/4600): ping endpoint can wait for leader +- [#4648](https://github.com/influxdb/influxdb/pull/4648): UDP Client (v2 client) +- [#4690](https://github.com/influxdb/influxdb/pull/4690): SHOW SHARDS now includes database and policy. Thanks @pires +- [#4676](https://github.com/influxdb/influxdb/pull/4676): UDP service listener performance enhancements +- [#4659](https://github.com/influxdb/influxdb/pull/4659): Support IF EXISTS for DROP DATABASE. Thanks @ch33hau +- [#4721](https://github.com/influxdb/influxdb/pull/4721): Export tsdb.InterfaceValues +- [#4681](https://github.com/influxdb/influxdb/pull/4681): Increase default buffer size for collectd and graphite listeners +- [#4659](https://github.com/influxdb/influxdb/pull/4659): Support IF EXISTS for DROP DATABASE ### Bugfixes +- [#4715](https://github.com/influxdb/influxdb/pull/4715): Fix panic during Raft-close. Fix [issue #4707](https://github.com/influxdb/influxdb/issues/4707). Thanks @oiooj +- [#4643](https://github.com/influxdb/influxdb/pull/4643): Fix panic during backup restoration. Thanks @oiooj +- [#4632](https://github.com/influxdb/influxdb/pull/4632): Fix parsing of IPv6 hosts in client package. Thanks @miguelxpn - [#4389](https://github.com/influxdb/influxdb/pull/4389): Don't add a new segment file on each hinted-handoff purge cycle. - [#4166](https://github.com/influxdb/influxdb/pull/4166): Fix parser error on invalid SHOW - [#3457](https://github.com/influxdb/influxdb/issues/3457): [0.9.3] cannot select field names with prefix + "." that match the measurement name +- [#4704](https://github.com/influxdb/influxdb/pull/4704). Tighten up command parsing within CLI. Thanks @pires - [#4225](https://github.com/influxdb/influxdb/pull/4225): Always display diags in name-sorted order - [#4111](https://github.com/influxdb/influxdb/pull/4111): Update pre-commit hook for go vet composites - [#4136](https://github.com/influxdb/influxdb/pull/4136): Return an error-on-write if target retention policy does not exist. Thanks for the report @ymettier @@ -33,6 +51,7 @@ - [#4124](https://github.com/influxdb/influxdb/issues/4124): Missing defer/recover/panic idiom in HTTPD service - [#4238](https://github.com/influxdb/influxdb/pull/4238): Fully disable hinted-handoff service if so requested. - [#4165](https://github.com/influxdb/influxdb/pull/4165): Tag all Go runtime stats when writing to internal database. +- [#4586](https://github.com/influxdb/influxdb/pull/4586): Exit when invalid engine is selected - [#4118](https://github.com/influxdb/influxdb/issues/4118): Return consistent, correct result for SHOW MEASUREMENTS with multiple AND conditions - [#4191](https://github.com/influxdb/influxdb/pull/4191): Correctly marshal remote mapper responses. Fixes [#4170](https://github.com/influxdb/influxdb/issues/4170) - [#4222](https://github.com/influxdb/influxdb/pull/4222): Graphite TCP connections should not block shutdown @@ -41,6 +60,8 @@ - [#4264](https://github.com/influxdb/influxdb/issues/4264): Refactor map functions to use list of values - [#4278](https://github.com/influxdb/influxdb/pull/4278): Fix error marshalling across the cluster - [#4149](https://github.com/influxdb/influxdb/pull/4149): Fix derivative unnecessarily requires aggregate function. Thanks @peekeri! +- [#4674](https://github.com/influxdb/influxdb/pull/4674): Fix panic during restore. Thanks @simcap. +- [#4725](https://github.com/influxdb/influxdb/pull/4725): Don't list deleted shards during SHOW SHARDS. - [#4237](https://github.com/influxdb/influxdb/issues/4237): DERIVATIVE() edge conditions - [#4263](https://github.com/influxdb/influxdb/issues/4263): derivative does not work when data is missing - [#4293](https://github.com/influxdb/influxdb/pull/4293): Ensure shell is invoked when touching PID file. Thanks @christopherjdickson @@ -56,6 +77,7 @@ - [#4344](https://github.com/influxdb/influxdb/issues/4344): Make client.Write default to client.precision if none is given. - [#3429](https://github.com/influxdb/influxdb/issues/3429): Incorrect parsing of regex containing '/' - [#4374](https://github.com/influxdb/influxdb/issues/4374): Add tsm1 quickcheck tests +- [#4644](https://github.com/influxdb/influxdb/pull/4644): Check for response errors during token check, fixes issue [#4641](https://github.com/influxdb/influxdb/issues/4641) - [#4377](https://github.com/influxdb/influxdb/pull/4377): Hinted handoff should not process dropped nodes - [#4365](https://github.com/influxdb/influxdb/issues/4365): Prevent panic in DecodeSameTypeBlock - [#4280](https://github.com/influxdb/influxdb/issues/4280): Only drop points matching WHERE clause @@ -75,6 +97,20 @@ - [#4486](https://github.com/influxdb/influxdb/pull/4486): Fix missing comments for runner package - [#4497](https://github.com/influxdb/influxdb/pull/4497): Fix sequence in meta proto - [#3367](https://github.com/influxdb/influxdb/issues/3367): Negative timestamps are parsed correctly by the line protocol. +- [#4563](https://github.com/influxdb/influxdb/pull/4536): Fix broken subscriptions updates. +- [#4538](https://github.com/influxdb/influxdb/issues/4538): Dropping database under a write load causes panics +- [#4582](https://github.com/influxdb/influxdb/pull/4582): Correct logging tags in cluster and TCP package. Thanks @oiooj +- [#4513](https://github.com/influxdb/influxdb/issues/4513): TSM1: panic: runtime error: index out of range +- [#4521](https://github.com/influxdb/influxdb/issues/4521): TSM1: panic: decode of short block: got 1, exp 9 +- [#4587](https://github.com/influxdb/influxdb/pull/4587): Prevent NaN float values from being stored +- [#4596](https://github.com/influxdb/influxdb/pull/4596): Skip empty string for start position when parsing line protocol @Thanks @ch33hau +- [#4610](https://github.com/influxdb/influxdb/pull/4610): Make internal stats names consistent with Go style. +- [#4625](https://github.com/influxdb/influxdb/pull/4625): Correctly handle bad write requests. Thanks @oiooj. +- [#4650](https://github.com/influxdb/influxdb/issues/4650): Importer should skip empty lines +- [#4651](https://github.com/influxdb/influxdb/issues/4651): Importer doesn't flush out last batch +- [#4602](https://github.com/influxdb/influxdb/issues/4602): Fixes data race between PointsWriter and Subscriber services. +- [#4691](https://github.com/influxdb/influxdb/issues/4691): Enable toml test `TestConfig_Encode`. +- [#4684](https://github.com/influxdb/influxdb/pull/4684): Add Graphite and UDP section to default config. Thanks @nkatsaros ## v0.9.4 [2015-09-14] diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/CONTRIBUTING.md b/Godeps/_workspace/src/github.com/influxdb/influxdb/CONTRIBUTING.md index ac94488ef..348a364eb 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/CONTRIBUTING.md +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/CONTRIBUTING.md @@ -64,12 +64,6 @@ To assist in review for the PR, please add the following to your pull request co - [ ] Sign [CLA](http://influxdb.com/community/cla.html) (if not already signed) ``` -Use of third-party packages ------------- -A third-party package is defined as one that is not part of the standard Go distribution. Generally speaking we prefer to minimize our use of third-party packages, and avoid them unless absolutely necessarily. We'll often write a little bit of code rather than pull in a third-party package. Of course, we do use some third-party packages -- most importantly we use [BoltDB](https://github.com/boltdb/bolt) as the storage engine. So to maximise the chance your change will be accepted by us, use only the standard libraries, or the third-party packages we have decided to use. - -For rationale, check out the post [The Case Against Third Party Libraries](http://blog.gopheracademy.com/advent-2014/case-against-3pl/). - Signing the CLA --------------- @@ -87,8 +81,8 @@ on how to install it see [the gvm page on github](https://github.com/moovweb/gvm After installing gvm you can install and set the default go version by running the following: - gvm install go1.5 - gvm use go1.5 --default + gvm install go1.5.1 + gvm use go1.5.1 --default Revision Control Systems ------------- @@ -234,6 +228,12 @@ go tool pprof ./influxd influxd.prof ``` Note that when you pass the binary to `go tool pprof` *you must specify the path to the binary*. +Use of third-party packages +------------ +A third-party package is defined as one that is not part of the standard Go distribution. Generally speaking we prefer to minimize our use of third-party packages, and avoid them unless absolutely necessarily. We'll often write a little bit of code rather than pull in a third-party package. Of course, we do use some third-party packages -- most importantly we use [BoltDB](https://github.com/boltdb/bolt) as the storage engine. So to maximise the chance your change will be accepted by us, use only the standard libraries, or the third-party packages we have decided to use. + +For rationale, check out the post [The Case Against Third Party Libraries](http://blog.gopheracademy.com/advent-2014/case-against-3pl/). + Continuous Integration testing ----- InfluxDB uses CircleCI for continuous integration testing. To see how the code is built and tested, check out [this file](https://github.com/influxdb/influxdb/blob/master/circle-test.sh). It closely follows the build and test process outlined above. You can see the exact version of Go InfluxDB uses for testing by consulting that file. diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/LICENSE_OF_DEPENDENCIES.md b/Godeps/_workspace/src/github.com/influxdb/influxdb/LICENSE_OF_DEPENDENCIES.md index abba2b241..7aae45f9d 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/LICENSE_OF_DEPENDENCIES.md +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/LICENSE_OF_DEPENDENCIES.md @@ -15,5 +15,5 @@ - github.com/golang/snappy [BSD LICENSE](https://github.com/golang/snappy/blob/master/LICENSE) - github.com/boltdb/bolt [MIT LICENSE](https://github.com/boltdb/bolt/blob/master/LICENSE) - collectd.org [ISC LICENSE](https://github.com/collectd/go-collectd/blob/master/LICENSE) -- golang.org/x/crypto/bcrypt [BSD LICENSE](https://go.googlesource.com/crypto/+/master/LICENSE) +- golang.org/x/crypto/* [BSD LICENSE](https://github.com/golang/crypto/blob/master/LICENSE) diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/QUERIES.md b/Godeps/_workspace/src/github.com/influxdb/influxdb/QUERIES.md index 7018e82c9..46a9eb1da 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/QUERIES.md +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/QUERIES.md @@ -160,7 +160,7 @@ And the show series output looks like this: # Continuous Queries -Continous queries are going to be inspired by MySQL `TRIGGER` syntax: +Continuous queries are going to be inspired by MySQL `TRIGGER` syntax: http://dev.mysql.com/doc/refman/5.0/en/trigger-syntax.html diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/circle-test.sh b/Godeps/_workspace/src/github.com/influxdb/influxdb/circle-test.sh index 2023eaac3..c8fded14f 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/circle-test.sh +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/circle-test.sh @@ -75,6 +75,10 @@ case $CIRCLE_NODE_INDEX in rc=${PIPESTATUS[0]} ;; 1) + INFLUXDB_DATA_ENGINE="tsm1" go test $PARALLELISM $TIMEOUT -v ./... 2>&1 | tee $CIRCLE_ARTIFACTS/test_logs.txt + rc=${PIPESTATUS[0]} + ;; + 2) # 32bit tests. if [[ -e ~/docker/image.tar ]]; then docker load -i ~/docker/image.tar; fi docker build -f Dockerfile_test_ubuntu32 -t ubuntu-32-influxdb-test . @@ -86,7 +90,7 @@ case $CIRCLE_NODE_INDEX in -c "cd /root/go/src/github.com/influxdb/influxdb && go get -t -d -v ./... && go build -v ./... && go test ${PARALLELISM} ${TIMEOUT} -v ./... 2>&1 | tee /tmp/artifacts/test_logs_i386.txt && exit \${PIPESTATUS[0]}" rc=$? ;; - 2) + 3) GORACE="halt_on_error=1" go test $PARALLELISM $TIMEOUT -v -race ./... 2>&1 | tee $CIRCLE_ARTIFACTS/test_logs_race.txt rc=${PIPESTATUS[0]} ;; diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/client/README.md b/Godeps/_workspace/src/github.com/influxdb/influxdb/client/README.md index 28f91e5cf..8a041128d 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/client/README.md +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/client/README.md @@ -212,10 +212,42 @@ for i, row := range res[0].Series[0].Values { } ``` +### Using the UDP Client + +The **InfluxDB** client also supports writing over UDP. + +```go +func WriteUDP() { + // Make client + c := client.NewUDPClient("localhost:8089") + + // Create a new point batch + bp, _ := client.NewBatchPoints(client.BatchPointsConfig{ + Precision: "s", + }) + + // Create a point and add to batch + tags := map[string]string{"cpu": "cpu-total"} + fields := map[string]interface{}{ + "idle": 10.1, + "system": 53.3, + "user": 46.6, + } + pt, err := client.NewPoint("cpu_usage", tags, fields, time.Now()) + if err != nil { + panic(err.Error()) + } + bp.AddPoint(pt) + + // Write the batch + c.Write(bp) +} +``` + ## Go Docs Please refer to -[http://godoc.org/github.com/influxdb/influxdb/client](http://godoc.org/github.com/influxdb/influxdb/client) +[http://godoc.org/github.com/influxdb/influxdb/client/v2](http://godoc.org/github.com/influxdb/influxdb/client/v2) for documentation. ## See Also diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/client/influxdb.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/client/influxdb.go index 057bb3e14..9e0d72717 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/client/influxdb.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/client/influxdb.go @@ -38,22 +38,21 @@ func ParseConnectionString(path string, ssl bool) (url.URL, error) { var host string var port int - if strings.Contains(path, ":") { - h := strings.Split(path, ":") - i, e := strconv.Atoi(h[1]) - if e != nil { - return url.URL{}, fmt.Errorf("invalid port number %q: %s\n", path, e) - } - port = i - if h[0] == "" { + h, p, err := net.SplitHostPort(path) + if err != nil { + if path == "" { host = DefaultHost } else { - host = h[0] + host = path } - } else { - host = path // If they didn't specify a port, always use the default port port = DefaultPort + } else { + host = h + port, err = strconv.Atoi(p) + if err != nil { + return url.URL{}, fmt.Errorf("invalid port number %q: %s\n", path, err) + } } u := url.URL{ @@ -62,6 +61,7 @@ func ParseConnectionString(path string, ssl bool) (url.URL, error) { if ssl { u.Scheme = "https" } + u.Host = net.JoinHostPort(host, strconv.Itoa(port)) return u, nil @@ -69,7 +69,7 @@ func ParseConnectionString(path string, ssl bool) (url.URL, error) { // Config is used to specify what server to connect to. // URL: The URL of the server connecting to. -// Username/Password are optional. They will be passed via basic auth if provided. +// Username/Password are optional. They will be passed via basic auth if provided. // UserAgent: If not provided, will default "InfluxDBClient", // Timeout: If not provided, will default to 0 (no timeout) type Config struct { @@ -180,7 +180,7 @@ func (c *Client) Query(q Query) (*Response, error) { if decErr != nil { return nil, decErr } - // If we don't have an error in our json response, and didn't get statusOK, then send back an error + // If we don't have an error in our json response, and didn't get StatusOK, then send back an error if resp.StatusCode != http.StatusOK && response.Error() == nil { return &response, fmt.Errorf("received status code %d from server", resp.StatusCode) } @@ -474,7 +474,10 @@ func (p *Point) MarshalJSON() ([]byte, error) { // MarshalString renders string representation of a Point with specified // precision. The default precision is nanoseconds. func (p *Point) MarshalString() string { - pt := models.NewPoint(p.Measurement, p.Tags, p.Fields, p.Time) + pt, err := models.NewPoint(p.Measurement, p.Tags, p.Fields, p.Time) + if err != nil { + return "# ERROR: " + err.Error() + " " + p.Measurement + } if p.Precision == "" || p.Precision == "ns" || p.Precision == "n" { return pt.String() } @@ -561,7 +564,7 @@ func normalizeFields(fields map[string]interface{}) map[string]interface{} { // BatchPoints is used to send batched data in a single write. // Database and Points are required // If no retention policy is specified, it will use the databases default retention policy. -// If tags are specified, they will be "merged" with all points. If a point already has that tag, it is ignored. +// If tags are specified, they will be "merged" with all points. If a point already has that tag, it will be ignored. // If time is specified, it will be applied to any point with an empty time. // Precision can be specified if the time is in epoch format (integer). // Valid values for Precision are n, u, ms, s, m, and h diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/client/influxdb_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/client/influxdb_test.go index 34cedc446..0a1981c83 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/client/influxdb_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/client/influxdb_test.go @@ -547,3 +547,14 @@ func TestClient_NoTimeout(t *testing.T) { t.Fatalf("unexpected error. expected %v, actual %v", nil, err) } } + +func TestClient_ParseConnectionString_IPv6(t *testing.T) { + path := "[fdf5:9ede:1875:0:a9ee:a600:8fe3:d495]:8086" + u, err := client.ParseConnectionString(path, false) + if err != nil { + t.Fatalf("unexpected error, expected %v, actual %v", nil, err) + } + if u.Host != path { + t.Fatalf("ipv6 parse failed, expected %s, actual %s", path, u.Host) + } +} diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/client/v2/client.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/client/v2/client.go index be3692a91..a908b06c1 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/client/v2/client.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/client/v2/client.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "net" "net/http" "net/url" "time" @@ -13,6 +14,12 @@ import ( "github.com/influxdb/influxdb/models" ) +// UDPPayloadSize is a reasonable default payload size for UDP packets that +// could be travelling over the internet. +const ( + UDPPayloadSize = 512 +) + type Config struct { // URL of the InfluxDB database URL *url.URL @@ -34,6 +41,15 @@ type Config struct { InsecureSkipVerify bool } +type UDPConfig struct { + // Addr should be of the form "host:port" or "[ipv6-host%zone]:port". + Addr string + + // PayloadSize is the maximum size of a UDP client message, optional + // Tune this based on your network. Defaults to UDPBufferSize. + PayloadSize int +} + type BatchPointsConfig struct { // Precision is the write precision of the points, defaults to "ns" Precision string @@ -48,12 +64,17 @@ type BatchPointsConfig struct { WriteConsistency string } +// Client is a client interface for writing & querying the database type Client interface { // Write takes a BatchPoints object and writes all Points to InfluxDB. Write(bp BatchPoints) error - // Query makes an InfluxDB Query on the database + // Query makes an InfluxDB Query on the database. This will fail if using + // the UDP client. Query(q Query) (*Response, error) + + // Close releases any resources a Client may be using. + Close() error } // NewClient creates a client interface from the given config. @@ -78,6 +99,41 @@ func NewClient(conf Config) Client { } } +// Close releases the client's resources. +func (c *client) Close() error { + return nil +} + +// NewUDPClient returns a client interface for writing to an InfluxDB UDP +// service from the given config. +func NewUDPClient(conf UDPConfig) (Client, error) { + var udpAddr *net.UDPAddr + udpAddr, err := net.ResolveUDPAddr("udp", conf.Addr) + if err != nil { + return nil, err + } + + conn, err := net.DialUDP("udp", nil, udpAddr) + if err != nil { + return nil, err + } + + payloadSize := conf.PayloadSize + if payloadSize == 0 { + payloadSize = UDPPayloadSize + } + + return &udpclient{ + conn: conn, + payloadSize: payloadSize, + }, nil +} + +// Close releases the udpclient's resources. +func (uc *udpclient) Close() error { + return uc.conn.Close() +} + type client struct { url *url.URL username string @@ -86,6 +142,11 @@ type client struct { httpClient *http.Client } +type udpclient struct { + conn *net.UDPConn + payloadSize int +} + // BatchPoints is an interface into a batched grouping of points to write into // InfluxDB together. BatchPoints is NOT thread-safe, you must create a separate // batch for each goroutine. @@ -198,14 +259,19 @@ func NewPoint( tags map[string]string, fields map[string]interface{}, t ...time.Time, -) *Point { +) (*Point, error) { var T time.Time if len(t) > 0 { T = t[0] } - return &Point{ - pt: models.NewPoint(name, tags, fields, T), + + pt, err := models.NewPoint(name, tags, fields, T) + if err != nil { + return nil, err } + return &Point{ + pt: pt, + }, nil } // String returns a line-protocol string of the Point @@ -243,11 +309,34 @@ func (p *Point) Fields() map[string]interface{} { return p.pt.Fields() } -func (c *client) Write(bp BatchPoints) error { - u := c.url - u.Path = "write" - +func (uc *udpclient) Write(bp BatchPoints) error { var b bytes.Buffer + var d time.Duration + d, _ = time.ParseDuration("1" + bp.Precision()) + + for _, p := range bp.Points() { + pointstring := p.pt.RoundedString(d) + "\n" + + // Write and reset the buffer if we reach the max size + if b.Len()+len(pointstring) >= uc.payloadSize { + if _, err := uc.conn.Write(b.Bytes()); err != nil { + return err + } + b.Reset() + } + + if _, err := b.WriteString(pointstring); err != nil { + return err + } + } + + _, err := uc.conn.Write(b.Bytes()) + return err +} + +func (c *client) Write(bp BatchPoints) error { + var b bytes.Buffer + for _, p := range bp.Points() { if _, err := b.WriteString(p.pt.PrecisionString(bp.Precision())); err != nil { return err @@ -258,6 +347,8 @@ func (c *client) Write(bp BatchPoints) error { } } + u := c.url + u.Path = "write" req, err := http.NewRequest("POST", u.String(), &b) if err != nil { return err @@ -327,28 +418,33 @@ type Result struct { Err error } +func (uc *udpclient) Query(q Query) (*Response, error) { + return nil, fmt.Errorf("Querying via UDP is not supported") +} + // Query sends a command to the server and returns the Response func (c *client) Query(q Query) (*Response, error) { u := c.url - u.Path = "query" - values := u.Query() - values.Set("q", q.Command) - values.Set("db", q.Database) - if q.Precision != "" { - values.Set("epoch", q.Precision) - } - u.RawQuery = values.Encode() req, err := http.NewRequest("GET", u.String(), nil) if err != nil { return nil, err } + req.Header.Set("Content-Type", "") req.Header.Set("User-Agent", c.useragent) if c.username != "" { req.SetBasicAuth(c.username, c.password) } + params := req.URL.Query() + params.Set("q", q.Command) + params.Set("db", q.Database) + if q.Precision != "" { + params.Set("epoch", q.Precision) + } + req.URL.RawQuery = params.Encode() + resp, err := c.httpClient.Do(req) if err != nil { return nil, err diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/client/v2/client_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/client/v2/client_test.go index 29a33c689..a1998f98b 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/client/v2/client_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/client/v2/client_test.go @@ -11,6 +11,53 @@ import ( "time" ) +func TestUDPClient_Query(t *testing.T) { + config := UDPConfig{Addr: "localhost:8089"} + c, err := NewUDPClient(config) + if err != nil { + t.Errorf("unexpected error. expected %v, actual %v", nil, err) + } + defer c.Close() + query := Query{} + _, err = c.Query(query) + if err == nil { + t.Error("Querying UDP client should fail") + } +} + +func TestUDPClient_Write(t *testing.T) { + config := UDPConfig{Addr: "localhost:8089"} + c, err := NewUDPClient(config) + if err != nil { + t.Errorf("unexpected error. expected %v, actual %v", nil, err) + } + defer c.Close() + + bp, err := NewBatchPoints(BatchPointsConfig{}) + if err != nil { + t.Errorf("unexpected error. expected %v, actual %v", nil, err) + } + + fields := make(map[string]interface{}) + fields["value"] = 1.0 + pt, _ := NewPoint("cpu", make(map[string]string), fields) + bp.AddPoint(pt) + + err = c.Write(bp) + if err != nil { + t.Errorf("unexpected error. expected %v, actual %v", nil, err) + } +} + +func TestUDPClient_BadAddr(t *testing.T) { + config := UDPConfig{Addr: "foobar@wahoo"} + c, err := NewUDPClient(config) + if err == nil { + defer c.Close() + t.Error("Expected resolve error") + } +} + func TestClient_Query(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var data Response @@ -22,11 +69,12 @@ func TestClient_Query(t *testing.T) { u, _ := url.Parse(ts.URL) config := Config{URL: u} c := NewClient(config) + defer c.Close() query := Query{} _, err := c.Query(query) if err != nil { - t.Fatalf("unexpected error. expected %v, actual %v", nil, err) + t.Errorf("unexpected error. expected %v, actual %v", nil, err) } } @@ -53,11 +101,12 @@ func TestClient_BasicAuth(t *testing.T) { u.User = url.UserPassword("username", "password") config := Config{URL: u, Username: "username", Password: "password"} c := NewClient(config) + defer c.Close() query := Query{} _, err := c.Query(query) if err != nil { - t.Fatalf("unexpected error. expected %v, actual %v", nil, err) + t.Errorf("unexpected error. expected %v, actual %v", nil, err) } } @@ -72,14 +121,15 @@ func TestClient_Write(t *testing.T) { u, _ := url.Parse(ts.URL) config := Config{URL: u} c := NewClient(config) + defer c.Close() bp, err := NewBatchPoints(BatchPointsConfig{}) if err != nil { - t.Fatalf("unexpected error. expected %v, actual %v", nil, err) + t.Errorf("unexpected error. expected %v, actual %v", nil, err) } err = c.Write(bp) if err != nil { - t.Fatalf("unexpected error. expected %v, actual %v", nil, err) + t.Errorf("unexpected error. expected %v, actual %v", nil, err) } } @@ -96,7 +146,7 @@ func TestClient_UserAgent(t *testing.T) { _, err := http.Get(ts.URL) if err != nil { - t.Fatalf("unexpected error. expected %v, actual %v", nil, err) + t.Errorf("unexpected error. expected %v, actual %v", nil, err) } tests := []struct { @@ -120,34 +170,35 @@ func TestClient_UserAgent(t *testing.T) { u, _ := url.Parse(ts.URL) config := Config{URL: u, UserAgent: test.userAgent} c := NewClient(config) + defer c.Close() receivedUserAgent = "" query := Query{} _, err = c.Query(query) if err != nil { - t.Fatalf("unexpected error. expected %v, actual %v", nil, err) + t.Errorf("unexpected error. expected %v, actual %v", nil, err) } if !strings.HasPrefix(receivedUserAgent, test.expected) { - t.Fatalf("Unexpected user agent. expected %v, actual %v", test.expected, receivedUserAgent) + t.Errorf("Unexpected user agent. expected %v, actual %v", test.expected, receivedUserAgent) } receivedUserAgent = "" bp, _ := NewBatchPoints(BatchPointsConfig{}) err = c.Write(bp) if err != nil { - t.Fatalf("unexpected error. expected %v, actual %v", nil, err) + t.Errorf("unexpected error. expected %v, actual %v", nil, err) } if !strings.HasPrefix(receivedUserAgent, test.expected) { - t.Fatalf("Unexpected user agent. expected %v, actual %v", test.expected, receivedUserAgent) + t.Errorf("Unexpected user agent. expected %v, actual %v", test.expected, receivedUserAgent) } receivedUserAgent = "" _, err := c.Query(query) if err != nil { - t.Fatalf("unexpected error. expected %v, actual %v", nil, err) + t.Errorf("unexpected error. expected %v, actual %v", nil, err) } if receivedUserAgent != test.expected { - t.Fatalf("Unexpected user agent. expected %v, actual %v", test.expected, receivedUserAgent) + t.Errorf("Unexpected user agent. expected %v, actual %v", test.expected, receivedUserAgent) } } } @@ -157,7 +208,7 @@ func TestClient_PointString(t *testing.T) { time1, _ := time.Parse(shortForm, "2013-Feb-03") tags := map[string]string{"cpu": "cpu-total"} fields := map[string]interface{}{"idle": 10.1, "system": 50.9, "user": 39.0} - p := NewPoint("cpu_usage", tags, fields, time1) + p, _ := NewPoint("cpu_usage", tags, fields, time1) s := "cpu_usage,cpu=cpu-total idle=10.1,system=50.9,user=39 1359849600000000000" if p.String() != s { @@ -174,7 +225,7 @@ func TestClient_PointString(t *testing.T) { func TestClient_PointWithoutTimeString(t *testing.T) { tags := map[string]string{"cpu": "cpu-total"} fields := map[string]interface{}{"idle": 10.1, "system": 50.9, "user": 39.0} - p := NewPoint("cpu_usage", tags, fields) + p, _ := NewPoint("cpu_usage", tags, fields) s := "cpu_usage,cpu=cpu-total idle=10.1,system=50.9,user=39" if p.String() != s { @@ -190,7 +241,7 @@ func TestClient_PointWithoutTimeString(t *testing.T) { func TestClient_PointName(t *testing.T) { tags := map[string]string{"cpu": "cpu-total"} fields := map[string]interface{}{"idle": 10.1, "system": 50.9, "user": 39.0} - p := NewPoint("cpu_usage", tags, fields) + p, _ := NewPoint("cpu_usage", tags, fields) exp := "cpu_usage" if p.Name() != exp { @@ -202,7 +253,7 @@ func TestClient_PointName(t *testing.T) { func TestClient_PointTags(t *testing.T) { tags := map[string]string{"cpu": "cpu-total"} fields := map[string]interface{}{"idle": 10.1, "system": 50.9, "user": 39.0} - p := NewPoint("cpu_usage", tags, fields) + p, _ := NewPoint("cpu_usage", tags, fields) if !reflect.DeepEqual(tags, p.Tags()) { t.Errorf("Error, got %v, expected %v", @@ -215,7 +266,7 @@ func TestClient_PointUnixNano(t *testing.T) { time1, _ := time.Parse(shortForm, "2013-Feb-03") tags := map[string]string{"cpu": "cpu-total"} fields := map[string]interface{}{"idle": 10.1, "system": 50.9, "user": 39.0} - p := NewPoint("cpu_usage", tags, fields, time1) + p, _ := NewPoint("cpu_usage", tags, fields, time1) exp := int64(1359849600000000000) if p.UnixNano() != exp { @@ -227,7 +278,7 @@ func TestClient_PointUnixNano(t *testing.T) { func TestClient_PointFields(t *testing.T) { tags := map[string]string{"cpu": "cpu-total"} fields := map[string]interface{}{"idle": 10.1, "system": 50.9, "user": 39.0} - p := NewPoint("cpu_usage", tags, fields) + p, _ := NewPoint("cpu_usage", tags, fields) if !reflect.DeepEqual(fields, p.Fields()) { t.Errorf("Error, got %v, expected %v", diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/client/v2/example/example.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/client/v2/example/example.go deleted file mode 100644 index 20fb1f9f4..000000000 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/client/v2/example/example.go +++ /dev/null @@ -1,129 +0,0 @@ -package client_example - -import ( - "fmt" - "log" - "math/rand" - "net/url" - "os" - "time" - - "github.com/influxdb/influxdb/client/v2" -) - -func ExampleNewClient() client.Client { - u, _ := url.Parse("http://localhost:8086") - - // NOTE: this assumes you've setup a user and have setup shell env variables, - // namely INFLUX_USER/INFLUX_PWD. If not just omit Username/Password below. - client := client.NewClient(client.Config{ - URL: u, - Username: os.Getenv("INFLUX_USER"), - Password: os.Getenv("INFLUX_PWD"), - }) - return client -} - -func ExampleWrite() { - // Make client - u, _ := url.Parse("http://localhost:8086") - c := client.NewClient(client.Config{ - URL: u, - }) - - // Create a new point batch - bp, _ := client.NewBatchPoints(client.BatchPointsConfig{ - Database: "BumbleBeeTuna", - Precision: "s", - }) - - // Create a point and add to batch - tags := map[string]string{"cpu": "cpu-total"} - fields := map[string]interface{}{ - "idle": 10.1, - "system": 53.3, - "user": 46.6, - } - pt := client.NewPoint("cpu_usage", tags, fields, time.Now()) - bp.AddPoint(pt) - - // Write the batch - c.Write(bp) -} - -// Write 1000 points -func ExampleWrite1000() { - sampleSize := 1000 - - // Make client - u, _ := url.Parse("http://localhost:8086") - clnt := client.NewClient(client.Config{ - URL: u, - }) - - rand.Seed(42) - - bp, _ := client.NewBatchPoints(client.BatchPointsConfig{ - Database: "systemstats", - Precision: "us", - }) - - for i := 0; i < sampleSize; i++ { - regions := []string{"us-west1", "us-west2", "us-west3", "us-east1"} - tags := map[string]string{ - "cpu": "cpu-total", - "host": fmt.Sprintf("host%d", rand.Intn(1000)), - "region": regions[rand.Intn(len(regions))], - } - - idle := rand.Float64() * 100.0 - fields := map[string]interface{}{ - "idle": idle, - "busy": 100.0 - idle, - } - - bp.AddPoint(client.NewPoint( - "cpu_usage", - tags, - fields, - time.Now(), - )) - } - - err := clnt.Write(bp) - if err != nil { - log.Fatal(err) - } -} - -func ExampleQuery() { - // Make client - u, _ := url.Parse("http://localhost:8086") - c := client.NewClient(client.Config{ - URL: u, - }) - - q := client.Query{ - Command: "SELECT count(value) FROM shapes", - Database: "square_holes", - Precision: "ns", - } - if response, err := c.Query(q); err == nil && response.Error() == nil { - log.Println(response.Results) - } -} - -func ExampleCreateDatabase() { - // Make client - u, _ := url.Parse("http://localhost:8086") - c := client.NewClient(client.Config{ - URL: u, - }) - - q := client.Query{ - Command: "CREATE DATABASE telegraf", - } - if response, err := c.Query(q); err == nil && response.Error() == nil { - log.Println(response.Results) - } -} diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/client/v2/example_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/client/v2/example_test.go new file mode 100644 index 000000000..1b520de49 --- /dev/null +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/client/v2/example_test.go @@ -0,0 +1,248 @@ +package client_test + +import ( + "fmt" + "log" + "math/rand" + "net/url" + "os" + "time" + + "github.com/influxdb/influxdb/client/v2" +) + +// Create a new client +func ExampleClient() client.Client { + u, _ := url.Parse("http://localhost:8086") + + // NOTE: this assumes you've setup a user and have setup shell env variables, + // namely INFLUX_USER/INFLUX_PWD. If not just omit Username/Password below. + client := client.NewClient(client.Config{ + URL: u, + Username: os.Getenv("INFLUX_USER"), + Password: os.Getenv("INFLUX_PWD"), + }) + return client +} + +// Write a point using the UDP client +func ExampleClient_uDP() { + // Make client + config := client.UDPConfig{Addr: "localhost:8089"} + c, err := client.NewUDPClient(config) + if err != nil { + panic(err.Error()) + } + defer c.Close() + + // Create a new point batch + bp, _ := client.NewBatchPoints(client.BatchPointsConfig{ + Precision: "s", + }) + + // Create a point and add to batch + tags := map[string]string{"cpu": "cpu-total"} + fields := map[string]interface{}{ + "idle": 10.1, + "system": 53.3, + "user": 46.6, + } + pt, err := client.NewPoint("cpu_usage", tags, fields, time.Now()) + if err != nil { + panic(err.Error()) + } + bp.AddPoint(pt) + + // Write the batch + c.Write(bp) +} + +// Write a point using the HTTP client +func ExampleClient_write() { + // Make client + u, _ := url.Parse("http://localhost:8086") + c := client.NewClient(client.Config{ + URL: u, + }) + defer c.Close() + + // Create a new point batch + bp, _ := client.NewBatchPoints(client.BatchPointsConfig{ + Database: "BumbleBeeTuna", + Precision: "s", + }) + + // Create a point and add to batch + tags := map[string]string{"cpu": "cpu-total"} + fields := map[string]interface{}{ + "idle": 10.1, + "system": 53.3, + "user": 46.6, + } + pt, err := client.NewPoint("cpu_usage", tags, fields, time.Now()) + if err != nil { + panic(err.Error()) + } + bp.AddPoint(pt) + + // Write the batch + c.Write(bp) +} + +// Create a batch and add a point +func ExampleBatchPoints() { + // Create a new point batch + bp, _ := client.NewBatchPoints(client.BatchPointsConfig{ + Database: "BumbleBeeTuna", + Precision: "s", + }) + + // Create a point and add to batch + tags := map[string]string{"cpu": "cpu-total"} + fields := map[string]interface{}{ + "idle": 10.1, + "system": 53.3, + "user": 46.6, + } + pt, err := client.NewPoint("cpu_usage", tags, fields, time.Now()) + if err != nil { + panic(err.Error()) + } + bp.AddPoint(pt) +} + +// Using the BatchPoints setter functions +func ExampleBatchPoints_setters() { + // Create a new point batch + bp, _ := client.NewBatchPoints(client.BatchPointsConfig{}) + bp.SetDatabase("BumbleBeeTuna") + bp.SetPrecision("ms") + + // Create a point and add to batch + tags := map[string]string{"cpu": "cpu-total"} + fields := map[string]interface{}{ + "idle": 10.1, + "system": 53.3, + "user": 46.6, + } + pt, err := client.NewPoint("cpu_usage", tags, fields, time.Now()) + if err != nil { + panic(err.Error()) + } + bp.AddPoint(pt) +} + +// Create a new point with a timestamp +func ExamplePoint() { + tags := map[string]string{"cpu": "cpu-total"} + fields := map[string]interface{}{ + "idle": 10.1, + "system": 53.3, + "user": 46.6, + } + pt, err := client.NewPoint("cpu_usage", tags, fields, time.Now()) + if err == nil { + fmt.Println("We created a point: ", pt.String()) + } +} + +// Create a new point without a timestamp +func ExamplePoint_withoutTime() { + tags := map[string]string{"cpu": "cpu-total"} + fields := map[string]interface{}{ + "idle": 10.1, + "system": 53.3, + "user": 46.6, + } + pt, err := client.NewPoint("cpu_usage", tags, fields) + if err == nil { + fmt.Println("We created a point w/o time: ", pt.String()) + } +} + +// Write 1000 points +func ExampleClient_write1000() { + sampleSize := 1000 + + // Make client + u, _ := url.Parse("http://localhost:8086") + clnt := client.NewClient(client.Config{ + URL: u, + }) + defer clnt.Close() + + rand.Seed(42) + + bp, _ := client.NewBatchPoints(client.BatchPointsConfig{ + Database: "systemstats", + Precision: "us", + }) + + for i := 0; i < sampleSize; i++ { + regions := []string{"us-west1", "us-west2", "us-west3", "us-east1"} + tags := map[string]string{ + "cpu": "cpu-total", + "host": fmt.Sprintf("host%d", rand.Intn(1000)), + "region": regions[rand.Intn(len(regions))], + } + + idle := rand.Float64() * 100.0 + fields := map[string]interface{}{ + "idle": idle, + "busy": 100.0 - idle, + } + + pt, err := client.NewPoint( + "cpu_usage", + tags, + fields, + time.Now(), + ) + if err != nil { + println("Error:", err.Error()) + continue + } + bp.AddPoint(pt) + } + + err := clnt.Write(bp) + if err != nil { + log.Fatal(err) + } +} + +// Make a Query +func ExampleClient_query() { + // Make client + u, _ := url.Parse("http://localhost:8086") + c := client.NewClient(client.Config{ + URL: u, + }) + defer c.Close() + + q := client.Query{ + Command: "SELECT count(value) FROM shapes", + Database: "square_holes", + Precision: "ns", + } + if response, err := c.Query(q); err == nil && response.Error() == nil { + log.Println(response.Results) + } +} + +// Create a Database with a query +func ExampleClient_createDatabase() { + // Make client + u, _ := url.Parse("http://localhost:8086") + c := client.NewClient(client.Config{ + URL: u, + }) + defer c.Close() + + q := client.Query{ + Command: "CREATE DATABASE telegraf", + } + if response, err := c.Query(q); err == nil && response.Error() == nil { + log.Println(response.Results) + } +} diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/balancer.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/balancer.go similarity index 99% rename from Godeps/_workspace/src/github.com/influxdb/influxdb/balancer.go rename to Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/balancer.go index d7cadd91f..cf7efddd2 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/balancer.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/balancer.go @@ -1,4 +1,4 @@ -package influxdb +package cluster import ( "math/rand" diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/balancer_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/balancer_test.go similarity index 90% rename from Godeps/_workspace/src/github.com/influxdb/influxdb/balancer_test.go rename to Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/balancer_test.go index ca1942c33..dd4a834cb 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/balancer_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/balancer_test.go @@ -1,10 +1,10 @@ -package influxdb_test +package cluster_test import ( "fmt" "testing" - "github.com/influxdb/influxdb" + "github.com/influxdb/influxdb/cluster" "github.com/influxdb/influxdb/meta" ) @@ -20,7 +20,7 @@ func NewNodes() []meta.NodeInfo { } func TestBalancerEmptyNodes(t *testing.T) { - b := influxdb.NewNodeBalancer([]meta.NodeInfo{}) + b := cluster.NewNodeBalancer([]meta.NodeInfo{}) got := b.Next() if got != nil { t.Errorf("expected nil, got %v", got) @@ -29,7 +29,7 @@ func TestBalancerEmptyNodes(t *testing.T) { func TestBalancerUp(t *testing.T) { nodes := NewNodes() - b := influxdb.NewNodeBalancer(nodes) + b := cluster.NewNodeBalancer(nodes) // First node in randomized round-robin order first := b.Next() @@ -52,7 +52,7 @@ func TestBalancerUp(t *testing.T) { /* func TestBalancerDown(t *testing.T) { nodes := NewNodes() - b := influxdb.NewNodeBalancer(nodes) + b := cluster.NewNodeBalancer(nodes) nodes[0].Down() @@ -78,7 +78,7 @@ func TestBalancerDown(t *testing.T) { /* func TestBalancerBackUp(t *testing.T) { nodes := newDataNodes() - b := influxdb.NewNodeBalancer(nodes) + b := cluster.NewNodeBalancer(nodes) nodes[0].Down() diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/points_writer.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/points_writer.go index 7bbb4b024..915058902 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/points_writer.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/points_writer.go @@ -23,16 +23,16 @@ type ConsistencyLevel int // The statistics generated by the "write" mdoule const ( statWriteReq = "req" - statPointWriteReq = "point_req" - statPointWriteReqLocal = "point_req_local" - statPointWriteReqRemote = "point_req_remote" - statWriteOK = "write_ok" - statWritePartial = "write_partial" - statWriteTimeout = "write_timeout" - statWriteErr = "write_error" - statWritePointReqHH = "point_req_hh" - statSubWriteOK = "sub_write_ok" - statSubWriteDrop = "sub_write_drop" + statPointWriteReq = "pointReq" + statPointWriteReqLocal = "pointReqLocal" + statPointWriteReqRemote = "pointReqRemote" + statWriteOK = "writeOk" + statWritePartial = "writePartial" + statWriteTimeout = "writeTimeout" + statWriteErr = "writeError" + statWritePointReqHH = "pointReqHH" + statSubWriteOK = "subWriteOk" + statSubWriteDrop = "subWriteDrop" ) const ( @@ -112,6 +112,7 @@ type PointsWriter struct { Subscriber interface { Points() chan<- *WritePointsRequest } + subPoints chan<- *WritePointsRequest statMap *expvar.Map } @@ -155,8 +156,9 @@ func (s *ShardMapping) MapPoint(shardInfo *meta.ShardInfo, p models.Point) { func (w *PointsWriter) Open() error { w.mu.Lock() defer w.mu.Unlock() - if w.closing == nil { - w.closing = make(chan struct{}) + w.closing = make(chan struct{}) + if w.Subscriber != nil { + w.subPoints = w.Subscriber.Points() } return nil } @@ -167,7 +169,12 @@ func (w *PointsWriter) Close() error { defer w.mu.Unlock() if w.closing != nil { close(w.closing) - w.closing = nil + } + if w.subPoints != nil { + // 'nil' channels always block so this makes the + // select statement in WritePoints hit its default case + // dropping any in-flight writes. + w.subPoints = nil } return nil } @@ -252,13 +259,19 @@ func (w *PointsWriter) WritePoints(p *WritePointsRequest) error { } // Send points to subscriptions if possible. - if w.Subscriber != nil { - select { - case w.Subscriber.Points() <- p: - w.statMap.Add(statSubWriteOK, 1) - default: - w.statMap.Add(statSubWriteDrop, 1) - } + ok := false + // We need to lock just in case the channel is about to be nil'ed + w.mu.RLock() + select { + case w.subPoints <- p: + ok = true + default: + } + w.mu.RUnlock() + if ok { + w.statMap.Add(statSubWriteOK, 1) + } else { + w.statMap.Add(statSubWriteDrop, 1) } for range shardMappings.Points { diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/points_writer_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/points_writer_test.go index 1ef8bd4b2..d0f57fbc7 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/points_writer_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/points_writer_test.go @@ -322,6 +322,9 @@ func TestPointsWriter_WritePoints(t *testing.T) { c.HintedHandoff = hh c.Subscriber = sub + c.Open() + defer c.Close() + err := c.WritePoints(pr) if err == nil && test.expErr != nil { t.Errorf("PointsWriter.WritePoints(): '%s' error: got %v, exp %v", test.name, err, test.expErr) diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/rpc.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/rpc.go index 57c0acccb..defbb4fd5 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/rpc.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/rpc.go @@ -113,9 +113,13 @@ type WritePointsRequest struct { // AddPoint adds a point to the WritePointRequest with field key 'value' func (w *WritePointsRequest) AddPoint(name string, value interface{}, timestamp time.Time, tags map[string]string) { - w.Points = append(w.Points, models.NewPoint( + pt, err := models.NewPoint( name, tags, map[string]interface{}{"value": value}, timestamp, - )) + ) + if err != nil { + return + } + w.Points = append(w.Points, pt) } // WriteShardRequest represents the a request to write a slice of points to a shard @@ -139,9 +143,13 @@ func (w *WriteShardRequest) Points() []models.Point { return w.unmarshalPoints() // AddPoint adds a new time series point func (w *WriteShardRequest) AddPoint(name string, value interface{}, timestamp time.Time, tags map[string]string) { - w.AddPoints([]models.Point{models.NewPoint( + pt, err := models.NewPoint( name, tags, map[string]interface{}{"value": value}, timestamp, - )}) + ) + if err != nil { + return + } + w.AddPoints([]models.Point{pt}) } // AddPoints adds a new time series point diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/service.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/service.go index 772bd2535..5169a6e33 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/service.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/service.go @@ -27,11 +27,11 @@ const MuxHeader = 2 // Statistics maintained by the cluster package const ( - writeShardReq = "write_shard_req" - writeShardPointsReq = "write_shard_points_req" - writeShardFail = "write_shard_fail" - mapShardReq = "map_shard_req" - mapShardResp = "map_shard_resp" + writeShardReq = "writeShardReq" + writeShardPointsReq = "writeShardPointsReq" + writeShardFail = "writeShardFail" + mapShardReq = "mapShardReq" + mapShardResp = "mapShardResp" ) // Service processes data received over raw TCP connections. @@ -61,7 +61,7 @@ type Service struct { func NewService(c Config) *Service { return &Service{ closing: make(chan struct{}), - Logger: log.New(os.Stderr, "[tcp] ", log.LstdFlags), + Logger: log.New(os.Stderr, "[cluster] ", log.LstdFlags), statMap: influxdb.NewStatistics("cluster", "cluster", nil), } } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/shard_writer_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/shard_writer_test.go index d8e188d3f..671342349 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/shard_writer_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/cluster/shard_writer_test.go @@ -28,7 +28,7 @@ func TestShardWriter_WriteShard_Success(t *testing.T) { // Build a single point. now := time.Now() var points []models.Point - points = append(points, models.NewPoint("cpu", models.Tags{"host": "server01"}, map[string]interface{}{"value": int64(100)}, now)) + points = append(points, models.MustNewPoint("cpu", models.Tags{"host": "server01"}, map[string]interface{}{"value": int64(100)}, now)) // Write to shard and close. if err := w.WriteShard(1, 2, points); err != nil { @@ -75,7 +75,7 @@ func TestShardWriter_WriteShard_Multiple(t *testing.T) { // Build a single point. now := time.Now() var points []models.Point - points = append(points, models.NewPoint("cpu", models.Tags{"host": "server01"}, map[string]interface{}{"value": int64(100)}, now)) + points = append(points, models.MustNewPoint("cpu", models.Tags{"host": "server01"}, map[string]interface{}{"value": int64(100)}, now)) // Write to shard twice and close. if err := w.WriteShard(1, 2, points); err != nil { @@ -125,7 +125,7 @@ func TestShardWriter_WriteShard_Error(t *testing.T) { shardID := uint64(1) ownerID := uint64(2) var points []models.Point - points = append(points, models.NewPoint( + points = append(points, models.MustNewPoint( "cpu", models.Tags{"host": "server01"}, map[string]interface{}{"value": int64(100)}, now, )) @@ -153,7 +153,7 @@ func TestShardWriter_Write_ErrDialTimeout(t *testing.T) { shardID := uint64(1) ownerID := uint64(2) var points []models.Point - points = append(points, models.NewPoint( + points = append(points, models.MustNewPoint( "cpu", models.Tags{"host": "server01"}, map[string]interface{}{"value": int64(100)}, now, )) @@ -176,7 +176,7 @@ func TestShardWriter_Write_ErrReadTimeout(t *testing.T) { shardID := uint64(1) ownerID := uint64(2) var points []models.Point - points = append(points, models.NewPoint( + points = append(points, models.MustNewPoint( "cpu", models.Tags{"host": "server01"}, map[string]interface{}{"value": int64(100)}, now, )) diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influx/main.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influx/main.go index 3bbad5fd2..8f0994799 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influx/main.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influx/main.go @@ -20,6 +20,7 @@ import ( "github.com/influxdb/influxdb/cluster" "github.com/influxdb/influxdb/importer/v8" "github.com/peterh/liner" + "io/ioutil" ) // These variables are populated via the Go linker. @@ -39,6 +40,10 @@ const ( defaultPPS = 0 ) +const ( + noTokenMsg = "Visit https://enterprise.influxdata.com to register for updates, InfluxDB server management, and monitoring.\n" +) + type CommandLine struct { Client *client.Client Line *liner.State @@ -163,7 +168,16 @@ Examples: c.Client.Addr()) return } + if c.Execute == "" && !c.Import { + token, err := c.DatabaseToken() + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to check token: %s\n", err.Error()) + return + } + if token == "" { + fmt.Printf(noTokenMsg) + } fmt.Printf("Connected to %s version %s\n", c.Client.Addr(), c.Version) } @@ -248,41 +262,54 @@ func showVersion() { func (c *CommandLine) ParseCommand(cmd string) bool { lcmd := strings.TrimSpace(strings.ToLower(cmd)) - switch { - case strings.HasPrefix(lcmd, "exit"): - // signal the program to exit - return false - case strings.HasPrefix(lcmd, "gopher"): - c.gopher() - case strings.HasPrefix(lcmd, "connect"): - c.connect(cmd) - case strings.HasPrefix(lcmd, "auth"): - c.SetAuth(cmd) - case strings.HasPrefix(lcmd, "help"): - c.help() - case strings.HasPrefix(lcmd, "format"): - c.SetFormat(cmd) - case strings.HasPrefix(lcmd, "precision"): - c.SetPrecision(cmd) - case strings.HasPrefix(lcmd, "consistency"): - c.SetWriteConsistency(cmd) - case strings.HasPrefix(lcmd, "settings"): - c.Settings() - case strings.HasPrefix(lcmd, "pretty"): - c.Pretty = !c.Pretty - if c.Pretty { - fmt.Println("Pretty print enabled") - } else { - fmt.Println("Pretty print disabled") + + split := strings.Split(lcmd, " ") + var tokens []string + for _, token := range split { + if token != "" { + tokens = append(tokens, token) + } + } + + if len(tokens) > 0 { + switch tokens[0] { + case "": + break + case "exit": + // signal the program to exit + return false + case "gopher": + c.gopher() + case "connect": + c.connect(cmd) + case "auth": + c.SetAuth(cmd) + case "help": + c.help() + case "history": + c.history() + case "format": + c.SetFormat(cmd) + case "precision": + c.SetPrecision(cmd) + case "consistency": + c.SetWriteConsistency(cmd) + case "settings": + c.Settings() + case "pretty": + c.Pretty = !c.Pretty + if c.Pretty { + fmt.Println("Pretty print enabled") + } else { + fmt.Println("Pretty print disabled") + } + case "use": + c.use(cmd) + case "insert": + c.Insert(cmd) + default: + c.ExecuteQuery(cmd) } - case strings.HasPrefix(lcmd, "use"): - c.use(cmd) - case strings.HasPrefix(lcmd, "insert"): - c.Insert(cmd) - case lcmd == "": - break - default: - c.ExecuteQuery(cmd) } return true } @@ -531,6 +558,24 @@ func (c *CommandLine) ExecuteQuery(query string) error { return nil } +func (c *CommandLine) DatabaseToken() (string, error) { + response, err := c.Client.Query(client.Query{Command: "SHOW DIAGNOSTICS for 'registration'"}) + if err != nil { + return "", err + } + if response.Error() != nil || len((*response).Results[0].Series) == 0 { + return "", nil + } + + // Look for position of "token" column. + for i, s := range (*response).Results[0].Series[0].Columns { + if s == "token" { + return (*response).Results[0].Series[0].Values[0][i].(string), nil + } + } + return "", nil +} + func (c *CommandLine) FormatResponse(response *client.Response, w io.Writer) { switch c.Format { case "json": @@ -724,6 +769,17 @@ func (c *CommandLine) help() { `) } +func (c *CommandLine) history() { + usr, err := user.Current() + // Only load history if we can get the user + if err == nil { + historyFile := filepath.Join(usr.HomeDir, ".influx_history") + if history, err := ioutil.ReadFile(historyFile); err == nil { + fmt.Print(string(history)) + } + } +} + func (c *CommandLine) gopher() { fmt.Println(` .-::-::://:-::- .:/++/' diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influx/main_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influx/main_test.go index 045fa319e..9c4260e9c 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influx/main_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influx/main_test.go @@ -20,6 +20,7 @@ func TestParseCommand_CommandsExist(t *testing.T) { {cmd: "gopher"}, {cmd: "connect"}, {cmd: "help"}, + {cmd: "history"}, {cmd: "pretty"}, {cmd: "use"}, {cmd: ""}, // test that a blank command just returns @@ -31,6 +32,42 @@ func TestParseCommand_CommandsExist(t *testing.T) { } } +func TestParseCommand_CommandsSamePrefix(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var data client.Response + w.WriteHeader(http.StatusNoContent) + _ = json.NewEncoder(w).Encode(data) + })) + defer ts.Close() + + u, _ := url.Parse(ts.URL) + config := client.Config{URL: *u} + c, err := client.NewClient(config) + if err != nil { + t.Fatalf("unexpected error. expected %v, actual %v", nil, err) + } + m := main.CommandLine{Client: c} + + tests := []struct { + cmd string + }{ + {cmd: "use db"}, + {cmd: "user nodb"}, + {cmd: "puse nodb"}, + {cmd: ""}, // test that a blank command just returns + } + for _, test := range tests { + if !m.ParseCommand(test.cmd) { + t.Fatalf(`Command failed for %q.`, test.cmd) + } + } + + if m.Database != "db" { + t.Fatalf(`Command "use" changed database to %q. Expected db`, m.Database) + } +} + func TestParseCommand_TogglePretty(t *testing.T) { t.Parallel() c := main.CommandLine{} @@ -217,3 +254,22 @@ func TestParseCommand_InsertInto(t *testing.T) { } } } + +func TestParseCommand_History(t *testing.T) { + t.Parallel() + c := main.CommandLine{} + tests := []struct { + cmd string + }{ + {cmd: "history"}, + {cmd: " history"}, + {cmd: "history "}, + {cmd: "History "}, + } + + for _, test := range tests { + if !c.ParseCommand(test.cmd) { + t.Fatalf(`Command "history" failed for %q.`, test.cmd) + } + } +} diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influx_inspect/tsm.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influx_inspect/tsm.go index afc0aa370..612abe129 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influx_inspect/tsm.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influx_inspect/tsm.go @@ -353,7 +353,8 @@ func cmdDumpTsm1(opts *tsdmDumpOpts) { encoded := buf[9:] - v, err := tsm1.DecodeBlock(buf) + var v []tsm1.Value + err := tsm1.DecodeBlock(buf, &v) if err != nil { fmt.Printf("error: %v\n", err.Error()) os.Exit(1) diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influx_stress/influx_stress.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influx_stress/influx_stress.go index bb61e5371..608a33dc8 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influx_stress/influx_stress.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influx_stress/influx_stress.go @@ -38,8 +38,6 @@ func main() { return } - fmt.Printf("%#v\n", cfg.Write) - if *batchSize != 0 { cfg.Write.BatchSize = *batchSize } @@ -64,8 +62,6 @@ func main() { cfg.Write.Precision = *precision } - fmt.Printf("%#v\n", cfg.Write) - d := make(chan struct{}) seriesQueryResults := make(chan runner.QueryResults) diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influxd/restore/restore.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influxd/restore/restore.go index 5a95f8726..616419fd2 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influxd/restore/restore.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influxd/restore/restore.go @@ -158,6 +158,7 @@ func (cmd *Command) unpackMeta(mr *snapshot.MultiReader, sf snapshot.File, confi store := meta.NewStore(config.Meta) store.RaftListener = newNopListener() store.ExecListener = newNopListener() + store.RPCListener = newNopListener() // Determine advertised address. _, port, err := net.SplitHostPort(config.Meta.BindAddress) @@ -172,6 +173,7 @@ func (cmd *Command) unpackMeta(mr *snapshot.MultiReader, sf snapshot.File, confi return fmt.Errorf("resolve tcp: addr=%s, err=%s", hostport, err) } store.Addr = addr + store.RemoteAddr = addr // Open the meta store. if err := store.Open(); err != nil { @@ -246,5 +248,12 @@ func (ln *nopListener) Accept() (net.Conn, error) { return nil, errors.New("listener closing") } -func (ln *nopListener) Close() error { close(ln.closing); return nil } +func (ln *nopListener) Close() error { + if ln.closing != nil { + close(ln.closing) + ln.closing = nil + } + return nil +} + func (ln *nopListener) Addr() net.Addr { return nil } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influxd/run/config.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influxd/run/config.go index 39558be6d..119e347c4 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influxd/run/config.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influxd/run/config.go @@ -69,8 +69,10 @@ func NewConfig() *Config { c.Monitor = monitor.NewConfig() c.Subscriber = subscriber.NewConfig() c.HTTPD = httpd.NewConfig() + c.Graphites = []graphite.Config{graphite.NewConfig()} c.Collectd = collectd.NewConfig() c.OpenTSDB = opentsdb.NewConfig() + c.UDPs = []udp.Config{udp.NewConfig()} c.ContinuousQuery = continuous_querier.NewConfig() c.Retention = retention.NewConfig() @@ -108,12 +110,12 @@ func NewDemoConfig() (*Config, error) { func (c *Config) Validate() error { if c.Meta.Dir == "" { return errors.New("Meta.Dir must be specified") - } else if c.Data.Dir == "" { - return errors.New("Data.Dir must be specified") } else if c.HintedHandoff.Dir == "" { return errors.New("HintedHandoff.Dir must be specified") - } else if c.Data.WALDir == "" { - return errors.New("Data.WALDir must be specified") + } + + if err := c.Data.Validate(); err != nil { + return err } for _, g := range c.Graphites { diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influxd/run/server.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influxd/run/server.go index 9a7a5c6e0..21cf00403 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influxd/run/server.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influxd/run/server.go @@ -1,17 +1,16 @@ package run import ( - "bytes" "fmt" "log" "net" - "net/http" "os" "runtime" "runtime/pprof" "strings" "time" + "github.com/influxdb/enterprise-client/v1" "github.com/influxdb/influxdb/cluster" "github.com/influxdb/influxdb/meta" "github.com/influxdb/influxdb/monitor" @@ -129,6 +128,7 @@ func NewServer(c *Config, buildInfo *BuildInfo) (*Server, error) { // Create the hinted handoff service s.HintedHandoff = hh.NewService(c.HintedHandoff, s.ShardWriter, s.MetaStore) + s.HintedHandoff.Monitor = s.Monitor // Create the Subscriber service s.Subscriber = subscriber.NewService(c.Subscriber) @@ -384,10 +384,6 @@ func (s *Server) Open() error { // Wait for the store to initialize. <-s.MetaStore.Ready() - if err := s.Monitor.Open(); err != nil { - return fmt.Errorf("open monitor: %v", err) - } - // Open TSDB store. if err := s.TSDBStore.Open(); err != nil { return fmt.Errorf("open tsdb store: %s", err) @@ -403,6 +399,16 @@ func (s *Server) Open() error { return fmt.Errorf("open subscriber: %s", err) } + // Open the points writer service + if err := s.PointsWriter.Open(); err != nil { + return fmt.Errorf("open points writer: %s", err) + } + + // Open the monitor service + if err := s.Monitor.Open(); err != nil { + return fmt.Errorf("open monitor: %v", err) + } + for _, service := range s.Services { if err := service.Open(); err != nil { return fmt.Errorf("open service: %s", err) @@ -443,6 +449,10 @@ func (s *Server) Close() error { s.Monitor.Close() } + if s.PointsWriter != nil { + s.PointsWriter.Close() + } + if s.HintedHandoff != nil { s.HintedHandoff.Close() } @@ -511,18 +521,28 @@ func (s *Server) reportServer() { return } - json := fmt.Sprintf(`[{ - "name":"reports", - "columns":["os", "arch", "version", "server_id", "cluster_id", "num_series", "num_measurements", "num_databases"], - "points":[["%s", "%s", "%s", "%x", "%x", "%d", "%d", "%d"]] - }]`, runtime.GOOS, runtime.GOARCH, s.buildInfo.Version, s.MetaStore.NodeID(), clusterID, numSeries, numMeasurements, numDatabases) - - data := bytes.NewBufferString(json) + cl := client.New("") + usage := client.Usage{ + Product: "influxdb", + Data: []client.UsageData{ + { + Values: client.Values{ + "os": runtime.GOOS, + "arch": runtime.GOARCH, + "version": s.buildInfo.Version, + "server_id": s.MetaStore.NodeID(), + "cluster_id": clusterID, + "num_series": numSeries, + "num_measurements": numMeasurements, + "num_databases": numDatabases, + }, + }, + }, + } log.Printf("Sending anonymous usage statistics to m.influxdb.com") - client := http.Client{Timeout: time.Duration(5 * time.Second)} - go client.Post("http://m.influxdb.com:8086/db/reporting/series?u=reporter&p=influxdb", "application/json", data) + go cl.Save(usage) } // monitorErrorChan reads an error channel and resends it through the server. diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influxd/run/server_helpers_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influxd/run/server_helpers_test.go index aeb81826d..620a48f84 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influxd/run/server_helpers_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influxd/run/server_helpers_test.go @@ -53,7 +53,6 @@ func OpenServer(c *run.Config, joinURLs string) *Server { if err := s.Open(); err != nil { panic(err.Error()) } - return s } @@ -77,12 +76,24 @@ func OpenServerWithVersion(c *run.Config, version string) *Server { return &s } +// OpenDefaultServer opens a test server with a default database & retention policy. +func OpenDefaultServer(c *run.Config, joinURLs string) *Server { + s := OpenServer(c, joinURLs) + if err := s.CreateDatabaseAndRetentionPolicy("db0", newRetentionPolicyInfo("rp0", 1, 0)); err != nil { + panic(err) + } + if err := s.MetaStore.SetDefaultRetentionPolicy("db0", "rp0"); err != nil { + panic(err) + } + return s +} + // Close shuts down the server and removes all temporary paths. func (s *Server) Close() { + s.Server.Close() os.RemoveAll(s.Config.Meta.Dir) os.RemoveAll(s.Config.Data.Dir) os.RemoveAll(s.Config.HintedHandoff.Dir) - s.Server.Close() } // URL returns the base URL for the httpd endpoint. @@ -180,6 +191,15 @@ func (s *Server) Write(db, rp, body string, params url.Values) (results string, return string(MustReadAll(resp.Body)), nil } +// MustWrite executes a write to the server. Panic on error. +func (s *Server) MustWrite(db, rp, body string, params url.Values) string { + results, err := s.Write(db, rp, body, params) + if err != nil { + panic(err) + } + return results +} + // NewConfig returns the default config with temporary paths. func NewConfig() *run.Config { c := run.NewConfig() @@ -347,6 +367,7 @@ func configureLogging(s *Server) { s.HintedHandoff.SetLogger(nullLogger) s.Monitor.SetLogger(nullLogger) s.QueryExecutor.SetLogger(nullLogger) + s.Subscriber.SetLogger(nullLogger) for _, service := range s.Services { if service, ok := service.(logSetter); ok { service.SetLogger(nullLogger) diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influxd/run/server_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influxd/run/server_test.go index 987b19637..3f633019a 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influxd/run/server_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/cmd/influxd/run/server_test.go @@ -8,6 +8,8 @@ import ( "strings" "testing" "time" + + "github.com/influxdb/influxdb/cluster" ) // Ensure that HTTP responses include the InfluxDB version. @@ -76,6 +78,16 @@ func TestServer_DatabaseCommands(t *testing.T) { command: `DROP DATABASE db1`, exp: `{"results":[{}]}`, }, + &Query{ + name: "drop database should error if it does not exists", + command: `DROP DATABASE db1`, + exp: `{"results":[{"error":"database not found: db1"}]}`, + }, + &Query{ + name: "drop database should not error with non-existing database db1 WITH IF EXISTS", + command: `DROP DATABASE IF EXISTS db1`, + exp: `{"results":[{}]}`, + }, &Query{ name: "show database should have no results", command: `SHOW DATABASES`, @@ -769,6 +781,39 @@ func TestServer_Write_LineProtocol_Integer(t *testing.T) { } } +// Ensure the server returns a partial write response when some points fail to parse. Also validate that +// the successfully parsed points can be queried. +func TestServer_Write_LineProtocol_Partial(t *testing.T) { + t.Parallel() + s := OpenServer(NewConfig(), "") + defer s.Close() + + if err := s.CreateDatabaseAndRetentionPolicy("db0", newRetentionPolicyInfo("rp0", 1, 1*time.Hour)); err != nil { + t.Fatal(err) + } + + now := now() + points := []string{ + "cpu,host=server01 value=100 " + strconv.FormatInt(now.UnixNano(), 10), + "cpu,host=server01 value=NaN " + strconv.FormatInt(now.UnixNano(), 20), + "cpu,host=server01 value=NaN " + strconv.FormatInt(now.UnixNano(), 30), + } + if res, err := s.Write("db0", "rp0", strings.Join(points, "\n"), nil); err == nil { + t.Fatal("expected error. got nil", err) + } else if exp := ``; exp != res { + t.Fatalf("unexpected results\nexp: %s\ngot: %s\n", exp, res) + } else if exp := "partial write"; !strings.Contains(err.Error(), exp) { + t.Fatalf("unexpected error: exp\nexp: %v\ngot: %v", exp, err) + } + + // Verify the data was written. + if res, err := s.Query(`SELECT * FROM db0.rp0.cpu GROUP BY *`); err != nil { + t.Fatal(err) + } else if exp := fmt.Sprintf(`{"results":[{"series":[{"name":"cpu","tags":{"host":"server01"},"columns":["time","value"],"values":[["%s",100]]}]}]}`, now.Format(time.RFC3339Nano)); exp != res { + t.Fatalf("unexpected results\nexp: %s\ngot: %s\n", exp, res) + } +} + // Ensure the server can query with default databases (via param) and default retention policy func TestServer_Query_DefaultDBAndRP(t *testing.T) { t.Parallel() @@ -1937,70 +1982,15 @@ func TestServer_Query_Regex(t *testing.T) { } } -func TestServer_Query_AggregatesCommon(t *testing.T) { +func TestServer_Query_Aggregates_Int(t *testing.T) { t.Parallel() - s := OpenServer(NewConfig(), "") + s := OpenDefaultServer(NewConfig(), "") defer s.Close() - if err := s.CreateDatabaseAndRetentionPolicy("db0", newRetentionPolicyInfo("rp0", 1, 0)); err != nil { - t.Fatal(err) - } - if err := s.MetaStore.SetDefaultRetentionPolicy("db0", "rp0"); err != nil { - t.Fatal(err) - } - - writes := []string{ - fmt.Sprintf(`int value=45 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), - - fmt.Sprintf(`intmax value=%s %d`, maxInt64(), mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), - fmt.Sprintf(`intmax value=%s %d`, maxInt64(), mustParseTime(time.RFC3339Nano, "2000-01-01T01:00:00Z").UnixNano()), - - fmt.Sprintf(`intmany,host=server01 value=2.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), - fmt.Sprintf(`intmany,host=server02 value=4.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:10Z").UnixNano()), - fmt.Sprintf(`intmany,host=server03 value=4.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:20Z").UnixNano()), - fmt.Sprintf(`intmany,host=server04 value=4.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:30Z").UnixNano()), - fmt.Sprintf(`intmany,host=server05 value=5.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:40Z").UnixNano()), - fmt.Sprintf(`intmany,host=server06 value=5.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:50Z").UnixNano()), - fmt.Sprintf(`intmany,host=server07 value=7.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:01:00Z").UnixNano()), - fmt.Sprintf(`intmany,host=server08 value=9.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:01:10Z").UnixNano()), - - fmt.Sprintf(`intoverlap,region=us-east value=20 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), - fmt.Sprintf(`intoverlap,region=us-east value=30 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:10Z").UnixNano()), - fmt.Sprintf(`intoverlap,region=us-west value=100 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), - fmt.Sprintf(`intoverlap,region=us-east otherVal=20 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:03Z").UnixNano()), - - fmt.Sprintf(`floatsingle value=45.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), - - fmt.Sprintf(`floatmax value=%s %d`, maxFloat64(), mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), - fmt.Sprintf(`floatmax value=%s %d`, maxFloat64(), mustParseTime(time.RFC3339Nano, "2000-01-01T01:00:00Z").UnixNano()), - - fmt.Sprintf(`floatmany,host=server01 value=2.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), - fmt.Sprintf(`floatmany,host=server02 value=4.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:10Z").UnixNano()), - fmt.Sprintf(`floatmany,host=server03 value=4.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:20Z").UnixNano()), - fmt.Sprintf(`floatmany,host=server04 value=4.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:30Z").UnixNano()), - fmt.Sprintf(`floatmany,host=server05 value=5.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:40Z").UnixNano()), - fmt.Sprintf(`floatmany,host=server06 value=5.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:50Z").UnixNano()), - fmt.Sprintf(`floatmany,host=server07 value=7.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:01:00Z").UnixNano()), - fmt.Sprintf(`floatmany,host=server08 value=9.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:01:10Z").UnixNano()), - - fmt.Sprintf(`floatoverlap,region=us-east value=20.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), - fmt.Sprintf(`floatoverlap,region=us-east value=30.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:10Z").UnixNano()), - fmt.Sprintf(`floatoverlap,region=us-west value=100.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), - fmt.Sprintf(`floatoverlap,region=us-east otherVal=20.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:03Z").UnixNano()), - - fmt.Sprintf(`load,region=us-east,host=serverA value=20.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), - fmt.Sprintf(`load,region=us-east,host=serverB value=30.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:10Z").UnixNano()), - fmt.Sprintf(`load,region=us-west,host=serverC value=100.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), - - fmt.Sprintf(`cpu,region=uk,host=serverZ,service=redis value=20.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:03Z").UnixNano()), - fmt.Sprintf(`cpu,region=uk,host=serverZ,service=mysql value=30.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:03Z").UnixNano()), - - fmt.Sprintf(`stringdata value="first" %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:03Z").UnixNano()), - fmt.Sprintf(`stringdata value="last" %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:04Z").UnixNano()), - } - test := NewTest("db0", "rp0") - test.write = strings.Join(writes, "\n") + test.write = strings.Join([]string{ + fmt.Sprintf(`int value=45 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), + }, "\n") test.addQueries([]*Query{ // int64 @@ -2010,12 +2000,82 @@ func TestServer_Query_AggregatesCommon(t *testing.T) { command: `SELECT STDDEV(value) FROM int`, exp: `{"results":[{"series":[{"name":"int","columns":["time","stddev"],"values":[["1970-01-01T00:00:00Z",null]]}]}]}`, }, + }...) + + for i, query := range test.queries { + if i == 0 { + if err := test.init(s); err != nil { + t.Fatalf("test init failed: %s", err) + } + } + if query.skip { + t.Logf("SKIP:: %s", query.name) + continue + } + if err := query.Execute(s); err != nil { + t.Error(query.Error(err)) + } else if !query.success() { + t.Error(query.failureMessage()) + } + } +} + +func TestServer_Query_Aggregates_IntMax(t *testing.T) { + t.Parallel() + s := OpenDefaultServer(NewConfig(), "") + defer s.Close() + + test := NewTest("db0", "rp0") + test.write = strings.Join([]string{ + fmt.Sprintf(`intmax value=%s %d`, maxInt64(), mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), + fmt.Sprintf(`intmax value=%s %d`, maxInt64(), mustParseTime(time.RFC3339Nano, "2000-01-01T01:00:00Z").UnixNano()), + }, "\n") + + test.addQueries([]*Query{ &Query{ name: "large mean and stddev - int", params: url.Values{"db": []string{"db0"}}, command: `SELECT MEAN(value), STDDEV(value) FROM intmax`, exp: `{"results":[{"series":[{"name":"intmax","columns":["time","mean","stddev"],"values":[["1970-01-01T00:00:00Z",` + maxInt64() + `,0]]}]}]}`, }, + }...) + + for i, query := range test.queries { + if i == 0 { + if err := test.init(s); err != nil { + t.Fatalf("test init failed: %s", err) + } + } + if query.skip { + t.Logf("SKIP:: %s", query.name) + continue + } + if err := query.Execute(s); err != nil { + t.Error(query.Error(err)) + } else if !query.success() { + t.Error(query.failureMessage()) + } + } +} + +func TestServer_Query_Aggregates_IntMany(t *testing.T) { + t.Parallel() + s := OpenDefaultServer(NewConfig(), "") + defer s.Close() + + test := NewTest("db0", "rp0") + test.write = strings.Join([]string{ + fmt.Sprintf(`intmany,host=server01 value=2.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), + fmt.Sprintf(`intmany,host=server02 value=4.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:10Z").UnixNano()), + fmt.Sprintf(`intmany,host=server03 value=4.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:20Z").UnixNano()), + fmt.Sprintf(`intmany,host=server04 value=4.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:30Z").UnixNano()), + fmt.Sprintf(`intmany,host=server05 value=5.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:40Z").UnixNano()), + fmt.Sprintf(`intmany,host=server06 value=5.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:50Z").UnixNano()), + fmt.Sprintf(`intmany,host=server07 value=7.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:01:00Z").UnixNano()), + fmt.Sprintf(`intmany,host=server08 value=9.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:01:10Z").UnixNano()), + }, "\n") + + test.addQueries([]*Query{ &Query{ name: "mean and stddev - int", params: url.Values{"db": []string{"db0"}}, @@ -2106,6 +2166,176 @@ func TestServer_Query_AggregatesCommon(t *testing.T) { command: `SELECT COUNT(DISTINCT host) FROM intmany`, exp: `{"results":[{"series":[{"name":"intmany","columns":["time","count"],"values":[["1970-01-01T00:00:00Z",0]]}]}]}`, }, + }...) + + for i, query := range test.queries { + if i == 0 { + if err := test.init(s); err != nil { + t.Fatalf("test init failed: %s", err) + } + } + if query.skip { + t.Logf("SKIP:: %s", query.name) + continue + } + if err := query.Execute(s); err != nil { + t.Error(query.Error(err)) + } else if !query.success() { + t.Error(query.failureMessage()) + } + } +} + +func TestServer_Query_Aggregates_IntMany_GroupBy(t *testing.T) { + t.Parallel() + s := OpenDefaultServer(NewConfig(), "") + defer s.Close() + + test := NewTest("db0", "rp0") + test.write = strings.Join([]string{ + fmt.Sprintf(`intmany,host=server01 value=2.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), + fmt.Sprintf(`intmany,host=server02 value=4.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:10Z").UnixNano()), + fmt.Sprintf(`intmany,host=server03 value=4.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:20Z").UnixNano()), + fmt.Sprintf(`intmany,host=server04 value=4.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:30Z").UnixNano()), + fmt.Sprintf(`intmany,host=server05 value=5.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:40Z").UnixNano()), + fmt.Sprintf(`intmany,host=server06 value=5.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:50Z").UnixNano()), + fmt.Sprintf(`intmany,host=server07 value=7.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:01:00Z").UnixNano()), + fmt.Sprintf(`intmany,host=server08 value=9.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:01:10Z").UnixNano()), + }, "\n") + + test.addQueries([]*Query{ + &Query{ + name: "max order by time with time specified group by 10s", + params: url.Values{"db": []string{"db0"}}, + command: `SELECT time, max(value) FROM intmany where time >= '2000-01-01T00:00:00Z' AND time <= '2000-01-01T00:01:14Z' group by time(10s)`, + exp: `{"results":[{"series":[{"name":"intmany","columns":["time","max"],"values":[["2000-01-01T00:00:00Z",2],["2000-01-01T00:00:10Z",4],["2000-01-01T00:00:20Z",4],["2000-01-01T00:00:30Z",4],["2000-01-01T00:00:40Z",5],["2000-01-01T00:00:50Z",5],["2000-01-01T00:01:00Z",7],["2000-01-01T00:01:10Z",9]]}]}]}`, + }, + &Query{ + name: "max order by time without time specified group by 30s", + params: url.Values{"db": []string{"db0"}}, + command: `SELECT max(value) FROM intmany where time >= '2000-01-01T00:00:00Z' AND time <= '2000-01-01T00:01:14Z' group by time(30s)`, + exp: `{"results":[{"series":[{"name":"intmany","columns":["time","max"],"values":[["2000-01-01T00:00:00Z",4],["2000-01-01T00:00:30Z",5],["2000-01-01T00:01:00Z",9]]}]}]}`, + }, + &Query{ + name: "max order by time with time specified group by 30s", + params: url.Values{"db": []string{"db0"}}, + command: `SELECT time, max(value) FROM intmany where time >= '2000-01-01T00:00:00Z' AND time <= '2000-01-01T00:01:14Z' group by time(30s)`, + exp: `{"results":[{"series":[{"name":"intmany","columns":["time","max"],"values":[["2000-01-01T00:00:10Z",4],["2000-01-01T00:00:40Z",5],["2000-01-01T00:01:10Z",9]]}]}]}`, + }, + &Query{ + name: "min order by time without time specified group by 15s", + params: url.Values{"db": []string{"db0"}}, + command: `SELECT min(value) FROM intmany where time >= '2000-01-01T00:00:00Z' AND time <= '2000-01-01T00:01:14Z' group by time(15s)`, + exp: `{"results":[{"series":[{"name":"intmany","columns":["time","min"],"values":[["2000-01-01T00:00:00Z",2],["2000-01-01T00:00:15Z",4],["2000-01-01T00:00:30Z",4],["2000-01-01T00:00:45Z",5],["2000-01-01T00:01:00Z",7]]}]}]}`, + }, + &Query{ + name: "min order by time with time specified group by 15s", + params: url.Values{"db": []string{"db0"}}, + command: `SELECT time, min(value) FROM intmany where time >= '2000-01-01T00:00:00Z' AND time <= '2000-01-01T00:01:14Z' group by time(15s)`, + exp: `{"results":[{"series":[{"name":"intmany","columns":["time","min"],"values":[["2000-01-01T00:00:00Z",2],["2000-01-01T00:00:20Z",4],["2000-01-01T00:00:30Z",4],["2000-01-01T00:00:50Z",5],["2000-01-01T00:01:00Z",7]]}]}]}`, + }, + &Query{ + name: "first order by time without time specified group by 15s", + params: url.Values{"db": []string{"db0"}}, + command: `SELECT first(value) FROM intmany where time >= '2000-01-01T00:00:00Z' AND time <= '2000-01-01T00:01:14Z' group by time(15s)`, + exp: `{"results":[{"series":[{"name":"intmany","columns":["time","first"],"values":[["2000-01-01T00:00:00Z",2],["2000-01-01T00:00:15Z",4],["2000-01-01T00:00:30Z",4],["2000-01-01T00:00:45Z",5],["2000-01-01T00:01:00Z",7]]}]}]}`, + }, + &Query{ + name: "first order by time with time specified group by 15s", + params: url.Values{"db": []string{"db0"}}, + command: `SELECT time, first(value) FROM intmany where time >= '2000-01-01T00:00:00Z' AND time <= '2000-01-01T00:01:14Z' group by time(15s)`, + exp: `{"results":[{"series":[{"name":"intmany","columns":["time","first"],"values":[["2000-01-01T00:00:00Z",2],["2000-01-01T00:00:20Z",4],["2000-01-01T00:00:30Z",4],["2000-01-01T00:00:50Z",5],["2000-01-01T00:01:00Z",7]]}]}]}`, + }, + &Query{ + name: "last order by time without time specified group by 15s", + params: url.Values{"db": []string{"db0"}}, + command: `SELECT last(value) FROM intmany where time >= '2000-01-01T00:00:00Z' AND time <= '2000-01-01T00:01:14Z' group by time(15s)`, + exp: `{"results":[{"series":[{"name":"intmany","columns":["time","last"],"values":[["2000-01-01T00:00:00Z",4],["2000-01-01T00:00:15Z",4],["2000-01-01T00:00:30Z",5],["2000-01-01T00:00:45Z",5],["2000-01-01T00:01:00Z",9]]}]}]}`, + }, + &Query{ + name: "last order by time with time specified group by 15s", + params: url.Values{"db": []string{"db0"}}, + command: `SELECT time, last(value) FROM intmany where time >= '2000-01-01T00:00:00Z' AND time <= '2000-01-01T00:01:14Z' group by time(15s)`, + exp: `{"results":[{"series":[{"name":"intmany","columns":["time","last"],"values":[["2000-01-01T00:00:10Z",4],["2000-01-01T00:00:20Z",4],["2000-01-01T00:00:40Z",5],["2000-01-01T00:00:50Z",5],["2000-01-01T00:01:10Z",9]]}]}]}`, + }, + }...) + + for i, query := range test.queries { + if i == 0 { + if err := test.init(s); err != nil { + t.Fatalf("test init failed: %s", err) + } + } + if query.skip { + t.Logf("SKIP:: %s", query.name) + continue + } + if err := query.Execute(s); err != nil { + t.Error(query.Error(err)) + } else if !query.success() { + t.Error(query.failureMessage()) + } + } +} + +func TestServer_Query_Aggregates_IntMany_OrderByDesc(t *testing.T) { + t.Parallel() + s := OpenDefaultServer(NewConfig(), "") + defer s.Close() + + test := NewTest("db0", "rp0") + test.write = strings.Join([]string{ + fmt.Sprintf(`intmany,host=server01 value=2.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), + fmt.Sprintf(`intmany,host=server02 value=4.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:10Z").UnixNano()), + fmt.Sprintf(`intmany,host=server03 value=4.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:20Z").UnixNano()), + fmt.Sprintf(`intmany,host=server04 value=4.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:30Z").UnixNano()), + fmt.Sprintf(`intmany,host=server05 value=5.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:40Z").UnixNano()), + fmt.Sprintf(`intmany,host=server06 value=5.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:50Z").UnixNano()), + fmt.Sprintf(`intmany,host=server07 value=7.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:01:00Z").UnixNano()), + fmt.Sprintf(`intmany,host=server08 value=9.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:01:10Z").UnixNano()), + }, "\n") + + test.addQueries([]*Query{ + &Query{ + name: "aggregate order by time desc", + params: url.Values{"db": []string{"db0"}}, + command: `SELECT max(value) FROM intmany where time >= '2000-01-01T00:00:00Z' AND time <= '2000-01-01T00:01:00Z' group by time(10s) order by time desc`, + exp: `{"results":[{"series":[{"name":"intmany","columns":["time","max"],"values":[["2000-01-01T00:01:00Z",7],["2000-01-01T00:00:50Z",5],["2000-01-01T00:00:40Z",5],["2000-01-01T00:00:30Z",4],["2000-01-01T00:00:20Z",4],["2000-01-01T00:00:10Z",4],["2000-01-01T00:00:00Z",2]]}]}]}`, + }, + }...) + + for i, query := range test.queries { + if i == 0 { + if err := test.init(s); err != nil { + t.Fatalf("test init failed: %s", err) + } + } + if query.skip { + t.Logf("SKIP:: %s", query.name) + continue + } + if err := query.Execute(s); err != nil { + t.Error(query.Error(err)) + } else if !query.success() { + t.Error(query.failureMessage()) + } + } +} + +func TestServer_Query_Aggregates_IntOverlap(t *testing.T) { + t.Parallel() + s := OpenDefaultServer(NewConfig(), "") + defer s.Close() + + test := NewTest("db0", "rp0") + test.write = strings.Join([]string{ + fmt.Sprintf(`intoverlap,region=us-east value=20 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), + fmt.Sprintf(`intoverlap,region=us-east value=30 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:10Z").UnixNano()), + fmt.Sprintf(`intoverlap,region=us-west value=100 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), + fmt.Sprintf(`intoverlap,region=us-east otherVal=20 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:03Z").UnixNano()), + }, "\n") + + test.addQueries([]*Query{ &Query{ name: "aggregation with no interval - int", params: url.Values{"db": []string{"db0"}}, @@ -2137,20 +2367,81 @@ func TestServer_Query_AggregatesCommon(t *testing.T) { command: `SELECT sum(value), mean(value), sum(value) / mean(value) as div FROM intoverlap GROUP BY region`, exp: `{"results":[{"series":[{"name":"intoverlap","tags":{"region":"us-east"},"columns":["time","sum","mean","div"],"values":[["1970-01-01T00:00:00Z",50,25,2]]},{"name":"intoverlap","tags":{"region":"us-west"},"columns":["time","div"],"values":[["1970-01-01T00:00:00Z",100,100,1]]}]}]}`, }, + }...) - // float64 + for i, query := range test.queries { + if i == 0 { + if err := test.init(s); err != nil { + t.Fatalf("test init failed: %s", err) + } + } + if query.skip { + t.Logf("SKIP:: %s", query.name) + continue + } + if err := query.Execute(s); err != nil { + t.Error(query.Error(err)) + } else if !query.success() { + t.Error(query.failureMessage()) + } + } +} + +func TestServer_Query_Aggregates_FloatSingle(t *testing.T) { + t.Parallel() + s := OpenDefaultServer(NewConfig(), "") + defer s.Close() + + test := NewTest("db0", "rp0") + test.write = strings.Join([]string{ + fmt.Sprintf(`floatsingle value=45.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), + }, "\n") + + test.addQueries([]*Query{ &Query{ name: "stddev with just one point - float", params: url.Values{"db": []string{"db0"}}, command: `SELECT STDDEV(value) FROM floatsingle`, exp: `{"results":[{"series":[{"name":"floatsingle","columns":["time","stddev"],"values":[["1970-01-01T00:00:00Z",null]]}]}]}`, }, - &Query{ - name: "large mean and stddev - float", - params: url.Values{"db": []string{"db0"}}, - command: `SELECT MEAN(value), STDDEV(value) FROM floatmax`, - exp: `{"results":[{"series":[{"name":"floatmax","columns":["time","mean","stddev"],"values":[["1970-01-01T00:00:00Z",` + maxFloat64() + `,0]]}]}]}`, - }, + }...) + + for i, query := range test.queries { + if i == 0 { + if err := test.init(s); err != nil { + t.Fatalf("test init failed: %s", err) + } + } + if query.skip { + t.Logf("SKIP:: %s", query.name) + continue + } + if err := query.Execute(s); err != nil { + t.Error(query.Error(err)) + } else if !query.success() { + t.Error(query.failureMessage()) + } + } +} + +func TestServer_Query_Aggregates_FloatMany(t *testing.T) { + t.Parallel() + s := OpenDefaultServer(NewConfig(), "") + defer s.Close() + + test := NewTest("db0", "rp0") + test.write = strings.Join([]string{ + fmt.Sprintf(`floatmany,host=server01 value=2.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), + fmt.Sprintf(`floatmany,host=server02 value=4.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:10Z").UnixNano()), + fmt.Sprintf(`floatmany,host=server03 value=4.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:20Z").UnixNano()), + fmt.Sprintf(`floatmany,host=server04 value=4.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:30Z").UnixNano()), + fmt.Sprintf(`floatmany,host=server05 value=5.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:40Z").UnixNano()), + fmt.Sprintf(`floatmany,host=server06 value=5.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:50Z").UnixNano()), + fmt.Sprintf(`floatmany,host=server07 value=7.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:01:00Z").UnixNano()), + fmt.Sprintf(`floatmany,host=server08 value=9.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:01:10Z").UnixNano()), + }, "\n") + + test.addQueries([]*Query{ &Query{ name: "mean and stddev - float", params: url.Values{"db": []string{"db0"}}, @@ -2235,6 +2526,40 @@ func TestServer_Query_AggregatesCommon(t *testing.T) { command: `SELECT COUNT(DISTINCT host) FROM floatmany`, exp: `{"results":[{"series":[{"name":"floatmany","columns":["time","count"],"values":[["1970-01-01T00:00:00Z",0]]}]}]}`, }, + }...) + + for i, query := range test.queries { + if i == 0 { + if err := test.init(s); err != nil { + t.Fatalf("test init failed: %s", err) + } + } + if query.skip { + t.Logf("SKIP:: %s", query.name) + continue + } + if err := query.Execute(s); err != nil { + t.Error(query.Error(err)) + } else if !query.success() { + t.Error(query.failureMessage()) + } + } +} + +func TestServer_Query_Aggregates_FloatOverlap(t *testing.T) { + t.Parallel() + s := OpenDefaultServer(NewConfig(), "") + defer s.Close() + + test := NewTest("db0", "rp0") + test.write = strings.Join([]string{ + fmt.Sprintf(`floatoverlap,region=us-east value=20.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), + fmt.Sprintf(`floatoverlap,region=us-east value=30.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:10Z").UnixNano()), + fmt.Sprintf(`floatoverlap,region=us-west value=100.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), + fmt.Sprintf(`floatoverlap,region=us-east otherVal=20.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:03Z").UnixNano()), + }, "\n") + + test.addQueries([]*Query{ &Query{ name: "aggregation with no interval - float", params: url.Values{"db": []string{"db0"}}, @@ -2265,7 +2590,127 @@ func TestServer_Query_AggregatesCommon(t *testing.T) { command: `SELECT sum(value) / mean(value) as div FROM floatoverlap GROUP BY region`, exp: `{"results":[{"series":[{"name":"floatoverlap","tags":{"region":"us-east"},"columns":["time","div"],"values":[["1970-01-01T00:00:00Z",2]]},{"name":"floatoverlap","tags":{"region":"us-west"},"columns":["time","div"],"values":[["1970-01-01T00:00:00Z",1]]}]}]}`, }, + }...) + for i, query := range test.queries { + if i == 0 { + if err := test.init(s); err != nil { + t.Fatalf("test init failed: %s", err) + } + } + if query.skip { + t.Logf("SKIP:: %s", query.name) + continue + } + if err := query.Execute(s); err != nil { + t.Error(query.Error(err)) + } else if !query.success() { + t.Error(query.failureMessage()) + } + } +} + +func TestServer_Query_Aggregates_Load(t *testing.T) { + t.Parallel() + s := OpenDefaultServer(NewConfig(), "") + defer s.Close() + + test := NewTest("db0", "rp0") + test.write = strings.Join([]string{ + fmt.Sprintf(`load,region=us-east,host=serverA value=20.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), + fmt.Sprintf(`load,region=us-east,host=serverB value=30.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:10Z").UnixNano()), + fmt.Sprintf(`load,region=us-west,host=serverC value=100.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:00Z").UnixNano()), + }, "\n") + + test.addQueries([]*Query{ + &Query{ + name: "group by multiple dimensions", + params: url.Values{"db": []string{"db0"}}, + command: `SELECT sum(value) FROM load GROUP BY region, host`, + exp: `{"results":[{"series":[{"name":"load","tags":{"host":"serverA","region":"us-east"},"columns":["time","sum"],"values":[["1970-01-01T00:00:00Z",20]]},{"name":"load","tags":{"host":"serverB","region":"us-east"},"columns":["time","sum"],"values":[["1970-01-01T00:00:00Z",30]]},{"name":"load","tags":{"host":"serverC","region":"us-west"},"columns":["time","sum"],"values":[["1970-01-01T00:00:00Z",100]]}]}]}`, + }, + &Query{ + name: "group by multiple dimensions", + params: url.Values{"db": []string{"db0"}}, + command: `SELECT sum(value)*2 FROM load`, + exp: `{"results":[{"series":[{"name":"load","columns":["time",""],"values":[["1970-01-01T00:00:00Z",300]]}]}]}`, + }, + &Query{ + name: "group by multiple dimensions", + params: url.Values{"db": []string{"db0"}}, + command: `SELECT sum(value)/2 FROM load`, + exp: `{"results":[{"series":[{"name":"load","columns":["time",""],"values":[["1970-01-01T00:00:00Z",75]]}]}]}`, + }, + }...) + + for i, query := range test.queries { + if i == 0 { + if err := test.init(s); err != nil { + t.Fatalf("test init failed: %s", err) + } + } + if query.skip { + t.Logf("SKIP:: %s", query.name) + continue + } + if err := query.Execute(s); err != nil { + t.Error(query.Error(err)) + } else if !query.success() { + t.Error(query.failureMessage()) + } + } +} + +func TestServer_Query_Aggregates_CPU(t *testing.T) { + t.Parallel() + s := OpenDefaultServer(NewConfig(), "") + defer s.Close() + + test := NewTest("db0", "rp0") + test.write = strings.Join([]string{ + fmt.Sprintf(`cpu,region=uk,host=serverZ,service=redis value=20.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:03Z").UnixNano()), + fmt.Sprintf(`cpu,region=uk,host=serverZ,service=mysql value=30.0 %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:03Z").UnixNano()), + }, "\n") + + test.addQueries([]*Query{ + &Query{ + name: "aggregation with WHERE and AND", + params: url.Values{"db": []string{"db0"}}, + command: `SELECT sum(value) FROM cpu WHERE region='uk' AND host='serverZ'`, + exp: `{"results":[{"series":[{"name":"cpu","columns":["time","sum"],"values":[["1970-01-01T00:00:00Z",50]]}]}]}`, + }, + }...) + + for i, query := range test.queries { + if i == 0 { + if err := test.init(s); err != nil { + t.Fatalf("test init failed: %s", err) + } + } + if query.skip { + t.Logf("SKIP:: %s", query.name) + continue + } + if err := query.Execute(s); err != nil { + t.Error(query.Error(err)) + } else if !query.success() { + t.Error(query.failureMessage()) + } + } +} + +func TestServer_Query_Aggregates_String(t *testing.T) { + t.Parallel() + s := OpenDefaultServer(NewConfig(), "") + defer s.Close() + + test := NewTest("db0", "rp0") + test.write = strings.Join([]string{ + fmt.Sprintf(`stringdata value="first" %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:03Z").UnixNano()), + fmt.Sprintf(`stringdata value="last" %d`, mustParseTime(time.RFC3339Nano, "2000-01-01T00:00:04Z").UnixNano()), + }, "\n") + + test.addQueries([]*Query{ // strings &Query{ name: "STDDEV on string data - string", @@ -2303,98 +2748,6 @@ func TestServer_Query_AggregatesCommon(t *testing.T) { command: `SELECT LAST(value) FROM stringdata`, exp: `{"results":[{"series":[{"name":"stringdata","columns":["time","last"],"values":[["2000-01-01T00:00:04Z","last"]]}]}]}`, }, - - // general queries - &Query{ - name: "group by multiple dimensions", - params: url.Values{"db": []string{"db0"}}, - command: `SELECT sum(value) FROM load GROUP BY region, host`, - exp: `{"results":[{"series":[{"name":"load","tags":{"host":"serverA","region":"us-east"},"columns":["time","sum"],"values":[["1970-01-01T00:00:00Z",20]]},{"name":"load","tags":{"host":"serverB","region":"us-east"},"columns":["time","sum"],"values":[["1970-01-01T00:00:00Z",30]]},{"name":"load","tags":{"host":"serverC","region":"us-west"},"columns":["time","sum"],"values":[["1970-01-01T00:00:00Z",100]]}]}]}`, - }, - &Query{ - name: "aggregation with WHERE and AND", - params: url.Values{"db": []string{"db0"}}, - command: `SELECT sum(value) FROM cpu WHERE region='uk' AND host='serverZ'`, - exp: `{"results":[{"series":[{"name":"cpu","columns":["time","sum"],"values":[["1970-01-01T00:00:00Z",50]]}]}]}`, - }, - - // Mathematics - &Query{ - name: "group by multiple dimensions", - params: url.Values{"db": []string{"db0"}}, - command: `SELECT sum(value)*2 FROM load`, - exp: `{"results":[{"series":[{"name":"load","columns":["time",""],"values":[["1970-01-01T00:00:00Z",300]]}]}]}`, - }, - &Query{ - name: "group by multiple dimensions", - params: url.Values{"db": []string{"db0"}}, - command: `SELECT sum(value)/2 FROM load`, - exp: `{"results":[{"series":[{"name":"load","columns":["time",""],"values":[["1970-01-01T00:00:00Z",75]]}]}]}`, - }, - - // group by - &Query{ - name: "max order by time with time specified group by 10s", - params: url.Values{"db": []string{"db0"}}, - command: `SELECT time, max(value) FROM intmany where time >= '2000-01-01T00:00:00Z' AND time <= '2000-01-01T00:01:14Z' group by time(10s)`, - exp: `{"results":[{"series":[{"name":"intmany","columns":["time","max"],"values":[["2000-01-01T00:00:00Z",2],["2000-01-01T00:00:10Z",4],["2000-01-01T00:00:20Z",4],["2000-01-01T00:00:30Z",4],["2000-01-01T00:00:40Z",5],["2000-01-01T00:00:50Z",5],["2000-01-01T00:01:00Z",7],["2000-01-01T00:01:10Z",9]]}]}]}`, - }, - &Query{ - name: "max order by time without time specified group by 30s", - params: url.Values{"db": []string{"db0"}}, - command: `SELECT max(value) FROM intmany where time >= '2000-01-01T00:00:00Z' AND time <= '2000-01-01T00:01:14Z' group by time(30s)`, - exp: `{"results":[{"series":[{"name":"intmany","columns":["time","max"],"values":[["2000-01-01T00:00:00Z",4],["2000-01-01T00:00:30Z",5],["2000-01-01T00:01:00Z",9]]}]}]}`, - }, - &Query{ - name: "max order by time with time specified group by 30s", - params: url.Values{"db": []string{"db0"}}, - command: `SELECT time, max(value) FROM intmany where time >= '2000-01-01T00:00:00Z' AND time <= '2000-01-01T00:01:14Z' group by time(30s)`, - exp: `{"results":[{"series":[{"name":"intmany","columns":["time","max"],"values":[["2000-01-01T00:00:10Z",4],["2000-01-01T00:00:40Z",5],["2000-01-01T00:01:10Z",9]]}]}]}`, - }, - &Query{ - name: "min order by time without time specified group by 15s", - params: url.Values{"db": []string{"db0"}}, - command: `SELECT min(value) FROM intmany where time >= '2000-01-01T00:00:00Z' AND time <= '2000-01-01T00:01:14Z' group by time(15s)`, - exp: `{"results":[{"series":[{"name":"intmany","columns":["time","min"],"values":[["2000-01-01T00:00:00Z",2],["2000-01-01T00:00:15Z",4],["2000-01-01T00:00:30Z",4],["2000-01-01T00:00:45Z",5],["2000-01-01T00:01:00Z",7]]}]}]}`, - }, - &Query{ - name: "min order by time with time specified group by 15s", - params: url.Values{"db": []string{"db0"}}, - command: `SELECT time, min(value) FROM intmany where time >= '2000-01-01T00:00:00Z' AND time <= '2000-01-01T00:01:14Z' group by time(15s)`, - exp: `{"results":[{"series":[{"name":"intmany","columns":["time","min"],"values":[["2000-01-01T00:00:00Z",2],["2000-01-01T00:00:20Z",4],["2000-01-01T00:00:30Z",4],["2000-01-01T00:00:50Z",5],["2000-01-01T00:01:00Z",7]]}]}]}`, - }, - &Query{ - name: "first order by time without time specified group by 15s", - params: url.Values{"db": []string{"db0"}}, - command: `SELECT first(value) FROM intmany where time >= '2000-01-01T00:00:00Z' AND time <= '2000-01-01T00:01:14Z' group by time(15s)`, - exp: `{"results":[{"series":[{"name":"intmany","columns":["time","first"],"values":[["2000-01-01T00:00:00Z",2],["2000-01-01T00:00:15Z",4],["2000-01-01T00:00:30Z",4],["2000-01-01T00:00:45Z",5],["2000-01-01T00:01:00Z",7]]}]}]}`, - }, - &Query{ - name: "first order by time with time specified group by 15s", - params: url.Values{"db": []string{"db0"}}, - command: `SELECT time, first(value) FROM intmany where time >= '2000-01-01T00:00:00Z' AND time <= '2000-01-01T00:01:14Z' group by time(15s)`, - exp: `{"results":[{"series":[{"name":"intmany","columns":["time","first"],"values":[["2000-01-01T00:00:00Z",2],["2000-01-01T00:00:20Z",4],["2000-01-01T00:00:30Z",4],["2000-01-01T00:00:50Z",5],["2000-01-01T00:01:00Z",7]]}]}]}`, - }, - &Query{ - name: "last order by time without time specified group by 15s", - params: url.Values{"db": []string{"db0"}}, - command: `SELECT last(value) FROM intmany where time >= '2000-01-01T00:00:00Z' AND time <= '2000-01-01T00:01:14Z' group by time(15s)`, - exp: `{"results":[{"series":[{"name":"intmany","columns":["time","last"],"values":[["2000-01-01T00:00:00Z",4],["2000-01-01T00:00:15Z",4],["2000-01-01T00:00:30Z",5],["2000-01-01T00:00:45Z",5],["2000-01-01T00:01:00Z",9]]}]}]}`, - }, - &Query{ - name: "last order by time with time specified group by 15s", - params: url.Values{"db": []string{"db0"}}, - command: `SELECT time, last(value) FROM intmany where time >= '2000-01-01T00:00:00Z' AND time <= '2000-01-01T00:01:14Z' group by time(15s)`, - exp: `{"results":[{"series":[{"name":"intmany","columns":["time","last"],"values":[["2000-01-01T00:00:10Z",4],["2000-01-01T00:00:20Z",4],["2000-01-01T00:00:40Z",5],["2000-01-01T00:00:50Z",5],["2000-01-01T00:01:10Z",9]]}]}]}`, - }, - - // order by time desc - &Query{ - name: "aggregate order by time desc", - params: url.Values{"db": []string{"db0"}}, - command: `SELECT max(value) FROM intmany where time >= '2000-01-01T00:00:00Z' AND time <= '2000-01-01T00:01:00Z' group by time(10s) order by time desc`, - exp: `{"results":[{"series":[{"name":"intmany","columns":["time","max"],"values":[["2000-01-01T00:01:00Z",7],["2000-01-01T00:00:50Z",5],["2000-01-01T00:00:40Z",5],["2000-01-01T00:00:30Z",4],["2000-01-01T00:00:20Z",4],["2000-01-01T00:00:10Z",4],["2000-01-01T00:00:00Z",2]]}]}]}`, - }, }...) for i, query := range test.queries { @@ -2899,6 +3252,11 @@ func TestServer_Query_TopInt(t *testing.T) { t.Logf("SKIP: %s", query.name) continue } + + println(">>>>", query.name) + if query.name != `top - memory - host tag with limit 2` { // FIXME: temporary + continue + } if err := query.Execute(s); err != nil { t.Error(query.Error(err)) } else if !query.success() { @@ -4948,3 +5306,33 @@ func TestServer_Query_IntoTarget(t *testing.T) { } } } + +// This test reproduced a data race with closing the +// Subscriber points channel while writes were in-flight in the PointsWriter. +func TestServer_ConcurrentPointsWriter_Subscriber(t *testing.T) { + t.Parallel() + s := OpenDefaultServer(NewConfig(), "") + defer s.Close() + + // goroutine to write points + done := make(chan struct{}) + go func() { + for { + select { + case <-done: + return + default: + wpr := &cluster.WritePointsRequest{ + Database: "db0", + RetentionPolicy: "rp0", + } + s.PointsWriter.WritePoints(wpr) + } + } + }() + + time.Sleep(10 * time.Millisecond) + + close(done) + // Race occurs on s.Close() +} diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/etc/config.sample.toml b/Godeps/_workspace/src/github.com/influxdb/influxdb/etc/config.sample.toml index 7bcd0d6e0..92016e302 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/etc/config.sample.toml +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/etc/config.sample.toml @@ -48,7 +48,9 @@ reporting-disabled = false # Controls the engine type for new shards. Options are b1, bz1, or tsm1. # b1 is the 0.9.2 storage engine, bz1 is the 0.9.3 and 0.9.4 engine. - # tsm1 is the 0.9.5 engine + # tsm1 is the 0.9.5 engine and is currenly EXPERIMENTAL. Until 0.9.5 is + # actually released data written into a tsm1 engine may be need to be wiped + # between upgrades. # engine ="bz1" # The following WAL settings are for the b1 storage engine used in 0.9.2. They won't @@ -85,6 +87,34 @@ reporting-disabled = false # log any sensitive data contained within a query. # query-log-enabled = true +### +### [hinted-handoff] +### +### Controls the hinted handoff feature, which allows nodes to temporarily +### store queued data when one node of a cluster is down for a short period +### of time. +### + +[hinted-handoff] + enabled = true + dir = "/var/opt/influxdb/hh" + max-size = 1073741824 + max-age = "168h" + retry-rate-limit = 0 + + # Hinted handoff will start retrying writes to down nodes at a rate of once per second. + # If any error occurs, it will backoff in an exponential manner, until the interval + # reaches retry-max-interval. Once writes to all nodes are successfully completed the + # interval will reset to retry-interval. + retry-interval = "1s" + retry-max-interval = "1m" + + # Interval between running checks for data that should be purged. Data is purged from + # hinted-handoff queues for two reasons. 1) The data is older than the max age, or + # 2) the target node has been dropped from the cluster. Data is never dropped until + # it has reached max-age however, for a dropped node or not. + purge-interval = "1h" + ### ### [cluster] ### @@ -106,6 +136,17 @@ reporting-disabled = false enabled = true check-interval = "30m" +### +### [shard-precreation] +### +### Controls the precreation of shards, so they are created before data arrives. +### Only shards that will exist in the future, at time of creation, are precreated. + +[shard-precreation] + enabled = true + check-interval = "10m" + advance-period = "30m" + ### ### Controls the system self-monitoring, statistics and diagnostics. ### @@ -171,6 +212,7 @@ reporting-disabled = false # batch-size = 1000 # will flush if this many points get buffered # batch-pending = 5 # number of batches that may be pending in memory # batch-timeout = "1s" # will flush at least this often even if we haven't hit buffer limit + # udp-read-buffer = 0 # UDP Read buffer size, 0 means OS default. UDP listener will fail if set above OS max. ## "name-schema" configures tag names for parsing the metric name from graphite protocol; ## separated by `name-separator`. @@ -211,6 +253,7 @@ reporting-disabled = false # batch-size = 1000 # will flush if this many points get buffered # batch-pending = 5 # number of batches that may be pending in memory # batch-timeout = "1s" # will flush at least this often even if we haven't hit buffer limit + # read-buffer = 0 # UDP Read buffer size, 0 means OS default. UDP listener will fail if set above OS max. ### ### [opentsdb] @@ -254,6 +297,7 @@ reporting-disabled = false # batch-size = 1000 # will flush if this many points get buffered # batch-pending = 5 # number of batches that may be pending in memory # batch-timeout = "1s" # will flush at least this often even if we haven't hit buffer limit + # read-buffer = 0 # UDP Read buffer size, 0 means OS default. UDP listener will fail if set above OS max. ### ### [continuous_queries] @@ -268,25 +312,3 @@ reporting-disabled = false recompute-no-older-than = "10m" compute-runs-per-interval = 10 compute-no-more-than = "2m" - -### -### [hinted-handoff] -### -### Controls the hinted handoff feature, which allows nodes to temporarily -### store queued data when one node of a cluster is down for a short period -### of time. -### - -[hinted-handoff] - enabled = true - dir = "/var/opt/influxdb/hh" - max-size = 1073741824 - max-age = "168h" - retry-rate-limit = 0 - - # Hinted handoff will start retrying writes to down nodes at a rate of once per second. - # If any error occurs, it will backoff in an exponential manner, until the interval - # reaches retry-max-interval. Once writes to all nodes are successfully completed the - # interval will reset to retry-interval. - retry-interval = "1s" - retry-max-interval = "1m" diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/importer/v8/importer.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/importer/v8/importer.go index 5095868f3..86e998fcd 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/importer/v8/importer.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/importer/v8/importer.go @@ -145,6 +145,10 @@ func (i *Importer) processDDL(scanner *bufio.Scanner) { if strings.HasPrefix(line, "#") { continue } + // Skip blank lines + if strings.TrimSpace(line) == "" { + continue + } i.queryExecutor(line) } } @@ -162,8 +166,14 @@ func (i *Importer) processDML(scanner *bufio.Scanner) { if strings.HasPrefix(line, "#") { continue } + // Skip blank lines + if strings.TrimSpace(line) == "" { + continue + } i.batchAccumulator(line, start) } + // Call batchWrite one last time to flush anything out in the batch + i.batchWrite() } func (i *Importer) execute(command string) { @@ -185,14 +195,7 @@ func (i *Importer) queryExecutor(command string) { func (i *Importer) batchAccumulator(line string, start time.Time) { i.batch = append(i.batch, line) if len(i.batch) == batchSize { - if e := i.batchWrite(); e != nil { - log.Println("error writing batch: ", e) - // Output failed lines to STDOUT so users can capture lines that failed to import - fmt.Println(strings.Join(i.batch, "\n")) - i.failedInserts += len(i.batch) - } else { - i.totalInserts += len(i.batch) - } + i.batchWrite() i.batch = i.batch[:0] // Give some status feedback every 100000 lines processed processed := i.totalInserts + i.failedInserts @@ -204,7 +207,7 @@ func (i *Importer) batchAccumulator(line string, start time.Time) { } } -func (i *Importer) batchWrite() error { +func (i *Importer) batchWrite() { // Accumulate the batch size to see how many points we have written this second i.throttlePointsWritten += len(i.batch) @@ -226,11 +229,20 @@ func (i *Importer) batchWrite() error { // Decrement the batch size back out as it is going to get called again i.throttlePointsWritten -= len(i.batch) - return i.batchWrite() + i.batchWrite() + return } _, e := i.client.WriteLineProtocol(strings.Join(i.batch, "\n"), i.database, i.retentionPolicy, i.config.Precision, i.config.WriteConsistency) + if e != nil { + log.Println("error writing batch: ", e) + // Output failed lines to STDOUT so users can capture lines that failed to import + fmt.Println(strings.Join(i.batch, "\n")) + i.failedInserts += len(i.batch) + } else { + i.totalInserts += len(i.batch) + } i.throttlePointsWritten = 0 i.lastWrite = time.Now() - return e + return } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/influxql/INFLUXQL.md b/Godeps/_workspace/src/github.com/influxdb/influxdb/influxql/INFLUXQL.md index 75be87358..106b80238 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/influxql/INFLUXQL.md +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/influxql/INFLUXQL.md @@ -84,15 +84,17 @@ _cpu_stats ``` ALL ALTER ANY AS ASC BEGIN BY CREATE CONTINUOUS DATABASE DATABASES DEFAULT -DELETE DESC DESTINATIONS DROP DURATION END -EXISTS EXPLAIN FIELD FROM GRANT GROUP -IF IN INNER INSERT INTO KEY +DELETE DESC DESTINATIONS DIAGNOSTICS DISTINCT DROP +DURATION END EXISTS EXPLAIN FIELD FOR +FORCE FROM GRANT GRANTS GROUP IF +IN INF INNER INSERT INTO KEY KEYS LIMIT SHOW MEASUREMENT MEASUREMENTS NOT OFFSET ON ORDER PASSWORD POLICY POLICIES PRIVILEGES QUERIES QUERY READ REPLICATION RETENTION -REVOKE SELECT SERIES SLIMIT SOFFSET SUBSCRIPTION -SUBSCRIPTIONS TAG TO USER USERS VALUES -WHERE WITH WRITE +REVOKE SELECT SERIES SERVER SERVERS SET +SHARDS SLIMIT SOFFSET STATS SUBSCRIPTION SUBSCRIPTIONS +TAG TO USER USERS VALUES WHERE +WITH WRITE ``` ## Literals diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/influxql/ast.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/influxql/ast.go index d269b0204..858998e1c 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/influxql/ast.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/influxql/ast.go @@ -340,12 +340,19 @@ func (s *CreateDatabaseStatement) RequiredPrivileges() ExecutionPrivileges { type DropDatabaseStatement struct { // Name of the database to be dropped. Name string + + // IfExists indicates whether to return without error if the database + // does not exists. + IfExists bool } // String returns a string representation of the drop database statement. func (s *DropDatabaseStatement) String() string { var buf bytes.Buffer _, _ = buf.WriteString("DROP DATABASE ") + if s.IfExists { + _, _ = buf.WriteString("IF EXISTS ") + } _, _ = buf.WriteString(s.Name) return buf.String() } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/influxql/parser.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/influxql/parser.go index 87570d223..c13a6b46b 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/influxql/parser.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/influxql/parser.go @@ -1458,6 +1458,16 @@ func (p *Parser) parseCreateDatabaseStatement() (*CreateDatabaseStatement, error func (p *Parser) parseDropDatabaseStatement() (*DropDatabaseStatement, error) { stmt := &DropDatabaseStatement{} + // Look for "IF EXISTS" + if tok, _, _ := p.scanIgnoreWhitespace(); tok == IF { + if err := p.parseTokens([]Token{EXISTS}); err != nil { + return nil, err + } + stmt.IfExists = true + } else { + p.unscan() + } + // Parse the name of the database to be dropped. lit, err := p.parseIdent() if err != nil { diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/influxql/parser_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/influxql/parser_test.go index 138881234..9a021be54 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/influxql/parser_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/influxql/parser_test.go @@ -1224,8 +1224,18 @@ func TestParser_ParseStatement(t *testing.T) { // DROP DATABASE statement { - s: `DROP DATABASE testdb`, - stmt: &influxql.DropDatabaseStatement{Name: "testdb"}, + s: `DROP DATABASE testdb`, + stmt: &influxql.DropDatabaseStatement{ + Name: "testdb", + IfExists: false, + }, + }, + { + s: `DROP DATABASE IF EXISTS testdb`, + stmt: &influxql.DropDatabaseStatement{ + Name: "testdb", + IfExists: true, + }, }, // DROP MEASUREMENT statement @@ -1599,6 +1609,8 @@ func TestParser_ParseStatement(t *testing.T) { {s: `CREATE DATABASE IF NOT`, err: `found EOF, expected EXISTS at line 1, char 24`}, {s: `CREATE DATABASE IF NOT EXISTS`, err: `found EOF, expected identifier at line 1, char 31`}, {s: `DROP DATABASE`, err: `found EOF, expected identifier at line 1, char 15`}, + {s: `DROP DATABASE IF`, err: `found EOF, expected EXISTS at line 1, char 18`}, + {s: `DROP DATABASE IF EXISTS`, err: `found EOF, expected identifier at line 1, char 25`}, {s: `DROP RETENTION`, err: `found EOF, expected POLICY at line 1, char 16`}, {s: `DROP RETENTION POLICY`, err: `found EOF, expected identifier at line 1, char 23`}, {s: `DROP RETENTION POLICY "1h.cpu"`, err: `found EOF, expected ON at line 1, char 31`}, diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/errors.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/errors.go index 4b7d7ba94..750ed6612 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/errors.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/errors.go @@ -33,9 +33,6 @@ var ( // ErrNodeUnableToDropSingleNode is returned if the node being dropped is the last // node in the cluster ErrNodeUnableToDropFinalNode = newError("unable to drop the final node in a cluster") - - // ErrNodeRaft is returned when attempting an operation prohibted for a Raft-node. - ErrNodeRaft = newError("node is a Raft node") ) var ( @@ -70,7 +67,7 @@ var ( // ErrRetentionPolicyDurationTooLow is returned when updating a retention // policy that has a duration lower than the allowed minimum. ErrRetentionPolicyDurationTooLow = newError(fmt.Sprintf("retention policy duration must be at least %s", - RetentionPolicyMinDuration)) + MinRetentionPolicyDuration)) // ErrReplicationFactorTooLow is returned when the replication factor is not in an // acceptable range. diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/internal/meta.pb.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/internal/meta.pb.go index 3289eb24f..13f02fc29 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/internal/meta.pb.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/internal/meta.pb.go @@ -42,6 +42,7 @@ It has these top-level messages: UpdateNodeCommand CreateSubscriptionCommand DropSubscriptionCommand + RemovePeerCommand Response ResponseHeader ErrorResponse @@ -119,6 +120,7 @@ const ( Command_UpdateNodeCommand Command_Type = 19 Command_CreateSubscriptionCommand Command_Type = 21 Command_DropSubscriptionCommand Command_Type = 22 + Command_RemovePeerCommand Command_Type = 23 ) var Command_Type_name = map[int32]string{ @@ -143,6 +145,7 @@ var Command_Type_name = map[int32]string{ 19: "UpdateNodeCommand", 21: "CreateSubscriptionCommand", 22: "DropSubscriptionCommand", + 23: "RemovePeerCommand", } var Command_Type_value = map[string]int32{ "CreateNodeCommand": 1, @@ -166,6 +169,7 @@ var Command_Type_value = map[string]int32{ "UpdateNodeCommand": 19, "CreateSubscriptionCommand": 21, "DropSubscriptionCommand": 22, + "RemovePeerCommand": 23, } func (x Command_Type) Enum() *Command_Type { @@ -1368,6 +1372,38 @@ var E_DropSubscriptionCommand_Command = &proto.ExtensionDesc{ Tag: "bytes,122,opt,name=command", } +type RemovePeerCommand struct { + ID *uint64 `protobuf:"varint,1,req,name=ID" json:"ID,omitempty"` + Addr *string `protobuf:"bytes,2,req,name=Addr" json:"Addr,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *RemovePeerCommand) Reset() { *m = RemovePeerCommand{} } +func (m *RemovePeerCommand) String() string { return proto.CompactTextString(m) } +func (*RemovePeerCommand) ProtoMessage() {} + +func (m *RemovePeerCommand) GetID() uint64 { + if m != nil && m.ID != nil { + return *m.ID + } + return 0 +} + +func (m *RemovePeerCommand) GetAddr() string { + if m != nil && m.Addr != nil { + return *m.Addr + } + return "" +} + +var E_RemovePeerCommand_Command = &proto.ExtensionDesc{ + ExtendedType: (*Command)(nil), + ExtensionType: (*RemovePeerCommand)(nil), + Field: 123, + Name: "internal.RemovePeerCommand.command", + Tag: "bytes,123,opt,name=command", +} + type Response struct { OK *bool `protobuf:"varint,1,req" json:"OK,omitempty"` Error *string `protobuf:"bytes,2,opt" json:"Error,omitempty"` @@ -1598,4 +1634,5 @@ func init() { proto.RegisterExtension(E_UpdateNodeCommand_Command) proto.RegisterExtension(E_CreateSubscriptionCommand_Command) proto.RegisterExtension(E_DropSubscriptionCommand_Command) + proto.RegisterExtension(E_RemovePeerCommand_Command) } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/internal/meta.proto b/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/internal/meta.proto index cddcb57df..8605187d8 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/internal/meta.proto +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/internal/meta.proto @@ -114,6 +114,7 @@ message Command { UpdateNodeCommand = 19; CreateSubscriptionCommand = 21; DropSubscriptionCommand = 22; + RemovePeerCommand = 23; } required Type type = 1; @@ -296,6 +297,14 @@ message DropSubscriptionCommand { required string RetentionPolicy = 3; } +message RemovePeerCommand { + extend Command { + optional RemovePeerCommand command = 123; + } + required uint64 ID = 1; + required string Addr = 2; +} + message Response { required bool OK = 1; optional string Error = 2; diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/rpc.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/rpc.go index d52c0b0a7..ee49b6677 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/rpc.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/rpc.go @@ -51,7 +51,7 @@ type Reply interface { // proxyLeader proxies the connection to the current raft leader func (r *rpc) proxyLeader(conn *net.TCPConn) { if r.store.Leader() == "" { - r.sendError(conn, "no leader") + r.sendError(conn, "no leader detected during proxyLeader") return } @@ -289,7 +289,7 @@ func (r *rpc) fetchMetaData(blocking bool) (*Data, error) { // Retrieve the current known leader. leader := r.store.Leader() if leader == "" { - return nil, errors.New("no leader") + return nil, errors.New("no leader detected during fetchMetaData") } var index, term uint64 diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/state.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/state.go index f980ba0ae..2612302c3 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/state.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/state.go @@ -28,6 +28,7 @@ type raftState interface { sync(index uint64, timeout time.Duration) error setPeers(addrs []string) error addPeer(addr string) error + removePeer(addr string) error peers() ([]string, error) invalidate() error close() error @@ -91,7 +92,7 @@ func (r *localRaft) invalidate() error { ms, err := r.store.rpc.fetchMetaData(false) if err != nil { - return err + return fmt.Errorf("error fetching meta data: %s", err) } r.updateMetaData(ms) @@ -208,11 +209,6 @@ func (r *localRaft) close() error { r.transport = nil } - if r.raftLayer != nil { - r.raftLayer.Close() - r.raftLayer = nil - } - // Shutdown raft. if r.raft != nil { if err := r.raft.Shutdown().Error(); err != nil { @@ -318,6 +314,18 @@ func (r *localRaft) addPeer(addr string) error { return nil } +// removePeer removes addr from the list of peers in the cluster. +func (r *localRaft) removePeer(addr string) error { + // Only do this on the leader + if !r.isLeader() { + return errors.New("not the leader") + } + if fut := r.raft.RemovePeer(addr); fut.Error() != nil { + return fut.Error() + } + return nil +} + // setPeers sets a list of peers in the cluster. func (r *localRaft) setPeers(addrs []string) error { return r.raft.SetPeers(addrs).Error() @@ -377,7 +385,7 @@ func (r *remoteRaft) updateMetaData(ms *Data) { func (r *remoteRaft) invalidate() error { ms, err := r.store.rpc.fetchMetaData(false) if err != nil { - return err + return fmt.Errorf("error fetching meta data: %s", err) } r.updateMetaData(ms) @@ -401,6 +409,11 @@ func (r *remoteRaft) addPeer(addr string) error { return fmt.Errorf("cannot add peer using remote raft") } +// removePeer does nothing for remoteRaft. +func (r *remoteRaft) removePeer(addr string) error { + return nil +} + func (r *remoteRaft) peers() ([]string, error) { return readPeersJSON(filepath.Join(r.store.path, "peers.json")) } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/statement_executor.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/statement_executor.go index 5a6fa4690..9727784f8 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/statement_executor.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/statement_executor.go @@ -174,15 +174,6 @@ func (e *StatementExecutor) executeDropServerStatement(q *influxql.DropServerSta return &influxql.Result{Err: ErrNodeNotFound} } - // Dropping only non-Raft nodes supported. - peers, err := e.Store.Peers() - if err != nil { - return &influxql.Result{Err: err} - } - if contains(peers, ni.Host) { - return &influxql.Result{Err: ErrNodeRaft} - } - err = e.Store.DeleteNode(q.NodeID, q.Force) return &influxql.Result{Err: err} } @@ -369,9 +360,15 @@ func (e *StatementExecutor) executeShowShardsStatement(stmt *influxql.ShowShards rows := []*models.Row{} for _, di := range dis { - row := &models.Row{Columns: []string{"id", "start_time", "end_time", "expiry_time", "owners"}, Name: di.Name} + row := &models.Row{Columns: []string{"id", "database", "retention_policy", "shard_group", "start_time", "end_time", "expiry_time", "owners"}, Name: di.Name} for _, rpi := range di.RetentionPolicies { for _, sgi := range rpi.ShardGroups { + // Shards associated with deleted shard groups are effectively deleted. + // Don't list them. + if sgi.Deleted() { + continue + } + for _, si := range sgi.Shards { ownerIDs := make([]uint64, len(si.Owners)) for i, owner := range si.Owners { @@ -380,6 +377,9 @@ func (e *StatementExecutor) executeShowShardsStatement(stmt *influxql.ShowShards row.Values = append(row.Values, []interface{}{ si.ID, + di.Name, + rpi.Name, + sgi.ID, sgi.StartTime.UTC().Format(time.RFC3339), sgi.EndTime.UTC().Format(time.RFC3339), sgi.EndTime.Add(rpi.Duration).UTC().Format(time.RFC3339), diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/statement_executor_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/statement_executor_test.go index 6c3e22347..f8c7e2f10 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/statement_executor_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/statement_executor_test.go @@ -166,8 +166,12 @@ func TestStatementExecutor_ExecuteStatement_DropServer(t *testing.T) { }, nil } - // Ensure Raft nodes cannot be dropped. - if res := e.ExecuteStatement(influxql.MustParseStatement(`DROP SERVER 1`)); res.Err != meta.ErrNodeRaft { + e.Store.DeleteNodeFn = func(id uint64, force bool) error { + return nil + } + + // Ensure Raft nodes can be dropped. + if res := e.ExecuteStatement(influxql.MustParseStatement(`DROP SERVER 1`)); res.Err != nil { t.Fatalf("unexpected error: %s", res.Err) } @@ -970,9 +974,11 @@ func TestStatementExecutor_ExecuteStatement_ShowShards(t *testing.T) { Name: "foo", RetentionPolicies: []meta.RetentionPolicyInfo{ { + Name: "rpi_foo", Duration: time.Second, ShardGroups: []meta.ShardGroupInfo{ { + ID: 66, StartTime: time.Unix(0, 0), EndTime: time.Unix(1, 0), Shards: []meta.ShardInfo{ @@ -1001,10 +1007,10 @@ func TestStatementExecutor_ExecuteStatement_ShowShards(t *testing.T) { } else if !reflect.DeepEqual(res.Series, models.Rows{ { Name: "foo", - Columns: []string{"id", "start_time", "end_time", "expiry_time", "owners"}, + Columns: []string{"id", "database", "retention_policy", "shard_group", "start_time", "end_time", "expiry_time", "owners"}, Values: [][]interface{}{ - {uint64(1), "1970-01-01T00:00:00Z", "1970-01-01T00:00:01Z", "1970-01-01T00:00:02Z", "1,2,3"}, - {uint64(2), "1970-01-01T00:00:00Z", "1970-01-01T00:00:01Z", "1970-01-01T00:00:02Z", ""}, + {uint64(1), "foo", "rpi_foo", uint64(66), "1970-01-01T00:00:00Z", "1970-01-01T00:00:01Z", "1970-01-01T00:00:02Z", "1,2,3"}, + {uint64(2), "foo", "rpi_foo", uint64(66), "1970-01-01T00:00:00Z", "1970-01-01T00:00:01Z", "1970-01-01T00:00:02Z", ""}, }, }, }) { diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/store.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/store.go index d5dfaca42..34900b934 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/store.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/store.go @@ -46,7 +46,6 @@ const ExecMagic = "EXEC" const ( AutoCreateRetentionPolicyName = "default" AutoCreateRetentionPolicyPeriod = 0 - RetentionPolicyMinDuration = time.Hour // MaxAutoCreatedRetentionPolicyReplicaN is the maximum replication factor that will // be set for auto-created retention policies. @@ -230,7 +229,6 @@ func (s *Store) Open() error { return nil }(); err != nil { - s.close() return err } @@ -375,6 +373,9 @@ func (s *Store) joinCluster() error { } func (s *Store) enableLocalRaft() error { + s.mu.Lock() + defer s.mu.Unlock() + if _, ok := s.raftState.(*localRaft); ok { return nil } @@ -395,15 +396,16 @@ func (s *Store) enableRemoteRaft() error { } func (s *Store) changeState(state raftState) error { - if err := s.raftState.close(); err != nil { - return err - } + if s.raftState != nil { + if err := s.raftState.close(); err != nil { + return err + } - // Clear out any persistent state - if err := s.raftState.remove(); err != nil { - return err + // Clear out any persistent state + if err := s.raftState.remove(); err != nil { + return err + } } - s.raftState = state if err := s.raftState.open(); err != nil { @@ -454,15 +456,34 @@ func (s *Store) close() error { } s.opened = false - // Notify goroutines of close. - close(s.closing) - // FIXME(benbjohnson): s.wg.Wait() + // Close our exec listener + if err := s.ExecListener.Close(); err != nil { + s.Logger.Printf("error closing ExecListener %s", err) + } + + // Close our RPC listener + if err := s.RPCListener.Close(); err != nil { + s.Logger.Printf("error closing ExecListener %s", err) + } if s.raftState != nil { s.raftState.close() - s.raftState = nil } + // Because a go routine could of already fired in the time we acquired the lock + // it could then try to acquire another lock, and will deadlock. + // For that reason, we will release our lock and signal the close so that + // all go routines can exit cleanly and fullfill their contract to the wait group. + s.mu.Unlock() + // Notify goroutines of close. + close(s.closing) + s.wg.Wait() + + // Now that all go routines are cleaned up, w lock to do final clean up and exit + s.mu.Lock() + + s.raftState = nil + return nil } @@ -519,7 +540,9 @@ func (s *Store) createLocalNode() error { } // Set ID locally. + s.mu.Lock() s.id = ni.ID + s.mu.Unlock() s.Logger.Printf("Created local node: id=%d, host=%s", s.id, s.RemoteAddr) @@ -578,9 +601,6 @@ func (s *Store) Err() <-chan error { return s.err } func (s *Store) IsLeader() bool { s.mu.RLock() defer s.mu.RUnlock() - if s.raftState == nil { - return false - } return s.raftState.isLeader() } @@ -619,6 +639,7 @@ func (s *Store) serveExecListener() { for { // Accept next TCP connection. + var err error conn, err := s.ExecListener.Accept() if err != nil { if strings.Contains(err.Error(), "connection closed") { @@ -631,6 +652,12 @@ func (s *Store) serveExecListener() { // Handle connection in a separate goroutine. s.wg.Add(1) go s.handleExecConn(conn) + + select { + case <-s.closing: + return + default: + } } } @@ -739,6 +766,12 @@ func (s *Store) serveRPCListener() { defer s.wg.Done() s.rpc.handleRPCConn(conn) }() + + select { + case <-s.closing: + return + default: + } } } @@ -829,12 +862,23 @@ func (s *Store) DeleteNode(id uint64, force bool) error { return ErrNodeNotFound } - return s.exec(internal.Command_DeleteNodeCommand, internal.E_DeleteNodeCommand_Command, + err := s.exec(internal.Command_DeleteNodeCommand, internal.E_DeleteNodeCommand_Command, &internal.DeleteNodeCommand{ ID: proto.Uint64(id), Force: proto.Bool(force), }, ) + if err != nil { + return err + } + + // Need to send a second message to remove the peer + return s.exec(internal.Command_RemovePeerCommand, internal.E_RemovePeerCommand_Command, + &internal.RemovePeerCommand{ + ID: proto.Uint64(id), + Addr: proto.String(ni.Host), + }, + ) } // Database returns a database by name. @@ -975,7 +1019,7 @@ func (s *Store) RetentionPolicies(database string) (a []RetentionPolicyInfo, err // CreateRetentionPolicy creates a new retention policy for a database. func (s *Store) CreateRetentionPolicy(database string, rpi *RetentionPolicyInfo) (*RetentionPolicyInfo, error) { - if rpi.Duration < RetentionPolicyMinDuration && rpi.Duration != 0 { + if rpi.Duration < MinRetentionPolicyDuration && rpi.Duration != 0 { return nil, ErrRetentionPolicyDurationTooLow } if err := s.exec(internal.Command_CreateRetentionPolicyCommand, internal.E_CreateRetentionPolicyCommand_Command, @@ -1443,10 +1487,10 @@ func (s *Store) PrecreateShardGroups(from, to time.Time) error { // Create successive shard group. nextShardGroupTime := g.EndTime.Add(1 * time.Nanosecond) if newGroup, err := s.CreateShardGroupIfNotExists(di.Name, rp.Name, nextShardGroupTime); err != nil { - s.Logger.Printf("failed to create successive shard group for group %d: %s", + s.Logger.Printf("failed to precreate successive shard group for group %d: %s", g.ID, err.Error()) } else { - s.Logger.Printf("new shard group %d successfully created for database %s, retention policy %s", + s.Logger.Printf("new shard group %d successfully precreated for database %s, retention policy %s", newGroup.ID, di.Name, rp.Name) } } @@ -1539,7 +1583,7 @@ func (s *Store) remoteExec(b []byte) error { // Retrieve the current known leader. leader := s.raftState.leader() if leader == "" { - return errors.New("no leader") + return errors.New("no leader detected during remoteExec") } // Create a connection to the leader. @@ -1650,6 +1694,8 @@ func (fsm *storeFSM) Apply(l *raft.Log) interface{} { err := func() interface{} { switch cmd.GetType() { + case internal.Command_RemovePeerCommand: + return fsm.applyRemovePeerCommand(&cmd) case internal.Command_CreateNodeCommand: return fsm.applyCreateNodeCommand(&cmd) case internal.Command_DeleteNodeCommand: @@ -1705,6 +1751,33 @@ func (fsm *storeFSM) Apply(l *raft.Log) interface{} { return err } +func (fsm *storeFSM) applyRemovePeerCommand(cmd *internal.Command) interface{} { + ext, _ := proto.GetExtension(cmd, internal.E_RemovePeerCommand_Command) + v := ext.(*internal.RemovePeerCommand) + + id := v.GetID() + addr := v.GetAddr() + + // Only do this if you are the leader + if fsm.raftState.isLeader() { + //Remove that node from the peer + fsm.Logger.Printf("removing peer for node id %d, %s", id, addr) + if err := fsm.raftState.removePeer(addr); err != nil { + fsm.Logger.Printf("error removing peer: %s", err) + } + } + + // If this is the node being shutdown, close raft + if fsm.id == id { + fsm.Logger.Printf("shutting down raft for %s", addr) + if err := fsm.raftState.close(); err != nil { + fsm.Logger.Printf("failed to shut down raft: %s", err) + } + } + + return nil +} + func (fsm *storeFSM) applyCreateNodeCommand(cmd *internal.Command) interface{} { ext, _ := proto.GetExtension(cmd, internal.E_CreateNodeCommand_Command) v := ext.(*internal.CreateNodeCommand) diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/store_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/store_test.go index 1996f611a..6c7244ea1 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/store_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/meta/store_test.go @@ -11,6 +11,7 @@ import ( "reflect" "sort" "strconv" + "sync" "testing" "time" @@ -971,6 +972,7 @@ func TestCluster_OpenRaft(t *testing.T) { // Ensure a multi-node cluster can restart func TestCluster_Restart(t *testing.T) { + t.Skip("ISSUE https://github.com/influxdb/influxdb/issues/4723") // Start a single node. c := MustOpenCluster(1) defer c.Close() @@ -1041,6 +1043,17 @@ func TestCluster_Restart(t *testing.T) { // ensure all the nodes see the same metastore data assertDatabaseReplicated(t, c) + var wg sync.WaitGroup + wg.Add(len(c.Stores)) + for _, s := range c.Stores { + go func(s *Store) { + defer wg.Done() + if err := s.Close(); err != nil { + t.Fatalf("error closing store %s", err) + } + }(s) + } + wg.Wait() } // Store is a test wrapper for meta.Store. @@ -1057,7 +1070,9 @@ func NewStore(c *meta.Config) *Store { s := &Store{ Store: meta.NewStore(c), } - s.Logger = log.New(&s.Stderr, "", log.LstdFlags) + if !testing.Verbose() { + s.Logger = log.New(&s.Stderr, "", log.LstdFlags) + } s.SetHashPasswordFn(mockHashPassword) return s } @@ -1219,9 +1234,16 @@ func (c *Cluster) Open() error { // Close shuts down all stores. func (c *Cluster) Close() error { + var wg sync.WaitGroup + wg.Add(len(c.Stores)) + for _, s := range c.Stores { - s.Close() + go func(s *Store) { + defer wg.Done() + s.Close() + }(s) } + wg.Wait() return nil } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/models/points.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/models/points.go index 277748679..6cdaba37f 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/models/points.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/models/points.go @@ -4,8 +4,10 @@ import ( "bytes" "fmt" "hash/fnv" + "math" "sort" "strconv" + "strings" "time" "github.com/influxdb/influxdb/pkg/escape" @@ -55,6 +57,11 @@ type Point interface { // is a timestamp associated with the point then it will be specified in the // given unit PrecisionString(precision string) string + + // RoundedString returns a string representation of the point object, if there + // is a timestamp associated with the point, then it will be rounded to the + // given duration + RoundedString(d time.Duration) string } // Points represents a sortable list of points by timestamp. @@ -112,7 +119,8 @@ func ParsePointsString(buf string) ([]Point, error) { } // ParsePoints returns a slice of Points from a text representation of a point -// with each point separated by newlines. +// with each point separated by newlines. If any points fail to parse, a non-nil error +// will be returned in addition to the points that parsed successfully. func ParsePoints(buf []byte) ([]Point, error) { return ParsePointsWithPrecision(buf, time.Now().UTC(), "n") } @@ -120,8 +128,9 @@ func ParsePoints(buf []byte) ([]Point, error) { func ParsePointsWithPrecision(buf []byte, defaultTime time.Time, precision string) ([]Point, error) { points := []Point{} var ( - pos int - block []byte + pos int + block []byte + failed []string ) for { pos, block = scanLine(buf, pos) @@ -150,15 +159,19 @@ func ParsePointsWithPrecision(buf []byte, defaultTime time.Time, precision strin pt, err := parsePoint(block[start:len(block)], defaultTime, precision) if err != nil { - return nil, fmt.Errorf("unable to parse '%s': %v", string(block[start:len(block)]), err) + failed = append(failed, fmt.Sprintf("unable to parse '%s': %v", string(block[start:len(block)]), err)) + } else { + points = append(points, pt) } - points = append(points, pt) if pos >= len(buf) { break } } + if len(failed) > 0 { + return points, fmt.Errorf("%s", strings.Join(failed, "\n")) + } return points, nil } @@ -614,14 +627,11 @@ func scanNumber(buf []byte, i int) (int, error) { continue } - // NaN is a valid float + // NaN is an unsupported value if i+2 < len(buf) && (buf[i] == 'N' || buf[i] == 'n') { - if (buf[i+1] == 'a' || buf[i+1] == 'A') && (buf[i+2] == 'N' || buf[i+2] == 'n') { - i += 3 - continue - } return i, fmt.Errorf("invalid number") } + if !isNumeric(buf[i]) { return i, fmt.Errorf("invalid number") } @@ -721,16 +731,11 @@ func scanBoolean(buf []byte, i int) (int, []byte, error) { // skipWhitespace returns the end position within buf, starting at i after // scanning over spaces in tags func skipWhitespace(buf []byte, i int) int { - for { - if i >= len(buf) { - return i + for i < len(buf) { + if buf[i] != ' ' && buf[i] != '\t' && buf[i] != 0 { + break } - - if buf[i] == ' ' || buf[i] == '\t' { - i += 1 - continue - } - break + i++ } return i } @@ -954,13 +959,33 @@ func unescapeStringField(in string) string { return string(out) } -// NewPoint returns a new point with the given measurement name, tags, fields and timestamp -func NewPoint(name string, tags Tags, fields Fields, time time.Time) Point { +// NewPoint returns a new point with the given measurement name, tags, fields and timestamp. If +// an unsupported field value (NaN) is passed, this function returns an error. +func NewPoint(name string, tags Tags, fields Fields, time time.Time) (Point, error) { + for key, value := range fields { + if fv, ok := value.(float64); ok { + // Ensure the caller validates and handles invalid field values + if math.IsNaN(fv) { + return nil, fmt.Errorf("NaN is an unsupported value for field %s", key) + } + } + } + return &point{ key: MakeKey([]byte(name), tags), time: time, fields: fields.MarshalBinary(), + }, nil +} + +// NewPoint returns a new point with the given measurement name, tags, fields and timestamp. If +// an unsupported field value (NaN) is passed, this function panics. +func MustNewPoint(name string, tags Tags, fields Fields, time time.Time) Point { + pt, err := NewPoint(name, tags, fields, time) + if err != nil { + panic(err.Error()) } + return pt } func (p *point) Data() []byte { @@ -1123,6 +1148,14 @@ func (p *point) PrecisionString(precision string) string { p.UnixNano()/p.GetPrecisionMultiplier(precision)) } +func (p *point) RoundedString(d time.Duration) string { + if p.Time().IsZero() { + return fmt.Sprintf("%s %s", p.Key(), string(p.fields)) + } + return fmt.Sprintf("%s %s %d", p.Key(), string(p.fields), + p.time.Round(d).UnixNano()) +} + func (p *point) unmarshalBinary() Fields { return newFieldsFromBinary(p.fields) } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/models/points_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/models/points_test.go index c6b7f08d9..d4b9f498f 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/models/points_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/models/points_test.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "math" + "math/rand" "reflect" "strconv" "strings" @@ -201,7 +202,7 @@ func TestParsePointNoFields(t *testing.T) { } func TestParsePointNoTimestamp(t *testing.T) { - test(t, "cpu value=1", models.NewPoint("cpu", nil, nil, time.Unix(0, 0))) + test(t, "cpu value=1", models.MustNewPoint("cpu", nil, nil, time.Unix(0, 0))) } func TestParsePointMissingQuote(t *testing.T) { @@ -524,7 +525,7 @@ func TestParsePointScientificIntInvalid(t *testing.T) { func TestParsePointUnescape(t *testing.T) { test(t, `foo\,bar value=1i`, - models.NewPoint( + models.MustNewPoint( "foo,bar", // comma in the name models.Tags{}, models.Fields{ @@ -534,7 +535,7 @@ func TestParsePointUnescape(t *testing.T) { // commas in measurement name test(t, `cpu\,main,regions=east\,west value=1.0`, - models.NewPoint( + models.MustNewPoint( "cpu,main", // comma in the name models.Tags{ "regions": "east,west", @@ -546,7 +547,7 @@ func TestParsePointUnescape(t *testing.T) { // spaces in measurement name test(t, `cpu\ load,region=east value=1.0`, - models.NewPoint( + models.MustNewPoint( "cpu load", // space in the name models.Tags{ "region": "east", @@ -558,7 +559,7 @@ func TestParsePointUnescape(t *testing.T) { // commas in tag names test(t, `cpu,region\,zone=east value=1.0`, - models.NewPoint("cpu", + models.MustNewPoint("cpu", models.Tags{ "region,zone": "east", // comma in the tag key }, @@ -569,7 +570,7 @@ func TestParsePointUnescape(t *testing.T) { // spaces in tag names test(t, `cpu,region\ zone=east value=1.0`, - models.NewPoint("cpu", + models.MustNewPoint("cpu", models.Tags{ "region zone": "east", // comma in the tag key }, @@ -580,7 +581,7 @@ func TestParsePointUnescape(t *testing.T) { // commas in tag values test(t, `cpu,regions=east\,west value=1.0`, - models.NewPoint("cpu", + models.MustNewPoint("cpu", models.Tags{ "regions": "east,west", // comma in the tag value }, @@ -591,7 +592,7 @@ func TestParsePointUnescape(t *testing.T) { // spaces in tag values test(t, `cpu,regions=east\ west value=1.0`, - models.NewPoint("cpu", + models.MustNewPoint("cpu", models.Tags{ "regions": "east west", // comma in the tag value }, @@ -602,7 +603,7 @@ func TestParsePointUnescape(t *testing.T) { // commas in field keys test(t, `cpu,regions=east value\,ms=1.0`, - models.NewPoint("cpu", + models.MustNewPoint("cpu", models.Tags{ "regions": "east", }, @@ -613,7 +614,7 @@ func TestParsePointUnescape(t *testing.T) { // spaces in field keys test(t, `cpu,regions=east value\ ms=1.0`, - models.NewPoint("cpu", + models.MustNewPoint("cpu", models.Tags{ "regions": "east", }, @@ -624,7 +625,7 @@ func TestParsePointUnescape(t *testing.T) { // tag with no value test(t, `cpu,regions=east value="1"`, - models.NewPoint("cpu", + models.MustNewPoint("cpu", models.Tags{ "regions": "east", "foobar": "", @@ -636,7 +637,7 @@ func TestParsePointUnescape(t *testing.T) { // commas in field values test(t, `cpu,regions=east value="1,0"`, - models.NewPoint("cpu", + models.MustNewPoint("cpu", models.Tags{ "regions": "east", }, @@ -647,7 +648,7 @@ func TestParsePointUnescape(t *testing.T) { // random character escaped test(t, `cpu,regions=eas\t value=1.0`, - models.NewPoint( + models.MustNewPoint( "cpu", models.Tags{ "regions": "eas\\t", @@ -659,7 +660,7 @@ func TestParsePointUnescape(t *testing.T) { // field keys using escape char. test(t, `cpu \a=1i`, - models.NewPoint( + models.MustNewPoint( "cpu", models.Tags{}, models.Fields{ @@ -669,7 +670,7 @@ func TestParsePointUnescape(t *testing.T) { // measurement, tag and tag value with equals test(t, `cpu=load,equals\=foo=tag\=value value=1i`, - models.NewPoint( + models.MustNewPoint( "cpu=load", // Not escaped models.Tags{ "equals=foo": "tag=value", // Tag and value unescaped @@ -684,7 +685,7 @@ func TestParsePointUnescape(t *testing.T) { func TestParsePointWithTags(t *testing.T) { test(t, "cpu,host=serverA,region=us-east value=1.0 1000000000", - models.NewPoint("cpu", + models.MustNewPoint("cpu", models.Tags{"host": "serverA", "region": "us-east"}, models.Fields{"value": 1.0}, time.Unix(1, 0))) } @@ -698,7 +699,7 @@ func TestParsPointWithDuplicateTags(t *testing.T) { func TestParsePointWithStringField(t *testing.T) { test(t, `cpu,host=serverA,region=us-east value=1.0,str="foo",str2="bar" 1000000000`, - models.NewPoint("cpu", + models.MustNewPoint("cpu", models.Tags{ "host": "serverA", "region": "us-east", @@ -712,7 +713,7 @@ func TestParsePointWithStringField(t *testing.T) { ) test(t, `cpu,host=serverA,region=us-east str="foo \" bar" 1000000000`, - models.NewPoint("cpu", + models.MustNewPoint("cpu", models.Tags{ "host": "serverA", "region": "us-east", @@ -727,7 +728,7 @@ func TestParsePointWithStringField(t *testing.T) { func TestParsePointWithStringWithSpaces(t *testing.T) { test(t, `cpu,host=serverA,region=us-east value=1.0,str="foo bar" 1000000000`, - models.NewPoint( + models.MustNewPoint( "cpu", models.Tags{ "host": "serverA", @@ -743,7 +744,7 @@ func TestParsePointWithStringWithSpaces(t *testing.T) { func TestParsePointWithStringWithNewline(t *testing.T) { test(t, "cpu,host=serverA,region=us-east value=1.0,str=\"foo\nbar\" 1000000000", - models.NewPoint( + models.MustNewPoint( "cpu", models.Tags{ "host": "serverA", @@ -760,7 +761,7 @@ func TestParsePointWithStringWithNewline(t *testing.T) { func TestParsePointWithStringWithCommas(t *testing.T) { // escaped comma test(t, `cpu,host=serverA,region=us-east value=1.0,str="foo\,bar" 1000000000`, - models.NewPoint( + models.MustNewPoint( "cpu", models.Tags{ "host": "serverA", @@ -775,7 +776,7 @@ func TestParsePointWithStringWithCommas(t *testing.T) { // non-escaped comma test(t, `cpu,host=serverA,region=us-east value=1.0,str="foo,bar" 1000000000`, - models.NewPoint( + models.MustNewPoint( "cpu", models.Tags{ "host": "serverA", @@ -792,7 +793,7 @@ func TestParsePointWithStringWithCommas(t *testing.T) { func TestParsePointQuotedMeasurement(t *testing.T) { // non-escaped comma test(t, `"cpu",host=serverA,region=us-east value=1.0 1000000000`, - models.NewPoint( + models.MustNewPoint( `"cpu"`, models.Tags{ "host": "serverA", @@ -807,7 +808,7 @@ func TestParsePointQuotedMeasurement(t *testing.T) { func TestParsePointQuotedTags(t *testing.T) { test(t, `cpu,"host"="serverA",region=us-east value=1.0 1000000000`, - models.NewPoint( + models.MustNewPoint( "cpu", models.Tags{ `"host"`: `"serverA"`, @@ -831,7 +832,7 @@ func TestParsePointsUnbalancedQuotedTags(t *testing.T) { } // Expected " in the tag value - exp := models.NewPoint("baz", models.Tags{"mytag": `"a`}, + exp := models.MustNewPoint("baz", models.Tags{"mytag": `"a`}, models.Fields{"x": float64(1)}, time.Unix(0, 1441103862125)) if pts[0].String() != exp.String() { @@ -839,7 +840,7 @@ func TestParsePointsUnbalancedQuotedTags(t *testing.T) { } // Expected two points to ensure we did not overscan the line - exp = models.NewPoint("baz", models.Tags{"mytag": `a`}, + exp = models.MustNewPoint("baz", models.Tags{"mytag": `a`}, models.Fields{"z": float64(1)}, time.Unix(0, 1441103862126)) if pts[1].String() != exp.String() { @@ -851,7 +852,7 @@ func TestParsePointsUnbalancedQuotedTags(t *testing.T) { func TestParsePointEscapedStringsAndCommas(t *testing.T) { // non-escaped comma and quotes test(t, `cpu,host=serverA,region=us-east value="{Hello\"{,}\" World}" 1000000000`, - models.NewPoint( + models.MustNewPoint( "cpu", models.Tags{ "host": "serverA", @@ -865,7 +866,7 @@ func TestParsePointEscapedStringsAndCommas(t *testing.T) { // escaped comma and quotes test(t, `cpu,host=serverA,region=us-east value="{Hello\"{\,}\" World}" 1000000000`, - models.NewPoint( + models.MustNewPoint( "cpu", models.Tags{ "host": "serverA", @@ -880,7 +881,7 @@ func TestParsePointEscapedStringsAndCommas(t *testing.T) { func TestParsePointWithStringWithEquals(t *testing.T) { test(t, `cpu,host=serverA,region=us-east str="foo=bar",value=1.0 1000000000`, - models.NewPoint( + models.MustNewPoint( "cpu", models.Tags{ "host": "serverA", @@ -896,7 +897,7 @@ func TestParsePointWithStringWithEquals(t *testing.T) { func TestParsePointWithStringWithBackslash(t *testing.T) { test(t, `cpu value="test\\\"" 1000000000`, - models.NewPoint( + models.MustNewPoint( "cpu", models.Tags{}, models.Fields{ @@ -906,7 +907,7 @@ func TestParsePointWithStringWithBackslash(t *testing.T) { ) test(t, `cpu value="test\\" 1000000000`, - models.NewPoint( + models.MustNewPoint( "cpu", models.Tags{}, models.Fields{ @@ -916,7 +917,7 @@ func TestParsePointWithStringWithBackslash(t *testing.T) { ) test(t, `cpu value="test\\\"" 1000000000`, - models.NewPoint( + models.MustNewPoint( "cpu", models.Tags{}, models.Fields{ @@ -926,7 +927,7 @@ func TestParsePointWithStringWithBackslash(t *testing.T) { ) test(t, `cpu value="test\"" 1000000000`, - models.NewPoint( + models.MustNewPoint( "cpu", models.Tags{}, models.Fields{ @@ -938,7 +939,7 @@ func TestParsePointWithStringWithBackslash(t *testing.T) { func TestParsePointWithBoolField(t *testing.T) { test(t, `cpu,host=serverA,region=us-east true=true,t=t,T=T,TRUE=TRUE,True=True,false=false,f=f,F=F,FALSE=FALSE,False=False 1000000000`, - models.NewPoint( + models.MustNewPoint( "cpu", models.Tags{ "host": "serverA", @@ -962,7 +963,7 @@ func TestParsePointWithBoolField(t *testing.T) { func TestParsePointUnicodeString(t *testing.T) { test(t, `cpu,host=serverA,region=us-east value="wè" 1000000000`, - models.NewPoint( + models.MustNewPoint( "cpu", models.Tags{ "host": "serverA", @@ -977,7 +978,7 @@ func TestParsePointUnicodeString(t *testing.T) { func TestParsePointNegativeTimestamp(t *testing.T) { test(t, `cpu value=1 -1`, - models.NewPoint( + models.MustNewPoint( "cpu", models.Tags{}, models.Fields{ @@ -989,7 +990,7 @@ func TestParsePointNegativeTimestamp(t *testing.T) { func TestParsePointMaxTimestamp(t *testing.T) { test(t, `cpu value=1 9223372036854775807`, - models.NewPoint( + models.MustNewPoint( "cpu", models.Tags{}, models.Fields{ @@ -1001,7 +1002,7 @@ func TestParsePointMaxTimestamp(t *testing.T) { func TestParsePointMinTimestamp(t *testing.T) { test(t, `cpu value=1 -9223372036854775807`, - models.NewPoint( + models.MustNewPoint( "cpu", models.Tags{}, models.Fields{ @@ -1040,7 +1041,7 @@ func TestParsePointInvalidTimestamp(t *testing.T) { func TestNewPointFloatWithoutDecimal(t *testing.T) { test(t, `cpu value=1 1000000000`, - models.NewPoint( + models.MustNewPoint( "cpu", models.Tags{}, models.Fields{ @@ -1051,7 +1052,7 @@ func TestNewPointFloatWithoutDecimal(t *testing.T) { } func TestNewPointNegativeFloat(t *testing.T) { test(t, `cpu value=-0.64 1000000000`, - models.NewPoint( + models.MustNewPoint( "cpu", models.Tags{}, models.Fields{ @@ -1063,7 +1064,7 @@ func TestNewPointNegativeFloat(t *testing.T) { func TestNewPointFloatNoDecimal(t *testing.T) { test(t, `cpu value=1. 1000000000`, - models.NewPoint( + models.MustNewPoint( "cpu", models.Tags{}, models.Fields{ @@ -1075,7 +1076,7 @@ func TestNewPointFloatNoDecimal(t *testing.T) { func TestNewPointFloatScientific(t *testing.T) { test(t, `cpu value=6.632243e+06 1000000000`, - models.NewPoint( + models.MustNewPoint( "cpu", models.Tags{}, models.Fields{ @@ -1087,7 +1088,7 @@ func TestNewPointFloatScientific(t *testing.T) { func TestNewPointLargeInteger(t *testing.T) { test(t, `cpu value=6632243i 1000000000`, - models.NewPoint( + models.MustNewPoint( "cpu", models.Tags{}, models.Fields{ @@ -1097,36 +1098,21 @@ func TestNewPointLargeInteger(t *testing.T) { ) } -func TestNewPointNaN(t *testing.T) { - test(t, `cpu value=NaN 1000000000`, - models.NewPoint( - "cpu", - models.Tags{}, - models.Fields{ - "value": math.NaN(), - }, - time.Unix(1, 0)), - ) +func TestParsePointNaN(t *testing.T) { + _, err := models.ParsePointsString("cpu value=NaN 1000000000") + if err == nil { + t.Fatalf("ParsePoints expected error, got nil") + } - test(t, `cpu value=nAn 1000000000`, - models.NewPoint( - "cpu", - models.Tags{}, - models.Fields{ - "value": math.NaN(), - }, - time.Unix(1, 0)), - ) + _, err = models.ParsePointsString("cpu value=nAn 1000000000") + if err == nil { + t.Fatalf("ParsePoints expected error, got nil") + } - test(t, `nan value=NaN`, - models.NewPoint( - "nan", - models.Tags{}, - models.Fields{ - "value": math.NaN(), - }, - time.Unix(0, 0)), - ) + _, err = models.ParsePointsString("cpu value=NaN") + if err == nil { + t.Fatalf("ParsePoints expected error, got nil") + } } func TestNewPointLargeNumberOfTags(t *testing.T) { @@ -1201,7 +1187,7 @@ func TestParsePointToString(t *testing.T) { t.Errorf("ParsePoint() to string mismatch:\n got %v\n exp %v", got, line) } - pt = models.NewPoint("cpu", models.Tags{"host": "serverA", "region": "us-east"}, + pt = models.MustNewPoint("cpu", models.Tags{"host": "serverA", "region": "us-east"}, models.Fields{"int": 10, "float": float64(11.0), "float2": float64(12.123), "bool": false, "str": "string val"}, time.Unix(1, 0)) @@ -1398,19 +1384,19 @@ cpu,host=serverA,region=us-east value=1.0 946730096789012345`, func TestNewPointEscaped(t *testing.T) { // commas - pt := models.NewPoint("cpu,main", models.Tags{"tag,bar": "value"}, models.Fields{"name,bar": 1.0}, time.Unix(0, 0)) + pt := models.MustNewPoint("cpu,main", models.Tags{"tag,bar": "value"}, models.Fields{"name,bar": 1.0}, time.Unix(0, 0)) if exp := `cpu\,main,tag\,bar=value name\,bar=1 0`; pt.String() != exp { t.Errorf("NewPoint().String() mismatch.\ngot %v\nexp %v", pt.String(), exp) } // spaces - pt = models.NewPoint("cpu main", models.Tags{"tag bar": "value"}, models.Fields{"name bar": 1.0}, time.Unix(0, 0)) + pt = models.MustNewPoint("cpu main", models.Tags{"tag bar": "value"}, models.Fields{"name bar": 1.0}, time.Unix(0, 0)) if exp := `cpu\ main,tag\ bar=value name\ bar=1 0`; pt.String() != exp { t.Errorf("NewPoint().String() mismatch.\ngot %v\nexp %v", pt.String(), exp) } // equals - pt = models.NewPoint("cpu=main", models.Tags{"tag=bar": "value=foo"}, models.Fields{"name=bar": 1.0}, time.Unix(0, 0)) + pt = models.MustNewPoint("cpu=main", models.Tags{"tag=bar": "value=foo"}, models.Fields{"name=bar": 1.0}, time.Unix(0, 0)) if exp := `cpu=main,tag\=bar=value\=foo name\=bar=1 0`; pt.String() != exp { t.Errorf("NewPoint().String() mismatch.\ngot %v\nexp %v", pt.String(), exp) } @@ -1418,14 +1404,14 @@ func TestNewPointEscaped(t *testing.T) { func TestNewPointUnhandledType(t *testing.T) { // nil value - pt := models.NewPoint("cpu", nil, models.Fields{"value": nil}, time.Unix(0, 0)) + pt := models.MustNewPoint("cpu", nil, models.Fields{"value": nil}, time.Unix(0, 0)) if exp := `cpu value= 0`; pt.String() != exp { t.Errorf("NewPoint().String() mismatch.\ngot %v\nexp %v", pt.String(), exp) } // unsupported type gets stored as string now := time.Unix(0, 0).UTC() - pt = models.NewPoint("cpu", nil, models.Fields{"value": now}, time.Unix(0, 0)) + pt = models.MustNewPoint("cpu", nil, models.Fields{"value": now}, time.Unix(0, 0)) if exp := `cpu value="1970-01-01 00:00:00 +0000 UTC" 0`; pt.String() != exp { t.Errorf("NewPoint().String() mismatch.\ngot %v\nexp %v", pt.String(), exp) } @@ -1500,7 +1486,7 @@ func TestPrecisionString(t *testing.T) { } for _, test := range tests { - pt := models.NewPoint("cpu", nil, tags, tm) + pt := models.MustNewPoint("cpu", nil, tags, tm) act := pt.PrecisionString(test.precision) if act != test.exp { @@ -1509,3 +1495,81 @@ func TestPrecisionString(t *testing.T) { } } } + +func TestRoundedString(t *testing.T) { + tags := map[string]interface{}{"value": float64(1)} + tm, _ := time.Parse(time.RFC3339Nano, "2000-01-01T12:34:56.789012345Z") + tests := []struct { + name string + precision time.Duration + exp string + }{ + { + name: "no precision", + precision: time.Duration(0), + exp: "cpu value=1 946730096789012345", + }, + { + name: "nanosecond precision", + precision: time.Nanosecond, + exp: "cpu value=1 946730096789012345", + }, + { + name: "microsecond precision", + precision: time.Microsecond, + exp: "cpu value=1 946730096789012000", + }, + { + name: "millisecond precision", + precision: time.Millisecond, + exp: "cpu value=1 946730096789000000", + }, + { + name: "second precision", + precision: time.Second, + exp: "cpu value=1 946730097000000000", + }, + { + name: "minute precision", + precision: time.Minute, + exp: "cpu value=1 946730100000000000", + }, + { + name: "hour precision", + precision: time.Hour, + exp: "cpu value=1 946731600000000000", + }, + } + + for _, test := range tests { + pt := models.MustNewPoint("cpu", nil, tags, tm) + act := pt.RoundedString(test.precision) + + if act != test.exp { + t.Errorf("%s: RoundedString() mismatch:\n actual: %v\n exp: %v", + test.name, act, test.exp) + } + } +} + +func TestParsePointsStringWithExtraBuffer(t *testing.T) { + b := make([]byte, 70*5000) + buf := bytes.NewBuffer(b) + key := "cpu,host=A,region=uswest" + buf.WriteString(fmt.Sprintf("%s value=%.3f 1\n", key, rand.Float64())) + + points, err := models.ParsePointsString(buf.String()) + if err != nil { + t.Fatalf("failed to write points: %s", err.Error()) + } + + pointKey := string(points[0].Key()) + + if len(key) != len(pointKey) { + t.Fatalf("expected length of both keys are same but got %d and %d", len(key), len(pointKey)) + } + + if key != pointKey { + t.Fatalf("expected both keys are same but got %s and %s", key, pointKey) + } +} diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/monitor/service.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/monitor/service.go index 084814224..4e0285b39 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/monitor/service.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/monitor/service.go @@ -368,7 +368,12 @@ func (m *Monitor) storeStatistics() { points := make(models.Points, 0, len(stats)) for _, s := range stats { - points = append(points, models.NewPoint(s.Name, s.Tags, s.Values, time.Now().Truncate(time.Second))) + pt, err := models.NewPoint(s.Name, s.Tags, s.Values, time.Now().Truncate(time.Second)) + if err != nil { + m.Logger.Printf("Dropping point %v: %v", s.Name, err) + continue + } + points = append(points, pt) } err = m.PointsWriter.WritePoints(&cluster.WritePointsRequest{ diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/package.sh b/Godeps/_workspace/src/github.com/influxdb/influxdb/package.sh index ddd4b56b2..3ca908a1b 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/package.sh +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/package.sh @@ -267,7 +267,7 @@ do_build() { fi date=`date -u --iso-8601=seconds` - go install $RACE -a -ldflags="-X main.version=$version -X main.branch=$branch -X main.commit=$commit -X main.buildTime='$date'" ./... + go install $RACE -a -ldflags="-X main.version=$version -X main.branch=$branch -X main.commit=$commit -X main.buildTime=$date" ./... if [ $? -ne 0 ]; then echo "Build failed, unable to create package -- aborting" cleanup_exit 1 diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/pkg/slices/strings.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/pkg/slices/strings.go index 16d6a13f7..094d4a32e 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/pkg/slices/strings.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/pkg/slices/strings.go @@ -2,6 +2,7 @@ package slices import "strings" +// Union combines two string sets func Union(setA, setB []string, ignoreCase bool) []string { for _, b := range setB { if ignoreCase { @@ -17,6 +18,7 @@ func Union(setA, setB []string, ignoreCase bool) []string { return setA } +// Exists checks if a string is in a set func Exists(set []string, find string) bool { for _, s := range set { if s == find { @@ -26,6 +28,7 @@ func Exists(set []string, find string) bool { return false } +// ExistsIgnoreCase checks if a string is in a set but ignores its case func ExistsIgnoreCase(set []string, find string) bool { find = strings.ToLower(find) for _, s := range set { diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/admin/config.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/admin/config.go index 860dff864..e3c91f8fc 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/admin/config.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/admin/config.go @@ -8,14 +8,14 @@ const ( type Config struct { Enabled bool `toml:"enabled"` BindAddress string `toml:"bind-address"` - HttpsEnabled bool `toml:"https-enabled"` - HttpsCertificate string `toml:"https-certificate"` + HTTPSEnabled bool `toml:"https-enabled"` + HTTPSCertificate string `toml:"https-certificate"` } func NewConfig() Config { return Config{ BindAddress: DefaultBindAddress, - HttpsEnabled: false, - HttpsCertificate: "/etc/ssl/influxdb.pem", + HTTPSEnabled: false, + HTTPSCertificate: "/etc/ssl/influxdb.pem", } } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/admin/config_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/admin/config_test.go index 1b0422505..1ad2e19dd 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/admin/config_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/admin/config_test.go @@ -24,9 +24,9 @@ https-certificate = "/dev/null" t.Fatalf("unexpected enabled: %v", c.Enabled) } else if c.BindAddress != ":8083" { t.Fatalf("unexpected bind address: %s", c.BindAddress) - } else if c.HttpsEnabled != true { - t.Fatalf("unexpected https enabled: %v", c.HttpsEnabled) - } else if c.HttpsCertificate != "/dev/null" { - t.Fatalf("unexpected https certificate: %v", c.HttpsCertificate) + } else if c.HTTPSEnabled != true { + t.Fatalf("unexpected https enabled: %v", c.HTTPSEnabled) + } else if c.HTTPSCertificate != "/dev/null" { + t.Fatalf("unexpected https certificate: %v", c.HTTPSCertificate) } } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/admin/service.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/admin/service.go index 2618bdb6b..0f174b093 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/admin/service.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/admin/service.go @@ -29,8 +29,8 @@ type Service struct { func NewService(c Config) *Service { return &Service{ addr: c.BindAddress, - https: c.HttpsEnabled, - cert: c.HttpsCertificate, + https: c.HTTPSEnabled, + cert: c.HTTPSCertificate, err: make(chan error), logger: log.New(os.Stderr, "[admin] ", log.LstdFlags), } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/collectd/README.md b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/collectd/README.md index 21f86b265..6ab1342de 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/collectd/README.md +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/collectd/README.md @@ -2,6 +2,11 @@ The _collectd_ input allows InfluxDB to accept data transmitted in collectd native format. This data is transmitted over UDP. +## A note on UDP/IP OS Buffer sizes + +If you're running Linux or FreeBSD, please adjust your OS UDP buffer +size limit, [see here for more details.](../udp/README.md#a-note-on-udpip-os-buffer-sizes) + ## Configuration Each collectd input allows the binding address, target database, and target retention policy to be set. If the database does not exist, it will be created automatically when the input is initialized. If the retention policy is not configured, then the default retention policy for the database is used. However if the retention policy is set, the retention policy must be explicitly created. The input will not automatically create it. @@ -13,3 +18,18 @@ The path to the collectd types database file may also be set ## Large UDP packets Please note that UDP packages larger than the standard size of 1452 are dropped at the time of ingestion, so be sure to set `MaxPacketSize` to 1452 in the collectd configuration. + +## Config Example + +``` +[collectd] + enabled = false + bind-address = ":25826" # the bind address + database = "collectd" # Name of the database that will be written to + retention-policy = "" + batch-size = 5000 # will flush if this many points get buffered + batch-pending = 10 # number of batches that may be pending in memory + batch-timeout = "10s" + read-buffer = 0 # UDP read buffer size, 0 means to use OS default + typesdb = "/usr/share/collectd/types.db" +``` diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/collectd/config.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/collectd/config.go index 427598ca6..a243458ae 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/collectd/config.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/collectd/config.go @@ -7,19 +7,38 @@ import ( ) const ( + // DefaultBindAddress is the default port to bind to DefaultBindAddress = ":25826" + // DefaultDatabase is the default DB to write to DefaultDatabase = "collectd" + // DefaultRetentionPolicy is the default retention policy of the writes DefaultRetentionPolicy = "" - DefaultBatchSize = 1000 + // DefaultBatchSize is the default write batch size. + DefaultBatchSize = 5000 - DefaultBatchPending = 5 + // DefaultBatchPending is the default number of pending write batches. + DefaultBatchPending = 10 + // DefaultBatchTimeout is the default batch timeout. DefaultBatchDuration = toml.Duration(10 * time.Second) DefaultTypesDB = "/usr/share/collectd/types.db" + + // DefaultReadBuffer is the default buffer size for the UDP listener. + // Sets the size of the operating system's receive buffer associated with + // the UDP traffic. Keep in mind that the OS must be able + // to handle the number set here or the UDP listener will error and exit. + // + // DefaultReadBuffer = 0 means to use the OS default, which is usually too + // small for high UDP performance. + // + // Increasing OS buffer limits: + // Linux: sudo sysctl -w net.core.rmem_max= + // BSD/Darwin: sudo sysctl -w kern.ipc.maxsockbuf= + DefaultReadBuffer = 0 ) // Config represents a configuration for the collectd service. @@ -31,6 +50,7 @@ type Config struct { BatchSize int `toml:"batch-size"` BatchPending int `toml:"batch-pending"` BatchDuration toml.Duration `toml:"batch-timeout"` + ReadBuffer int `toml:"read-buffer"` TypesDB string `toml:"typesdb"` } @@ -40,6 +60,7 @@ func NewConfig() Config { BindAddress: DefaultBindAddress, Database: DefaultDatabase, RetentionPolicy: DefaultRetentionPolicy, + ReadBuffer: DefaultReadBuffer, BatchSize: DefaultBatchSize, BatchPending: DefaultBatchPending, BatchDuration: DefaultBatchDuration, diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/collectd/service.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/collectd/service.go index ddd9bce5c..e8bf39d22 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/collectd/service.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/collectd/service.go @@ -22,13 +22,13 @@ const leaderWaitTimeout = 30 * time.Second // statistics gathered by the collectd service. const ( - statPointsReceived = "points_rx" - statBytesReceived = "bytes_rx" - statPointsParseFail = "points_parse_fail" - statReadFail = "read_fail" - statBatchesTrasmitted = "batches_tx" - statPointsTransmitted = "points_tx" - statBatchesTransmitFail = "batches_tx_fail" + statPointsReceived = "pointsRx" + statBytesReceived = "bytesRx" + statPointsParseFail = "pointsParseFail" + statReadFail = "readFail" + statBatchesTrasmitted = "batchesTx" + statPointsTransmitted = "pointsTx" + statBatchesTransmitFail = "batchesTxFail" ) // pointsWriter is an internal interface to make testing easier. @@ -53,7 +53,7 @@ type Service struct { wg sync.WaitGroup err chan error stop chan struct{} - ln *net.UDPConn + conn *net.UDPConn batcher *tsdb.PointBatcher typesdb gollectd.Types addr net.Addr @@ -118,13 +118,21 @@ func (s *Service) Open() error { s.addr = addr // Start listening - ln, err := net.ListenUDP("udp", addr) + conn, err := net.ListenUDP("udp", addr) if err != nil { return fmt.Errorf("unable to listen on UDP: %s", err) } - s.ln = ln - s.Logger.Println("Listening on UDP: ", ln.LocalAddr().String()) + if s.Config.ReadBuffer != 0 { + err = conn.SetReadBuffer(s.Config.ReadBuffer) + if err != nil { + return fmt.Errorf("unable to set UDP read buffer to %d: %s", + s.Config.ReadBuffer, err) + } + } + s.conn = conn + + s.Logger.Println("Listening on UDP: ", conn.LocalAddr().String()) // Start the points batcher. s.batcher = tsdb.NewPointBatcher(s.Config.BatchSize, s.Config.BatchPending, time.Duration(s.Config.BatchDuration)) @@ -147,8 +155,8 @@ func (s *Service) Close() error { if s.stop != nil { close(s.stop) } - if s.ln != nil { - s.ln.Close() + if s.conn != nil { + s.conn.Close() } if s.batcher != nil { s.batcher.Stop() @@ -157,7 +165,7 @@ func (s *Service) Close() error { // Release all remaining resources. s.stop = nil - s.ln = nil + s.conn = nil s.batcher = nil s.Logger.Println("collectd UDP closed") return nil @@ -179,7 +187,7 @@ func (s *Service) Err() chan error { return s.err } // Addr returns the listener's address. Returns nil if listener is closed. func (s *Service) Addr() net.Addr { - return s.ln.LocalAddr() + return s.conn.LocalAddr() } func (s *Service) serve() { @@ -204,7 +212,7 @@ func (s *Service) serve() { // Keep processing. } - n, _, err := s.ln.ReadFromUDP(buffer) + n, _, err := s.conn.ReadFromUDP(buffer) if err != nil { s.statMap.Add(statReadFail, 1) s.Logger.Printf("collectd ReadFromUDP error: %s", err) @@ -293,7 +301,11 @@ func Unmarshal(packet *gollectd.Packet) []models.Point { if packet.TypeInstance != "" { tags["type_instance"] = packet.TypeInstance } - p := models.NewPoint(name, tags, fields, timestamp) + p, err := models.NewPoint(name, tags, fields, timestamp) + // Drop points values of NaN since they are not supported + if err != nil { + continue + } points = append(points, p) } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/continuous_querier/service.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/continuous_querier/service.go index e6c1360b6..e25891d2a 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/continuous_querier/service.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/continuous_querier/service.go @@ -23,9 +23,9 @@ const ( // Statistics for the CQ service. const ( - statQueryOK = "query_ok" - statQueryFail = "query_fail" - statPointsWritten = "points_written" + statQueryOK = "queryOk" + statQueryFail = "queryFail" + statPointsWritten = "pointsWritten" ) // ContinuousQuerier represents a service that executes continuous queries. diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/graphite/README.md b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/graphite/README.md index e2e7deaa0..5c2cb6925 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/graphite/README.md +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/graphite/README.md @@ -1,10 +1,17 @@ -# Configuration +# The graphite Input + +## A note on UDP/IP OS Buffer sizes + +If you're using UDP input and running Linux or FreeBSD, please adjust your UDP buffer +size limit, [see here for more details.](../udp/README.md#a-note-on-udpip-os-buffer-sizes) + +## Configuration Each Graphite input allows the binding address, target database, and protocol to be set. If the database does not exist, it will be created automatically when the input is initialized. The write-consistency-level can also be set. If any write operations do not meet the configured consistency guarantees, an error will occur and the data will not be indexed. The default consistency-level is `ONE`. Each Graphite input also performs internal batching of the points it receives, as batched writes to the database are more efficient. The default _batch size_ is 1000, _pending batch_ factor is 5, with a _batch timeout_ of 1 second. This means the input will write batches of maximum size 1000, but if a batch has not reached 1000 points within 1 second of the first point being added to a batch, it will emit that batch regardless of size. The pending batch factor controls how many batches can be in memory at once, allowing the input to transmit a batch, while still building other batches. -# Parsing Metrics +## Parsing Metrics The graphite plugin allows measurements to be saved using the graphite line protocol. By default, enabling the graphite plugin will allow you to collect metrics and store them using the metric name as the measurement. If you send a metric named `servers.localhost.cpu.loadavg.10`, it will store the full metric name as the measurement with no extracted tags. @@ -95,10 +102,12 @@ For example, servers.localhost.cpu.loadavg.10 servers.host123.elasticsearch.cache_hits 100 servers.host456.mysql.tx_count 10 +servers.host789.prod.mysql.tx_count 10 ``` * `servers.*` would match all values * `servers.*.mysql` would match `servers.host456.mysql.tx_count 10` * `servers.localhost.*` would match `servers.localhost.cpu.loadavg` +* `servers.*.*.mysql` would match `servers.host789.prod.mysql.tx_count 10` ## Default Templates @@ -145,7 +154,7 @@ If you need to add the same set of tags to all metrics, you can define them glob #] ``` -## Customized Config +## Customized Config ``` [[graphite]] enabled = true @@ -165,3 +174,21 @@ If you need to add the same set of tags to all metrics, you can define them glob ".measurement*", ] ``` + +## Two graphite listener, UDP & TCP, Config + +``` +[[graphite]] + enabled = true + bind-address = ":2003" + protocol = "tcp" + # consistency-level = "one" + +[[graphite]] + enabled = true + bind-address = ":2004" # the bind address + protocol = "udp" # protocol to read via + udp-read-buffer = 8388608 # (8*1024*1024) UDP read buffer size +``` + + diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/graphite/config.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/graphite/config.go index a1fe7a79d..cedefdc34 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/graphite/config.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/graphite/config.go @@ -26,21 +26,34 @@ const ( // measurment parts in a template. DefaultSeparator = "." - // DefaultBatchSize is the default Graphite batch size. - DefaultBatchSize = 1000 + // DefaultBatchSize is the default write batch size. + DefaultBatchSize = 5000 - // DefaultBatchPending is the default number of pending Graphite batches. - DefaultBatchPending = 5 + // DefaultBatchPending is the default number of pending write batches. + DefaultBatchPending = 10 // DefaultBatchTimeout is the default Graphite batch timeout. DefaultBatchTimeout = time.Second + + // DefaultUDPReadBuffer is the default buffer size for the UDP listener. + // Sets the size of the operating system's receive buffer associated with + // the UDP traffic. Keep in mind that the OS must be able + // to handle the number set here or the UDP listener will error and exit. + // + // DefaultReadBuffer = 0 means to use the OS default, which is usually too + // small for high UDP performance. + // + // Increasing OS buffer limits: + // Linux: sudo sysctl -w net.core.rmem_max= + // BSD/Darwin: sudo sysctl -w kern.ipc.maxsockbuf= + DefaultUDPReadBuffer = 0 ) // Config represents the configuration for Graphite endpoints. type Config struct { + Enabled bool `toml:"enabled"` BindAddress string `toml:"bind-address"` Database string `toml:"database"` - Enabled bool `toml:"enabled"` Protocol string `toml:"protocol"` BatchSize int `toml:"batch-size"` BatchPending int `toml:"batch-pending"` @@ -49,6 +62,20 @@ type Config struct { Templates []string `toml:"templates"` Tags []string `toml:"tags"` Separator string `toml:"separator"` + UDPReadBuffer int `toml:"udp-read-buffer"` +} + +func NewConfig() Config { + return Config{ + BindAddress: DefaultBindAddress, + Database: DefaultDatabase, + Protocol: DefaultProtocol, + BatchSize: DefaultBatchSize, + BatchPending: DefaultBatchPending, + BatchTimeout: toml.Duration(DefaultBatchTimeout), + ConsistencyLevel: DefaultConsistencyLevel, + Separator: DefaultSeparator, + } } // WithDefaults takes the given config and returns a new config with any required @@ -79,6 +106,9 @@ func (c *Config) WithDefaults() *Config { if d.Separator == "" { d.Separator = DefaultSeparator } + if d.UDPReadBuffer == 0 { + d.UDPReadBuffer = DefaultUDPReadBuffer + } return &d } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/graphite/parser.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/graphite/parser.go index 6ecde5fb7..e41054a37 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/graphite/parser.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/graphite/parser.go @@ -116,6 +116,10 @@ func (p *Parser) Parse(line string) (models.Point, error) { return nil, fmt.Errorf(`field "%s" value: %s`, fields[0], err) } + if math.IsNaN(v) || math.IsInf(v, 0) { + return nil, fmt.Errorf(`field "%s" value: '%v" is unsupported`, fields[0], v) + } + fieldValues := map[string]interface{}{} if field != "" { fieldValues[field] = v @@ -150,9 +154,7 @@ func (p *Parser) Parse(line string) (models.Point, error) { tags[k] = v } } - point := models.NewPoint(measurement, tags, fieldValues, timestamp) - - return point, nil + return models.NewPoint(measurement, tags, fieldValues, timestamp) } // Apply extracts the template fields form the given line and returns the diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/graphite/parser_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/graphite/parser_test.go index fbd19c5a2..aa77eed7f 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/graphite/parser_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/graphite/parser_test.go @@ -1,7 +1,6 @@ package graphite_test import ( - "math" "strconv" "testing" "time" @@ -224,22 +223,9 @@ func TestParseNaN(t *testing.T) { t.Fatalf("unexpected error creating parser, got %v", err) } - pt, err := p.Parse("servers.localhost.cpu_load NaN 1435077219") - if err != nil { - t.Fatalf("parse error: %v", err) - } - - exp := models.NewPoint("servers.localhost.cpu_load", - models.Tags{}, - models.Fields{"value": math.NaN()}, - time.Unix(1435077219, 0)) - - if exp.String() != pt.String() { - t.Errorf("parse mismatch: got %v, exp %v", pt.String(), exp.String()) - } - - if !math.IsNaN(pt.Fields()["value"].(float64)) { - t.Errorf("parse value mismatch: expected NaN") + _, err = p.Parse("servers.localhost.cpu_load NaN 1435077219") + if err == nil { + t.Fatalf("expected error. got nil") } } @@ -249,7 +235,7 @@ func TestFilterMatchDefault(t *testing.T) { t.Fatalf("unexpected error creating parser, got %v", err) } - exp := models.NewPoint("miss.servers.localhost.cpu_load", + exp := models.MustNewPoint("miss.servers.localhost.cpu_load", models.Tags{}, models.Fields{"value": float64(11)}, time.Unix(1435077219, 0)) @@ -270,7 +256,7 @@ func TestFilterMatchMultipleMeasurement(t *testing.T) { t.Fatalf("unexpected error creating parser, got %v", err) } - exp := models.NewPoint("cpu.cpu_load.10", + exp := models.MustNewPoint("cpu.cpu_load.10", models.Tags{"host": "localhost"}, models.Fields{"value": float64(11)}, time.Unix(1435077219, 0)) @@ -294,7 +280,7 @@ func TestFilterMatchMultipleMeasurementSeparator(t *testing.T) { t.Fatalf("unexpected error creating parser, got %v", err) } - exp := models.NewPoint("cpu_cpu_load_10", + exp := models.MustNewPoint("cpu_cpu_load_10", models.Tags{"host": "localhost"}, models.Fields{"value": float64(11)}, time.Unix(1435077219, 0)) @@ -315,7 +301,7 @@ func TestFilterMatchSingle(t *testing.T) { t.Fatalf("unexpected error creating parser, got %v", err) } - exp := models.NewPoint("cpu_load", + exp := models.MustNewPoint("cpu_load", models.Tags{"host": "localhost"}, models.Fields{"value": float64(11)}, time.Unix(1435077219, 0)) @@ -336,7 +322,7 @@ func TestParseNoMatch(t *testing.T) { t.Fatalf("unexpected error creating parser, got %v", err) } - exp := models.NewPoint("servers.localhost.memory.VmallocChunk", + exp := models.MustNewPoint("servers.localhost.memory.VmallocChunk", models.Tags{}, models.Fields{"value": float64(11)}, time.Unix(1435077219, 0)) @@ -357,7 +343,7 @@ func TestFilterMatchWildcard(t *testing.T) { t.Fatalf("unexpected error creating parser, got %v", err) } - exp := models.NewPoint("cpu_load", + exp := models.MustNewPoint("cpu_load", models.Tags{"host": "localhost"}, models.Fields{"value": float64(11)}, time.Unix(1435077219, 0)) @@ -380,7 +366,7 @@ func TestFilterMatchExactBeforeWildcard(t *testing.T) { t.Fatalf("unexpected error creating parser, got %v", err) } - exp := models.NewPoint("cpu_load", + exp := models.MustNewPoint("cpu_load", models.Tags{"host": "localhost"}, models.Fields{"value": float64(11)}, time.Unix(1435077219, 0)) @@ -408,7 +394,7 @@ func TestFilterMatchMostLongestFilter(t *testing.T) { t.Fatalf("unexpected error creating parser, got %v", err) } - exp := models.NewPoint("cpu_load", + exp := models.MustNewPoint("cpu_load", models.Tags{"host": "localhost", "resource": "cpu"}, models.Fields{"value": float64(11)}, time.Unix(1435077219, 0)) @@ -435,7 +421,7 @@ func TestFilterMatchMultipleWildcards(t *testing.T) { t.Fatalf("unexpected error creating parser, got %v", err) } - exp := models.NewPoint("cpu_load", + exp := models.MustNewPoint("cpu_load", models.Tags{"host": "server01"}, models.Fields{"value": float64(11)}, time.Unix(1435077219, 0)) @@ -460,7 +446,7 @@ func TestParseDefaultTags(t *testing.T) { t.Fatalf("unexpected error creating parser, got %v", err) } - exp := models.NewPoint("cpu_load", + exp := models.MustNewPoint("cpu_load", models.Tags{"host": "localhost", "region": "us-east", "zone": "1c"}, models.Fields{"value": float64(11)}, time.Unix(1435077219, 0)) @@ -484,7 +470,7 @@ func TestParseDefaultTemplateTags(t *testing.T) { t.Fatalf("unexpected error creating parser, got %v", err) } - exp := models.NewPoint("cpu_load", + exp := models.MustNewPoint("cpu_load", models.Tags{"host": "localhost", "region": "us-east", "zone": "1c"}, models.Fields{"value": float64(11)}, time.Unix(1435077219, 0)) @@ -508,7 +494,7 @@ func TestParseDefaultTemplateTagsOverridGlobal(t *testing.T) { t.Fatalf("unexpected error creating parser, got %v", err) } - exp := models.NewPoint("cpu_load", + exp := models.MustNewPoint("cpu_load", models.Tags{"host": "localhost", "region": "us-east", "zone": "1c"}, models.Fields{"value": float64(11)}, time.Unix(1435077219, 0)) @@ -532,7 +518,7 @@ func TestParseTemplateWhitespace(t *testing.T) { t.Fatalf("unexpected error creating parser, got %v", err) } - exp := models.NewPoint("cpu_load", + exp := models.MustNewPoint("cpu_load", models.Tags{"host": "localhost", "region": "us-east", "zone": "1c"}, models.Fields{"value": float64(11)}, time.Unix(1435077219, 0)) diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/graphite/service.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/graphite/service.go index 5ed24dbd1..a0d46bdf1 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/graphite/service.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/graphite/service.go @@ -5,7 +5,6 @@ import ( "expvar" "fmt" "log" - "math" "net" "os" "strings" @@ -26,15 +25,15 @@ const ( // statistics gathered by the graphite package. const ( - statPointsReceived = "points_rx" - statBytesReceived = "bytes_rx" - statPointsParseFail = "points_parse_fail" - statPointsUnsupported = "points_unsupported_fail" - statBatchesTrasmitted = "batches_tx" - statPointsTransmitted = "points_tx" - statBatchesTransmitFail = "batches_tx_fail" - statConnectionsActive = "connections_active" - statConnectionsHandled = "connections_handled" + statPointsReceived = "pointsRx" + statBytesReceived = "bytesRx" + statPointsParseFail = "pointsParseFail" + statPointsUnsupported = "pointsUnsupportedFail" + statBatchesTrasmitted = "batchesTx" + statPointsTransmitted = "pointsTx" + statBatchesTransmitFail = "batchesTxFail" + statConnectionsActive = "connsActive" + statConnectionsHandled = "connsHandled" ) type tcpConnection struct { @@ -56,6 +55,7 @@ type Service struct { batchPending int batchTimeout time.Duration consistencyLevel cluster.ConsistencyLevel + udpReadBuffer int batcher *tsdb.PointBatcher parser *Parser @@ -96,6 +96,7 @@ func NewService(c Config) (*Service, error) { protocol: d.Protocol, batchSize: d.BatchSize, batchPending: d.BatchPending, + udpReadBuffer: d.UDPReadBuffer, batchTimeout: time.Duration(d.BatchTimeout), logger: log.New(os.Stderr, "[graphite] ", log.LstdFlags), tcpConnections: make(map[string]*tcpConnection), @@ -295,6 +296,14 @@ func (s *Service) openUDPServer() (net.Addr, error) { return nil, err } + if s.udpReadBuffer != 0 { + err = s.udpConn.SetReadBuffer(s.udpReadBuffer) + if err != nil { + return nil, fmt.Errorf("unable to set UDP read buffer to %d: %s", + s.udpReadBuffer, err) + } + } + buf := make([]byte, udpBufferSize) s.wg.Add(1) go func() { @@ -325,21 +334,11 @@ func (s *Service) handleLine(line string) { // Parse it. point, err := s.parser.Parse(line) if err != nil { - s.logger.Printf("unable to parse line: %s", err) + s.logger.Printf("unable to parse line: %s: %s", line, err) s.statMap.Add(statPointsParseFail, 1) return } - f, ok := point.Fields()["value"].(float64) - if ok { - // Drop NaN and +/-Inf data points since they are not supported values - if math.IsNaN(f) || math.IsInf(f, 0) { - s.logger.Printf("dropping unsupported value: '%v'", line) - s.statMap.Add(statPointsUnsupported, 1) - return - } - } - s.batcher.In() <- point } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/graphite/service_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/graphite/service_test.go index 7f983c56e..7415f6cbf 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/graphite/service_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/graphite/service_test.go @@ -38,16 +38,17 @@ func Test_ServerGraphiteTCP(t *testing.T) { WritePointsFn: func(req *cluster.WritePointsRequest) error { defer wg.Done() + pt, _ := models.NewPoint( + "cpu", + map[string]string{}, + map[string]interface{}{"value": 23.456}, + time.Unix(now.Unix(), 0)) + if req.Database != "graphitedb" { t.Fatalf("unexpected database: %s", req.Database) } else if req.RetentionPolicy != "" { t.Fatalf("unexpected retention policy: %s", req.RetentionPolicy) - } else if req.Points[0].String() != - models.NewPoint( - "cpu", - map[string]string{}, - map[string]interface{}{"value": 23.456}, - time.Unix(now.Unix(), 0)).String() { + } else if req.Points[0].String() != pt.String() { } return nil }, @@ -107,16 +108,16 @@ func Test_ServerGraphiteUDP(t *testing.T) { WritePointsFn: func(req *cluster.WritePointsRequest) error { defer wg.Done() + pt, _ := models.NewPoint( + "cpu", + map[string]string{}, + map[string]interface{}{"value": 23.456}, + time.Unix(now.Unix(), 0)) if req.Database != "graphitedb" { t.Fatalf("unexpected database: %s", req.Database) } else if req.RetentionPolicy != "" { t.Fatalf("unexpected retention policy: %s", req.RetentionPolicy) - } else if req.Points[0].String() != - models.NewPoint( - "cpu", - map[string]string{}, - map[string]interface{}{"value": 23.456}, - time.Unix(now.Unix(), 0)).String() { + } else if req.Points[0].String() != pt.String() { t.Fatalf("unexpected points: %#v", req.Points[0].String()) } return nil diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/config.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/config.go index b5ffe715f..c631f5d9c 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/config.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/config.go @@ -28,6 +28,10 @@ const ( // DefaultRetryMaxInterval is the maximum the hinted handoff retry interval // will ever be. DefaultRetryMaxInterval = time.Minute + + // DefaultPurgeInterval is the amount of time the system waits before attempting + // to purge hinted handoff data due to age or inactive nodes. + DefaultPurgeInterval = time.Hour ) type Config struct { @@ -38,6 +42,7 @@ type Config struct { RetryRateLimit int64 `toml:"retry-rate-limit"` RetryInterval toml.Duration `toml:"retry-interval"` RetryMaxInterval toml.Duration `toml:"retry-max-interval"` + PurgeInterval toml.Duration `toml:"purge-interval"` } func NewConfig() Config { @@ -48,5 +53,6 @@ func NewConfig() Config { RetryRateLimit: DefaultRetryRateLimit, RetryInterval: toml.Duration(DefaultRetryInterval), RetryMaxInterval: toml.Duration(DefaultRetryMaxInterval), + PurgeInterval: toml.Duration(DefaultPurgeInterval), } } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/config_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/config_test.go index 9963a31af..cc3b82eaf 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/config_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/config_test.go @@ -18,6 +18,7 @@ retry-max-interval = "100m" max-size=2048 max-age="20m" retry-rate-limit=1000 +purge-interval = "1h" `, &c); err != nil { t.Fatal(err) } @@ -47,4 +48,8 @@ retry-rate-limit=1000 t.Fatalf("unexpected retry rate limit: got %v, exp %v", c.RetryRateLimit, exp) } + if exp := time.Hour; c.PurgeInterval.String() != exp.String() { + t.Fatalf("unexpected purge interval: got %v, exp %v", c.PurgeInterval, exp) + } + } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/node_processor.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/node_processor.go new file mode 100644 index 000000000..f2315efcf --- /dev/null +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/node_processor.go @@ -0,0 +1,293 @@ +package hh + +import ( + "encoding/binary" + "expvar" + "fmt" + "io" + "log" + "os" + "strings" + "sync" + "time" + + "github.com/influxdb/influxdb" + "github.com/influxdb/influxdb/models" +) + +// NodeProcessor encapsulates a queue of hinted-handoff data for a node, and the +// transmission of the data to the node. +type NodeProcessor struct { + PurgeInterval time.Duration // Interval between periodic purge checks + RetryInterval time.Duration // Interval between periodic write-to-node attempts. + RetryMaxInterval time.Duration // Max interval between periodic write-to-node attempts. + MaxSize int64 // Maximum size an underlying queue can get. + MaxAge time.Duration // Maximum age queue data can get before purging. + RetryRateLimit int64 // Limits the rate data is sent to node. + nodeID uint64 + dir string + + mu sync.RWMutex + wg sync.WaitGroup + done chan struct{} + + queue *queue + meta metaStore + writer shardWriter + + statMap *expvar.Map + Logger *log.Logger +} + +// NewNodeProcessor returns a new NodeProcessor for the given node, using dir for +// the hinted-handoff data. +func NewNodeProcessor(nodeID uint64, dir string, w shardWriter, m metaStore) *NodeProcessor { + key := strings.Join([]string{"hh_processor", dir}, ":") + tags := map[string]string{"node": fmt.Sprintf("%d", nodeID), "path": dir} + + return &NodeProcessor{ + PurgeInterval: DefaultPurgeInterval, + RetryInterval: DefaultRetryInterval, + RetryMaxInterval: DefaultRetryMaxInterval, + MaxSize: DefaultMaxSize, + MaxAge: DefaultMaxAge, + nodeID: nodeID, + dir: dir, + writer: w, + meta: m, + statMap: influxdb.NewStatistics(key, "hh_processor", tags), + Logger: log.New(os.Stderr, "[handoff] ", log.LstdFlags), + } +} + +// Open opens the NodeProcessor. It will read and write data present in dir, and +// start transmitting data to the node. A NodeProcessor must be opened before it +// can accept hinted data. +func (n *NodeProcessor) Open() error { + n.mu.Lock() + defer n.mu.Unlock() + + if n.done != nil { + // Already open. + return nil + } + n.done = make(chan struct{}) + + // Create the queue directory if it doesn't already exist. + if err := os.MkdirAll(n.dir, 0700); err != nil { + return fmt.Errorf("mkdir all: %s", err) + } + + // Create the queue of hinted-handoff data. + queue, err := newQueue(n.dir, n.MaxSize) + if err != nil { + return err + } + if err := queue.Open(); err != nil { + return err + } + n.queue = queue + + n.wg.Add(1) + go n.run() + + return nil +} + +// Close closes the NodeProcessor, terminating all data tranmission to the node. +// When closed it will not accept hinted-handoff data. +func (n *NodeProcessor) Close() error { + n.mu.Lock() + defer n.mu.Unlock() + + if n.done == nil { + // Already closed. + return nil + } + + close(n.done) + n.wg.Wait() + n.done = nil + + return n.queue.Close() +} + +// Purge deletes all hinted-handoff data under management by a NodeProcessor. +// The NodeProcessor should be in the closed state before calling this function. +func (n *NodeProcessor) Purge() error { + n.mu.Lock() + defer n.mu.Unlock() + + if n.done != nil { + return fmt.Errorf("node processor is open") + } + + return os.RemoveAll(n.dir) +} + +// WriteShard writes hinted-handoff data for the given shard and node. Since it may manipulate +// hinted-handoff queues, and be called concurrently, it takes a lock during queue access. +func (n *NodeProcessor) WriteShard(shardID uint64, points []models.Point) error { + n.mu.RLock() + defer n.mu.RUnlock() + + if n.done == nil { + return fmt.Errorf("node processor is closed") + } + + n.statMap.Add(writeShardReq, 1) + n.statMap.Add(writeShardReqPoints, int64(len(points))) + + b := marshalWrite(shardID, points) + return n.queue.Append(b) +} + +// LastModified returns the time the NodeProcessor last receieved hinted-handoff data. +func (n *NodeProcessor) LastModified() (time.Time, error) { + t, err := n.queue.LastModified() + if err != nil { + return time.Time{}, err + } + return t.UTC(), nil +} + +// run attempts to send any existing hinted handoff data to the target node. It also purges +// any hinted handoff data older than the configured time. +func (n *NodeProcessor) run() { + defer n.wg.Done() + + currInterval := time.Duration(n.RetryInterval) + if currInterval > time.Duration(n.RetryMaxInterval) { + currInterval = time.Duration(n.RetryMaxInterval) + } + + for { + select { + case <-n.done: + return + + case <-time.After(n.PurgeInterval): + if err := n.queue.PurgeOlderThan(time.Now().Add(-n.MaxAge)); err != nil { + n.Logger.Printf("failed to purge for node %d: %s", n.nodeID, err.Error()) + } + + case <-time.After(currInterval): + limiter := NewRateLimiter(n.RetryRateLimit) + for { + c, err := n.SendWrite() + if err != nil { + if err == io.EOF { + // No more data, return to configured interval + currInterval = time.Duration(n.RetryInterval) + } else { + currInterval = currInterval * 2 + if currInterval > time.Duration(n.RetryMaxInterval) { + currInterval = time.Duration(n.RetryMaxInterval) + } + } + break + } + + // Success! Ensure backoff is cancelled. + currInterval = time.Duration(n.RetryInterval) + + // Update how many bytes we've sent + limiter.Update(c) + + // Block to maintain the throughput rate + time.Sleep(limiter.Delay()) + } + } + } +} + +// SendWrite attempts to sent the current block of hinted data to the target node. If successful, +// it returns the number of bytes it sent and advances to the next block. Otherwise returns EOF +// when there is no more data or the node is inactive. +func (n *NodeProcessor) SendWrite() (int, error) { + n.mu.RLock() + defer n.mu.RUnlock() + + active, err := n.Active() + if err != nil { + return 0, err + } + if !active { + return 0, io.EOF + } + + // Get the current block from the queue + buf, err := n.queue.Current() + if err != nil { + return 0, err + } + + // unmarshal the byte slice back to shard ID and points + shardID, points, err := unmarshalWrite(buf) + if err != nil { + n.Logger.Printf("unmarshal write failed: %v", err) + // Try to skip it. + if err := n.queue.Advance(); err != nil { + n.Logger.Printf("failed to advance queue for node %d: %s", n.nodeID, err.Error()) + } + return 0, err + } + + if err := n.writer.WriteShard(shardID, n.nodeID, points); err != nil { + n.statMap.Add(writeNodeReqFail, 1) + return 0, err + } + n.statMap.Add(writeNodeReq, 1) + n.statMap.Add(writeNodeReqPoints, int64(len(points))) + + if err := n.queue.Advance(); err != nil { + n.Logger.Printf("failed to advance queue for node %d: %s", n.nodeID, err.Error()) + } + + return len(buf), nil +} + +func (n *NodeProcessor) Head() string { + qp, err := n.queue.Position() + if err != nil { + return "" + } + return qp.head +} + +func (n *NodeProcessor) Tail() string { + qp, err := n.queue.Position() + if err != nil { + return "" + } + return qp.tail +} + +// Active returns whether this node processor is for a currently active node. +func (n *NodeProcessor) Active() (bool, error) { + nio, err := n.meta.Node(n.nodeID) + if err != nil { + n.Logger.Printf("failed to determine if node %d is active: %s", n.nodeID, err.Error()) + return false, err + } + return nio != nil, nil +} + +func marshalWrite(shardID uint64, points []models.Point) []byte { + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, shardID) + for _, p := range points { + b = append(b, []byte(p.String())...) + b = append(b, '\n') + } + return b +} + +func unmarshalWrite(b []byte) (uint64, []models.Point, error) { + if len(b) < 8 { + return 0, nil, fmt.Errorf("too short: len = %d", len(b)) + } + ownerID := binary.BigEndian.Uint64(b[:8]) + points, err := models.ParsePoints(b[8:]) + return ownerID, points, err +} diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/node_processor_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/node_processor_test.go new file mode 100644 index 000000000..404765e14 --- /dev/null +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/node_processor_test.go @@ -0,0 +1,155 @@ +package hh + +import ( + "io" + "io/ioutil" + "os" + "testing" + "time" + + "github.com/influxdb/influxdb/meta" + "github.com/influxdb/influxdb/models" +) + +type fakeShardWriter struct { + ShardWriteFn func(shardID, nodeID uint64, points []models.Point) error +} + +func (f *fakeShardWriter) WriteShard(shardID, nodeID uint64, points []models.Point) error { + return f.ShardWriteFn(shardID, nodeID, points) +} + +type fakeMetaStore struct { + NodeFn func(nodeID uint64) (*meta.NodeInfo, error) +} + +func (f *fakeMetaStore) Node(nodeID uint64) (*meta.NodeInfo, error) { + return f.NodeFn(nodeID) +} + +func TestNodeProcessorSendBlock(t *testing.T) { + dir, err := ioutil.TempDir("", "node_processor_test") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + + // expected data to be queue and sent to the shardWriter + var expShardID, expNodeID, count = uint64(100), uint64(200), 0 + pt := models.MustNewPoint("cpu", models.Tags{"foo": "bar"}, models.Fields{"value": 1.0}, time.Unix(0, 0)) + + sh := &fakeShardWriter{ + ShardWriteFn: func(shardID, nodeID uint64, points []models.Point) error { + count += 1 + if shardID != expShardID { + t.Errorf("SendWrite() shardID mismatch: got %v, exp %v", shardID, expShardID) + } + if nodeID != expNodeID { + t.Errorf("SendWrite() nodeID mismatch: got %v, exp %v", nodeID, expNodeID) + } + + if exp := 1; len(points) != exp { + t.Fatalf("SendWrite() points mismatch: got %v, exp %v", len(points), exp) + } + + if points[0].String() != pt.String() { + t.Fatalf("SendWrite() points mismatch:\n got %v\n exp %v", points[0].String(), pt.String()) + } + + return nil + }, + } + metastore := &fakeMetaStore{ + NodeFn: func(nodeID uint64) (*meta.NodeInfo, error) { + if nodeID == expNodeID { + return &meta.NodeInfo{}, nil + } + return nil, nil + }, + } + + n := NewNodeProcessor(expNodeID, dir, sh, metastore) + if n == nil { + t.Fatalf("Failed to create node processor: %v", err) + } + + if err := n.Open(); err != nil { + t.Fatalf("Failed to open node processor: %v", err) + } + + // Check the active state. + active, err := n.Active() + if err != nil { + t.Fatalf("Failed to check node processor state: %v", err) + } + if !active { + t.Fatalf("Node processor state is unexpected value of: %v", active) + } + + // This should queue a write for the active node. + if err := n.WriteShard(expShardID, []models.Point{pt}); err != nil { + t.Fatalf("SendWrite() failed to write points: %v", err) + } + + // This should send the write to the shard writer + if _, err := n.SendWrite(); err != nil { + t.Fatalf("SendWrite() failed to write points: %v", err) + } + + if exp := 1; count != exp { + t.Fatalf("SendWrite() write count mismatch: got %v, exp %v", count, exp) + } + + // All data should have been handled so no writes should be sent again + if _, err := n.SendWrite(); err != nil && err != io.EOF { + t.Fatalf("SendWrite() failed to write points: %v", err) + } + + // Count should stay the same + if exp := 1; count != exp { + t.Fatalf("SendWrite() write count mismatch: got %v, exp %v", count, exp) + } + + // Make the node inactive. + sh.ShardWriteFn = func(shardID, nodeID uint64, points []models.Point) error { + t.Fatalf("write sent to inactive node") + return nil + } + metastore.NodeFn = func(nodeID uint64) (*meta.NodeInfo, error) { + return nil, nil + } + + // Check the active state. + active, err = n.Active() + if err != nil { + t.Fatalf("Failed to check node processor state: %v", err) + } + if active { + t.Fatalf("Node processor state is unexpected value of: %v", active) + } + + // This should queue a write for the node. + if err := n.WriteShard(expShardID, []models.Point{pt}); err != nil { + t.Fatalf("SendWrite() failed to write points: %v", err) + } + + // This should not send the write to the shard writer since the node is inactive. + if _, err := n.SendWrite(); err != nil && err != io.EOF { + t.Fatalf("SendWrite() failed to write points: %v", err) + } + + if exp := 1; count != exp { + t.Fatalf("SendWrite() write count mismatch: got %v, exp %v", count, exp) + } + + if err := n.Close(); err != nil { + t.Fatalf("Failed to close node processor: %v", err) + } + + // Confirm that purging works ok. + if err := n.Purge(); err != nil { + t.Fatalf("Failed to purge node processor: %v", err) + } + if _, err := os.Stat(dir); !os.IsNotExist(err) { + t.Fatalf("Node processor directory still present after purge") + } +} diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/processor.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/processor.go deleted file mode 100644 index b63013d73..000000000 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/processor.go +++ /dev/null @@ -1,341 +0,0 @@ -package hh - -import ( - "encoding/binary" - "expvar" - "fmt" - "io" - "io/ioutil" - "log" - "os" - "path/filepath" - "strconv" - "sync" - "time" - - "github.com/influxdb/influxdb" - "github.com/influxdb/influxdb/models" - "github.com/influxdb/influxdb/tsdb" -) - -const ( - pointsHint = "points_hint" - pointsWrite = "points_write" - bytesWrite = "bytes_write" - writeErr = "write_err" - unmarshalErr = "unmarshal_err" - advanceErr = "advance_err" - currentErr = "current_err" -) - -type Processor struct { - mu sync.RWMutex - - dir string - maxSize int64 - maxAge time.Duration - retryRateLimit int64 - - queues map[uint64]*queue - meta metaStore - writer shardWriter - metastore metaStore - Logger *log.Logger - - // Shard-level and node-level HH stats. - shardStatMaps map[uint64]*expvar.Map - nodeStatMaps map[uint64]*expvar.Map -} - -type ProcessorOptions struct { - MaxSize int64 - RetryRateLimit int64 -} - -func NewProcessor(dir string, writer shardWriter, metastore metaStore, options ProcessorOptions) (*Processor, error) { - p := &Processor{ - dir: dir, - queues: map[uint64]*queue{}, - writer: writer, - metastore: metastore, - Logger: log.New(os.Stderr, "[handoff] ", log.LstdFlags), - shardStatMaps: make(map[uint64]*expvar.Map), - nodeStatMaps: make(map[uint64]*expvar.Map), - } - p.setOptions(options) - - // Create the root directory if it doesn't already exist. - if err := os.MkdirAll(dir, 0700); err != nil { - return nil, fmt.Errorf("mkdir all: %s", err) - } - - if err := p.loadQueues(); err != nil { - return p, err - } - return p, nil -} - -func (p *Processor) setOptions(options ProcessorOptions) { - p.maxSize = DefaultMaxSize - if options.MaxSize != 0 { - p.maxSize = options.MaxSize - } - - p.retryRateLimit = DefaultRetryRateLimit - if options.RetryRateLimit != 0 { - p.retryRateLimit = options.RetryRateLimit - } -} - -func (p *Processor) loadQueues() error { - files, err := ioutil.ReadDir(p.dir) - if err != nil { - return err - } - - for _, file := range files { - nodeID, err := strconv.ParseUint(file.Name(), 10, 64) - if err != nil { - return err - } - - if _, err := p.addQueue(nodeID); err != nil { - return err - } - } - return nil -} - -// addQueue adds a hinted-handoff queue for the given node. This function is not thread-safe -// and the caller must ensure this function is not called concurrently. -func (p *Processor) addQueue(nodeID uint64) (*queue, error) { - path := filepath.Join(p.dir, strconv.FormatUint(nodeID, 10)) - if err := os.MkdirAll(path, 0700); err != nil { - return nil, err - } - - queue, err := newQueue(path, p.maxSize) - if err != nil { - return nil, err - } - if err := queue.Open(); err != nil { - return nil, err - } - p.queues[nodeID] = queue - - // Create node stats for this queue. - key := fmt.Sprintf("hh_processor:node:%d", nodeID) - tags := map[string]string{"nodeID": strconv.FormatUint(nodeID, 10)} - p.nodeStatMaps[nodeID] = influxdb.NewStatistics(key, "hh_processor", tags) - return queue, nil -} - -// WriteShard writes hinted-handoff data for the given shard and node. Since it may manipulate -// hinted-handoff queues, and be called concurrently, it takes a lock during queue access. -func (p *Processor) WriteShard(shardID, ownerID uint64, points []models.Point) error { - p.mu.RLock() - queue, ok := p.queues[ownerID] - p.mu.RUnlock() - if !ok { - if err := func() error { - // Check again under write-lock. - p.mu.Lock() - defer p.mu.Unlock() - - queue, ok = p.queues[ownerID] - if !ok { - var err error - if queue, err = p.addQueue(ownerID); err != nil { - return err - } - } - return nil - }(); err != nil { - return err - } - } - - // Update stats - p.updateShardStats(shardID, pointsHint, int64(len(points))) - p.nodeStatMaps[ownerID].Add(pointsHint, int64(len(points))) - - b := p.marshalWrite(shardID, points) - return queue.Append(b) -} - -func (p *Processor) Process() error { - p.mu.RLock() - defer p.mu.RUnlock() - - activeQueues, err := p.activeQueues() - if err != nil { - return err - } - - res := make(chan error, len(activeQueues)) - for nodeID, q := range activeQueues { - go func(nodeID uint64, q *queue) { - - // Log how many writes we successfully sent at the end - var sent int - start := time.Now() - defer func(start time.Time) { - if sent > 0 { - p.Logger.Printf("%d queued writes sent to node %d in %s", sent, nodeID, time.Since(start)) - } - }(start) - - limiter := NewRateLimiter(p.retryRateLimit) - for { - // Get the current block from the queue - buf, err := q.Current() - if err != nil { - if err != io.EOF { - p.nodeStatMaps[nodeID].Add(currentErr, 1) - } - res <- nil - break - } - - // unmarshal the byte slice back to shard ID and points - shardID, points, err := p.unmarshalWrite(buf) - if err != nil { - p.nodeStatMaps[nodeID].Add(unmarshalErr, 1) - p.Logger.Printf("unmarshal write failed: %v", err) - if err := q.Advance(); err != nil { - p.nodeStatMaps[nodeID].Add(advanceErr, 1) - res <- err - } - - // Skip and try the next block. - continue - } - - // Try to send the write to the node - if err := p.writer.WriteShard(shardID, nodeID, points); err != nil && tsdb.IsRetryable(err) { - p.nodeStatMaps[nodeID].Add(writeErr, 1) - p.Logger.Printf("remote write failed: %v", err) - res <- nil - break - } - p.updateShardStats(shardID, pointsWrite, int64(len(points))) - p.nodeStatMaps[nodeID].Add(pointsWrite, int64(len(points))) - - // If we get here, the write succeeded so advance the queue to the next item - if err := q.Advance(); err != nil { - p.nodeStatMaps[nodeID].Add(advanceErr, 1) - res <- err - return - } - - sent += 1 - - // Update how many bytes we've sent - limiter.Update(len(buf)) - p.updateShardStats(shardID, bytesWrite, int64(len(buf))) - p.nodeStatMaps[nodeID].Add(bytesWrite, int64(len(buf))) - - // Block to maintain the throughput rate - time.Sleep(limiter.Delay()) - - } - }(nodeID, q) - } - - for range activeQueues { - err := <-res - if err != nil { - return err - } - } - return nil -} - -func (p *Processor) marshalWrite(shardID uint64, points []models.Point) []byte { - b := make([]byte, 8) - binary.BigEndian.PutUint64(b, shardID) - for _, p := range points { - b = append(b, []byte(p.String())...) - b = append(b, '\n') - } - return b -} - -func (p *Processor) unmarshalWrite(b []byte) (uint64, []models.Point, error) { - if len(b) < 8 { - return 0, nil, fmt.Errorf("too short: len = %d", len(b)) - } - ownerID := binary.BigEndian.Uint64(b[:8]) - points, err := models.ParsePoints(b[8:]) - return ownerID, points, err -} - -func (p *Processor) updateShardStats(shardID uint64, stat string, inc int64) { - m, ok := p.shardStatMaps[shardID] - if !ok { - key := fmt.Sprintf("hh_processor:shard:%d", shardID) - tags := map[string]string{"shardID": strconv.FormatUint(shardID, 10)} - p.shardStatMaps[shardID] = influxdb.NewStatistics(key, "hh_processor", tags) - m = p.shardStatMaps[shardID] - } - m.Add(stat, inc) -} - -func (p *Processor) activeQueues() (map[uint64]*queue, error) { - queues := make(map[uint64]*queue) - for id, q := range p.queues { - ni, err := p.metastore.Node(id) - if err != nil { - return nil, err - } - if ni != nil { - queues[id] = q - } - } - return queues, nil -} - -func (p *Processor) PurgeOlderThan(when time.Duration) error { - p.mu.Lock() - defer p.mu.Unlock() - - for _, queue := range p.queues { - if err := queue.PurgeOlderThan(time.Now().Add(-when)); err != nil { - return err - } - } - return nil -} - -func (p *Processor) PurgeInactiveOlderThan(when time.Duration) error { - p.mu.Lock() - defer p.mu.Unlock() - - for nodeID, queue := range p.queues { - // Only delete queues for inactive nodes. - ni, err := p.metastore.Node(nodeID) - if err != nil { - return err - } - if ni != nil { - continue - } - - last, err := queue.LastModified() - if err != nil { - return err - } - if last.Before(time.Now().Add(-when)) { - // Close and remove the queue. - if err := queue.Close(); err != nil { - return err - } - if err := queue.Remove(); err != nil { - return err - } - - delete(p.queues, nodeID) - } - } - return nil -} diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/processor_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/processor_test.go deleted file mode 100644 index fa74c8338..000000000 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/processor_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package hh - -import ( - "io/ioutil" - "testing" - "time" - - "github.com/influxdb/influxdb/meta" - "github.com/influxdb/influxdb/models" -) - -type fakeShardWriter struct { - ShardWriteFn func(shardID, nodeID uint64, points []models.Point) error -} - -func (f *fakeShardWriter) WriteShard(shardID, nodeID uint64, points []models.Point) error { - return f.ShardWriteFn(shardID, nodeID, points) -} - -type fakeMetaStore struct { - NodeFn func(nodeID uint64) (*meta.NodeInfo, error) -} - -func (f *fakeMetaStore) Node(nodeID uint64) (*meta.NodeInfo, error) { - return f.NodeFn(nodeID) -} - -func TestProcessorProcess(t *testing.T) { - dir, err := ioutil.TempDir("", "processor_test") - if err != nil { - t.Fatalf("failed to create temp dir: %v", err) - } - - // expected data to be queue and sent to the shardWriter - var expShardID, activeNodeID, inactiveNodeID, count = uint64(100), uint64(200), uint64(300), 0 - pt := models.NewPoint("cpu", models.Tags{"foo": "bar"}, models.Fields{"value": 1.0}, time.Unix(0, 0)) - - sh := &fakeShardWriter{ - ShardWriteFn: func(shardID, nodeID uint64, points []models.Point) error { - count += 1 - if shardID != expShardID { - t.Errorf("Process() shardID mismatch: got %v, exp %v", shardID, expShardID) - } - if nodeID != activeNodeID { - t.Errorf("Process() nodeID mismatch: got %v, exp %v", nodeID, activeNodeID) - } - - if exp := 1; len(points) != exp { - t.Fatalf("Process() points mismatch: got %v, exp %v", len(points), exp) - } - - if points[0].String() != pt.String() { - t.Fatalf("Process() points mismatch:\n got %v\n exp %v", points[0].String(), pt.String()) - } - - return nil - }, - } - metastore := &fakeMetaStore{ - NodeFn: func(nodeID uint64) (*meta.NodeInfo, error) { - if nodeID == activeNodeID { - return &meta.NodeInfo{}, nil - } - return nil, nil - }, - } - - p, err := NewProcessor(dir, sh, metastore, ProcessorOptions{MaxSize: 1024}) - if err != nil { - t.Fatalf("Process() failed to create processor: %v", err) - } - - // This should queue a write for the active node. - if err := p.WriteShard(expShardID, activeNodeID, []models.Point{pt}); err != nil { - t.Fatalf("Process() failed to write points: %v", err) - } - - // This should queue a write for the inactive node. - if err := p.WriteShard(expShardID, inactiveNodeID, []models.Point{pt}); err != nil { - t.Fatalf("Process() failed to write points: %v", err) - } - - // This should send the write to the shard writer - if err := p.Process(); err != nil { - t.Fatalf("Process() failed to write points: %v", err) - } - - if exp := 1; count != exp { - t.Fatalf("Process() write count mismatch: got %v, exp %v", count, exp) - } - - // All active nodes should have been handled so no writes should be sent again - if err := p.Process(); err != nil { - t.Fatalf("Process() failed to write points: %v", err) - } - - // Count should stay the same - if exp := 1; count != exp { - t.Fatalf("Process() write count mismatch: got %v, exp %v", count, exp) - } - - // Make the inactive node active. - sh.ShardWriteFn = func(shardID, nodeID uint64, points []models.Point) error { - count += 1 - if shardID != expShardID { - t.Errorf("Process() shardID mismatch: got %v, exp %v", shardID, expShardID) - } - if nodeID != inactiveNodeID { - t.Errorf("Process() nodeID mismatch: got %v, exp %v", nodeID, activeNodeID) - } - - if exp := 1; len(points) != exp { - t.Fatalf("Process() points mismatch: got %v, exp %v", len(points), exp) - } - - if points[0].String() != pt.String() { - t.Fatalf("Process() points mismatch:\n got %v\n exp %v", points[0].String(), pt.String()) - } - - return nil - } - metastore.NodeFn = func(nodeID uint64) (*meta.NodeInfo, error) { - return &meta.NodeInfo{}, nil - } - - // This should send the final write to the shard writer - if err := p.Process(); err != nil { - t.Fatalf("Process() failed to write points: %v", err) - } - - if exp := 2; count != exp { - t.Fatalf("Process() write count mismatch: got %v, exp %v", count, exp) - } - - // All queues should have been handled, so no more writes should result. - if err := p.Process(); err != nil { - t.Fatalf("Process() failed to write points: %v", err) - } - - if exp := 2; count != exp { - t.Fatalf("Process() write count mismatch: got %v, exp %v", count, exp) - } -} diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/queue.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/queue.go index 5152bfc36..19c004fcc 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/queue.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/queue.go @@ -72,6 +72,10 @@ type queue struct { // The segments that exist on disk segments segments } +type queuePos struct { + head string + tail string +} type segments []*segment @@ -211,7 +215,21 @@ func (l *queue) LastModified() (time.Time, error) { if l.tail != nil { return l.tail.lastModified() } - return time.Time{}, nil + return time.Time{}.UTC(), nil +} + +func (l *queue) Position() (*queuePos, error) { + l.mu.RLock() + defer l.mu.RUnlock() + + qp := &queuePos{} + if l.head != nil { + qp.head = fmt.Sprintf("%s:%d", l.head.path, l.head.pos) + } + if l.tail != nil { + qp.tail = fmt.Sprintf("%s:%d", l.tail.path, l.tail.filePos()) + } + return qp, nil } // diskUsage returns the total size on disk used by the queue @@ -606,7 +624,7 @@ func (l *segment) lastModified() (time.Time, error) { if err != nil { return time.Time{}, err } - return stats.ModTime(), nil + return stats.ModTime().UTC(), nil } func (l *segment) diskUsage() int64 { diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/service.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/service.go index 455428a26..514a9c889 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/service.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/hh/service.go @@ -3,9 +3,11 @@ package hh import ( "expvar" "fmt" - "io" + "io/ioutil" "log" "os" + "path/filepath" + "strconv" "strings" "sync" "time" @@ -13,15 +15,17 @@ import ( "github.com/influxdb/influxdb" "github.com/influxdb/influxdb/meta" "github.com/influxdb/influxdb/models" + "github.com/influxdb/influxdb/monitor" ) var ErrHintedHandoffDisabled = fmt.Errorf("hinted handoff disabled") const ( - writeShardReq = "write_shard_req" - writeShardReqPoints = "write_shard_req_points" - processReq = "process_req" - processReqFail = "process_req_fail" + writeShardReq = "writeShardReq" + writeShardReqPoints = "writeShardReqPoints" + writeNodeReq = "writeNodeReq" + writeNodeReqFail = "writeNodeReqFail" + writeNodeReqPoints = "writeNodeReqPoints" ) type Service struct { @@ -29,17 +33,18 @@ type Service struct { wg sync.WaitGroup closing chan struct{} + processors map[uint64]*NodeProcessor + statMap *expvar.Map Logger *log.Logger cfg Config - ShardWriter shardWriter + shardWriter shardWriter + metastore metaStore - HintedHandoff interface { - WriteShard(shardID, ownerID uint64, points []models.Point) error - Process() error - PurgeOlderThan(when time.Duration) error - PurgeInactiveOlderThan(when time.Duration) error + Monitor interface { + RegisterDiagnosticsClient(name string, client monitor.DiagsClient) + DeregisterDiagnosticsClient(name string) } } @@ -56,55 +61,81 @@ func NewService(c Config, w shardWriter, m metaStore) *Service { key := strings.Join([]string{"hh", c.Dir}, ":") tags := map[string]string{"path": c.Dir} - s := &Service{ - cfg: c, - statMap: influxdb.NewStatistics(key, "hh", tags), - Logger: log.New(os.Stderr, "[handoff] ", log.LstdFlags), + return &Service{ + cfg: c, + closing: make(chan struct{}), + processors: make(map[uint64]*NodeProcessor), + statMap: influxdb.NewStatistics(key, "hh", tags), + Logger: log.New(os.Stderr, "[handoff] ", log.LstdFlags), + shardWriter: w, + metastore: m, } - processor, err := NewProcessor(c.Dir, w, m, ProcessorOptions{ - MaxSize: c.MaxSize, - RetryRateLimit: c.RetryRateLimit, - }) - if err != nil { - s.Logger.Fatalf("Failed to start hinted handoff processor: %v", err) - } - - processor.Logger = s.Logger - s.HintedHandoff = processor - return s } func (s *Service) Open() error { - if !s.cfg.Enabled { - // Allow Open to proceed, but don't anything. - return nil - } - - s.Logger.Printf("Starting hinted handoff service") - s.mu.Lock() defer s.mu.Unlock() - + if !s.cfg.Enabled { + // Allow Open to proceed, but don't do anything. + return nil + } + s.Logger.Printf("Starting hinted handoff service") s.closing = make(chan struct{}) - s.Logger.Printf("Using data dir: %v", s.cfg.Dir) + // Register diagnostics if a Monitor service is available. + if s.Monitor != nil { + s.Monitor.RegisterDiagnosticsClient("hh", s) + } - s.wg.Add(3) - go s.retryWrites() - go s.expireWrites() - go s.deleteInactiveQueues() + // Create the root directory if it doesn't already exist. + s.Logger.Printf("Using data dir: %v", s.cfg.Dir) + if err := os.MkdirAll(s.cfg.Dir, 0700); err != nil { + return fmt.Errorf("mkdir all: %s", err) + } + + // Create a node processor for each node directory. + files, err := ioutil.ReadDir(s.cfg.Dir) + if err != nil { + return err + } + + for _, file := range files { + nodeID, err := strconv.ParseUint(file.Name(), 10, 64) + if err != nil { + // Not a number? Skip it. + continue + } + + n := NewNodeProcessor(nodeID, s.pathforNode(nodeID), s.shardWriter, s.metastore) + if err := n.Open(); err != nil { + return err + } + s.processors[nodeID] = n + } + + s.wg.Add(1) + go s.purgeInactiveProcessors() return nil } func (s *Service) Close() error { + s.Logger.Println("shutting down hh service") s.mu.Lock() defer s.mu.Unlock() + for _, p := range s.processors { + if err := p.Close(); err != nil { + return err + } + } + if s.closing != nil { close(s.closing) } s.wg.Wait() + s.closing = nil + return nil } @@ -115,76 +146,125 @@ func (s *Service) SetLogger(l *log.Logger) { // WriteShard queues the points write for shardID to node ownerID to handoff queue func (s *Service) WriteShard(shardID, ownerID uint64, points []models.Point) error { - s.statMap.Add(writeShardReq, 1) - s.statMap.Add(writeShardReqPoints, int64(len(points))) if !s.cfg.Enabled { return ErrHintedHandoffDisabled } + s.statMap.Add(writeShardReq, 1) + s.statMap.Add(writeShardReqPoints, int64(len(points))) - return s.HintedHandoff.WriteShard(shardID, ownerID, points) -} + s.mu.RLock() + processor, ok := s.processors[ownerID] + s.mu.RUnlock() + if !ok { + if err := func() error { + // Check again under write-lock. + s.mu.Lock() + defer s.mu.Unlock() -func (s *Service) retryWrites() { - defer s.wg.Done() - currInterval := time.Duration(s.cfg.RetryInterval) - if currInterval > time.Duration(s.cfg.RetryMaxInterval) { - currInterval = time.Duration(s.cfg.RetryMaxInterval) - } - - for { - - select { - case <-s.closing: - return - case <-time.After(currInterval): - s.statMap.Add(processReq, 1) - if err := s.HintedHandoff.Process(); err != nil && err != io.EOF { - s.statMap.Add(processReqFail, 1) - s.Logger.Printf("retried write failed: %v", err) - - currInterval = currInterval * 2 - if currInterval > time.Duration(s.cfg.RetryMaxInterval) { - currInterval = time.Duration(s.cfg.RetryMaxInterval) + processor, ok = s.processors[ownerID] + if !ok { + processor = NewNodeProcessor(ownerID, s.pathforNode(ownerID), s.shardWriter, s.metastore) + if err := processor.Open(); err != nil { + return err } - } else { - // Success! Return to configured interval. - currInterval = time.Duration(s.cfg.RetryInterval) + s.processors[ownerID] = processor } + return nil + }(); err != nil { + return err } } + + if err := processor.WriteShard(shardID, points); err != nil { + return err + } + + return nil } -// expireWrites will cause the handoff queues to remove writes that are older -// than the configured threshold -func (s *Service) expireWrites() { +// Diagnostics returns diagnostic information. +func (s *Service) Diagnostics() (*monitor.Diagnostic, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + d := &monitor.Diagnostic{ + Columns: []string{"node", "active", "last modified", "head", "tail"}, + Rows: make([][]interface{}, 0, len(s.processors)), + } + + for k, v := range s.processors { + lm, err := v.LastModified() + if err != nil { + return nil, err + } + + active := "no" + b, err := v.Active() + if err != nil { + return nil, err + } + if b { + active = "yes" + } + + d.Rows = append(d.Rows, []interface{}{k, active, lm, v.Head(), v.Tail()}) + } + return d, nil +} + +// purgeInactiveProcessors will cause the service to remove processors for inactive nodes. +func (s *Service) purgeInactiveProcessors() { defer s.wg.Done() - ticker := time.NewTicker(time.Hour) + ticker := time.NewTicker(time.Duration(s.cfg.PurgeInterval)) defer ticker.Stop() + for { select { case <-s.closing: return case <-ticker.C: - if err := s.HintedHandoff.PurgeOlderThan(time.Duration(s.cfg.MaxAge)); err != nil { - s.Logger.Printf("purge write failed: %v", err) - } + func() { + s.mu.Lock() + defer s.mu.Unlock() + + for k, v := range s.processors { + lm, err := v.LastModified() + if err != nil { + s.Logger.Printf("failed to determine LastModified for processor %d: %s", k, err.Error()) + continue + } + + active, err := v.Active() + if err != nil { + s.Logger.Printf("failed to determine if node %d is active: %s", k, err.Error()) + continue + } + if active { + // Node is active. + continue + } + + if !lm.Before(time.Now().Add(-time.Duration(s.cfg.MaxAge))) { + // Node processor contains too-young data. + continue + } + + if err := v.Close(); err != nil { + s.Logger.Printf("failed to close node processor %d: %s", k, err.Error()) + continue + } + if err := v.Purge(); err != nil { + s.Logger.Printf("failed to purge node processor %d: %s", k, err.Error()) + continue + } + delete(s.processors, k) + } + }() } } } -// deleteInactiveQueues will cause the service to remove queues for inactive nodes. -func (s *Service) deleteInactiveQueues() { - defer s.wg.Done() - ticker := time.NewTicker(time.Hour) - defer ticker.Stop() - for { - select { - case <-s.closing: - return - case <-ticker.C: - if err := s.HintedHandoff.PurgeInactiveOlderThan(time.Duration(s.cfg.MaxAge)); err != nil { - s.Logger.Printf("delete queues failed: %v", err) - } - } - } +// pathforNode returns the directory for HH data, for the given node. +func (s *Service) pathforNode(nodeID uint64) string { + return filepath.Join(s.cfg.Dir, fmt.Sprintf("%d", nodeID)) } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/httpd/config.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/httpd/config.go index a5dfb9826..83724e3b9 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/httpd/config.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/httpd/config.go @@ -7,8 +7,8 @@ type Config struct { LogEnabled bool `toml:"log-enabled"` WriteTracing bool `toml:"write-tracing"` PprofEnabled bool `toml:"pprof-enabled"` - HttpsEnabled bool `toml:"https-enabled"` - HttpsCertificate string `toml:"https-certificate"` + HTTPSEnabled bool `toml:"https-enabled"` + HTTPSCertificate string `toml:"https-certificate"` } func NewConfig() Config { @@ -16,7 +16,7 @@ func NewConfig() Config { Enabled: true, BindAddress: ":8086", LogEnabled: true, - HttpsEnabled: false, - HttpsCertificate: "/etc/ssl/influxdb.pem", + HTTPSEnabled: false, + HTTPSCertificate: "/etc/ssl/influxdb.pem", } } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/httpd/config_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/httpd/config_test.go index 9cf22ee8d..9a8d034cd 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/httpd/config_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/httpd/config_test.go @@ -36,10 +36,10 @@ https-certificate = "/dev/null" t.Fatalf("unexpected write tracing: %v", c.WriteTracing) } else if c.PprofEnabled != true { t.Fatalf("unexpected pprof enabled: %v", c.PprofEnabled) - } else if c.HttpsEnabled != true { - t.Fatalf("unexpected https enabled: %v", c.HttpsEnabled) - } else if c.HttpsCertificate != "/dev/null" { - t.Fatalf("unexpected https certificate: %v", c.HttpsCertificate) + } else if c.HTTPSEnabled != true { + t.Fatalf("unexpected https enabled: %v", c.HTTPSEnabled) + } else if c.HTTPSCertificate != "/dev/null" { + t.Fatalf("unexpected https certificate: %v", c.HTTPSCertificate) } } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/httpd/handler.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/httpd/handler.go index 1152271a3..0631e2535 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/httpd/handler.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/httpd/handler.go @@ -55,6 +55,7 @@ type Handler struct { Version string MetaStore interface { + WaitForLeader(timeout time.Duration) error Database(name string) (*meta.DatabaseInfo, error) Authenticate(username, password string) (ui *meta.UserInfo, err error) Users() ([]meta.UserInfo, error) @@ -461,7 +462,7 @@ func (h *Handler) serveWriteLine(w http.ResponseWriter, r *http.Request, body [] } // check that the byte is in the standard ascii code range - if body[i] > 32 { + if body[i] > 32 || i >= len(body)-1 { break } i += 1 @@ -473,13 +474,14 @@ func (h *Handler) serveWriteLine(w http.ResponseWriter, r *http.Request, body [] precision = "n" } - points, err := models.ParsePointsWithPrecision(body, time.Now().UTC(), precision) - if err != nil { - if err.Error() == "EOF" { + points, parseError := models.ParsePointsWithPrecision(body, time.Now().UTC(), precision) + // Not points parsed correctly so return the error now + if parseError != nil && len(points) == 0 { + if parseError.Error() == "EOF" { w.WriteHeader(http.StatusOK) return } - h.writeError(w, influxql.Result{Err: err}, http.StatusBadRequest) + h.writeError(w, influxql.Result{Err: parseError}, http.StatusBadRequest) return } @@ -534,6 +536,13 @@ func (h *Handler) serveWriteLine(w http.ResponseWriter, r *http.Request, body [] h.statMap.Add(statPointsWrittenFail, int64(len(points))) h.writeError(w, influxql.Result{Err: err}, http.StatusInternalServerError) return + } else if parseError != nil { + // We wrote some of the points + h.statMap.Add(statPointsWrittenOK, int64(len(points))) + // The other points failed to parse which means the client sent invalid line protocol. We return a 400 + // response code as well as the lines that failed to parse. + h.writeError(w, influxql.Result{Err: fmt.Errorf("partial write:\n%v", parseError)}, http.StatusBadRequest) + return } h.statMap.Add(statPointsWrittenOK, int64(len(points))) @@ -547,6 +556,21 @@ func (h *Handler) serveOptions(w http.ResponseWriter, r *http.Request) { // servePing returns a simple response to let the client know the server is running. func (h *Handler) servePing(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + wfl := q.Get("wait_for_leader") + + if wfl != "" { + d, err := time.ParseDuration(wfl) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + if err := h.MetaStore.WaitForLeader(d); err != nil { + w.WriteHeader(http.StatusServiceUnavailable) + return + } + } + h.statMap.Add(statPingRequest, 1) w.WriteHeader(http.StatusNoContent) } @@ -905,7 +929,11 @@ func NormalizeBatchPoints(bp client.BatchPoints) ([]models.Point, error) { return points, fmt.Errorf("missing fields") } // Need to convert from a client.Point to a influxdb.Point - points = append(points, models.NewPoint(p.Measurement, p.Tags, p.Fields, p.Time)) + pt, err := models.NewPoint(p.Measurement, p.Tags, p.Fields, p.Time) + if err != nil { + return points, err + } + points = append(points, pt) } return points, nil diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/httpd/handler_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/httpd/handler_test.go index 3e90087ac..ba9ec98da 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/httpd/handler_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/httpd/handler_test.go @@ -1,6 +1,7 @@ package httpd_test import ( + "bytes" "encoding/json" "errors" "fmt" @@ -284,6 +285,76 @@ func TestHandler_Query_ErrResult(t *testing.T) { } } +// Ensure the handler handles ping requests correctly. +func TestHandler_Ping(t *testing.T) { + h := NewHandler(false) + w := httptest.NewRecorder() + h.ServeHTTP(w, MustNewRequest("GET", "/ping", nil)) + if w.Code != http.StatusNoContent { + t.Fatalf("unexpected status: %d", w.Code) + } + h.ServeHTTP(w, MustNewRequest("HEAD", "/ping", nil)) + if w.Code != http.StatusNoContent { + t.Fatalf("unexpected status: %d", w.Code) + } +} + +// Ensure the handler handles ping requests correctly, when waiting for leader. +func TestHandler_PingWaitForLeader(t *testing.T) { + h := NewHandler(false) + w := httptest.NewRecorder() + h.ServeHTTP(w, MustNewRequest("GET", "/ping?wait_for_leader=1s", nil)) + if w.Code != http.StatusNoContent { + t.Fatalf("unexpected status: %d", w.Code) + } + h.ServeHTTP(w, MustNewRequest("HEAD", "/ping?wait_for_leader=1s", nil)) + if w.Code != http.StatusNoContent { + t.Fatalf("unexpected status: %d", w.Code) + } +} + +// Ensure the handler handles ping requests correctly, when timeout expires waiting for leader. +func TestHandler_PingWaitForLeaderTimeout(t *testing.T) { + h := NewHandler(false) + h.MetaStore.WaitForLeaderFn = func(d time.Duration) error { + return fmt.Errorf("timeout") + } + w := httptest.NewRecorder() + h.ServeHTTP(w, MustNewRequest("GET", "/ping?wait_for_leader=1s", nil)) + if w.Code != http.StatusServiceUnavailable { + t.Fatalf("unexpected status: %d", w.Code) + } + h.ServeHTTP(w, MustNewRequest("HEAD", "/ping?wait_for_leader=1s", nil)) + if w.Code != http.StatusServiceUnavailable { + t.Fatalf("unexpected status: %d", w.Code) + } +} + +// Ensure the handler handles bad ping requests +func TestHandler_PingWaitForLeaderBadRequest(t *testing.T) { + h := NewHandler(false) + w := httptest.NewRecorder() + h.ServeHTTP(w, MustNewRequest("GET", "/ping?wait_for_leader=1xxx", nil)) + if w.Code != http.StatusBadRequest { + t.Fatalf("unexpected status: %d", w.Code) + } + h.ServeHTTP(w, MustNewRequest("HEAD", "/ping?wait_for_leader=abc", nil)) + if w.Code != http.StatusBadRequest { + t.Fatalf("unexpected status: %d", w.Code) + } +} + +// Ensure write endpoint can handle bad requests +func TestHandler_HandleBadRequestBody(t *testing.T) { + b := bytes.NewReader(make([]byte, 10)) + h := NewHandler(false) + w := httptest.NewRecorder() + h.ServeHTTP(w, MustNewRequest("POST", "/write", b)) + if w.Code != http.StatusBadRequest { + t.Fatalf("unexpected status: %d", w.Code) + } +} + func TestMarshalJSON_NoPretty(t *testing.T) { if b := httpd.MarshalJSON(struct { Name string `json:"name"` @@ -326,7 +397,7 @@ func TestNormalizeBatchPoints(t *testing.T) { }, }, p: []models.Point{ - models.NewPoint("cpu", map[string]string{"region": "useast"}, map[string]interface{}{"value": 1.0}, now), + models.MustNewPoint("cpu", map[string]string{"region": "useast"}, map[string]interface{}{"value": 1.0}, now), }, }, { @@ -338,7 +409,7 @@ func TestNormalizeBatchPoints(t *testing.T) { }, }, p: []models.Point{ - models.NewPoint("cpu", map[string]string{"region": "useast"}, map[string]interface{}{"value": 1.0}, now), + models.MustNewPoint("cpu", map[string]string{"region": "useast"}, map[string]interface{}{"value": 1.0}, now), }, }, { @@ -351,8 +422,8 @@ func TestNormalizeBatchPoints(t *testing.T) { }, }, p: []models.Point{ - models.NewPoint("cpu", map[string]string{"day": "monday", "region": "useast"}, map[string]interface{}{"value": 1.0}, now), - models.NewPoint("memory", map[string]string{"day": "monday"}, map[string]interface{}{"value": 2.0}, now), + models.MustNewPoint("cpu", map[string]string{"day": "monday", "region": "useast"}, map[string]interface{}{"value": 1.0}, now), + models.MustNewPoint("memory", map[string]string{"day": "monday"}, map[string]interface{}{"value": 2.0}, now), }, }, } @@ -397,9 +468,18 @@ func NewHandler(requireAuthentication bool) *Handler { // HandlerMetaStore is a mock implementation of Handler.MetaStore. type HandlerMetaStore struct { - DatabaseFn func(name string) (*meta.DatabaseInfo, error) - AuthenticateFn func(username, password string) (ui *meta.UserInfo, err error) - UsersFn func() ([]meta.UserInfo, error) + WaitForLeaderFn func(d time.Duration) error + DatabaseFn func(name string) (*meta.DatabaseInfo, error) + AuthenticateFn func(username, password string) (ui *meta.UserInfo, err error) + UsersFn func() ([]meta.UserInfo, error) +} + +func (s *HandlerMetaStore) WaitForLeader(d time.Duration) error { + if s.WaitForLeaderFn == nil { + // Default behaviour is to assume there is a leader. + return nil + } + return s.WaitForLeaderFn(d) } func (s *HandlerMetaStore) Database(name string) (*meta.DatabaseInfo, error) { diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/httpd/service.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/httpd/service.go index 6e0db0bfc..0c063af20 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/httpd/service.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/httpd/service.go @@ -15,16 +15,16 @@ import ( // statistics gathered by the httpd package. const ( - statRequest = "req" // Number of HTTP requests served - statCQRequest = "cq_req" // Number of CQ-execute requests served - statQueryRequest = "query_req" // Number of query requests served - statWriteRequest = "write_req" // Number of write requests serverd - statPingRequest = "ping_req" // Number of ping requests served - statWriteRequestBytesReceived = "write_req_bytes" // Sum of all bytes in write requests - statQueryRequestBytesTransmitted = "query_resp_bytes" // Sum of all bytes returned in query reponses - statPointsWrittenOK = "points_written_ok" // Number of points written OK - statPointsWrittenFail = "points_written_fail" // Number of points that failed to be written - statAuthFail = "auth_fail" // Number of authentication failures + statRequest = "req" // Number of HTTP requests served + statCQRequest = "cqReq" // Number of CQ-execute requests served + statQueryRequest = "queryReq" // Number of query requests served + statWriteRequest = "writeReq" // Number of write requests serverd + statPingRequest = "pingReq" // Number of ping requests served + statWriteRequestBytesReceived = "writeReqBytes" // Sum of all bytes in write requests + statQueryRequestBytesTransmitted = "queryRespBytes" // Sum of all bytes returned in query reponses + statPointsWrittenOK = "pointsWritteOk" // Number of points written OK + statPointsWrittenFail = "pointsWrittenFail" // Number of points that failed to be written + statAuthFail = "authFail" // Number of authentication failures ) // Service manages the listener and handler for an HTTP endpoint. @@ -51,8 +51,8 @@ func NewService(c Config) *Service { s := &Service{ addr: c.BindAddress, - https: c.HttpsEnabled, - cert: c.HttpsCertificate, + https: c.HTTPSEnabled, + cert: c.HTTPSCertificate, err: make(chan error), Handler: NewHandler( c.AuthEnabled, diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/opentsdb/handler.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/opentsdb/handler.go index 8457f28bb..d011c05c8 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/opentsdb/handler.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/opentsdb/handler.go @@ -109,7 +109,12 @@ func (h *Handler) servePut(w http.ResponseWriter, r *http.Request) { ts = time.Unix(p.Time/1000, (p.Time%1000)*1000) } - points = append(points, models.NewPoint(p.Metric, p.Tags, map[string]interface{}{"value": p.Value}, ts)) + pt, err := models.NewPoint(p.Metric, p.Tags, map[string]interface{}{"value": p.Value}, ts) + if err != nil { + h.Logger.Printf("Dropping point %v: %v", p.Metric, err) + continue + } + points = append(points, pt) } // Write points. diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/opentsdb/service.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/opentsdb/service.go index fa7f4fbd7..5de12341b 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/opentsdb/service.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/opentsdb/service.go @@ -27,21 +27,21 @@ const leaderWaitTimeout = 30 * time.Second // statistics gathered by the openTSDB package. const ( - statHTTPConnectionsHandled = "http_connections_handled" - statTelnetConnectionsActive = "tl_connections_active" - statTelnetConnectionsHandled = "tl_connections_handled" - statTelnetPointsReceived = "tl_points_rx" - statTelnetBytesReceived = "tl_bytes_rx" - statTelnetReadError = "tl_read_err" - statTelnetBadLine = "tl_bad_line" - statTelnetBadTime = "tl_bad_time" - statTelnetBadTag = "tl_bad_tag" - statTelnetBadFloat = "tl_bad_float" - statBatchesTrasmitted = "batches_tx" - statPointsTransmitted = "points_tx" - statBatchesTransmitFail = "batches_tx_fail" - statConnectionsActive = "connections_active" - statConnectionsHandled = "connections_handled" + statHTTPConnectionsHandled = "httpConnsHandled" + statTelnetConnectionsActive = "tlConnsActive" + statTelnetConnectionsHandled = "tlConnsHandled" + statTelnetPointsReceived = "tlPointsRx" + statTelnetBytesReceived = "tlBytesRx" + statTelnetReadError = "tlReadErr" + statTelnetBadLine = "tlBadLine" + statTelnetBadTime = "tlBadTime" + statTelnetBadTag = "tlBadTag" + statTelnetBadFloat = "tlBadFloat" + statBatchesTrasmitted = "batchesTx" + statPointsTransmitted = "pointsTx" + statBatchesTransmitFail = "batchesTxFail" + statConnectionsActive = "connsActive" + statConnectionsHandled = "connsHandled" ) // Service manages the listener and handler for an HTTP endpoint. @@ -327,14 +327,21 @@ func (s *Service) handleTelnetConn(conn net.Conn) { } fields := make(map[string]interface{}) - fields["value"], err = strconv.ParseFloat(valueStr, 64) + fv, err := strconv.ParseFloat(valueStr, 64) if err != nil { s.statMap.Add(statTelnetBadFloat, 1) s.Logger.Printf("bad float '%s' from %s", valueStr, remoteAddr) continue } + fields["value"] = fv - s.batcher.In() <- models.NewPoint(measurement, tags, fields, t) + pt, err := models.NewPoint(measurement, tags, fields, t) + if err != nil { + s.statMap.Add(statTelnetBadFloat, 1) + s.Logger.Printf("bad float '%s' from %s", valueStr, remoteAddr) + continue + } + s.batcher.In() <- pt } } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/opentsdb/service_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/opentsdb/service_test.go index 5b115b164..4d979486a 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/opentsdb/service_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/opentsdb/service_test.go @@ -38,7 +38,7 @@ func TestService_Telnet(t *testing.T) { } else if req.RetentionPolicy != "" { t.Fatalf("unexpected retention policy: %s", req.RetentionPolicy) } else if !reflect.DeepEqual(req.Points, []models.Point{ - models.NewPoint( + models.MustNewPoint( "sys.cpu.user", map[string]string{"host": "webserver01", "cpu": "0"}, map[string]interface{}{"value": 42.5}, @@ -92,7 +92,7 @@ func TestService_HTTP(t *testing.T) { } else if req.RetentionPolicy != "" { t.Fatalf("unexpected retention policy: %s", req.RetentionPolicy) } else if !reflect.DeepEqual(req.Points, []models.Point{ - models.NewPoint( + models.MustNewPoint( "sys.cpu.nice", map[string]string{"dc": "lga", "host": "web01"}, map[string]interface{}{"value": 18.0}, diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/registration/service.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/registration/service.go index 203b96899..ce19ac758 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/registration/service.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/registration/service.go @@ -1,17 +1,14 @@ package registration import ( - "bytes" - "encoding/json" "fmt" - "io/ioutil" "log" - "net/http" "net/url" "os" "sync" "time" + "github.com/influxdb/enterprise-client/v1" "github.com/influxdb/influxdb/monitor" ) @@ -103,6 +100,10 @@ func (s *Service) registerServer() error { if !s.enabled || s.token == "" { return nil } + + cl := client.New(s.token) + cl.URL = s.url.String() + clusterID, err := s.MetaStore.ClusterID() if err != nil { s.logger.Printf("failed to retrieve cluster ID for registration: %s", err.Error()) @@ -112,41 +113,26 @@ func (s *Service) registerServer() error { if err != nil { return err } - j := map[string]interface{}{ - "cluster_id": fmt.Sprintf("%d", clusterID), - "server_id": fmt.Sprintf("%d", s.MetaStore.NodeID()), - "host": hostname, - "product": "influxdb", - "version": s.version, + + server := client.Server{ + ClusterID: fmt.Sprintf("%d", clusterID), + ServerID: fmt.Sprintf("%d", s.MetaStore.NodeID()), + Host: hostname, + Product: "influxdb", + Version: s.version, } - b, err := json.Marshal(j) - if err != nil { - return err - } - url := fmt.Sprintf("%s/api/v1/servers?token=%s", s.url.String(), s.token) s.wg.Add(1) go func() { defer s.wg.Done() - client := http.Client{Timeout: time.Duration(5 * time.Second)} - resp, err := client.Post(url, "application/json", bytes.NewBuffer(b)) + resp, err := cl.Save(server) + if err != nil { - s.logger.Printf("failed to register server with %s: %s", s.url.String(), err.Error()) + s.logger.Printf("failed to register server with %s: received code %s, error: %s", s.url.String(), resp.Status, err) return } s.updateLastContact(time.Now().UTC()) - - defer resp.Body.Close() - if resp.StatusCode == http.StatusCreated { - return - } - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - s.logger.Printf("failed to read response from registration server: %s", err.Error()) - return - } - s.logger.Printf("failed to register server with %s: received code %s, body: %s", s.url.String(), resp.Status, string(body)) }() return nil } @@ -157,7 +143,9 @@ func (s *Service) reportStats() { // No reporting, for now, without token. return } - statsURL := fmt.Sprintf("%s/api/v1/stats/influxdb?token=%s", s.url.String(), s.token) + + cl := client.New(s.token) + cl.URL = s.url.String() clusterID, err := s.MetaStore.ClusterID() if err != nil { @@ -175,30 +163,28 @@ func (s *Service) reportStats() { continue } - o := map[string]interface{}{ - "cluster_id": fmt.Sprintf("%d", clusterID), - "server_id": fmt.Sprintf("%d", s.MetaStore.NodeID()), - "stats": stats, + st := client.Stats{ + Product: "influxdb", + ClusterID: fmt.Sprintf("%d", clusterID), + ServerID: fmt.Sprintf("%d", s.MetaStore.NodeID()), } - b, err := json.Marshal(o) - if err != nil { - s.logger.Printf("failed to JSON-encode stats: %s", err.Error()) - continue + data := make([]client.StatsData, len(stats)) + for i, x := range stats { + data[i] = client.StatsData{ + Name: x.Name, + Tags: x.Tags, + Values: x.Values, + } } + st.Data = data - client := http.Client{Timeout: time.Duration(5 * time.Second)} - resp, err := client.Post(statsURL, "application/json", bytes.NewBuffer(b)) + resp, err := cl.Save(st) if err != nil { - s.logger.Printf("failed to post statistics to %s: %s", statsURL, err.Error()) + s.logger.Printf("failed to post statistics to Enterprise: repsonse code: %d: error: %s", resp.StatusCode, err) continue } s.updateLastContact(time.Now().UTC()) - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - s.logger.Printf("failed to post statistics to %s: repsonse code: %d", statsURL, resp.StatusCode) - continue - } case <-s.done: return } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/retention/service.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/retention/service.go index a83721ebe..0ec03e69a 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/retention/service.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/retention/service.go @@ -106,22 +106,28 @@ func (s *Service) deleteShards() { case <-ticker.C: s.logger.Println("retention policy shard deletion check commencing") - deletedShardIDs := make(map[uint64]struct{}, 0) + type deletionInfo struct { + db string + rp string + } + deletedShardIDs := make(map[uint64]deletionInfo, 0) s.MetaStore.VisitRetentionPolicies(func(d meta.DatabaseInfo, r meta.RetentionPolicyInfo) { for _, g := range r.DeletedShardGroups() { for _, sh := range g.Shards { - deletedShardIDs[sh.ID] = struct{}{} + deletedShardIDs[sh.ID] = deletionInfo{db: d.Name, rp: r.Name} } } }) for _, id := range s.TSDBStore.ShardIDs() { - if _, ok := deletedShardIDs[id]; ok { + if di, ok := deletedShardIDs[id]; ok { if err := s.TSDBStore.DeleteShard(id); err != nil { - s.logger.Printf("failed to delete shard ID %d: %s", id, err.Error()) + s.logger.Printf("failed to delete shard ID %d from database %s, retention policy %s: %s", + id, di.db, di.rp, err.Error()) continue } - s.logger.Printf("shard ID %d deleted", id) + s.logger.Printf("shard ID %d from database %s, retention policy %s, deleted", + id, di.db, di.rp) } } } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/subscriber/service.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/subscriber/service.go index 01e6488bc..961defc7e 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/subscriber/service.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/subscriber/service.go @@ -16,8 +16,8 @@ import ( // Statistics for the Subscriber service. const ( - statPointsWritten = "points_written" - statWriteFailures = "write_failures" + statPointsWritten = "pointsWritten" + statWriteFailures = "writeFailures" ) type PointsWriter interface { @@ -56,6 +56,7 @@ func NewService(c Config) *Service { Logger: log.New(os.Stderr, "[subscriber] ", log.LstdFlags), statMap: influxdb.NewStatistics("subscriber", "subscriber", nil), points: make(chan *cluster.WritePointsRequest), + closed: true, } } @@ -91,6 +92,11 @@ func (s *Service) Close() error { return nil } +// SetLogger sets the internal logger to the logger passed in. +func (s *Service) SetLogger(l *log.Logger) { + s.Logger = l +} + func (s *Service) waitForMetaUpdates() { for { err := s.MetaStore.WaitForDataChanged() @@ -100,9 +106,10 @@ func (s *Service) waitForMetaUpdates() { } else { //Check that we haven't been closed before performing update. s.mu.Lock() - if !s.closed { + if s.closed { s.mu.Unlock() - break + s.Logger.Println("service closed not updating") + return } s.mu.Unlock() s.Update() @@ -113,7 +120,6 @@ func (s *Service) waitForMetaUpdates() { // start new and stop deleted subscriptions. func (s *Service) Update() error { - s.Logger.Println("updating subscriptions") dbis, err := s.MetaStore.Databases() if err != nil { return err @@ -145,6 +151,7 @@ func (s *Service) Update() error { for se := range s.subs { if !allEntries[se] { delete(s.subs, se) + s.Logger.Println("deleted old subscription for", se.db, se.rp) } } @@ -183,6 +190,7 @@ func (s *Service) createSubscription(se subEntry, mode string, destinations []st key := strings.Join([]string{"subscriber", se.db, se.rp, se.name, dest}, ":") statMaps[i] = influxdb.NewStatistics(key, "subscriber", tags) } + s.Logger.Println("created new subscription for", se.db, se.rp) return &balancewriter{ bm: bm, writers: writers, diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/subscriber/service_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/subscriber/service_test.go index 9a4109b52..6b3ee2904 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/subscriber/service_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/subscriber/service_test.go @@ -387,3 +387,62 @@ func TestService_Multiple(t *testing.T) { } close(dataChanged) } + +func TestService_WaitForDataChanged(t *testing.T) { + dataChanged := make(chan bool) + ms := MetaStore{} + ms.WaitForDataChangedFn = func() error { + <-dataChanged + return nil + } + calls := make(chan bool, 2) + ms.DatabasesFn = func() ([]meta.DatabaseInfo, error) { + calls <- true + return nil, nil + } + + s := subscriber.NewService(subscriber.NewConfig()) + s.MetaStore = ms + // Explicitly closed below for testing + s.Open() + + // Should be called once during open + select { + case <-calls: + case <-time.After(10 * time.Millisecond): + t.Fatal("expected call") + } + + select { + case <-calls: + t.Fatal("unexpected call") + case <-time.After(time.Millisecond): + } + + // Signal that data has changed + dataChanged <- true + + // Should be called once more after data changed + select { + case <-calls: + case <-time.After(10 * time.Millisecond): + t.Fatal("expected call") + } + + select { + case <-calls: + t.Fatal("unexpected call") + case <-time.After(time.Millisecond): + } + + //Close service ensure not called + s.Close() + dataChanged <- true + select { + case <-calls: + t.Fatal("unexpected call") + case <-time.After(time.Millisecond): + } + + close(dataChanged) +} diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/udp/README.md b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/udp/README.md index e9faf0cf7..73df83fb5 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/udp/README.md +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/udp/README.md @@ -1,13 +1,125 @@ -# Configuration +# The UDP Input + +## A note on UDP/IP OS Buffer sizes + +Some OSes (most notably, Linux) place very restricive limits on the performance +of UDP protocols. It is _highly_ recommended that you increase these OS limits to +at least 8MB before trying to run large amounts of UDP traffic to your instance. +8MB is just a recommendation, and should be adjusted to be inline with your +`read-buffer` plugin setting. + +### Linux +Check the current UDP/IP receive buffer limit by typing the following commands: + +``` +sysctl net.core.rmem_max +``` + +If the values are less than 8388608 bytes you should add the following lines to the /etc/sysctl.conf file: + +``` +net.core.rmem_max=8388608 +``` + +Changes to /etc/sysctl.conf do not take effect until reboot. To update the values immediately, type the following commands as root: + +``` +sysctl -w net.core.rmem_max=8388608 +``` + +### BSD/Darwin + +On BSD/Darwin systems you need to add about a 15% padding to the kernel limit +socket buffer. Meaning if you want an 8MB buffer (8388608 bytes) you need to set +the kernel limit to `8388608*1.15 = 9646900`. This is not documented anywhere but +happens +[in the kernel here.](https://github.com/freebsd/freebsd/blob/master/sys/kern/uipc_sockbuf.c#L63-L64) + +Check the current UDP/IP buffer limit by typing the following command: + +``` +sysctl kern.ipc.maxsockbuf +``` + +If the value is less than 9646900 bytes you should add the following lines to the /etc/sysctl.conf file (create it if necessary): + +``` +kern.ipc.maxsockbuf=9646900 +``` + +Changes to /etc/sysctl.conf do not take effect until reboot. To update the values immediately, type the following commands as root: + +``` +sysctl -w kern.ipc.maxsockbuf=9646900 +``` + +### Using the read-buffer option for the UDP listener + +The `read-buffer` option allows users to set the buffer size for the UDP listener. +It Sets the size of the operating system's receive buffer associated with +the UDP traffic. Keep in mind that the OS must be able +to handle the number set here or the UDP listener will error and exit. + +`read-buffer = 0` means to use the OS default, which is usually too +small for high UDP performance. + +## Configuration Each UDP input allows the binding address, target database, and target retention policy to be set. If the database does not exist, it will be created automatically when the input is initialized. If the retention policy is not configured, then the default retention policy for the database is used. However if the retention policy is set, the retention policy must be explicitly created. The input will not automatically create it. Each UDP input also performs internal batching of the points it receives, as batched writes to the database are more efficient. The default _batch size_ is 1000, _pending batch_ factor is 5, with a _batch timeout_ of 1 second. This means the input will write batches of maximum size 1000, but if a batch has not reached 1000 points within 1 second of the first point being added to a batch, it will emit that batch regardless of size. The pending batch factor controls how many batches can be in memory at once, allowing the input to transmit a batch, while still building other batches. -# Processing +## Processing The UDP input can receive up to 64KB per read, and splits the received data by newline. Each part is then interpreted as line-protocol encoded points, and parsed accordingly. -# UDP is connectionless +## UDP is connectionless Since UDP is a connectionless protocol there is no way to signal to the data source if any error occurs, and if data has even been successfully indexed. This should be kept in mind when deciding if and when to use the UDP input. The built-in UDP statistics are useful for monitoring the UDP inputs. + +## Config Examples + +One UDP listener + +``` +# influxd.conf +... +[[udp]] + enabled = true + bind-address = ":8089" # the bind address + database = "telegraf" # Name of the database that will be written to + batch-size = 5000 # will flush if this many points get buffered + batch-timeout = "1s" # will flush at least this often even if the batch-size is not reached + batch-pending = 10 # number of batches that may be pending in memory + read-buffer = 0 # UDP read buffer, 0 means to use OS default +... +``` + +Multiple UDP listeners + +``` +# influxd.conf +... +[[udp]] + # Default UDP for Telegraf + enabled = true + bind-address = ":8089" # the bind address + database = "telegraf" # Name of the database that will be written to + batch-size = 5000 # will flush if this many points get buffered + batch-timeout = "1s" # will flush at least this often even if the batch-size is not reached + batch-pending = 10 # number of batches that may be pending in memory + read-buffer = 0 # UDP read buffer size, 0 means to use OS default + +[[udp]] + # High-traffic UDP + enabled = true + bind-address = ":80891" # the bind address + database = "mymetrics" # Name of the database that will be written to + batch-size = 5000 # will flush if this many points get buffered + batch-timeout = "1s" # will flush at least this often even if the batch-size is not reached + batch-pending = 100 # number of batches that may be pending in memory + read-buffer = 8388608 # (8*1024*1024) UDP read buffer size +... +``` + + diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/udp/config.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/udp/config.go index aea15bc62..e552a514e 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/udp/config.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/udp/config.go @@ -7,17 +7,36 @@ import ( ) const ( + // DefaultBindAddress is the default binding interface if none is specified. + DefaultBindAddress = ":8089" + // DefaultDatabase is the default database for UDP traffic. DefaultDatabase = "udp" + // DefaultRetentionPolicy is the default retention policy used for writes. + DefaultRetentionPolicy = "" + // DefaultBatchSize is the default UDP batch size. - DefaultBatchSize = 1000 + DefaultBatchSize = 5000 // DefaultBatchPending is the default number of pending UDP batches. - DefaultBatchPending = 5 + DefaultBatchPending = 10 // DefaultBatchTimeout is the default UDP batch timeout. DefaultBatchTimeout = time.Second + + // DefaultReadBuffer is the default buffer size for the UDP listener. + // Sets the size of the operating system's receive buffer associated with + // the UDP traffic. Keep in mind that the OS must be able + // to handle the number set here or the UDP listener will error and exit. + // + // DefaultReadBuffer = 0 means to use the OS default, which is usually too + // small for high UDP performance. + // + // Increasing OS buffer limits: + // Linux: sudo sysctl -w net.core.rmem_max= + // BSD/Darwin: sudo sysctl -w kern.ipc.maxsockbuf= + DefaultReadBuffer = 0 ) type Config struct { @@ -28,9 +47,21 @@ type Config struct { RetentionPolicy string `toml:"retention-policy"` BatchSize int `toml:"batch-size"` BatchPending int `toml:"batch-pending"` + ReadBuffer int `toml:"read-buffer"` BatchTimeout toml.Duration `toml:"batch-timeout"` } +func NewConfig() Config { + return Config{ + BindAddress: DefaultBindAddress, + Database: DefaultDatabase, + RetentionPolicy: DefaultRetentionPolicy, + BatchSize: DefaultBatchSize, + BatchPending: DefaultBatchPending, + BatchTimeout: toml.Duration(DefaultBatchTimeout), + } +} + // WithDefaults takes the given config and returns a new config with any required // default values set. func (c *Config) WithDefaults() *Config { @@ -47,5 +78,8 @@ func (c *Config) WithDefaults() *Config { if d.BatchTimeout == 0 { d.BatchTimeout = toml.Duration(DefaultBatchTimeout) } + if d.ReadBuffer == 0 { + d.ReadBuffer = DefaultReadBuffer + } return &d } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/udp/service.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/udp/service.go index 02b9df494..d28738810 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/services/udp/service.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/services/udp/service.go @@ -18,18 +18,23 @@ import ( ) const ( + // Maximum UDP packet size + // see https://en.wikipedia.org/wiki/User_Datagram_Protocol#Packet_structure UDPBufferSize = 65536 + + // Arbitrary, testing indicated that this doesn't typically get over 10 + parserChanLen = 1000 ) // statistics gathered by the UDP package. const ( - statPointsReceived = "points_rx" - statBytesReceived = "bytes_rx" - statPointsParseFail = "points_parse_fail" - statReadFail = "read_fail" - statBatchesTrasmitted = "batches_tx" - statPointsTransmitted = "points_tx" - statBatchesTransmitFail = "batches_tx_fail" + statPointsReceived = "pointsRx" + statBytesReceived = "bytesRx" + statPointsParseFail = "pointsParseFail" + statReadFail = "readFail" + statBatchesTrasmitted = "batchesTx" + statPointsTransmitted = "pointsTx" + statBatchesTransmitFail = "batchesTxFail" ) // @@ -43,8 +48,9 @@ type Service struct { wg sync.WaitGroup done chan struct{} - batcher *tsdb.PointBatcher - config Config + parserChan chan []byte + batcher *tsdb.PointBatcher + config Config PointsWriter interface { WritePoints(p *cluster.WritePointsRequest) error @@ -61,10 +67,11 @@ type Service struct { func NewService(c Config) *Service { d := *c.WithDefaults() return &Service{ - config: d, - done: make(chan struct{}), - batcher: tsdb.NewPointBatcher(d.BatchSize, d.BatchPending, time.Duration(d.BatchTimeout)), - Logger: log.New(os.Stderr, "[udp] ", log.LstdFlags), + config: d, + done: make(chan struct{}), + parserChan: make(chan []byte, parserChanLen), + batcher: tsdb.NewPointBatcher(d.BatchSize, d.BatchPending, time.Duration(d.BatchTimeout)), + Logger: log.New(os.Stderr, "[udp] ", log.LstdFlags), } } @@ -98,16 +105,26 @@ func (s *Service) Open() (err error) { return err } + if s.config.ReadBuffer != 0 { + err = s.conn.SetReadBuffer(s.config.ReadBuffer) + if err != nil { + s.Logger.Printf("Failed to set UDP read buffer to %d: %s", + s.config.ReadBuffer, err) + return err + } + } + s.Logger.Printf("Started listening on UDP: %s", s.config.BindAddress) - s.wg.Add(2) + s.wg.Add(3) go s.serve() - go s.writePoints() + go s.parser() + go s.writer() return nil } -func (s *Service) writePoints() { +func (s *Service) writer() { defer s.wg.Done() for { @@ -137,7 +154,6 @@ func (s *Service) serve() { s.batcher.Start() for { - buf := make([]byte, UDPBufferSize) select { case <-s.done: @@ -145,27 +161,39 @@ func (s *Service) serve() { return default: // Keep processing. + buf := make([]byte, UDPBufferSize) + n, _, err := s.conn.ReadFromUDP(buf) + if err != nil { + s.statMap.Add(statReadFail, 1) + s.Logger.Printf("Failed to read UDP message: %s", err) + continue + } + s.statMap.Add(statBytesReceived, int64(n)) + s.parserChan <- buf[:n] } + } +} - n, _, err := s.conn.ReadFromUDP(buf) - if err != nil { - s.statMap.Add(statReadFail, 1) - s.Logger.Printf("Failed to read UDP message: %s", err) - continue - } - s.statMap.Add(statBytesReceived, int64(n)) +func (s *Service) parser() { + defer s.wg.Done() - points, err := models.ParsePoints(buf[:n]) - if err != nil { - s.statMap.Add(statPointsParseFail, 1) - s.Logger.Printf("Failed to parse points: %s", err) - continue - } + for { + select { + case <-s.done: + return + case buf := <-s.parserChan: + points, err := models.ParsePoints(buf) + if err != nil { + s.statMap.Add(statPointsParseFail, 1) + s.Logger.Printf("Failed to parse points: %s", err) + return + } - for _, point := range points { - s.batcher.In() <- point + for _, point := range points { + s.batcher.In() <- point + } + s.statMap.Add(statPointsReceived, int64(len(points))) } - s.statMap.Add(statPointsReceived, int64(len(points))) } } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/stress/runner.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/stress/runner.go index bda0f2d00..1c758a065 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/stress/runner.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/stress/runner.go @@ -275,7 +275,6 @@ func Run(cfg *Config, done chan struct{}, ts chan time.Time) (totalPoints int, f fmt.Println("ERROR: ", err.Error()) } failedRequests += 1 - //totalPoints -= len(b.Points) totalPoints -= cfg.Write.BatchSize lastSuccess = false mu.Unlock() diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tcp/mux.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tcp/mux.go index 5da8d0cce..98ce99176 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tcp/mux.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tcp/mux.go @@ -35,7 +35,7 @@ func NewMux() *Mux { return &Mux{ m: make(map[byte]*listener), Timeout: DefaultTimeout, - Logger: log.New(os.Stderr, "", log.LstdFlags), + Logger: log.New(os.Stderr, "[tcp] ", log.LstdFlags), } } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/toml/toml_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/toml/toml_test.go index 640abbcbc..dcf56b1ce 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/toml/toml_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/toml/toml_test.go @@ -1,14 +1,19 @@ package toml_test import ( + "bytes" + "strings" "testing" + "time" - "github.com/influxdb/influxdb/toml" + "github.com/BurntSushi/toml" + "github.com/influxdb/influxdb/cmd/influxd/run" + itoml "github.com/influxdb/influxdb/toml" ) // Ensure that megabyte sizes can be parsed. func TestSize_UnmarshalText_MB(t *testing.T) { - var s toml.Size + var s itoml.Size if err := s.UnmarshalText([]byte("200m")); err != nil { t.Fatalf("unexpected error: %s", err) } else if s != 200*(1<<20) { @@ -18,7 +23,7 @@ func TestSize_UnmarshalText_MB(t *testing.T) { // Ensure that gigabyte sizes can be parsed. func TestSize_UnmarshalText_GB(t *testing.T) { - var s toml.Size + var s itoml.Size if err := s.UnmarshalText([]byte("1g")); err != nil { t.Fatalf("unexpected error: %s", err) } else if s != 1073741824 { @@ -26,17 +31,15 @@ func TestSize_UnmarshalText_GB(t *testing.T) { } } -/* func TestConfig_Encode(t *testing.T) { - var c influxdb.Config - c.Monitoring.WriteInterval = influxdb.Duration(time.Minute) + var c run.Config + c.Cluster.WriteTimeout = itoml.Duration(time.Minute) buf := new(bytes.Buffer) if err := toml.NewEncoder(buf).Encode(&c); err != nil { t.Fatal("Failed to encode: ", err) } - got, search := buf.String(), `write-interval = "1m0s"` + got, search := buf.String(), `write-timeout = "1m0s"` if !strings.Contains(got, search) { t.Fatalf("Encoding config failed.\nfailed to find %s in:\n%s\n", search, got) } } -*/ diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/aggregate.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/aggregate.go new file mode 100644 index 000000000..2f58685b4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/aggregate.go @@ -0,0 +1,892 @@ +package tsdb + +import ( + "errors" + "fmt" + "sort" + "strings" + "time" + + "github.com/influxdb/influxdb/influxql" + "github.com/influxdb/influxdb/models" + "github.com/influxdb/influxdb/pkg/slices" +) + +// AggregateExecutor represents a mapper for execute aggregate SELECT statements. +type AggregateExecutor struct { + stmt *influxql.SelectStatement + mappers []*StatefulMapper +} + +// NewAggregateExecutor returns a new AggregateExecutor. +func NewAggregateExecutor(stmt *influxql.SelectStatement, mappers []Mapper) *AggregateExecutor { + e := &AggregateExecutor{ + stmt: stmt, + mappers: make([]*StatefulMapper, 0, len(mappers)), + } + + for _, m := range mappers { + e.mappers = append(e.mappers, &StatefulMapper{m, nil, false}) + } + + return e +} + +// close closes the executor such that all resources are released. +// Once closed, an executor may not be re-used. +func (e *AggregateExecutor) close() { + if e != nil { + for _, m := range e.mappers { + m.Close() + } + } +} + +// Execute begins execution of the query and returns a channel to receive rows. +func (e *AggregateExecutor) Execute() <-chan *models.Row { + out := make(chan *models.Row, 0) + go e.execute(out) + return out +} + +func (e *AggregateExecutor) execute(out chan *models.Row) { + // It's important to close all resources when execution completes. + defer e.close() + + // Create the functions which will reduce values from mappers for + // a given interval. The function offsets within this slice match + // the offsets within the value slices that are returned by the + // mapper. + reduceFuncs, err := e.initReduceFuncs() + if err != nil { + out <- &models.Row{Err: err} + return + } + + // Put together the rows to return, starting with columns. + columnNames := e.stmt.ColumnNames() + + // Open the mappers. + if err := e.openMappers(); err != nil { + out <- &models.Row{Err: err} + return + } + + // Filter out empty sets if there are multiple tag sets. + hasMultipleTagSets := e.hasMultipleTagSets() + ascending := e.ascending() + + // Prime each mapper's chunk buffer. + if err := e.initMappers(); err != nil { + out <- &models.Row{Err: err} + return + } + + // Keep looping until all mappers drained. + for !e.mappersDrained() { + chunks, err := e.readNextTagset() + if err != nil { + out <- &models.Row{Err: err} + return + } + + // Prep a row, ready for kicking out. + row := &models.Row{ + Name: chunks[0].Name, + Tags: chunks[0].Tags, + Columns: columnNames, + } + + // Prep for bucketing data by start time of the interval. + buckets := map[int64][][]interface{}{} + + var chunkValues []*MapperValue + for _, chunk := range chunks { + for _, chunkValue := range chunk.Values { + chunkValues = append(chunkValues, chunkValue) + } + } + sort.Sort(MapperValues(chunkValues)) + + for _, chunkValue := range chunkValues { + startTime := chunkValue.Time + values := chunkValue.Value.([]interface{}) + + if _, ok := buckets[startTime]; !ok { + buckets[startTime] = make([][]interface{}, len(values)) + } + for i, v := range values { + buckets[startTime][i] = append(buckets[startTime][i], v) + } + } + + // Now, after the loop above, within each time bucket is a slice. Within the element of each + // slice is another slice of interface{}, ready for passing to the reducer functions. + + // Work each bucket of time, in time ascending order. + tMins := make(int64Slice, 0, len(buckets)) + for k, _ := range buckets { + tMins = append(tMins, k) + } + + if ascending { + sort.Sort(tMins) + } else { + sort.Sort(sort.Reverse(tMins)) + } + + values := make([][]interface{}, len(tMins)) + for i, t := range tMins { + values[i] = make([]interface{}, 0, len(columnNames)) + values[i] = append(values[i], time.Unix(0, t).UTC()) // Time value is always first. + + for j, f := range reduceFuncs { + reducedVal := f(buckets[t][j]) + values[i] = append(values[i], reducedVal) + } + } + + // Perform aggregate unwraps + values, err = e.processFunctions(values, columnNames) + if err != nil { + out <- &models.Row{Err: err} + } + + // Perform any mathematics. + values = processForMath(e.stmt.Fields, values) + + // Handle any fill options + values = e.processFill(values) + + // process derivatives + values = e.processDerivative(values) + + // If we have multiple tag sets we'll want to filter out the empty ones + if hasMultipleTagSets && resultsEmpty(values) { + continue + } + + row.Values = values + out <- row + } + + close(out) +} + +// initReduceFuncs returns a list of reduce functions for the aggregates in the query. +func (e *AggregateExecutor) initReduceFuncs() ([]reduceFunc, error) { + calls := e.stmt.FunctionCalls() + fns := make([]reduceFunc, len(calls)) + for i, c := range calls { + fn, err := initializeReduceFunc(c) + if err != nil { + return nil, err + } + fns[i] = fn + } + return fns, nil +} + +// openMappers opens all the mappers. +func (e *AggregateExecutor) openMappers() error { + for _, m := range e.mappers { + if err := m.Open(); err != nil { + return err + } + } + return nil +} + +// initMappers buffers the first chunk of each mapper. +func (e *AggregateExecutor) initMappers() error { + for _, m := range e.mappers { + chunk, err := m.NextChunk() + if err != nil { + return err + } + m.bufferedChunk = chunk + + if m.bufferedChunk == nil { + m.drained = true + } + } + return nil +} + +// hasMultipleTagSets returns true if there is more than one tagset in the mappers. +func (e *AggregateExecutor) hasMultipleTagSets() bool { + set := make(map[string]struct{}) + for _, m := range e.mappers { + for _, t := range m.TagSets() { + set[t] = struct{}{} + if len(set) > 1 { + return true + } + } + } + return false +} + +// ascending returns true if statement is sorted in ascending order. +func (e *AggregateExecutor) ascending() bool { + if len(e.stmt.SortFields) == 0 { + return true + } + return e.stmt.SortFields[0].Ascending +} + +// mappersDrained returns whether all the executors Mappers have been drained of data. +func (e *AggregateExecutor) mappersDrained() bool { + for _, m := range e.mappers { + if !m.drained { + return false + } + } + return true +} + +// nextMapperTagset returns the alphabetically lowest tagset across all Mappers. +func (e *AggregateExecutor) nextMapperTagSet() string { + tagset := "" + for _, m := range e.mappers { + if m.bufferedChunk != nil { + if tagset == "" { + tagset = m.bufferedChunk.key() + } else if m.bufferedChunk.key() < tagset { + tagset = m.bufferedChunk.key() + } + } + } + return tagset +} + +// readNextTagset returns all chunks for the next tagset. +func (e *AggregateExecutor) readNextTagset() ([]*MapperOutput, error) { + // Send out data for the next alphabetically-lowest tagset. + // All Mappers send out in this order so collect data for this tagset, ignoring all others. + tagset := e.nextMapperTagSet() + chunks := []*MapperOutput{} + + // Pull as much as possible from each mapper. Stop when a mapper offers + // data for a new tagset, or empties completely. + for _, m := range e.mappers { + if m.drained { + continue + } + + for { + if m.bufferedChunk == nil { + chunk, err := m.NextChunk() + if err != nil { + return nil, err + } + m.bufferedChunk = chunk + + if m.bufferedChunk == nil { + m.drained = true + break + } + } + + // Got a chunk. Can we use it? + if m.bufferedChunk.key() != tagset { + break // No, so just leave it in the buffer. + } + + // We can, take it. + chunks = append(chunks, m.bufferedChunk) + m.bufferedChunk = nil + } + } + + return chunks, nil +} + +// processFill will take the results and return new results (or the same if no fill modifications are needed) +// with whatever fill options the query has. +func (e *AggregateExecutor) processFill(results [][]interface{}) [][]interface{} { + // don't do anything if we're supposed to leave the nulls + if e.stmt.Fill == influxql.NullFill { + return results + } + + if e.stmt.Fill == influxql.NoFill { + // remove any rows that have even one nil value. This one is tricky because they could have multiple + // aggregates, but this option means that any row that has even one nil gets purged. + newResults := make([][]interface{}, 0, len(results)) + for _, vals := range results { + hasNil := false + // start at 1 because the first value is always time + for j := 1; j < len(vals); j++ { + if vals[j] == nil { + hasNil = true + break + } + } + if !hasNil { + newResults = append(newResults, vals) + } + } + return newResults + } + + // They're either filling with previous values or a specific number + for i, vals := range results { + // start at 1 because the first value is always time + for j := 1; j < len(vals); j++ { + if vals[j] == nil { + switch e.stmt.Fill { + case influxql.PreviousFill: + if i != 0 { + vals[j] = results[i-1][j] + } + case influxql.NumberFill: + vals[j] = e.stmt.FillValue + } + } + } + } + return results +} + +// processDerivative returns the derivatives of the results +func (e *AggregateExecutor) processDerivative(results [][]interface{}) [][]interface{} { + // Return early if we're not supposed to process the derivatives + if e.stmt.HasDerivative() { + interval, err := derivativeInterval(e.stmt) + if err != nil { + return results // XXX need to handle this better. + } + + // Determines whether to drop negative differences + isNonNegative := e.stmt.FunctionCalls()[0].Name == "non_negative_derivative" + return ProcessAggregateDerivative(results, isNonNegative, interval) + } + return results +} + +func (e *AggregateExecutor) processFunctions(results [][]interface{}, columnNames []string) ([][]interface{}, error) { + callInPosition := e.stmt.FunctionCallsByPosition() + hasTimeField := e.stmt.HasTimeFieldSpecified() + + var err error + for i, calls := range callInPosition { + // We can only support expanding fields if a single selector call was specified + // i.e. select tx, max(rx) from foo + // If you have multiple selectors or aggregates, there is no way of knowing who gets to insert the values, so we don't + // i.e. select tx, max(rx), min(rx) from foo + if len(calls) == 1 { + var c *influxql.Call + c = calls[0] + + switch c.Name { + case "top", "bottom": + results, err = e.processAggregates(results, columnNames, c) + if err != nil { + return results, err + } + case "first", "last", "min", "max": + results, err = e.processSelectors(results, i, hasTimeField, columnNames) + if err != nil { + return results, err + } + } + } + } + + return results, nil +} + +func (e *AggregateExecutor) processSelectors(results [][]interface{}, callPosition int, hasTimeField bool, columnNames []string) ([][]interface{}, error) { + // if the columns doesn't have enough columns, expand it + for i, columns := range results { + if len(columns) != len(columnNames) { + columns = append(columns, make([]interface{}, len(columnNames)-len(columns))...) + } + for j := 1; j < len(columns); j++ { + switch v := columns[j].(type) { + case PositionPoint: + tMin := columns[0].(time.Time) + results[i] = e.selectorPointToQueryResult(columns, hasTimeField, callPosition, v, tMin, columnNames) + } + } + } + return results, nil +} + +func (e *AggregateExecutor) selectorPointToQueryResult(columns []interface{}, hasTimeField bool, columnIndex int, p PositionPoint, tMin time.Time, columnNames []string) []interface{} { + callCount := len(e.stmt.FunctionCalls()) + if callCount == 1 { + tm := time.Unix(0, p.Time).UTC() + // If we didn't explicity ask for time, and we have a group by, then use TMIN for the time returned + if len(e.stmt.Dimensions) > 0 && !hasTimeField { + tm = tMin.UTC() + } + columns[0] = tm + } + + for i, c := range columnNames { + // skip over time, we already handled that above + if i == 0 { + continue + } + if (i == columnIndex && hasTimeField) || (i == columnIndex+1 && !hasTimeField) { + // Check to see if we previously processed this column, if so, continue + if _, ok := columns[i].(PositionPoint); !ok && columns[i] != nil { + continue + } + columns[i] = p.Value + continue + } + + if callCount == 1 { + // Always favor fields over tags if there is a name collision + if t, ok := p.Fields[c]; ok { + columns[i] = t + } else if t, ok := p.Tags[c]; ok { + // look in the tags for a value + columns[i] = t + } + } + } + return columns +} + +func (e *AggregateExecutor) processAggregates(results [][]interface{}, columnNames []string, call *influxql.Call) ([][]interface{}, error) { + var values [][]interface{} + + // Check if we have a group by, if not, rewrite the entire result by flattening it out + for _, vals := range results { + // start at 1 because the first value is always time + for j := 1; j < len(vals); j++ { + switch v := vals[j].(type) { + case PositionPoints: + tMin := vals[0].(time.Time) + for _, p := range v { + result := e.aggregatePointToQueryResult(p, tMin, call, columnNames) + values = append(values, result) + } + case nil: + continue + default: + return nil, fmt.Errorf("unrechable code - processAggregates for type %T %v", v, v) + } + } + } + return values, nil +} + +func (e *AggregateExecutor) aggregatePointToQueryResult(p PositionPoint, tMin time.Time, call *influxql.Call, columnNames []string) []interface{} { + tm := time.Unix(0, p.Time).UTC() + // If we didn't explicity ask for time, and we have a group by, then use TMIN for the time returned + if len(e.stmt.Dimensions) > 0 && !e.stmt.HasTimeFieldSpecified() { + tm = tMin.UTC() + } + vals := []interface{}{tm} + for _, c := range columnNames { + if c == call.Name { + vals = append(vals, p.Value) + continue + } + // TODO in the future fields will also be available to us. + // we should always favor fields over tags if there is a name collision + + // look in the tags for a value + if t, ok := p.Tags[c]; ok { + vals = append(vals, t) + } + } + return vals +} + +// AggregateMapper runs the map phase for aggregate SELECT queries. +type AggregateMapper struct { + shard *Shard + stmt *influxql.SelectStatement + qmin, qmax int64 // query time range + + tx Tx + cursors []CursorSet + cursorIndex int + + interval int // Current interval for which data is being fetched. + intervalN int // Maximum number of intervals to return. + intervalSize int64 // Size of each interval. + qminWindow int64 // Minimum time of the query floored to start of interval. + + mapFuncs []mapFunc // The mapping functions. + fieldNames []string // the field name being read for mapping. + + selectFields []string + selectTags []string + whereFields []string +} + +// NewAggregateMapper returns a new instance of AggregateMapper. +func NewAggregateMapper(sh *Shard, stmt *influxql.SelectStatement) *AggregateMapper { + return &AggregateMapper{ + shard: sh, + stmt: stmt, + } +} + +// Open opens and initializes the mapper. +func (m *AggregateMapper) Open() error { + // Ignore if node has the shard but hasn't written to it yet. + if m.shard == nil { + return nil + } + + // Rewrite statement. + stmt, err := m.shard.index.RewriteSelectStatement(m.stmt) + if err != nil { + return err + } + m.stmt = stmt + + // Set all time-related parameters on the mapper. + m.qmin, m.qmax = influxql.TimeRangeAsEpochNano(m.stmt.Condition) + + if err := m.initializeMapFunctions(); err != nil { + return err + } + + // For GROUP BY time queries, limit the number of data points returned by the limit and offset + d, err := m.stmt.GroupByInterval() + if err != nil { + return err + } + + m.intervalSize = d.Nanoseconds() + if m.qmin == 0 || m.intervalSize == 0 { + m.intervalN = 1 + m.intervalSize = m.qmax - m.qmin + } else { + intervalTop := m.qmax/m.intervalSize*m.intervalSize + m.intervalSize + intervalBottom := m.qmin / m.intervalSize * m.intervalSize + m.intervalN = int((intervalTop - intervalBottom) / m.intervalSize) + } + + if m.stmt.Limit > 0 || m.stmt.Offset > 0 { + // ensure that the offset isn't higher than the number of points we'd get + if m.stmt.Offset > m.intervalN { + return nil + } + + // Take the lesser of either the pre computed number of GROUP BY buckets that + // will be in the result or the limit passed in by the user + if m.stmt.Limit < m.intervalN { + m.intervalN = m.stmt.Limit + } + } + + // If we are exceeding our MaxGroupByPoints error out + if m.intervalN > MaxGroupByPoints { + return errors.New("too many points in the group by interval. maybe you forgot to specify a where time clause?") + } + + // Ensure that the start time for the results is on the start of the window. + m.qminWindow = m.qmin + if m.intervalSize > 0 && m.intervalN > 1 { + m.qminWindow = m.qminWindow / m.intervalSize * m.intervalSize + } + + // Get a read-only transaction. + tx, err := m.shard.engine.Begin(false) + if err != nil { + return err + } + m.tx = tx + + // Collect measurements. + mms := Measurements(m.shard.index.MeasurementsByName(m.stmt.SourceNames())) + m.selectFields = mms.SelectFields(m.stmt) + m.selectTags = mms.SelectTags(m.stmt) + m.whereFields = mms.WhereFields(m.stmt) + + // Open cursors for each measurement. + for _, mm := range mms { + if err := m.openMeasurement(mm); err != nil { + return err + } + } + + return nil +} + +func (m *AggregateMapper) openMeasurement(mm *Measurement) error { + // Validate that ANY GROUP BY is not a field for the measurement. + if err := mm.ValidateGroupBy(m.stmt); err != nil { + return err + } + + // Validate the fields and tags asked for exist and keep track of which are in the select vs the where + selectFields := mm.SelectFields(m.stmt) + selectTags := mm.SelectTags(m.stmt) + + // If we only have tags in our select clause we just return + if len(selectFields) == 0 && len(selectTags) > 0 { + return fmt.Errorf("statement must have at least one field in select clause") + } + + // Calculate tag sets and apply SLIMIT/SOFFSET. + tagSets, err := mm.DimensionTagSets(m.stmt) + if err != nil { + return err + } + tagSets = m.stmt.LimitTagSets(tagSets) + + // Create all cursors for reading the data from this shard. + for _, t := range tagSets { + cursorSet := CursorSet{ + Measurement: mm.Name, + Tags: t.Tags, + } + if len(t.Tags) == 0 { + cursorSet.Key = mm.Name + } else { + cursorSet.Key = strings.Join([]string{mm.Name, string(MarshalTags(t.Tags))}, "|") + } + + for i, key := range t.SeriesKeys { + fields := slices.Union(selectFields, m.fieldNames, false) + c := m.tx.Cursor(key, fields, m.shard.FieldCodec(mm.Name), true) + if c == nil { + continue + } + + seriesTags := m.shard.index.TagsForSeries(key) + cursorSet.Cursors = append(cursorSet.Cursors, NewTagsCursor(c, t.Filters[i], seriesTags)) + } + + // tsc.Init(m.qmin) + m.cursors = append(m.cursors, cursorSet) + } + + sort.Sort(CursorSets(m.cursors)) + + return nil +} + +// initializeMapFunctions initialize the mapping functions for the mapper. +func (m *AggregateMapper) initializeMapFunctions() error { + // Set up each mapping function for this statement. + aggregates := m.stmt.FunctionCalls() + m.mapFuncs = make([]mapFunc, len(aggregates)) + m.fieldNames = make([]string, len(m.mapFuncs)) + + for i, c := range aggregates { + mfn, err := initializeMapFunc(c) + if err != nil { + return err + } + m.mapFuncs[i] = mfn + + // Check for calls like `derivative(lmean(value), 1d)` + var nested *influxql.Call = c + if fn, ok := c.Args[0].(*influxql.Call); ok { + nested = fn + } + switch lit := nested.Args[0].(type) { + case *influxql.VarRef: + m.fieldNames[i] = lit.Val + case *influxql.Distinct: + if c.Name != "count" { + return fmt.Errorf("aggregate call didn't contain a field %s", c.String()) + } + m.fieldNames[i] = lit.Val + default: + return fmt.Errorf("aggregate call didn't contain a field %s", c.String()) + } + } + + return nil +} + +// Close closes the mapper. +func (m *AggregateMapper) Close() { + if m != nil && m.tx != nil { + m.tx.Rollback() + } + return +} + +// TagSets returns the list of tag sets for which this mapper has data. +func (m *AggregateMapper) TagSets() []string { return CursorSets(m.cursors).Keys() } + +// Fields returns all SELECT fields. +func (m *AggregateMapper) Fields() []string { return append(m.selectFields, m.selectTags...) } + +// NextChunk returns the next interval of data. +// Tagsets are always processed in the same order as AvailTagsSets(). +// When there is no more data for any tagset nil is returned. +func (m *AggregateMapper) NextChunk() (interface{}, error) { + var tmin, tmax int64 + for { + // All tagset cursors processed. NextChunk'ing complete. + if m.cursorIndex == len(m.cursors) { + return nil, nil + } + + // All intervals complete for this tagset. Move to the next tagset. + tmin, tmax = m.nextInterval() + if tmin < 0 { + m.interval = 0 + m.cursorIndex++ + continue + } + break + } + + // Prep the return data for this tagset. + // This will hold data for a single interval for a single tagset. + cursorSet := m.cursors[m.cursorIndex] + output := &MapperOutput{ + Name: cursorSet.Measurement, + Tags: cursorSet.Tags, + Fields: m.selectFields, + cursorKey: cursorSet.Key, + } + + // Always clamp tmin and tmax. This can happen as bucket-times are bucketed to the nearest + // interval. This is necessary to grab the "partial" buckets at the beginning and end of the time range + qmin, qmax := tmin, tmax + if qmin < m.qmin { + qmin = m.qmin + } + if qmax > m.qmax { + qmax = m.qmax + 1 + } + + for _, c := range cursorSet.Cursors { + mapperValue := &MapperValue{ + Time: tmin, + Value: make([]interface{}, len(m.mapFuncs)), + } + + for i := range m.mapFuncs { + // Build a map input from the cursor. + input := &MapInput{ + TMin: -1, + Items: readMapItems(c, m.fieldNames[i], qmin, qmin, qmax), + } + if len(m.stmt.Dimensions) > 0 && !m.stmt.HasTimeFieldSpecified() { + input.TMin = tmin + } + + // Execute the map function which walks the entire interval, and aggregates the result. + value := m.mapFuncs[i](input) + if value == nil { + continue + } + mapperValue.Value.([]interface{})[i] = value + } + output.Values = append(output.Values, mapperValue) + } + + return output, nil +} + +func readMapItems(c *TagsCursor, field string, seek, tmin, tmax int64) []MapItem { + var items []MapItem + var seeked bool + for { + var timestamp int64 + var value interface{} + if !seeked { + timestamp, value = c.SeekTo(seek) + seeked = true + } else { + timestamp, value = c.Next() + } + + // We're done if the point is outside the query's time range [tmin:tmax). + if timestamp != tmin && (timestamp < tmin || timestamp >= tmax) { + return items + } + + // Convert values to fields map. + fields, ok := value.(map[string]interface{}) + if !ok { + fields = map[string]interface{}{"": value} + } + + // Value didn't match, look for the next one. + if value == nil { + continue + } + + // Filter value. + if c.filter != nil { + // Convert value to a map for filter evaluation. + m, ok := value.(map[string]interface{}) + if !ok { + m = map[string]interface{}{field: value} + } + + // If filter fails then skip to the next value. + if !influxql.EvalBool(c.filter, m) { + continue + } + } + + // Filter out single field, if specified. + if m, ok := value.(map[string]interface{}); ok { + value = m[field] + } + if value == nil { + continue + } + + items = append(items, MapItem{ + Timestamp: timestamp, + Value: value, + Fields: fields, + Tags: c.tags, + }) + } +} + +// nextInterval returns the next interval for which to return data. +// If start is less than 0 there are no more intervals. +func (m *AggregateMapper) nextInterval() (start, end int64) { + t := m.qminWindow + int64(m.interval+m.stmt.Offset)*m.intervalSize + + // On to next interval. + m.interval++ + if t > m.qmax || m.interval > m.intervalN { + start, end = -1, 1 + } else { + start, end = t, t+m.intervalSize + } + return +} + +type CursorSet struct { + Measurement string + Tags map[string]string + Key string + Cursors []*TagsCursor +} + +// CursorSets represents a sortable slice of CursorSet. +type CursorSets []CursorSet + +func (a CursorSets) Len() int { return len(a) } +func (a CursorSets) Less(i, j int) bool { return a[i].Key < a[j].Key } +func (a CursorSets) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +func (a CursorSets) Keys() []string { + keys := make([]string, len(a)) + for i := range a { + keys[i] = a[i].Key + } + sort.Strings(keys) + return keys +} + +type int64Slice []int64 + +func (a int64Slice) Len() int { return len(a) } +func (a int64Slice) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a int64Slice) Less(i, j int) bool { return a[i] < a[j] } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/config.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/config.go index 52d182c11..df3141842 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/config.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/config.go @@ -1,6 +1,10 @@ package tsdb import ( + "errors" + "fmt" + "log" + "os" "time" "github.com/influxdb/influxdb/toml" @@ -98,8 +102,14 @@ type Config struct { } func NewConfig() Config { + defaultEngine := DefaultEngine + if engine := os.Getenv("INFLUXDB_DATA_ENGINE"); engine != "" { + log.Println("TSDB engine selected via environment variable:", engine) + defaultEngine = engine + } + return Config{ - Engine: DefaultEngine, + Engine: defaultEngine, MaxWALSize: DefaultMaxWALSize, WALFlushInterval: toml.Duration(DefaultWALFlushInterval), WALPartitionFlushDelay: toml.Duration(DefaultWALPartitionFlushDelay), @@ -120,3 +130,24 @@ func NewConfig() Config { QueryLogEnabled: true, } } + +func (c *Config) Validate() error { + if c.Dir == "" { + return errors.New("Data.Dir must be specified") + } else if c.WALDir == "" { + return errors.New("Data.WALDir must be specified") + } + + valid := false + for _, e := range RegisteredEngines() { + if e == c.Engine { + valid = true + break + } + } + if !valid { + return fmt.Errorf("unrecognized engine %s", c.Engine) + } + + return nil +} diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/cursor.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/cursor.go index d9f157839..b7b7c6b75 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/cursor.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/cursor.go @@ -154,8 +154,7 @@ type TagSetCursor struct { cursors []*TagsCursor // Underlying tags cursors. currentTags map[string]string // the current tags for the underlying series cursor in play - SelectFields []string // fields to be selected - SelectWhereFields []string // fields in both the select and where clause to be returned or filtered on + SelectFields []string // fields to be selected // Min-heap of cursors ordered by timestamp. heap *pointHeap diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine.go index fb1b2108c..946079028 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine.go @@ -63,6 +63,16 @@ func RegisterEngine(name string, fn NewEngineFunc) { newEngineFuncs[name] = fn } +// RegisteredEngines returns the slice of currently registered engines. +func RegisteredEngines() []string { + a := make([]string, 0, len(newEngineFuncs)) + for k, _ := range newEngineFuncs { + a = append(a, k) + } + sort.Strings(a) + return a +} + // NewEngine returns an instance of an engine based on its format. // If the path does not exist then the DefaultFormat is used. func NewEngine(path string, walPath string, options EngineOptions) (Engine, error) { diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/bz1/bz1.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/bz1/bz1.go index 881b82dc4..72d19052a 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/bz1/bz1.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/bz1/bz1.go @@ -33,12 +33,12 @@ const ( ) const ( - statSlowInsert = "slow_insert" - statPointsWrite = "points_write" - statPointsWriteDedupe = "points_write_dedupe" - statBlocksWrite = "blks_write" - statBlocksWriteBytes = "blks_write_bytes" - statBlocksWriteBytesCompress = "blks_write_bytes_c" + statSlowInsert = "slowInsert" + statPointsWrite = "pointsWrite" + statPointsWriteDedupe = "pointsWriteDedupe" + statBlocksWrite = "blksWrite" + statBlocksWriteBytes = "blksWriteBytes" + statBlocksWriteBytesCompress = "blksWriteBytesC" ) func init() { diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/bz1/bz1_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/bz1/bz1_test.go index 0b0cb1e60..6a73478ea 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/bz1/bz1_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/bz1/bz1_test.go @@ -99,11 +99,11 @@ func TestEngine_WritePoints_PointsWriter(t *testing.T) { // Points to be inserted. points := []models.Point{ - models.NewPoint("cpu", models.Tags{}, models.Fields{}, time.Unix(0, 1)), - models.NewPoint("cpu", models.Tags{}, models.Fields{}, time.Unix(0, 0)), - models.NewPoint("cpu", models.Tags{}, models.Fields{}, time.Unix(1, 0)), + models.MustNewPoint("cpu", models.Tags{}, models.Fields{}, time.Unix(0, 1)), + models.MustNewPoint("cpu", models.Tags{}, models.Fields{}, time.Unix(0, 0)), + models.MustNewPoint("cpu", models.Tags{}, models.Fields{}, time.Unix(1, 0)), - models.NewPoint("cpu", models.Tags{"host": "serverA"}, models.Fields{}, time.Unix(0, 0)), + models.MustNewPoint("cpu", models.Tags{"host": "serverA"}, models.Fields{}, time.Unix(0, 0)), } // Mock points writer to ensure points are passed through. diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/cursor.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/cursor.go index 22a44cd6b..6ec413d54 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/cursor.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/cursor.go @@ -189,7 +189,7 @@ type cursor struct { pos uint32 // vals is the current decoded block of Values we're iterating from - vals Values + vals []Value ascending bool @@ -207,6 +207,7 @@ func newCursor(id uint64, files []*dataFile, ascending bool) *cursor { id: id, ascending: ascending, files: files, + vals: make([]Value, 0), } } @@ -472,7 +473,8 @@ func (c *cursor) blockLength(pos uint32) uint32 { func (c *cursor) decodeBlock(position uint32) { length := c.blockLength(position) block := c.f.mmap[position+blockHeaderSize : position+blockHeaderSize+length] - c.vals, _ = DecodeBlock(block) + c.vals = c.vals[:0] + _ = DecodeBlock(block, &c.vals) // only adavance the position if we're asceending. // Descending queries use the blockPositions diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/encoding.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/encoding.go index eea74acd3..cd8ea59f5 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/encoding.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/encoding.go @@ -92,7 +92,7 @@ func (a Values) Encode(buf []byte) ([]byte, error) { // DecodeBlock takes a byte array and will decode into values of the appropriate type // based on the block -func DecodeBlock(block []byte) (Values, error) { +func DecodeBlock(block []byte, vals *[]Value) error { if len(block) <= encodedBlockHeaderSize { panic(fmt.Sprintf("decode of short block: got %v, exp %v", len(block), encodedBlockHeaderSize)) } @@ -100,13 +100,13 @@ func DecodeBlock(block []byte) (Values, error) { blockType := block[8] switch blockType { case BlockFloat64: - return decodeFloatBlock(block) + return decodeFloatBlock(block, vals) case BlockInt64: - return decodeInt64Block(block) + return decodeInt64Block(block, vals) case BlockBool: - return decodeBoolBlock(block) + return decodeBoolBlock(block, vals) case BlockString: - return decodeStringBlock(block) + return decodeStringBlock(block, vals) default: panic(fmt.Sprintf("unknown block type: %d", blockType)) } @@ -183,7 +183,10 @@ func encodeFloatBlock(buf []byte, values []Value) ([]byte, error) { return nil, err } // Encoded float values - vb := venc.Bytes() + vb, err := venc.Bytes() + if err != nil { + return nil, err + } // Prepend the first timestamp of the block in the first 8 bytes and the block // in the next byte, followed by the block @@ -192,14 +195,14 @@ func encodeFloatBlock(buf []byte, values []Value) ([]byte, error) { return block, nil } -func decodeFloatBlock(block []byte) ([]Value, error) { +func decodeFloatBlock(block []byte, a *[]Value) error { // The first 8 bytes is the minimum timestamp of the block block = block[8:] // Block type is the next block, make sure we actually have a float block blockType := block[0] if blockType != BlockFloat64 { - return nil, fmt.Errorf("invalid block type: exp %d, got %d", BlockFloat64, blockType) + return fmt.Errorf("invalid block type: exp %d, got %d", BlockFloat64, blockType) } block = block[1:] @@ -209,27 +212,26 @@ func decodeFloatBlock(block []byte) ([]Value, error) { dec := NewTimeDecoder(tb) iter, err := NewFloatDecoder(vb) if err != nil { - return nil, err + return err } // Decode both a timestamp and value - var a []Value for dec.Next() && iter.Next() { ts := dec.Read() v := iter.Values() - a = append(a, &FloatValue{ts, v}) + *a = append(*a, &FloatValue{ts, v}) } // Did timestamp decoding have an error? if dec.Error() != nil { - return nil, dec.Error() + return dec.Error() } // Did float decoding have an error? if iter.Error() != nil { - return nil, iter.Error() + return iter.Error() } - return a, nil + return nil } type BoolValue struct { @@ -290,14 +292,14 @@ func encodeBoolBlock(buf []byte, values []Value) ([]byte, error) { return block, nil } -func decodeBoolBlock(block []byte) ([]Value, error) { +func decodeBoolBlock(block []byte, a *[]Value) error { // The first 8 bytes is the minimum timestamp of the block block = block[8:] // Block type is the next block, make sure we actually have a float block blockType := block[0] if blockType != BlockBool { - return nil, fmt.Errorf("invalid block type: exp %d, got %d", BlockBool, blockType) + return fmt.Errorf("invalid block type: exp %d, got %d", BlockBool, blockType) } block = block[1:] @@ -308,23 +310,22 @@ func decodeBoolBlock(block []byte) ([]Value, error) { vdec := NewBoolDecoder(vb) // Decode both a timestamp and value - var a []Value for dec.Next() && vdec.Next() { ts := dec.Read() v := vdec.Read() - a = append(a, &BoolValue{ts, v}) + *a = append(*a, &BoolValue{ts, v}) } // Did timestamp decoding have an error? if dec.Error() != nil { - return nil, dec.Error() + return dec.Error() } // Did bool decoding have an error? if vdec.Error() != nil { - return nil, vdec.Error() + return vdec.Error() } - return a, nil + return nil } type Int64Value struct { @@ -374,13 +375,13 @@ func encodeInt64Block(buf []byte, values []Value) ([]byte, error) { return append(block, packBlock(tb, vb)...), nil } -func decodeInt64Block(block []byte) ([]Value, error) { +func decodeInt64Block(block []byte, a *[]Value) error { // slice off the first 8 bytes (min timestmap for the block) block = block[8:] blockType := block[0] if blockType != BlockInt64 { - return nil, fmt.Errorf("invalid block type: exp %d, got %d", BlockInt64, blockType) + return fmt.Errorf("invalid block type: exp %d, got %d", BlockInt64, blockType) } block = block[1:] @@ -393,23 +394,22 @@ func decodeInt64Block(block []byte) ([]Value, error) { vDec := NewInt64Decoder(vb) // Decode both a timestamp and value - var a []Value for tsDec.Next() && vDec.Next() { ts := tsDec.Read() v := vDec.Read() - a = append(a, &Int64Value{ts, v}) + *a = append(*a, &Int64Value{ts, v}) } // Did timestamp decoding have an error? if tsDec.Error() != nil { - return nil, tsDec.Error() + return tsDec.Error() } // Did int64 decoding have an error? if vDec.Error() != nil { - return nil, vDec.Error() + return vDec.Error() } - return a, nil + return nil } type StringValue struct { @@ -459,13 +459,13 @@ func encodeStringBlock(buf []byte, values []Value) ([]byte, error) { return append(block, packBlock(tb, vb)...), nil } -func decodeStringBlock(block []byte) ([]Value, error) { +func decodeStringBlock(block []byte, a *[]Value) error { // slice off the first 8 bytes (min timestmap for the block) block = block[8:] blockType := block[0] if blockType != BlockString { - return nil, fmt.Errorf("invalid block type: exp %d, got %d", BlockString, blockType) + return fmt.Errorf("invalid block type: exp %d, got %d", BlockString, blockType) } block = block[1:] @@ -477,27 +477,26 @@ func decodeStringBlock(block []byte) ([]Value, error) { tsDec := NewTimeDecoder(tb) vDec, err := NewStringDecoder(vb) if err != nil { - return nil, err + return err } // Decode both a timestamp and value - var a []Value for tsDec.Next() && vDec.Next() { ts := tsDec.Read() v := vDec.Read() - a = append(a, &StringValue{ts, v}) + *a = append(*a, &StringValue{ts, v}) } // Did timestamp decoding have an error? if tsDec.Error() != nil { - return nil, tsDec.Error() + return tsDec.Error() } // Did string decoding have an error? if vDec.Error() != nil { - return nil, vDec.Error() + return vDec.Error() } - return a, nil + return nil } func packBlockHeader(firstTime time.Time, blockType byte) []byte { diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/encoding_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/encoding_test.go index 26b9b0bd7..3c72afc53 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/encoding_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/encoding_test.go @@ -1,52 +1,51 @@ package tsm1_test import ( - // "math/rand" - "fmt" "reflect" "testing" "time" + "github.com/davecgh/go-spew/spew" "github.com/influxdb/influxdb/tsdb/engine/tsm1" ) func TestEncoding_FloatBlock(t *testing.T) { valueCount := 1000 times := getTimes(valueCount, 60, time.Second) - values := make(tsm1.Values, len(times)) + values := make([]tsm1.Value, len(times)) for i, t := range times { values[i] = tsm1.NewValue(t, float64(i)) } - b, err := values.Encode(nil) + b, err := tsm1.Values(values).Encode(nil) if err != nil { t.Fatalf("unexpected error: %v", err) } - decodedValues, err := tsm1.DecodeBlock(b) - if err != nil { + var decodedValues []tsm1.Value + if err := tsm1.DecodeBlock(b, &decodedValues); err != nil { t.Fatalf("unexpected error decoding block: %v", err) } if !reflect.DeepEqual(decodedValues, values) { - t.Fatalf("unexpected results:\n\tgot: %v\n\texp: %v\n", decodedValues, values) + t.Fatalf("unexpected results:\n\tgot: %s\n\texp: %s\n", spew.Sdump(decodedValues), spew.Sdump(values)) } } func TestEncoding_FloatBlock_ZeroTime(t *testing.T) { - values := make(tsm1.Values, 3) + values := make([]tsm1.Value, 3) for i := 0; i < 3; i++ { values[i] = tsm1.NewValue(time.Unix(0, 0), float64(i)) } - b, err := values.Encode(nil) + b, err := tsm1.Values(values).Encode(nil) if err != nil { t.Fatalf("unexpected error: %v", err) } - decodedValues, err := tsm1.DecodeBlock(b) - if err != nil { + var decodedValues []tsm1.Value + if err := tsm1.DecodeBlock(b, &decodedValues); err != nil { t.Fatalf("unexpected error decoding block: %v", err) } @@ -56,20 +55,20 @@ func TestEncoding_FloatBlock_ZeroTime(t *testing.T) { } func TestEncoding_FloatBlock_SimilarFloats(t *testing.T) { - values := make(tsm1.Values, 5) + values := make([]tsm1.Value, 5) values[0] = tsm1.NewValue(time.Unix(0, 1444238178437870000), 6.00065e+06) values[1] = tsm1.NewValue(time.Unix(0, 1444238185286830000), 6.000656e+06) values[2] = tsm1.NewValue(time.Unix(0, 1444238188441501000), 6.000657e+06) values[3] = tsm1.NewValue(time.Unix(0, 1444238195286811000), 6.000659e+06) values[4] = tsm1.NewValue(time.Unix(0, 1444238198439917000), 6.000661e+06) - b, err := values.Encode(nil) + b, err := tsm1.Values(values).Encode(nil) if err != nil { t.Fatalf("unexpected error: %v", err) } - decodedValues, err := tsm1.DecodeBlock(b) - if err != nil { + var decodedValues []tsm1.Value + if err := tsm1.DecodeBlock(b, &decodedValues); err != nil { t.Fatalf("unexpected error decoding block: %v", err) } @@ -81,18 +80,18 @@ func TestEncoding_FloatBlock_SimilarFloats(t *testing.T) { func TestEncoding_IntBlock_Basic(t *testing.T) { valueCount := 1000 times := getTimes(valueCount, 60, time.Second) - values := make(tsm1.Values, len(times)) + values := make([]tsm1.Value, len(times)) for i, t := range times { values[i] = tsm1.NewValue(t, int64(i)) } - b, err := values.Encode(nil) + b, err := tsm1.Values(values).Encode(nil) if err != nil { t.Fatalf("unexpected error: %v", err) } - decodedValues, err := tsm1.DecodeBlock(b) - if err != nil { + var decodedValues []tsm1.Value + if err := tsm1.DecodeBlock(b, &decodedValues); err != nil { t.Fatalf("unexpected error decoding block: %v", err) } @@ -115,7 +114,7 @@ func TestEncoding_IntBlock_Basic(t *testing.T) { func TestEncoding_IntBlock_Negatives(t *testing.T) { valueCount := 1000 times := getTimes(valueCount, 60, time.Second) - values := make(tsm1.Values, len(times)) + values := make([]tsm1.Value, len(times)) for i, t := range times { v := int64(i) if i%2 == 0 { @@ -124,13 +123,13 @@ func TestEncoding_IntBlock_Negatives(t *testing.T) { values[i] = tsm1.NewValue(t, int64(v)) } - b, err := values.Encode(nil) + b, err := tsm1.Values(values).Encode(nil) if err != nil { t.Fatalf("unexpected error: %v", err) } - decodedValues, err := tsm1.DecodeBlock(b) - if err != nil { + var decodedValues []tsm1.Value + if err := tsm1.DecodeBlock(b, &decodedValues); err != nil { t.Fatalf("unexpected error decoding block: %v", err) } @@ -142,7 +141,7 @@ func TestEncoding_IntBlock_Negatives(t *testing.T) { func TestEncoding_BoolBlock_Basic(t *testing.T) { valueCount := 1000 times := getTimes(valueCount, 60, time.Second) - values := make(tsm1.Values, len(times)) + values := make([]tsm1.Value, len(times)) for i, t := range times { v := true if i%2 == 0 { @@ -151,13 +150,13 @@ func TestEncoding_BoolBlock_Basic(t *testing.T) { values[i] = tsm1.NewValue(t, v) } - b, err := values.Encode(nil) + b, err := tsm1.Values(values).Encode(nil) if err != nil { t.Fatalf("unexpected error: %v", err) } - decodedValues, err := tsm1.DecodeBlock(b) - if err != nil { + var decodedValues []tsm1.Value + if err := tsm1.DecodeBlock(b, &decodedValues); err != nil { t.Fatalf("unexpected error decoding block: %v", err) } @@ -169,18 +168,18 @@ func TestEncoding_BoolBlock_Basic(t *testing.T) { func TestEncoding_StringBlock_Basic(t *testing.T) { valueCount := 1000 times := getTimes(valueCount, 60, time.Second) - values := make(tsm1.Values, len(times)) + values := make([]tsm1.Value, len(times)) for i, t := range times { values[i] = tsm1.NewValue(t, fmt.Sprintf("value %d", i)) } - b, err := values.Encode(nil) + b, err := tsm1.Values(values).Encode(nil) if err != nil { t.Fatalf("unexpected error: %v", err) } - decodedValues, err := tsm1.DecodeBlock(b) - if err != nil { + var decodedValues []tsm1.Value + if err := tsm1.DecodeBlock(b, &decodedValues); err != nil { t.Fatalf("unexpected error decoding block: %v", err) } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/float.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/float.go index 6f5a6e65f..76059abc3 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/float.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/float.go @@ -11,6 +11,7 @@ this version. import ( "bytes" + "fmt" "math" "github.com/dgryski/go-bits" @@ -29,6 +30,7 @@ const ( // FloatEncoder encodes multiple float64s into a byte slice type FloatEncoder struct { val float64 + err error leading uint64 trailing uint64 @@ -52,20 +54,25 @@ func NewFloatEncoder() *FloatEncoder { } -func (s *FloatEncoder) Bytes() []byte { - return append([]byte{floatCompressedGorilla << 4}, s.buf.Bytes()...) +func (s *FloatEncoder) Bytes() ([]byte, error) { + return append([]byte{floatCompressedGorilla << 4}, s.buf.Bytes()...), s.err } func (s *FloatEncoder) Finish() { if !s.finished { // write an end-of-stream record + s.finished = true s.Push(math.NaN()) s.bw.Flush(bitstream.Zero) - s.finished = true } } func (s *FloatEncoder) Push(v float64) { + // Only allow NaN as a sentinel value + if math.IsNaN(v) && !s.finished { + s.err = fmt.Errorf("unsupported value: NaN") + return + } if s.first { // first point s.val = v diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/float_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/float_test.go index 7419558f8..33ae3211a 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/float_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/float_test.go @@ -1,6 +1,7 @@ package tsm1_test import ( + "math" "reflect" "testing" "testing/quick" @@ -29,7 +30,10 @@ func TestFloatEncoder_Simple(t *testing.T) { s.Finish() - b := s.Bytes() + b, err := s.Bytes() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } it, err := tsm1.NewFloatDecoder(b) if err != nil { @@ -85,7 +89,10 @@ func TestFloatEncoder_SimilarFloats(t *testing.T) { s.Finish() - b := s.Bytes() + b, err := s.Bytes() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } it, err := tsm1.NewFloatDecoder(b) if err != nil { @@ -142,14 +149,16 @@ var TwoHoursData = []struct { } func TestFloatEncoder_Roundtrip(t *testing.T) { - s := tsm1.NewFloatEncoder() for _, p := range TwoHoursData { s.Push(p.v) } s.Finish() - b := s.Bytes() + b, err := s.Bytes() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } it, err := tsm1.NewFloatDecoder(b) if err != nil { @@ -176,6 +185,21 @@ func TestFloatEncoder_Roundtrip(t *testing.T) { } } +func TestFloatEncoder_Roundtrip_NaN(t *testing.T) { + + s := tsm1.NewFloatEncoder() + s.Push(1.0) + s.Push(math.NaN()) + s.Push(2.0) + s.Finish() + + _, err := s.Bytes() + + if err == nil { + t.Fatalf("expected error. got nil") + } +} + func Test_FloatEncoder_Quick(t *testing.T) { quick.Check(func(values []float64) bool { // Write values to encoder. @@ -187,7 +211,12 @@ func Test_FloatEncoder_Quick(t *testing.T) { // Read values out of decoder. got := make([]float64, 0, len(values)) - dec, err := tsm1.NewFloatDecoder(enc.Bytes()) + b, err := enc.Bytes() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + dec, err := tsm1.NewFloatDecoder(b) if err != nil { t.Fatal(err) } @@ -220,7 +249,10 @@ func BenchmarkFloatDecoder(b *testing.B) { s.Push(tt.v) } s.Finish() - bytes := s.Bytes() + bytes, err := s.Bytes() + if err != nil { + b.Fatalf("unexpected error: %v", err) + } b.ResetTimer() diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/tsm1.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/tsm1.go index a211f3f81..e3ab4d4f7 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/tsm1.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/tsm1.go @@ -11,7 +11,6 @@ import ( "math" "os" "path/filepath" - "reflect" "sort" "strings" "sync" @@ -43,15 +42,22 @@ const ( // keys have hash collisions and what their actual IDs are CollisionsFileExtension = "collisions" - //CheckpointExtension is the extension given to files that checkpoint. + // CheckpointExtension is the extension given to files that checkpoint a rewrite or compaction. // The checkpoint files are created when a new file is first created. They // are removed after the file has been synced and is safe for use. If a file // has an associated checkpoint file, it wasn't safely written and both should be removed CheckpointExtension = "check" + // CompactionExtension is the extension given to the file that marks when a compaction has been + // fully written, but the compacted files have not yet been deleted. It is used for cleanup + // if the server was not cleanly shutdown before the compacted files could be deleted. + CompactionExtension = "compact" + // keyFieldSeparator separates the series key from the field name in the composite key // that identifies a specific field in series keyFieldSeparator = "#!~#" + + blockBufferSize = 1024 * 1024 ) type TimePrecision uint8 @@ -68,10 +74,7 @@ func init() { } const ( - MaxDataFileSize = 1024 * 1024 * 1024 // 1GB - - // DefaultRotateBlockSize is the default size to rotate to a new compressed block - DefaultRotateBlockSize = 512 * 1024 // 512KB + MaxDataFileSize = 1024 * 1024 * 1024 * 2 // 2GB DefaultRotateFileSize = 5 * 1024 * 1024 // 5MB @@ -106,13 +109,13 @@ type Engine struct { WAL *Log RotateFileSize uint32 + MaxFileSize uint32 SkipCompaction bool CompactionAge time.Duration MinCompactionFileCount int IndexCompactionFullAge time.Duration IndexMinCompactionInterval time.Duration MaxPointsPerBlock int - RotateBlockSize int // filesLock is only for modifying and accessing the files slice filesLock sync.RWMutex @@ -154,12 +157,12 @@ func NewEngine(path string, walPath string, opt tsdb.EngineOptions) tsdb.Engine HashSeriesField: hashSeriesField, WAL: w, RotateFileSize: DefaultRotateFileSize, + MaxFileSize: MaxDataFileSize, CompactionAge: opt.Config.IndexCompactionAge, MinCompactionFileCount: opt.Config.IndexMinCompactionFileCount, IndexCompactionFullAge: opt.Config.IndexCompactionFullAge, IndexMinCompactionInterval: opt.Config.IndexMinCompactionInterval, MaxPointsPerBlock: DefaultMaxPointsPerBlock, - RotateBlockSize: DefaultRotateBlockSize, } e.WAL.IndexWriter = e @@ -173,7 +176,9 @@ func (e *Engine) Path() string { return e.path } func (e *Engine) PerformMaintenance() { if f := e.WAL.shouldFlush(); f != noFlush { go func() { - e.WAL.flush(f) + if err := e.WAL.flush(f); err != nil { + e.logger.Printf("PerformMaintenance: WAL flush failed: %v", err) + } }() return } @@ -198,7 +203,11 @@ func (e *Engine) PerformMaintenance() { } } - go e.Compact(true) + go func() { + if err := e.Compact(true); err != nil { + e.logger.Printf("PerformMaintenance: error during compaction: %v", err) + } + }() } // Format returns the format type of this engine @@ -218,6 +227,8 @@ func (e *Engine) Open() error { e.cleanupMetafile(IDsFileExtension) e.cleanupMetafile(CollisionsFileExtension) + e.cleanupUnfinishedCompaction() + files, err := filepath.Glob(filepath.Join(e.path, fmt.Sprintf("*.%s", Format))) if err != nil { return err @@ -267,6 +278,39 @@ func (e *Engine) Open() error { return nil } +// cleanupUnfinishedConpaction will read any compaction markers. If the marker exists, the compaction finished successfully, +// but didn't get fully cleaned up. Remove the old files and their checkpoints +func (e *Engine) cleanupUnfinishedCompaction() { + files, err := filepath.Glob(filepath.Join(e.path, fmt.Sprintf("*.%s", CompactionExtension))) + if err != nil { + panic(fmt.Sprintf("error getting compaction checkpoints: %s", err.Error())) + } + + for _, fn := range files { + f, err := os.OpenFile(fn, os.O_RDONLY, 0666) + if err != nil { + panic(fmt.Sprintf("error opening compaction info file: %s", err.Error())) + } + data, err := ioutil.ReadAll(f) + if err != nil { + panic(fmt.Sprintf("error reading compaction info file: %s", err.Error())) + } + + c := &compactionCheckpoint{} + err = json.Unmarshal(data, c) + if err == nil { + c.cleanup() + } + + if err := f.Close(); err != nil { + panic(fmt.Sprintf("error closing compaction checkpoint: %s", err.Error())) + } + if err := os.RemoveAll(f.Name()); err != nil { + panic(fmt.Sprintf("error removing compaction checkpoint: %s", err.Error())) + } + } +} + // Close closes the engine. func (e *Engine) Close() error { // get all the locks so queries, writes, and compactions stop before closing @@ -280,6 +324,8 @@ func (e *Engine) Close() error { e.filesLock.Lock() defer e.filesLock.Unlock() + e.WAL.Close() + // ensure all deletes have been processed e.deletesPending.Wait() @@ -441,7 +487,11 @@ func (e *Engine) Write(pointsByKey map[string]Values, measurementFieldsToSave ma } if !e.SkipCompaction && e.shouldCompact() { - go e.Compact(false) + go func() { + if err := e.Compact(false); err != nil { + e.logger.Printf("Write: error during compaction: %v", err) + } + }() } return nil @@ -470,6 +520,7 @@ func (e *Engine) filesAndLock(min, max int64) (a dataFiles, lockStart, lockEnd i a = make([]*dataFile, 0) files := e.copyFilesCollection() + e.filesLock.RLock() for _, f := range e.files { fmin, fmax := f.MinTime(), f.MaxTime() if min < fmax && fmin >= fmin { @@ -478,6 +529,7 @@ func (e *Engine) filesAndLock(min, max int64) (a dataFiles, lockStart, lockEnd i a = append(a, f) } } + e.filesLock.RUnlock() if len(a) > 0 { lockStart = a[0].MinTime() @@ -496,7 +548,7 @@ func (e *Engine) filesAndLock(min, max int64) (a dataFiles, lockStart, lockEnd i // were waiting for a write lock on the range. Make sure the files are still the // same after we got the lock, otherwise try again. This shouldn't happen often. filesAfterLock := e.copyFilesCollection() - if reflect.DeepEqual(files, filesAfterLock) { + if dataFilesEquals(files, filesAfterLock) { return } @@ -504,11 +556,11 @@ func (e *Engine) filesAndLock(min, max int64) (a dataFiles, lockStart, lockEnd i } } -func (e *Engine) Compact(fullCompaction bool) error { +// getCompactionFiles will return the list of files ready to be compacted along with the min and +// max time of the write lock obtained for compaction +func (e *Engine) getCompactionFiles(fullCompaction bool) (minTime, maxTime int64, files dataFiles) { // we're looping here to ensure that the files we've marked to compact are // still there after we've obtained the write lock - var minTime, maxTime int64 - var files dataFiles for { if fullCompaction { files = e.copyFilesCollection() @@ -516,7 +568,7 @@ func (e *Engine) Compact(fullCompaction bool) error { files = e.filesToCompact() } if len(files) < 2 { - return nil + return minTime, maxTimeOffset, nil } minTime = files[0].MinTime() maxTime = files[len(files)-1].MaxTime() @@ -531,13 +583,53 @@ func (e *Engine) Compact(fullCompaction bool) error { } else { filesAfterLock = e.filesToCompact() } - if !reflect.DeepEqual(files, filesAfterLock) { + if !dataFilesEquals(files, filesAfterLock) { e.writeLock.UnlockRange(minTime, maxTime) continue } // we've got the write lock and the files are all there - break + return + } +} + +// compactToNewFiles will compact the passed in data files into as few files as possible +func (e *Engine) compactToNewFiles(minTime, maxTime int64, files dataFiles) []*os.File { + fileName := e.nextFileName() + e.logger.Printf("Starting compaction in %s of %d files to new file %s", e.path, len(files), fileName) + + compaction := newCompactionJob(files, minTime, maxTime, e.MaxFileSize, e.MaxPointsPerBlock) + compaction.newCurrentFile(fileName) + + // loop writing data until we've read through all the files + for { + nextID := compaction.nextID() + if nextID == dataFileEOF { + break + } + + // write data for this ID while rotating to new files if necessary + for { + moreToWrite := compaction.writeBlocksForID(nextID) + if !moreToWrite { + break + } + compaction.newCurrentFile(e.nextFileName()) + } + } + + // close out the current compacted file + compaction.writeOutCurrentFile() + + return compaction.newFiles +} + +// Compact will compact data files in the directory into the fewest possible data files they +// can be combined into +func (e *Engine) Compact(fullCompaction bool) error { + minTime, maxTime, files := e.getCompactionFiles(fullCompaction) + if len(files) < 2 { + return nil } // mark the compaction as running @@ -557,143 +649,37 @@ func (e *Engine) Compact(fullCompaction bool) error { e.filesLock.Unlock() }() - var s string - if fullCompaction { - s = "FULL " - } - fileName := e.nextFileName() - e.logger.Printf("Starting %scompaction in partition %s of %d files to new file %s", s, e.path, len(files), fileName) st := time.Now() - positions := make([]uint32, len(files)) - ids := make([]uint64, len(files)) + newFiles := e.compactToNewFiles(minTime, maxTime, files) - // initilaize for writing - f, err := e.openFileAndCheckpoint(fileName) - - for i, df := range files { - ids[i] = btou64(df.mmap[4:12]) - positions[i] = 4 - } - currentPosition := uint32(fileHeaderSize) - var newPositions []uint32 - var newIDs []uint64 - buf := make([]byte, e.RotateBlockSize) - for { - // find the min ID so we can write it to the file - minID := uint64(math.MaxUint64) - for _, id := range ids { - if minID > id && id != 0 { - minID = id - } - } - if minID == math.MaxUint64 { // we've emptied all the files - break - } - - newIDs = append(newIDs, minID) - newPositions = append(newPositions, currentPosition) - - // write the blocks in order from the files with this id. as we - // go merge blocks together from one file to another, if the right size - var previousValues Values - for i, id := range ids { - if id != minID { - continue - } - df := files[i] - pos := positions[i] - fid, _, block := df.block(pos) - if fid != id { - panic("not possible") - } - newPos := pos + uint32(blockHeaderSize+len(block)) - positions[i] = newPos - - // write the blocks out to file that are already at their size limit - for { - // write the values, the block or combine with previous - if len(previousValues) > 0 { - decoded, err := DecodeBlock(block) - if err != nil { - panic(fmt.Sprintf("failure decoding block: %v", err)) - } - previousValues = append(previousValues, decoded...) - } else if len(block) > e.RotateBlockSize { - if _, err := f.Write(df.mmap[pos:newPos]); err != nil { - return err - } - currentPosition += uint32(newPos - pos) - } else { - // TODO: handle decode error - previousValues, _ = DecodeBlock(block) - } - - // write the previous values and clear if we've hit the limit - if len(previousValues) > e.MaxPointsPerBlock { - b, err := previousValues.Encode(buf) - if err != nil { - panic(fmt.Sprintf("failure encoding block: %v", err)) - } - - if err := e.writeBlock(f, id, b); err != nil { - // fail hard. If we can't write a file someone needs to get woken up - panic(fmt.Sprintf("failure writing block: %s", err.Error())) - } - currentPosition += uint32(blockHeaderSize + len(b)) - previousValues = nil - } - - // if the next block is the same ID, we don't need to decode this one - // so we can just write it out to the file - nextID, _, nextBlock := df.block(newPos) - - // move to the next block in this file only if the id is the same - if nextID != id { - // flush remaining values - if len(previousValues) > 0 { - b, err := previousValues.Encode(buf) - if err != nil { - panic(fmt.Sprintf("failure encoding block: %v", err)) - } - currentPosition += uint32(blockHeaderSize + len(b)) - previousValues = nil - if err := e.writeBlock(f, id, b); err != nil { - panic(fmt.Sprintf("error writing file %s: %s", f.Name(), err.Error())) - } - } - ids[i] = nextID - break - } - pos = newPos - newPos = pos + uint32(blockHeaderSize+len(nextBlock)) - positions[i] = newPos - block = nextBlock - } - } - - if len(previousValues) > 0 { - b, err := previousValues.Encode(buf) - if err != nil { - panic(fmt.Sprintf("failure encoding block: %v", err)) - } - - if err := e.writeBlock(f, minID, b); err != nil { - // fail hard. If we can't write a file someone needs to get woken up - panic(fmt.Sprintf("failure writing block: %s", err.Error())) - } - currentPosition += uint32(blockHeaderSize + len(b)) + newDataFiles := make(dataFiles, len(newFiles)) + for i, f := range newFiles { + // now open it as a memory mapped data file + newDF, err := NewDataFile(f) + if err != nil { + return err } + newDataFiles[i] = newDF } - newDF, err := e.writeIndexAndGetDataFile(f, minTime, maxTime, newIDs, newPositions) + // write the compaction file to note that we've successfully commpleted the write portion of compaction + compactedFileNames := make([]string, len(files)) + newFileNames := make([]string, len(newFiles)) + for i, f := range files { + compactedFileNames[i] = f.f.Name() + } + for i, f := range newFiles { + newFileNames[i] = f.Name() + } + compactionCheckpointName, err := e.writeCompactionCheckpointFile(compactedFileNames, newFileNames) if err != nil { return err } // update engine with new file pointers e.filesLock.Lock() - var newFiles dataFiles + var replacementFiles dataFiles for _, df := range e.files { // exclude any files that were compacted include := true @@ -704,69 +690,90 @@ func (e *Engine) Compact(fullCompaction bool) error { } } if include { - newFiles = append(newFiles, df) + replacementFiles = append(replacementFiles, df) } } - newFiles = append(newFiles, newDF) - sort.Sort(newFiles) - e.files = newFiles + replacementFiles = append(replacementFiles, newDataFiles...) + sort.Sort(replacementFiles) + e.files = replacementFiles e.filesLock.Unlock() e.logger.Printf("Compaction of %s took %s", e.path, time.Since(st)) - // delete the old files in a goroutine so running queries won't block the write - // from completing - e.deletesPending.Add(1) - go func() { - for _, f := range files { - if err := f.Delete(); err != nil { - e.logger.Println("ERROR DELETING:", f.f.Name()) - } - } - e.deletesPending.Done() - }() + e.clearCompactedFiles(compactionCheckpointName, newFiles, files) return nil } -func (e *Engine) writeBlock(f *os.File, id uint64, block []byte) error { - if _, err := f.Write(append(u64tob(id), u32tob(uint32(len(block)))...)); err != nil { - return err +// clearCompactedFiles will remove the compaction checkpoints for new files, remove the old compacted files, and +// finally remove the compaction checkpoint +func (e *Engine) clearCompactedFiles(compactionCheckpointName string, newFiles []*os.File, oldFiles dataFiles) { + // delete the old files in a goroutine so running queries won't block the write + // from completing + e.deletesPending.Add(1) + go func() { + // first clear out the compaction checkpoints + for _, f := range newFiles { + if err := removeCheckpoint(f.Name()); err != nil { + // panic here since continuing could cause data loss. It's better to fail hard so + // everything can be recovered on restart + panic(fmt.Sprintf("error removing checkpoint file %s: %s", f.Name(), err.Error())) + } + } + + // now delete the underlying data files + for _, f := range oldFiles { + if err := f.Delete(); err != nil { + panic(fmt.Sprintf("error deleting old file after compaction %s: %s", f.f.Name(), err.Error())) + } + } + + // finally remove the compaction marker + if err := os.RemoveAll(compactionCheckpointName); err != nil { + e.logger.Printf("error removing %s: %s", compactionCheckpointName, err.Error()) + } + + e.deletesPending.Done() + }() +} + +// writeCompactionCheckpointFile will save the compacted filenames and new filenames in +// a file. This is used on startup to clean out files that weren't deleted if the server +// wasn't shut down cleanly. +func (e *Engine) writeCompactionCheckpointFile(compactedFiles, newFiles []string) (string, error) { + m := &compactionCheckpoint{ + CompactedFiles: compactedFiles, + NewFiles: newFiles, } - _, err := f.Write(block) - return err + + data, err := json.Marshal(m) + if err != nil { + return "", err + } + + // make the compacted filename the same name as the first compacted file, but with the compacted extension + name := strings.Split(filepath.Base(compactedFiles[0]), ".")[0] + fn := fmt.Sprintf("%s.%s", name, CompactionExtension) + fileName := filepath.Join(filepath.Dir(compactedFiles[0]), fn) + + f, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR, 0666) + if err != nil { + return "", err + } + if _, err := f.Write(data); err != nil { + f.Close() + return fileName, err + } + + return fileName, f.Close() } func (e *Engine) writeIndexAndGetDataFile(f *os.File, minTime, maxTime int64, ids []uint64, newPositions []uint32) (*dataFile, error) { - // write the file index, starting with the series ids and their positions - for i, id := range ids { - if _, err := f.Write(u64tob(id)); err != nil { - return nil, err - } - if _, err := f.Write(u32tob(newPositions[i])); err != nil { - return nil, err - } - } - - // write the min time, max time - if _, err := f.Write(append(u64tob(uint64(minTime)), u64tob(uint64(maxTime))...)); err != nil { + if err := writeIndex(f, minTime, maxTime, ids, newPositions); err != nil { return nil, err } - // series count - if _, err := f.Write(u32tob(uint32(len(ids)))); err != nil { - return nil, err - } - - // sync it and see4k back to the beginning to hand off to the mmap - if err := f.Sync(); err != nil { - return nil, err - } - if _, err := f.Seek(0, 0); err != nil { - return nil, err - } - - if err := e.removeCheckpoint(f.Name()); err != nil { + if err := removeCheckpoint(f.Name()); err != nil { return nil, err } @@ -797,7 +804,7 @@ func (e *Engine) filesToCompact() dataFiles { var a dataFiles for _, df := range e.files { - if time.Since(df.modTime) > e.CompactionAge && df.size < MaxDataFileSize { + if time.Since(df.modTime) > e.CompactionAge && df.size < e.MaxFileSize { a = append(a, df) } else if len(a) > 0 { // only compact contiguous ranges. If we hit the negative case and @@ -885,7 +892,6 @@ func (e *Engine) convertKeysAndWriteMetadata(pointsByKey map[string]Values, meas if maxTime < values.MaxTime() { maxTime = values.MaxTime() } - valuesByID[id] = values } @@ -1028,15 +1034,15 @@ func (e *Engine) rewriteFile(oldDF *dataFile, valuesByID map[uint64]Values) erro // always write in order by ID sort.Sort(uint64slice(ids)) - f, err := e.openFileAndCheckpoint(e.nextFileName()) + f, err := openFileAndCheckpoint(e.nextFileName()) if err != nil { return err } - if oldDF == nil { + if oldDF == nil || oldDF.Deleted() { e.logger.Printf("writing new index file %s", f.Name()) } else { - e.logger.Printf("rewriting index file %s with %s", oldDF.f.Name(), f.Name()) + e.logger.Printf("rewriting index file %s with %s", oldDF.Name(), f.Name()) } // now combine the old file data with the new values, keeping track of @@ -1087,7 +1093,7 @@ func (e *Engine) rewriteFile(oldDF *dataFile, valuesByID map[uint64]Values) erro return err } - if err := e.writeBlock(f, id, block); err != nil { + if err := writeBlock(f, id, block); err != nil { f.Close() return err } @@ -1111,8 +1117,10 @@ func (e *Engine) rewriteFile(oldDF *dataFile, valuesByID map[uint64]Values) erro nv, newBlock, err := e.DecodeAndCombine(newVals, block, buf[:0], nextTime, hasFutureBlock) newVals = nv if err != nil { + f.Close() return err } + if _, err := f.Write(append(u64tob(id), u32tob(uint32(len(newBlock)))...)); err != nil { f.Close() return err @@ -1263,7 +1271,7 @@ func (e *Engine) flushDeletes() error { } func (e *Engine) writeNewFileExcludeDeletes(oldDF *dataFile) *dataFile { - f, err := e.openFileAndCheckpoint(e.nextFileName()) + f, err := openFileAndCheckpoint(e.nextFileName()) if err != nil { panic(fmt.Sprintf("error opening new data file: %s", err.Error())) } @@ -1342,9 +1350,6 @@ func (e *Engine) replaceCompressedFile(name string, data []byte) error { if err := f.Close(); err != nil { return err } - if err := os.Remove(name); err != nil && !os.IsNotExist(err) { - return err - } return os.Rename(tmpName, filepath.Join(e.path, name)) } @@ -1533,13 +1538,6 @@ func (e *Engine) writeFields(fields map[string]*tsdb.MeasurementFields) error { return err } fieldsFileName := filepath.Join(e.path, FieldsFileExtension) - - if _, err := os.Stat(fieldsFileName); !os.IsNotExist(err) { - if err := os.Remove(fieldsFileName); err != nil { - return err - } - } - return os.Rename(fn, fieldsFileName) } @@ -1607,13 +1605,6 @@ func (e *Engine) writeSeries(series map[string]*tsdb.Series) error { return err } seriesFileName := filepath.Join(e.path, SeriesFileExtension) - - if _, err := os.Stat(seriesFileName); !os.IsNotExist(err) { - if err := os.Remove(seriesFileName); err != nil && err != os.ErrNotExist { - return err - } - } - return os.Rename(fn, seriesFileName) } @@ -1654,7 +1645,8 @@ func (e *Engine) DecodeAndCombine(newValues Values, block, buf []byte, nextTime return newValues, block, nil } - values, err := DecodeBlock(block) + var values []Value + err := DecodeBlock(block, &values) if err != nil { panic(fmt.Sprintf("failure decoding block: %v", err)) } @@ -1668,12 +1660,12 @@ func (e *Engine) DecodeAndCombine(newValues Values, block, buf []byte, nextTime }) values = append(values, newValues[:pos]...) remainingValues = newValues[pos:] - values = values.Deduplicate() + values = Values(values).Deduplicate() } else { - requireSort := values.MaxTime() >= newValues.MinTime() + requireSort := Values(values).MaxTime() >= newValues.MinTime() values = append(values, newValues...) if requireSort { - values = values.Deduplicate() + values = Values(values).Deduplicate() } } @@ -1682,7 +1674,7 @@ func (e *Engine) DecodeAndCombine(newValues Values, block, buf []byte, nextTime values = values[:e.MaxPointsPerBlock] } - encoded, err := values.Encode(buf) + encoded, err := Values(values).Encode(buf) if err != nil { return nil, nil, err } @@ -1701,12 +1693,12 @@ func (e *Engine) removeFileIfCheckpointExists(fileName string) bool { } // there's a checkpoint so we know this file isn't safe so we should remove it - err = os.Remove(fileName) + err = os.RemoveAll(fileName) if err != nil { panic(fmt.Sprintf("error removing file %s", err.Error())) } - err = os.Remove(checkpointName) + err = os.RemoveAll(checkpointName) if err != nil { panic(fmt.Sprintf("error removing file %s", err.Error())) } @@ -1745,41 +1737,211 @@ func (e *Engine) cleanupMetafile(name string) { } } -// openFileAndCehckpoint will create a checkpoint file, open a new file for -// writing a data index, write the header and return the file -func (e *Engine) openFileAndCheckpoint(fileName string) (*os.File, error) { - checkpointFile := fmt.Sprintf("%s.%s", fileName, CheckpointExtension) - cf, err := os.OpenFile(checkpointFile, os.O_CREATE, 0666) - if err != nil { - return nil, err - } - // _, err = cf.Write(u32tob(magicNumber)) - // if err != nil { - // panic(err) - // } - if err := cf.Close(); err != nil { - return nil, err - } - _, err = os.Stat(checkpointFile) +// compactionJob contains the data and methods for compacting multiple data files +// into fewer larger data files that ideally have larger blocks of points together +type compactionJob struct { + idsInCurrentFile []uint64 + startingPositions []uint32 + newFiles []*os.File - f, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR, 0666) - if err != nil { - return nil, err - } + dataFilesToCompact []*dataFile + dataFilePositions []uint32 + currentDataFileIDs []uint64 - // write the header, which is just the magic number - if _, err := f.Write(u32tob(magicNumber)); err != nil { - f.Close() - return nil, err - } + currentFile *os.File + currentPosition uint32 - return f, nil + maxFileSize uint32 + maxPointsPerBlock int + + minTime int64 + maxTime int64 + + // leftoverValues holds values from an ID that is getting split across multiple + // compacted data files + leftoverValues Values + + // buffer for encoding + buf []byte } -// removeCheckpoint removes the checkpoint for a new data file that was getting written -func (e *Engine) removeCheckpoint(fileName string) error { - checkpointFile := fmt.Sprintf("%s.%s", fileName, CheckpointExtension) - return os.Remove(checkpointFile) +// dataFileOEF is a sentinel values marking that there is no more data to be read from the data file +const dataFileEOF = uint64(math.MaxUint64) + +func newCompactionJob(files dataFiles, minTime, maxTime int64, maxFileSize uint32, maxPointsPerBlock int) *compactionJob { + c := &compactionJob{ + dataFilesToCompact: files, + dataFilePositions: make([]uint32, len(files)), + currentDataFileIDs: make([]uint64, len(files)), + maxFileSize: maxFileSize, + maxPointsPerBlock: maxPointsPerBlock, + minTime: minTime, + maxTime: maxTime, + buf: make([]byte, blockBufferSize), + } + + // set the starting positions and ids for the files getting compacted + for i, df := range files { + c.dataFilePositions[i] = uint32(fileHeaderSize) + c.currentDataFileIDs[i] = df.idForPosition(uint32(fileHeaderSize)) + } + + return c +} + +// newCurrentFile will create a new compaction file and reset the ids and positions +// in the file so we can write the index out later +func (c *compactionJob) newCurrentFile(fileName string) { + c.idsInCurrentFile = make([]uint64, 0) + c.startingPositions = make([]uint32, 0) + + f, err := openFileAndCheckpoint(fileName) + if err != nil { + panic(fmt.Sprintf("error opening new file: %s", err.Error())) + } + c.currentFile = f + c.currentPosition = uint32(fileHeaderSize) +} + +// writeBlocksForID will read data for the given ID from all the files getting compacted +// and write it into a new compacted file. Blocks from different files will be combined to +// create larger blocks in the compacted file. If the compacted file goes over the max +// file size limit, true will be returned indicating that its time to create a new compaction file +func (c *compactionJob) writeBlocksForID(id uint64) bool { + // mark this ID as new and track its starting position + c.idsInCurrentFile = append(c.idsInCurrentFile, id) + c.startingPositions = append(c.startingPositions, c.currentPosition) + + // loop through the files in order emptying each one of its data for this ID + + // first handle any values that didn't get written to the previous + // compaction file because it was too large + previousValues := c.leftoverValues + c.leftoverValues = nil + rotateFile := false + for i, df := range c.dataFilesToCompact { + idForFile := c.currentDataFileIDs[i] + + // if the next ID in this file doesn't match, move to the next file + if idForFile != id { + continue + } + + var newFilePosition uint32 + var nextID uint64 + + // write out the values and keep track of the next ID and position in this file + previousValues, rotateFile, newFilePosition, nextID = c.writeIDFromFile(id, previousValues, c.dataFilePositions[i], df) + c.dataFilePositions[i] = newFilePosition + c.currentDataFileIDs[i] = nextID + + // if we hit the max file size limit, return so a new file to compact into can be allocated + if rotateFile { + c.leftoverValues = previousValues + c.writeOutCurrentFile() + return true + } + } + + if len(previousValues) > 0 { + bytesWritten := writeValues(c.currentFile, id, previousValues, c.buf) + c.currentPosition += bytesWritten + } + + return false +} + +// writeIDFromFile will read all data from the passed in file for the given ID and either buffer the values in memory if below the +// max points allowed in a block, or write out to the file. The remaining buffer will be returned along with a bool indicating if +// we need a new file to compact into, the current position of the data file now that we've read data, and the next ID to be read +// from the data file +func (c *compactionJob) writeIDFromFile(id uint64, previousValues Values, filePosition uint32, df *dataFile) (Values, bool, uint32, uint64) { + for { + // check if we're at the end of the file + indexPosition := df.indexPosition() + if filePosition >= indexPosition { + return previousValues, false, filePosition, dataFileEOF + } + + // check if we're at the end of the blocks for this ID + nextID, _, block := df.block(filePosition) + if nextID != id { + return previousValues, false, filePosition, nextID + } + + blockLength := uint32(blockHeaderSize + len(block)) + filePosition += blockLength + + // decode the block and append to previous values + // TODO: update this so that blocks already at their limit don't need decoding + var values []Value + err := DecodeBlock(block, &values) + if err != nil { + panic(fmt.Sprintf("error decoding block: %s", err.Error())) + } + + previousValues = append(previousValues, values...) + + // if we've hit the block limit, encode and write out to the file + if len(previousValues) > c.maxPointsPerBlock { + valuesToEncode := previousValues[:c.maxPointsPerBlock] + previousValues = previousValues[c.maxPointsPerBlock:] + + bytesWritten := writeValues(c.currentFile, id, valuesToEncode, c.buf) + c.currentPosition += bytesWritten + + // if we're at the limit of what should go into the current file, + // return the values we've decoded and return the ID in the next + // block + if c.shouldRotateCurrentFile() { + if filePosition >= indexPosition { + return previousValues, true, filePosition, dataFileEOF + } + + nextID, _, _ = df.block(filePosition) + return previousValues, true, filePosition, nextID + } + } + } +} + +// nextID returns the lowest number ID to be read from one of the data files getting +// compacted. Will return an EOF if all files have been read and compacted +func (c *compactionJob) nextID() uint64 { + minID := dataFileEOF + for _, id := range c.currentDataFileIDs { + if minID > id { + minID = id + } + } + + // if the min is still EOF, we're done with all the data from all files to compact + if minID == dataFileEOF { + return dataFileEOF + } + + return minID +} + +// writeOutCurrentFile will write the index out to the current file in preparation for a new file to compact into +func (c *compactionJob) writeOutCurrentFile() { + if c.currentFile == nil { + return + } + + // write out the current file + if err := writeIndex(c.currentFile, c.minTime, c.maxTime, c.idsInCurrentFile, c.startingPositions); err != nil { + panic(fmt.Sprintf("error writing index: %s", err.Error())) + } + + // mark it as a new file and reset + c.newFiles = append(c.newFiles, c.currentFile) + c.currentFile = nil +} + +// shouldRotateCurrentFile returns true if the current file is over the max file size +func (c *compactionJob) shouldRotateCurrentFile() bool { + return c.currentPosition+footerSize(len(c.idsInCurrentFile)) > c.maxFileSize } type dataFile struct { @@ -1804,6 +1966,11 @@ const ( ) func NewDataFile(f *os.File) (*dataFile, error) { + // seek back to the beginning to hand off to the mmap + if _, err := f.Seek(0, 0); err != nil { + return nil, err + } + fInfo, err := f.Stat() if err != nil { return nil, err @@ -1821,6 +1988,22 @@ func NewDataFile(f *os.File) (*dataFile, error) { }, nil } +func (d *dataFile) Name() string { + d.mu.RLock() + defer d.mu.RUnlock() + + if d.Deleted() { + return "" + } + return d.f.Name() +} + +func (d *dataFile) Deleted() bool { + d.mu.RLock() + defer d.mu.RUnlock() + return d.f == nil +} + func (d *dataFile) Close() error { d.mu.Lock() defer d.mu.Unlock() @@ -1830,10 +2013,16 @@ func (d *dataFile) Close() error { func (d *dataFile) Delete() error { d.mu.Lock() defer d.mu.Unlock() + if err := d.close(); err != nil { return err } - err := os.Remove(d.f.Name()) + + if d.f == nil { + return nil + } + + err := os.RemoveAll(d.f.Name()) if err != nil { return err } @@ -1855,22 +2044,49 @@ func (d *dataFile) close() error { } func (d *dataFile) MinTime() int64 { + d.mu.RLock() + defer d.mu.RUnlock() + + if len(d.mmap) == 0 { + return 0 + } minTimePosition := d.size - minTimeOffset timeBytes := d.mmap[minTimePosition : minTimePosition+timeSize] return int64(btou64(timeBytes)) } func (d *dataFile) MaxTime() int64 { + d.mu.RLock() + defer d.mu.RUnlock() + + if len(d.mmap) == 0 { + return 0 + } + maxTimePosition := d.size - maxTimeOffset timeBytes := d.mmap[maxTimePosition : maxTimePosition+timeSize] return int64(btou64(timeBytes)) } func (d *dataFile) SeriesCount() uint32 { + d.mu.RLock() + defer d.mu.RUnlock() + + if len(d.mmap) == 0 { + return 0 + } + return btou32(d.mmap[d.size-seriesCountSize:]) } func (d *dataFile) IDToPosition() map[uint64]uint32 { + d.mu.RLock() + defer d.mu.RUnlock() + + if len(d.mmap) == 0 { + return nil + } + count := int(d.SeriesCount()) m := make(map[uint64]uint32) @@ -1886,6 +2102,13 @@ func (d *dataFile) IDToPosition() map[uint64]uint32 { } func (d *dataFile) indexPosition() uint32 { + d.mu.RLock() + defer d.mu.RUnlock() + + if len(d.mmap) == 0 { + return 0 + } + return d.size - uint32(d.SeriesCount()*12+20) } @@ -1893,6 +2116,12 @@ func (d *dataFile) indexPosition() uint32 { // first block for the given ID. If zero is returned the ID doesn't // have any data in this file. func (d *dataFile) StartingPositionForID(id uint64) uint32 { + d.mu.RLock() + defer d.mu.RUnlock() + + if len(d.mmap) == 0 { + return 0 + } seriesCount := d.SeriesCount() indexStart := d.indexPosition() @@ -1919,13 +2148,20 @@ func (d *dataFile) StartingPositionForID(id uint64) uint32 { } func (d *dataFile) block(pos uint32) (id uint64, t int64, block []byte) { + d.mu.RLock() + defer d.mu.RUnlock() + + if len(d.mmap) == 0 { + return 0, 0, nil + } + defer func() { if r := recover(); r != nil { panic(fmt.Sprintf("panic decoding file: %s at position %d for id %d at time %d", d.f.Name(), pos, id, t)) } }() if pos < d.indexPosition() { - id = btou64(d.mmap[pos : pos+8]) + id = d.idForPosition(pos) length := btou32(d.mmap[pos+8 : pos+12]) block = d.mmap[pos+blockHeaderSize : pos+blockHeaderSize+length] t = int64(btou64(d.mmap[pos+blockHeaderSize : pos+blockHeaderSize+8])) @@ -1933,12 +2169,158 @@ func (d *dataFile) block(pos uint32) (id uint64, t int64, block []byte) { return } +// idForPosition assumes the position is the start of an ID and will return the converted bytes as a uint64 ID +func (d *dataFile) idForPosition(pos uint32) uint64 { + d.mu.RLock() + defer d.mu.RUnlock() + + if len(d.mmap) == 0 { + return 0 + } + + return btou64(d.mmap[pos : pos+seriesIDSize]) +} + type dataFiles []*dataFile func (a dataFiles) Len() int { return len(a) } func (a dataFiles) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a dataFiles) Less(i, j int) bool { return a[i].MinTime() < a[j].MinTime() } +func dataFilesEquals(a, b []*dataFile) bool { + if len(a) != len(b) { + return false + } + for i, v := range a { + if v.MinTime() != b[i].MinTime() && v.MaxTime() != b[i].MaxTime() { + return false + } + } + return true +} + +// compactionCheckpoint holds the new files and compacted files from a compaction +type compactionCheckpoint struct { + CompactedFiles []string + NewFiles []string +} + +// cleanup will remove all the checkpoint files and old compacted files from a compaction +// that finsihed, but didn't get to cleanup yet +func (c *compactionCheckpoint) cleanup() { + for _, fn := range c.CompactedFiles { + cn := checkpointFileName(fn) + if err := os.RemoveAll(cn); err != nil { + panic(fmt.Sprintf("error removing checkpoint file: %s", err.Error())) + } + if err := os.RemoveAll(fn); err != nil { + panic(fmt.Sprintf("error removing old data file: %s", err.Error())) + } + } + + for _, fn := range c.NewFiles { + cn := checkpointFileName(fn) + if err := os.RemoveAll(cn); err != nil { + panic(fmt.Sprintf("error removing checkpoint file: %s", err.Error())) + } + } +} + +// footerSize will return what the size of the index and footer of a data file +// will be given the passed in series count +func footerSize(seriesCount int) uint32 { + timeSizes := 2 * timeSize + return uint32(seriesCount*(seriesIDSize+seriesPositionSize) + timeSizes + seriesCountSize) +} + +// writeValues will encode the values and write them as a compressed block to the file +func writeValues(f *os.File, id uint64, values Values, buf []byte) uint32 { + b, err := values.Encode(buf) + if err != nil { + panic(fmt.Sprintf("failure encoding block: %s", err.Error())) + } + + if err := writeBlock(f, id, b); err != nil { + // fail hard. If we can't write a file someone needs to get woken up + panic(fmt.Sprintf("failure writing block: %s", err.Error())) + } + + return uint32(blockHeaderSize + len(b)) +} + +// writeBlock will write a compressed block including its header +func writeBlock(f *os.File, id uint64, block []byte) error { + if _, err := f.Write(append(u64tob(id), u32tob(uint32(len(block)))...)); err != nil { + return err + } + _, err := f.Write(block) + return err +} + +// writeIndex will write out the index block and the footer of the file. After this call it should +// be a read only file that can be mmap'd as a dataFile +func writeIndex(f *os.File, minTime, maxTime int64, ids []uint64, newPositions []uint32) error { + // write the file index, starting with the series ids and their positions + for i, id := range ids { + if _, err := f.Write(u64tob(id)); err != nil { + return err + } + if _, err := f.Write(u32tob(newPositions[i])); err != nil { + return err + } + } + + // write the min time, max time + if _, err := f.Write(append(u64tob(uint64(minTime)), u64tob(uint64(maxTime))...)); err != nil { + return err + } + + // series count + if _, err := f.Write(u32tob(uint32(len(ids)))); err != nil { + return err + } + + // sync it + return f.Sync() +} + +// openFileAndCehckpoint will create a checkpoint file, open a new file for +// writing a data index, write the header and return the file +func openFileAndCheckpoint(fileName string) (*os.File, error) { + checkpointFile := checkpointFileName(fileName) + cf, err := os.OpenFile(checkpointFile, os.O_CREATE, 0666) + if err != nil { + return nil, err + } + if err := cf.Close(); err != nil { + return nil, err + } + + f, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR, 0666) + if err != nil { + return nil, err + } + + // write the header, which is just the magic number + if _, err := f.Write(u32tob(magicNumber)); err != nil { + f.Close() + return nil, err + } + + return f, nil +} + +// checkpointFileName will return the checkpoint name for the data files +func checkpointFileName(fileName string) string { + return fmt.Sprintf("%s.%s", fileName, CheckpointExtension) +} + +// removeCheckpoint removes the checkpoint for a new data file that was getting written +func removeCheckpoint(fileName string) error { + checkpointFile := fmt.Sprintf("%s.%s", fileName, CheckpointExtension) + return os.RemoveAll(checkpointFile) +} + // u64tob converts a uint64 into an 8-byte slice. func u64tob(v uint64) []byte { b := make([]byte, 8) @@ -1960,10 +2342,17 @@ func btou32(b []byte) uint32 { return uint32(binary.BigEndian.Uint32(b)) } +// hashSeriesField will take the fnv-1a hash of the key. It returns the value +// or 1 if the hash is either 0 or the max uint64. It does this to keep sentinel +// values available. func hashSeriesField(key string) uint64 { h := fnv.New64a() h.Write([]byte(key)) - return h.Sum64() + n := h.Sum64() + if n == uint64(0) || n == uint64(math.MaxUint64) { + return 1 + } + return n } // SeriesFieldKey combine a series key and field name for a unique string to be hashed to a numeric ID diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/tsm1_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/tsm1_test.go index d9a851aa1..444a63a91 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/tsm1_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/tsm1_test.go @@ -7,6 +7,7 @@ import ( "math" "os" "reflect" + "sync" "testing" "time" @@ -238,6 +239,225 @@ func TestEngine_WriteOverwritePreviousPoint(t *testing.T) { } } +// Tests that writing a point that before the earliest point +// is queryable before and after a full compaction +func TestEngine_Write_BeforeFirstPoint(t *testing.T) { + e := OpenDefaultEngine() + defer e.Close() + + e.RotateFileSize = 1 + + fields := []string{"value"} + + p1 := parsePoint("cpu,host=A value=1.1 1000000000") + p2 := parsePoint("cpu,host=A value=1.2 2000000000") + p3 := parsePoint("cpu,host=A value=1.3 3000000000") + p4 := parsePoint("cpu,host=A value=1.4 0000000000") // earlier than first point + + verify := func(points []models.Point) { + tx2, _ := e.Begin(false) + defer tx2.Rollback() + c := tx2.Cursor("cpu,host=A", fields, nil, true) + k, v := c.SeekTo(0) + + for _, p := range points { + if k != p.UnixNano() { + t.Fatalf("time wrong:\n\texp:%d\n\tgot:%d\n", p.UnixNano(), k) + } + if v != p.Fields()["value"] { + t.Fatalf("data wrong:\n\texp:%f\n\tgot:%f", p.Fields()["value"], v.(float64)) + } + k, v = c.Next() + } + } + + // Write each point individually to force file rotation + for _, p := range []models.Point{p1, p2} { + if err := e.WritePoints([]models.Point{p}, nil, nil); err != nil { + t.Fatalf("failed to write points: %s", err.Error()) + } + } + + verify([]models.Point{p1, p2}) + + // Force a full compaction + e.CompactionAge = time.Duration(0) + if err := e.Compact(true); err != nil { + t.Fatalf("failed to run full compaction: %v", err) + } + + // Write a point before the earliest data file + if err := e.WritePoints([]models.Point{p3, p4}, nil, nil); err != nil { + t.Fatalf("failed to write points: %s", err.Error()) + } + + // Verify earlier point is returned in the correct order before compaction + verify([]models.Point{p4, p1, p2, p3}) + + if err := e.Compact(true); err != nil { + t.Fatalf("failed to run full compaction: %v", err) + } + // Verify earlier point is returned in the correct order after compaction + verify([]models.Point{p4, p1, p2, p3}) +} + +// Tests that writing a series with different fields is queryable before and +// after a full compaction +func TestEngine_Write_MixedFields(t *testing.T) { + e := OpenDefaultEngine() + defer e.Close() + + e.RotateFileSize = 1 + + fields := []string{"value", "value2"} + + p1 := parsePoint("cpu,host=A value=1.1 1000000000") + p2 := parsePoint("cpu,host=A value=1.2,value2=2.1 2000000000") + p3 := parsePoint("cpu,host=A value=1.3 3000000000") + p4 := parsePoint("cpu,host=A value=1.4 4000000000") + + verify := func(points []models.Point) { + tx2, _ := e.Begin(false) + defer tx2.Rollback() + c := tx2.Cursor("cpu,host=A", fields, nil, true) + k, v := c.SeekTo(0) + + for _, p := range points { + if k != p.UnixNano() { + t.Fatalf("time wrong:\n\texp:%d\n\tgot:%d\n", p.UnixNano(), k) + } + + pv := v.(map[string]interface{}) + for _, key := range fields { + if pv[key] != p.Fields()[key] { + t.Fatalf("data wrong:\n\texp:%v\n\tgot:%v", p.Fields()[key], pv[key]) + } + } + k, v = c.Next() + } + } + + // Write each point individually to force file rotation + for _, p := range []models.Point{p1, p2} { + if err := e.WritePoints([]models.Point{p}, nil, nil); err != nil { + t.Fatalf("failed to write points: %s", err.Error()) + } + } + + verify([]models.Point{p1, p2}) + + // Force a full compaction + e.CompactionAge = time.Duration(0) + if err := e.Compact(true); err != nil { + t.Fatalf("failed to run full compaction: %v", err) + } + + // Write a point before the earliest data file + if err := e.WritePoints([]models.Point{p3, p4}, nil, nil); err != nil { + t.Fatalf("failed to write points: %s", err.Error()) + } + + // Verify points returned in the correct order before compaction + verify([]models.Point{p1, p2, p3, p4}) + + if err := e.Compact(true); err != nil { + t.Fatalf("failed to run full compaction: %v", err) + } + // Verify points returned in the correct order after compaction + verify([]models.Point{p1, p2, p3, p4}) +} + +// Tests that writing and compactions running concurrently does not +// fail. +func TestEngine_WriteCompaction_Concurrent(t *testing.T) { + e := OpenDefaultEngine() + defer e.Close() + + e.RotateFileSize = 1 + + done := make(chan struct{}) + total := 1000 + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + i := 0 + for { + if i > total { + return + } + + pt := models.MustNewPoint("cpu", + map[string]string{"host": "A"}, + map[string]interface{}{"value": i}, + time.Unix(int64(i), 0), + ) + if err := e.WritePoints([]models.Point{pt}, nil, nil); err != nil { + t.Fatalf("failed to write points: %s", err.Error()) + } + i++ + } + }() + + // Force a compactions to happen + e.CompactionAge = time.Duration(0) + + // Run compactions concurrently + wg.Add(1) + go func() { + defer wg.Done() + for { + select { + case <-done: + return + default: + } + + if err := e.Compact(false); err != nil { + t.Fatalf("failed to run full compaction: %v", err) + } + } + }() + + // Let the goroutines run for a second + select { + case <-time.After(1 * time.Second): + close(done) + } + + // Wait for them to exit + wg.Wait() + + tx2, _ := e.Begin(false) + defer tx2.Rollback() + c := tx2.Cursor("cpu,host=A", []string{"value"}, nil, true) + k, v := c.SeekTo(0) + + // Verify we wrote and can read all the points + i := 0 + for { + if exp := time.Unix(int64(i), 0).UnixNano(); k != exp { + t.Fatalf("time wrong:\n\texp:%d\n\tgot:%d\n", exp, k) + } + + if exp := int64(i); v != exp { + t.Fatalf("value wrong:\n\texp:%v\n\tgot:%v", exp, v) + } + + k, v = c.Next() + if k == tsdb.EOF { + break + } + i += 1 + } + + if i != total { + t.Fatalf("point count mismatch: got %v, exp %v", i, total) + } + +} + func TestEngine_CursorCombinesWALAndIndex(t *testing.T) { e := OpenDefaultEngine() defer e.Close() @@ -860,7 +1080,6 @@ func TestEngine_CompactionWithCopiedBlocks(t *testing.T) { e.RotateFileSize = 10 e.MaxPointsPerBlock = 1 - e.RotateBlockSize = 10 p1 := parsePoint("cpu,host=A value=1.1 1000000000") p2 := parsePoint("cpu,host=A value=1.2 2000000000") @@ -1453,6 +1672,134 @@ func TestEngine_DecodeAndCombine_NoNewValues(t *testing.T) { } } +func TestEngine_Write_Concurrent(t *testing.T) { + e := OpenDefaultEngine() + defer e.Engine.Close() + + values1 := make(tsm1.Values, 1) + values1[0] = tsm1.NewValue(time.Unix(0, 0), float64(1)) + + pointsByKey1 := map[string]tsm1.Values{ + "foo": values1, + } + + values2 := make(tsm1.Values, 1) + values2[0] = tsm1.NewValue(time.Unix(10, 0), float64(1)) + + pointsByKey2 := map[string]tsm1.Values{ + "foo": values2, + } + + var wg sync.WaitGroup + for i := 0; i < 4; i++ { + wg.Add(1) + go func() { + e.Write(pointsByKey1, nil, nil) + wg.Done() + }() + wg.Add(1) + go func() { + e.Write(pointsByKey2, nil, nil) + wg.Done() + }() + } + wg.Wait() +} + +// Ensure the index won't compact files that would cause the +// resulting file to be larger than the max file size +func TestEngine_IndexFileSizeLimitedDuringCompaction(t *testing.T) { + e := OpenDefaultEngine() + defer e.Close() + + e.RotateFileSize = 10 + e.MaxFileSize = 100 + e.MaxPointsPerBlock = 3 + + p1 := parsePoint("cpu,host=A value=1.1 1000000000") + p2 := parsePoint("cpu,host=A value=1.2 2000000000") + p3 := parsePoint("cpu,host=A value=1.3 3000000000") + p4 := parsePoint("cpu,host=A value=1.5 4000000000") + p5 := parsePoint("cpu,host=A value=1.6 5000000000") + p6 := parsePoint("cpu,host=B value=2.1 2000000000") + + if err := e.WritePoints([]models.Point{p1, p2}, nil, nil); err != nil { + t.Fatalf("failed to write points: %s", err.Error()) + } + if err := e.WritePoints([]models.Point{p3}, nil, nil); err != nil { + t.Fatalf("failed to write points: %s", err.Error()) + } + + if err := e.WritePoints([]models.Point{p4, p5, p6}, nil, nil); err != nil { + t.Fatalf("failed to write points: %s", err.Error()) + } + + if count := e.DataFileCount(); count != 3 { + t.Fatalf("execpted 3 data file but got %d", count) + } + + if err := checkPoints(e, "cpu,host=A", []models.Point{p1, p2, p3, p4, p5}); err != nil { + t.Fatal(err.Error()) + } + if err := checkPoints(e, "cpu,host=B", []models.Point{p6}); err != nil { + t.Fatal(err.Error()) + } + + if err := e.Compact(true); err != nil { + t.Fatalf("error compacting: %s", err.Error()) + } + + if err := checkPoints(e, "cpu,host=A", []models.Point{p1, p2, p3, p4, p5}); err != nil { + t.Fatal(err.Error()) + } + if err := checkPoints(e, "cpu,host=B", []models.Point{p6}); err != nil { + t.Fatal(err.Error()) + } + + if count := e.DataFileCount(); count != 2 { + t.Fatalf("expected 1 data file but got %d", count) + } +} + +// Ensure the index will split a large data file in two if a write +// will cause the resulting data file to be larger than the max file +// size limit +func TestEngine_IndexFilesSplitOnWriteToLargeFile(t *testing.T) { +} + +// checkPoints will ensure that the engine has the points passed in the block +// along with checking that seeks in the middle work and an EOF is hit at the end +func checkPoints(e *Engine, key string, points []models.Point) error { + tx, _ := e.Begin(false) + defer tx.Rollback() + c := tx.Cursor(key, []string{"value"}, nil, true) + k, v := c.SeekTo(0) + if k != points[0].UnixNano() { + return fmt.Errorf("wrong time:\n\texp: %d\n\tgot: %d", points[0].UnixNano(), k) + } + if got := points[0].Fields()["value"]; v != got { + return fmt.Errorf("wrong value:\n\texp: %v\n\tgot: %v", v, got) + } + points = points[1:] + for _, p := range points { + k, v = c.Next() + if k != p.UnixNano() { + return fmt.Errorf("wrong time:\n\texp: %d\n\tgot: %d", p.UnixNano(), k) + } + if got := p.Fields()["value"]; v != got { + return fmt.Errorf("wrong value:\n\texp: %v\n\tgot: %v", v, got) + } + } + k, _ = c.Next() + if k != tsdb.EOF { + return fmt.Errorf("expected EOF but got: %d", k) + } + + // TODO: seek tests + + return nil +} + // Engine represents a test wrapper for tsm1.Engine. type Engine struct { *tsm1.Engine diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/wal.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/wal.go index 053b88cac..5b0cf60bb 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/wal.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/wal.go @@ -58,6 +58,8 @@ const ( deleteEntry walEntryType = 0x04 ) +var ErrWALClosed = fmt.Errorf("WAL closed") + type Log struct { path string @@ -68,6 +70,7 @@ type Log struct { currentSegmentSize int // cache and flush variables + closing chan struct{} cacheLock sync.RWMutex lastWriteTime time.Time flushRunning bool @@ -128,6 +131,7 @@ func NewLog(path string) *Log { FlushMemorySizeThreshold: tsdb.DefaultFlushMemorySizeThreshold, MaxMemorySizeThreshold: tsdb.DefaultMaxMemorySizeThreshold, logger: log.New(os.Stderr, "[tsm1wal] ", log.LstdFlags), + closing: make(chan struct{}), } } @@ -136,7 +140,6 @@ func (l *Log) Path() string { return l.path } // Open opens and initializes the Log. Will recover from previous unclosed shutdowns func (l *Log) Open() error { - if l.LoggingEnabled { l.logger.Printf("tsm1 WAL starting with %d flush memory size threshold and %d max memory size threshold\n", l.FlushMemorySizeThreshold, l.MaxMemorySizeThreshold) l.logger.Printf("tsm1 WAL writing to %s\n", l.path) @@ -148,6 +151,7 @@ func (l *Log) Open() error { l.cache = make(map[string]Values) l.cacheDirtySort = make(map[string]bool) l.measurementFieldsCache = make(map[string]*tsdb.MeasurementFields) + l.closing = make(chan struct{}) // flush out any WAL entries that are there from before if err := l.readAndFlushWAL(); err != nil { @@ -194,8 +198,8 @@ func (l *Log) Cursor(series string, fields []string, dec *tsdb.FieldCodec, ascen func (l *Log) WritePoints(points []models.Point, fields map[string]*tsdb.MeasurementFields, series []*tsdb.SeriesCreate) error { // add everything to the cache, or return an error if we've hit our max memory - if addedToCache := l.addToCache(points, fields, series, true); !addedToCache { - return fmt.Errorf("WAL backed up flushing to index, hit max memory") + if err := l.addToCache(points, fields, series, true); err != nil { + return err } // make the write durable if specified @@ -240,7 +244,9 @@ func (l *Log) WritePoints(points []models.Point, fields map[string]*tsdb.Measure // usually skipping the cache is only for testing purposes and this was the easiest // way to represent the logic (to cache and then immediately flush) if l.SkipCache { - l.flush(idleFlush) + if err := l.flush(idleFlush); err != nil { + return err + } } return nil @@ -248,20 +254,31 @@ func (l *Log) WritePoints(points []models.Point, fields map[string]*tsdb.Measure // addToCache will add the points, measurements, and fields to the cache and return true if successful. They will be queryable // immediately after return and will be flushed at the next flush cycle. Before adding to the cache we check if we're over the -// max memory threshold. If we are we request a flush in a new goroutine and return false, indicating we didn't add the values +// max memory threshold. If we are we request a flush in a new goroutine and return an error, indicating we didn't add the values // to the cache and that writes should return a failure. -func (l *Log) addToCache(points []models.Point, fields map[string]*tsdb.MeasurementFields, series []*tsdb.SeriesCreate, checkMemory bool) bool { +func (l *Log) addToCache(points []models.Point, fields map[string]*tsdb.MeasurementFields, series []*tsdb.SeriesCreate, checkMemory bool) error { l.cacheLock.Lock() defer l.cacheLock.Unlock() + // Make sure the log has not been closed + select { + case <-l.closing: + return ErrWALClosed + default: + } + // if we should check memory and we're over the threshold, mark a flush as running and kick one off in a goroutine if checkMemory && l.memorySize > l.FlushMemorySizeThreshold { if !l.flushRunning { l.flushRunning = true - go l.flush(memoryFlush) + go func() { + if err := l.flush(memoryFlush); err != nil { + l.logger.Printf("addToCache: failed to flush: %v", err) + } + }() } if l.memorySize > l.MaxMemorySizeThreshold { - return false + return fmt.Errorf("WAL backed up flushing to index, hit max memory") } } @@ -289,7 +306,7 @@ func (l *Log) addToCache(points []models.Point, fields map[string]*tsdb.Measurem l.seriesToCreateCache = append(l.seriesToCreateCache, series...) l.lastWriteTime = time.Now() - return true + return nil } func (l *Log) LastWriteTime() time.Time { @@ -371,6 +388,7 @@ func (l *Log) readFileToCache(fileName string) error { case pointsEntry: points, err := models.ParsePoints(data) if err != nil { + l.logger.Printf("failed to parse points: %v", err) return err } l.addToCache(points, nil, nil, false) @@ -405,10 +423,18 @@ func (l *Log) writeToLog(writeType walEntryType, data []byte) error { l.writeLock.Lock() defer l.writeLock.Unlock() + // Make sure the log has not been closed + select { + case <-l.closing: + return ErrWALClosed + default: + } + if l.currentSegmentFile == nil || l.currentSegmentSize > DefaultSegmentSize { if err := l.newSegmentFile(); err != nil { - // fail hard since we can't write data - panic(fmt.Sprintf("error opening new segment file for wal: %s", err.Error())) + // A drop database or RP call could trigger this error if writes were in-flight + // when the drop statement executes. + return fmt.Errorf("error opening new segment file for wal: %s", err.Error()) } } @@ -495,10 +521,16 @@ func (l *Log) deleteKeysFromCache(keys []string) { // Close will finish any flush that is currently in process and close file handles func (l *Log) Close() error { - l.writeLock.Lock() l.cacheLock.Lock() - defer l.writeLock.Unlock() + l.writeLock.Lock() defer l.cacheLock.Unlock() + defer l.writeLock.Unlock() + + // If cache is nil, then we're not open. This avoids a double-close in tests. + if l.cache != nil { + // Close, but don't set to nil so future goroutines can still be signaled + close(l.closing) + } l.cache = nil l.measurementFieldsCache = nil @@ -514,6 +546,13 @@ func (l *Log) Close() error { // flush writes all wal data in memory to the index func (l *Log) flush(flush flushType) error { + // Make sure the log has not been closed + select { + case <-l.closing: + return ErrWALClosed + default: + } + // only flush if there isn't one already running. Memory flushes are only triggered // by writes, which will mark the flush as running, so we can ignore it. l.cacheLock.Lock() @@ -538,15 +577,18 @@ func (l *Log) flush(flush flushType) error { if flush == idleFlush { if l.currentSegmentFile != nil { if err := l.currentSegmentFile.Close(); err != nil { - return err + l.cacheLock.Unlock() + l.writeLock.Unlock() + return fmt.Errorf("error closing current segment: %v", err) } l.currentSegmentFile = nil l.currentSegmentSize = 0 } } else { if err := l.newSegmentFile(); err != nil { - // there's no recovering from this, fail hard - panic(fmt.Sprintf("error creating new wal file: %s", err.Error())) + l.cacheLock.Unlock() + l.writeLock.Unlock() + return fmt.Errorf("error creating new wal file: %v", err) } } l.writeLock.Unlock() @@ -596,6 +638,7 @@ func (l *Log) flush(flush flushType) error { startTime := time.Now() if err := l.IndexWriter.Write(l.flushCache, mfc, scc); err != nil { + l.logger.Printf("failed to flush to index: %v", err) return err } if l.LoggingEnabled { @@ -617,9 +660,9 @@ func (l *Log) flush(flush flushType) error { return err } if id <= lastFileID { - err := os.Remove(fn) + err := os.RemoveAll(fn) if err != nil { - return err + return fmt.Errorf("failed to remove: %v: %v", fn, err) } } } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/wal_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/wal_test.go index 0db5b91b7..f854a0e62 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/wal_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/tsm1/wal_test.go @@ -145,6 +145,138 @@ func TestLog_TestWriteQueryOpen(t *testing.T) { } } +// Tests that concurrent flushes and writes do not trigger race conditions +func TestLog_WritePoints_FlushConcurrent(t *testing.T) { + w := NewLog() + defer w.Close() + w.FlushMemorySizeThreshold = 1000 + total := 1000 + + w.IndexWriter.WriteFn = func(valuesByKey map[string]tsm1.Values, measurementFieldsToSave map[string]*tsdb.MeasurementFields, seriesToCreate []*tsdb.SeriesCreate) error { + return nil + } + + if err := w.Open(); err != nil { + t.Fatalf("error opening: %s", err.Error()) + } + + done := make(chan struct{}) + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + for { + select { + case <-done: + return + default: + } + + // Force an idle flush + if err := w.Flush(); err != nil { + t.Fatalf("failed to run full compaction: %v", err) + } + // Allow some time so memory flush can occur due to writes + time.Sleep(10 * time.Millisecond) + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + i := 0 + for { + if i > total { + return + } + select { + case <-done: + return + default: + } + + pt := models.MustNewPoint("cpu", + map[string]string{"host": "A"}, + map[string]interface{}{"value": i}, + time.Unix(int64(i), 0), + ) + + if err := w.WritePoints([]models.Point{pt}, nil, nil); err != nil { + t.Fatalf("failed to write points: %s", err.Error()) + } + i++ + } + }() + + // Let the goroutines run for a second + select { + case <-time.After(1 * time.Second): + close(done) + } + + // Wait for them to exit + wg.Wait() +} + +// Tests that concurrent writes when the WAL closes do not cause race conditions. +func TestLog_WritePoints_CloseConcurrent(t *testing.T) { + w := NewLog() + defer w.Close() + w.FlushMemorySizeThreshold = 1000 + total := 1000 + + w.IndexWriter.WriteFn = func(valuesByKey map[string]tsm1.Values, measurementFieldsToSave map[string]*tsdb.MeasurementFields, seriesToCreate []*tsdb.SeriesCreate) error { + return nil + } + + if err := w.Open(); err != nil { + t.Fatalf("error opening: %s", err.Error()) + } + + done := make(chan struct{}) + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + i := 0 + for { + if i > total { + return + } + select { + case <-done: + return + default: + } + + pt := models.MustNewPoint("cpu", + map[string]string{"host": "A"}, + map[string]interface{}{"value": i}, + time.Unix(int64(i), 0), + ) + + if err := w.WritePoints([]models.Point{pt}, nil, nil); err != nil && err != tsm1.ErrWALClosed { + t.Fatalf("failed to write points: %s", err.Error()) + } + i++ + } + }() + + time.Sleep(10 * time.Millisecond) + if err := w.Close(); err != nil { + t.Fatalf("failed to close WAL: %v", err) + } + + // Let the goroutines run for a second + select { + case <-time.After(1 * time.Second): + close(done) + } + + // Wait for them to exit + wg.Wait() +} + // Ensure the log can handle random data. func TestLog_Quick(t *testing.T) { if testing.Short() { @@ -403,7 +535,7 @@ type Point struct { Time time.Time } -func (p *Point) Encode() models.Point { return models.NewPoint(p.Name, p.Tags, p.Fields, p.Time) } +func (p *Point) Encode() models.Point { return models.MustNewPoint(p.Name, p.Tags, p.Fields, p.Time) } type Series struct { Name string diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/wal/wal.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/wal/wal.go index 0bec6c2a3..8e98207f7 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/wal/wal.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/engine/wal/wal.go @@ -72,19 +72,19 @@ const ( // Statistics maintained by the WAL const ( - statPointsWriteReq = "points_write_req" - statPointsWrite = "points_write" + statPointsWriteReq = "pointsWriteReq" + statPointsWrite = "pointsWrite" statFlush = "flush" - statAutoFlush = "auto_flush" - statIdleFlush = "idle_flush" - statMetadataFlush = "meta_flush" - statThresholdFlush = "threshold_flush" - statMemoryFlush = "mem_flush" - statSeriesFlushed = "series_flush" - statPointsFlushed = "points_flush" - statFlushDuration = "flush_duration" - statWriteFail = "write_fail" - statMemorySize = "mem_size" + statAutoFlush = "autoFlush" + statIdleFlush = "idleFlush" + statMetadataFlush = "metaFlush" + statThresholdFlush = "thresholdFlush" + statMemoryFlush = "memFlush" + statSeriesFlushed = "seriesFlush" + statPointsFlushed = "pointsFlush" + statFlushDuration = "flushDuration" + statWriteFail = "writeFail" + statMemorySize = "memSize" ) // flushType indiciates why a flush and compaction are being run so the partition can diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/executor.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/executor.go index fc6ccd392..11016e232 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/executor.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/executor.go @@ -1,1158 +1,8 @@ package tsdb -import ( - "fmt" - "math" - "sort" - "time" - - "github.com/influxdb/influxdb/influxql" - "github.com/influxdb/influxdb/models" -) - -const ( - // Return an error if the user is trying to select more than this number of points in a group by statement. - // Most likely they specified a group by interval without time boundaries. - MaxGroupByPoints = 100000 - - // Since time is always selected, the column count when selecting only a single other value will be 2 - SelectColumnCountWithOneValue = 2 - - // IgnoredChunkSize is what gets passed into Mapper.Begin for aggregate queries as they don't chunk points out - IgnoredChunkSize = 0 -) +import "github.com/influxdb/influxdb/models" // Executor is an interface for a query executor. type Executor interface { Execute() <-chan *models.Row } - -type SelectExecutor struct { - stmt *influxql.SelectStatement - mappers []*StatefulMapper - chunkSize int - limitedTagSets map[string]struct{} // Set tagsets for which data has reached the LIMIT. -} - -// NewSelectExecutor returns a new SelectExecutor. -func NewSelectExecutor(stmt *influxql.SelectStatement, mappers []Mapper, chunkSize int) *SelectExecutor { - a := []*StatefulMapper{} - for _, m := range mappers { - a = append(a, &StatefulMapper{m, nil, false}) - } - return &SelectExecutor{ - stmt: stmt, - mappers: a, - chunkSize: chunkSize, - limitedTagSets: make(map[string]struct{}), - } -} - -// Execute begins execution of the query and returns a channel to receive rows. -func (e *SelectExecutor) Execute() <-chan *models.Row { - // Create output channel and stream data in a separate goroutine. - out := make(chan *models.Row, 0) - - // Certain operations on the SELECT statement can be performed by the SelectExecutor without - // assistance from the Mappers. This allows the SelectExecutor to prepare aggregation functions - // and mathematical functions. - e.stmt.RewriteDistinct() - - if (e.stmt.IsRawQuery && !e.stmt.HasDistinct()) || e.stmt.IsSimpleDerivative() { - go e.executeRaw(out) - } else { - go e.executeAggregate(out) - } - return out -} - -// mappersDrained returns whether all the executors Mappers have been drained of data. -func (e *SelectExecutor) mappersDrained() bool { - for _, m := range e.mappers { - if !m.drained { - return false - } - } - return true -} - -// nextMapperTagset returns the alphabetically lowest tagset across all Mappers. -func (e *SelectExecutor) nextMapperTagSet() string { - tagset := "" - for _, m := range e.mappers { - if m.bufferedChunk != nil { - if tagset == "" { - tagset = m.bufferedChunk.key() - } else if m.bufferedChunk.key() < tagset { - tagset = m.bufferedChunk.key() - } - } - } - return tagset -} - -// nextMapperLowestTime returns the lowest minimum time across all Mappers, for the given tagset. -func (e *SelectExecutor) nextMapperLowestTime(tagset string) int64 { - minTime := int64(math.MaxInt64) - for _, m := range e.mappers { - if !m.drained && m.bufferedChunk != nil { - if m.bufferedChunk.key() != tagset { - continue - } - t := m.bufferedChunk.Values[len(m.bufferedChunk.Values)-1].Time - if t < minTime { - minTime = t - } - } - } - return minTime -} - -// nextMapperHighestTime returns the highest time across all Mappers, for the given tagset. -func (e *SelectExecutor) nextMapperHighestTime(tagset string) int64 { - maxTime := int64(math.MinInt64) - for _, m := range e.mappers { - if !m.drained && m.bufferedChunk != nil { - if m.bufferedChunk.key() != tagset { - continue - } - t := m.bufferedChunk.Values[0].Time - if t > maxTime { - maxTime = t - } - } - } - return maxTime -} - -// tagSetIsLimited returns whether data for the given tagset has been LIMITed. -func (e *SelectExecutor) tagSetIsLimited(tagset string) bool { - _, ok := e.limitedTagSets[tagset] - return ok -} - -// limitTagSet marks the given taset as LIMITed. -func (e *SelectExecutor) limitTagSet(tagset string) { - e.limitedTagSets[tagset] = struct{}{} -} - -func (e *SelectExecutor) executeRaw(out chan *models.Row) { - // It's important that all resources are released when execution completes. - defer e.close() - - // Open the mappers. - for _, m := range e.mappers { - if err := m.Open(); err != nil { - out <- &models.Row{Err: err} - return - } - } - - // Get the distinct fields across all mappers. - var selectFields, aliasFields []string - if e.stmt.HasWildcard() { - sf := newStringSet() - for _, m := range e.mappers { - sf.add(m.Fields()...) - } - selectFields = sf.list() - aliasFields = selectFields - } else { - selectFields = e.stmt.Fields.Names() - aliasFields = e.stmt.Fields.AliasNames() - } - - // Used to read ahead chunks from mappers. - var rowWriter *limitedRowWriter - var currTagset string - - // Keep looping until all mappers drained. - var err error - for { - // Get the next chunk from each Mapper. - for _, m := range e.mappers { - if m.drained { - continue - } - - // Set the next buffered chunk on the mapper, or mark it drained. - for { - if m.bufferedChunk == nil { - m.bufferedChunk, err = m.NextChunk() - if err != nil { - out <- &models.Row{Err: err} - return - } - if m.bufferedChunk == nil { - // Mapper can do no more for us. - m.drained = true - break - } - - // If the SELECT query is on more than 1 field, but the chunks values from the Mappers - // only contain a single value, create k-v pairs using the field name of the chunk - // and the value of the chunk. If there is only 1 SELECT field across all mappers then - // there is no need to create k-v pairs, and there is no need to distinguish field data, - // as it is all for the *same* field. - if len(selectFields) > 1 && len(m.bufferedChunk.Fields) == 1 { - fieldKey := m.bufferedChunk.Fields[0] - - for i := range m.bufferedChunk.Values { - field := map[string]interface{}{fieldKey: m.bufferedChunk.Values[i].Value} - m.bufferedChunk.Values[i].Value = field - } - } - } - - if e.tagSetIsLimited(m.bufferedChunk.Name) { - // chunk's tagset is limited, so no good. Try again. - m.bufferedChunk = nil - continue - } - // This mapper has a chunk available, and it is not limited. - break - } - } - - // All Mappers done? - if e.mappersDrained() { - rowWriter.Flush() - break - } - - // Send out data for the next alphabetically-lowest tagset. All Mappers emit data in this order, - // so by always continuing with the lowest tagset until it is finished, we process all data in - // the required order, and don't "miss" any. - tagset := e.nextMapperTagSet() - if tagset != currTagset { - currTagset = tagset - // Tagset has changed, time for a new rowWriter. Be sure to kick out any residual values. - rowWriter.Flush() - rowWriter = nil - } - - ascending := true - if len(e.stmt.SortFields) > 0 { - ascending = e.stmt.SortFields[0].Ascending - } - - var timeBoundary int64 - - if ascending { - // Process the mapper outputs. We can send out everything up to the min of the last time - // of the chunks for the next tagset. - timeBoundary = e.nextMapperLowestTime(tagset) - } else { - timeBoundary = e.nextMapperHighestTime(tagset) - } - - // Now empty out all the chunks up to the min time. Create new output struct for this data. - var chunkedOutput *MapperOutput - for _, m := range e.mappers { - if m.drained { - continue - } - - chunkBoundary := false - if ascending { - chunkBoundary = m.bufferedChunk.Values[0].Time > timeBoundary - } else { - chunkBoundary = m.bufferedChunk.Values[0].Time < timeBoundary - } - - // This mapper's next chunk is not for the next tagset, or the very first value of - // the chunk is at a higher acceptable timestamp. Skip it. - if m.bufferedChunk.key() != tagset || chunkBoundary { - continue - } - - // Find the index of the point up to the min. - ind := len(m.bufferedChunk.Values) - for i, mo := range m.bufferedChunk.Values { - if ascending && mo.Time > timeBoundary { - ind = i - break - } else if !ascending && mo.Time < timeBoundary { - ind = i - break - } - - } - - // Add up to the index to the values - if chunkedOutput == nil { - chunkedOutput = &MapperOutput{ - Name: m.bufferedChunk.Name, - Tags: m.bufferedChunk.Tags, - cursorKey: m.bufferedChunk.key(), - } - chunkedOutput.Values = m.bufferedChunk.Values[:ind] - } else { - chunkedOutput.Values = append(chunkedOutput.Values, m.bufferedChunk.Values[:ind]...) - } - - // Clear out the values being sent out, keep the remainder. - m.bufferedChunk.Values = m.bufferedChunk.Values[ind:] - - // If we emptied out all the values, clear the mapper's buffered chunk. - if len(m.bufferedChunk.Values) == 0 { - m.bufferedChunk = nil - } - } - - if ascending { - // Sort the values by time first so we can then handle offset and limit - sort.Sort(MapperValues(chunkedOutput.Values)) - } else { - sort.Sort(sort.Reverse(MapperValues(chunkedOutput.Values))) - } - - // Now that we have full name and tag details, initialize the rowWriter. - // The Name and Tags will be the same for all mappers. - if rowWriter == nil { - rowWriter = &limitedRowWriter{ - limit: e.stmt.Limit, - offset: e.stmt.Offset, - chunkSize: e.chunkSize, - name: chunkedOutput.Name, - tags: chunkedOutput.Tags, - selectNames: selectFields, - aliasNames: aliasFields, - fields: e.stmt.Fields, - c: out, - } - } - if e.stmt.HasDerivative() { - interval, err := derivativeInterval(e.stmt) - if err != nil { - out <- &models.Row{Err: err} - return - } - rowWriter.transformer = &RawQueryDerivativeProcessor{ - IsNonNegative: e.stmt.FunctionCalls()[0].Name == "non_negative_derivative", - DerivativeInterval: interval, - } - } - - // Emit the data via the limiter. - if limited := rowWriter.Add(chunkedOutput.Values); limited { - // Limit for this tagset was reached, mark it and start draining a new tagset. - e.limitTagSet(chunkedOutput.key()) - continue - } - } - - close(out) -} - -func (e *SelectExecutor) executeAggregate(out chan *models.Row) { - // It's important to close all resources when execution completes. - defer e.close() - - // Create the functions which will reduce values from mappers for - // a given interval. The function offsets within this slice match - // the offsets within the value slices that are returned by the - // mapper. - aggregates := e.stmt.FunctionCalls() - reduceFuncs := make([]reduceFunc, len(aggregates)) - for i, c := range aggregates { - reduceFunc, err := initializeReduceFunc(c) - if err != nil { - out <- &models.Row{Err: err} - return - } - reduceFuncs[i] = reduceFunc - } - - // Put together the rows to return, starting with columns. - columnNames := e.stmt.ColumnNames() - - // Open the mappers. - for _, m := range e.mappers { - if err := m.Open(); err != nil { - out <- &models.Row{Err: err} - return - } - } - - // Build the set of available tagsets across all mappers. This is used for - // later checks. - availTagSets := newStringSet() - for _, m := range e.mappers { - for _, t := range m.TagSets() { - availTagSets.add(t) - } - } - - // Prime each mapper's chunk buffer. - var err error - for _, m := range e.mappers { - m.bufferedChunk, err = m.NextChunk() - if err != nil { - out <- &models.Row{Err: err} - return - } - if m.bufferedChunk == nil { - m.drained = true - } - } - - ascending := true - if len(e.stmt.SortFields) > 0 { - ascending = e.stmt.SortFields[0].Ascending - } - - // Keep looping until all mappers drained. - for !e.mappersDrained() { - // Send out data for the next alphabetically-lowest tagset. All Mappers send out in this order - // so collect data for this tagset, ignoring all others. - tagset := e.nextMapperTagSet() - chunks := []*MapperOutput{} - - // Pull as much as possible from each mapper. Stop when a mapper offers - // data for a new tagset, or empties completely. - for _, m := range e.mappers { - if m.drained { - continue - } - - for { - if m.bufferedChunk == nil { - m.bufferedChunk, err = m.NextChunk() - if err != nil { - out <- &models.Row{Err: err} - return - } - if m.bufferedChunk == nil { - m.drained = true - break - } - } - - // Got a chunk. Can we use it? - if m.bufferedChunk.key() != tagset { - // No, so just leave it in the buffer. - break - } - // We can, take it. - chunks = append(chunks, m.bufferedChunk) - m.bufferedChunk = nil - } - } - - // Prep a row, ready for kicking out. - var row *models.Row - - // Prep for bucketing data by start time of the interval. - buckets := map[int64][][]interface{}{} - - for _, chunk := range chunks { - if row == nil { - row = &models.Row{ - Name: chunk.Name, - Tags: chunk.Tags, - Columns: columnNames, - } - } - - startTime := chunk.Values[0].Time - _, ok := buckets[startTime] - values := chunk.Values[0].Value.([]interface{}) - if !ok { - buckets[startTime] = make([][]interface{}, len(values)) - } - for i, v := range values { - buckets[startTime][i] = append(buckets[startTime][i], v) - } - } - - // Now, after the loop above, within each time bucket is a slice. Within the element of each - // slice is another slice of interface{}, ready for passing to the reducer functions. - - // Work each bucket of time, in time ascending order. - tMins := make(int64arr, 0, len(buckets)) - for k, _ := range buckets { - tMins = append(tMins, k) - } - - if ascending { - sort.Sort(tMins) - } else { - sort.Sort(sort.Reverse(tMins)) - } - - values := make([][]interface{}, len(tMins)) - for i, t := range tMins { - values[i] = make([]interface{}, 0, len(columnNames)) - values[i] = append(values[i], time.Unix(0, t).UTC()) // Time value is always first. - - for j, f := range reduceFuncs { - reducedVal := f(buckets[t][j]) - values[i] = append(values[i], reducedVal) - } - } - - // Perform aggregate unwraps - values, err = e.processFunctions(values, columnNames) - if err != nil { - out <- &models.Row{Err: err} - } - - // Perform any mathematics. - values = processForMath(e.stmt.Fields, values) - - // Handle any fill options - values = e.processFill(values) - - // process derivatives - values = e.processDerivative(values) - - // If we have multiple tag sets we'll want to filter out the empty ones - if len(availTagSets) > 1 && resultsEmpty(values) { - continue - } - - row.Values = values - out <- row - } - - close(out) -} - -// processFill will take the results and return new results (or the same if no fill modifications are needed) -// with whatever fill options the query has. -func (e *SelectExecutor) processFill(results [][]interface{}) [][]interface{} { - // don't do anything if we're supposed to leave the nulls - if e.stmt.Fill == influxql.NullFill { - return results - } - - if e.stmt.Fill == influxql.NoFill { - // remove any rows that have even one nil value. This one is tricky because they could have multiple - // aggregates, but this option means that any row that has even one nil gets purged. - newResults := make([][]interface{}, 0, len(results)) - for _, vals := range results { - hasNil := false - // start at 1 because the first value is always time - for j := 1; j < len(vals); j++ { - if vals[j] == nil { - hasNil = true - break - } - } - if !hasNil { - newResults = append(newResults, vals) - } - } - return newResults - } - - // They're either filling with previous values or a specific number - for i, vals := range results { - // start at 1 because the first value is always time - for j := 1; j < len(vals); j++ { - if vals[j] == nil { - switch e.stmt.Fill { - case influxql.PreviousFill: - if i != 0 { - vals[j] = results[i-1][j] - } - case influxql.NumberFill: - vals[j] = e.stmt.FillValue - } - } - } - } - return results -} - -// processDerivative returns the derivatives of the results -func (e *SelectExecutor) processDerivative(results [][]interface{}) [][]interface{} { - // Return early if we're not supposed to process the derivatives - if e.stmt.HasDerivative() { - interval, err := derivativeInterval(e.stmt) - if err != nil { - return results // XXX need to handle this better. - } - - // Determines whether to drop negative differences - isNonNegative := e.stmt.FunctionCalls()[0].Name == "non_negative_derivative" - return ProcessAggregateDerivative(results, isNonNegative, interval) - } - return results -} - -// Close closes the executor such that all resources are released. Once closed, -// an executor may not be re-used. -func (e *SelectExecutor) close() { - if e != nil { - for _, m := range e.mappers { - m.Close() - } - } -} - -func (e *SelectExecutor) processFunctions(results [][]interface{}, columnNames []string) ([][]interface{}, error) { - callInPosition := e.stmt.FunctionCallsByPosition() - hasTimeField := e.stmt.HasTimeFieldSpecified() - - var err error - for i, calls := range callInPosition { - // We can only support expanding fields if a single selector call was specified - // i.e. select tx, max(rx) from foo - // If you have multiple selectors or aggregates, there is no way of knowing who gets to insert the values, so we don't - // i.e. select tx, max(rx), min(rx) from foo - if len(calls) == 1 { - var c *influxql.Call - c = calls[0] - - switch c.Name { - case "top", "bottom": - results, err = e.processAggregates(results, columnNames, c) - if err != nil { - return results, err - } - case "first", "last", "min", "max": - results, err = e.processSelectors(results, i, hasTimeField, columnNames) - if err != nil { - return results, err - } - } - } - } - - return results, nil -} - -func (e *SelectExecutor) processSelectors(results [][]interface{}, callPosition int, hasTimeField bool, columnNames []string) ([][]interface{}, error) { - // if the columns doesn't have enough columns, expand it - for i, columns := range results { - if len(columns) != len(columnNames) { - columns = append(columns, make([]interface{}, len(columnNames)-len(columns))...) - } - for j := 1; j < len(columns); j++ { - switch v := columns[j].(type) { - case PositionPoint: - tMin := columns[0].(time.Time) - results[i] = e.selectorPointToQueryResult(columns, hasTimeField, callPosition, v, tMin, columnNames) - } - } - } - return results, nil -} - -func (e *SelectExecutor) selectorPointToQueryResult(columns []interface{}, hasTimeField bool, columnIndex int, p PositionPoint, tMin time.Time, columnNames []string) []interface{} { - callCount := len(e.stmt.FunctionCalls()) - if callCount == 1 { - tm := time.Unix(0, p.Time).UTC() - // If we didn't explicity ask for time, and we have a group by, then use TMIN for the time returned - if len(e.stmt.Dimensions) > 0 && !hasTimeField { - tm = tMin.UTC() - } - columns[0] = tm - } - - for i, c := range columnNames { - // skip over time, we already handled that above - if i == 0 { - continue - } - if (i == columnIndex && hasTimeField) || (i == columnIndex+1 && !hasTimeField) { - // Check to see if we previously processed this column, if so, continue - if _, ok := columns[i].(PositionPoint); !ok && columns[i] != nil { - continue - } - columns[i] = p.Value - continue - } - - if callCount == 1 { - // Always favor fields over tags if there is a name collision - if t, ok := p.Fields[c]; ok { - columns[i] = t - } else if t, ok := p.Tags[c]; ok { - // look in the tags for a value - columns[i] = t - } - } - } - return columns -} - -func (e *SelectExecutor) processAggregates(results [][]interface{}, columnNames []string, call *influxql.Call) ([][]interface{}, error) { - var values [][]interface{} - - // Check if we have a group by, if not, rewrite the entire result by flattening it out - for _, vals := range results { - // start at 1 because the first value is always time - for j := 1; j < len(vals); j++ { - switch v := vals[j].(type) { - case PositionPoints: - tMin := vals[0].(time.Time) - for _, p := range v { - result := e.aggregatePointToQueryResult(p, tMin, call, columnNames) - values = append(values, result) - } - case nil: - continue - default: - return nil, fmt.Errorf("unrechable code - processAggregates for type %T %v", v, v) - } - } - } - return values, nil -} - -func (e *SelectExecutor) aggregatePointToQueryResult(p PositionPoint, tMin time.Time, call *influxql.Call, columnNames []string) []interface{} { - tm := time.Unix(0, p.Time).UTC() - // If we didn't explicity ask for time, and we have a group by, then use TMIN for the time returned - if len(e.stmt.Dimensions) > 0 && !e.stmt.HasTimeFieldSpecified() { - tm = tMin.UTC() - } - vals := []interface{}{tm} - for _, c := range columnNames { - if c == call.Name { - vals = append(vals, p.Value) - continue - } - // TODO in the future fields will also be available to us. - // we should always favor fields over tags if there is a name collision - - // look in the tags for a value - if t, ok := p.Tags[c]; ok { - vals = append(vals, t) - } - } - return vals -} - -// limitedRowWriter accepts raw mapper values, and will emit those values as rows in chunks -// of the given size. If the chunk size is 0, no chunking will be performed. In addiiton if -// limit is reached, outstanding values will be emitted. If limit is zero, no limit is enforced. -type limitedRowWriter struct { - chunkSize int - limit int - offset int - name string - tags map[string]string - fields influxql.Fields - selectNames []string - aliasNames []string - c chan *models.Row - - currValues []*MapperValue - totalOffSet int - totalSent int - - transformer interface { - Process(input []*MapperValue) []*MapperValue - } -} - -// Add accepts a slice of values, and will emit those values as per chunking requirements. -// If limited is returned as true, the limit was also reached and no more values should be -// added. In that case only up the limit of values are emitted. -func (r *limitedRowWriter) Add(values []*MapperValue) (limited bool) { - if r.currValues == nil { - r.currValues = make([]*MapperValue, 0, r.chunkSize) - } - - // Enforce offset. - if r.totalOffSet < r.offset { - // Still some offsetting to do. - offsetRequired := r.offset - r.totalOffSet - if offsetRequired >= len(values) { - r.totalOffSet += len(values) - return false - } else { - // Drop leading values and keep going. - values = values[offsetRequired:] - r.totalOffSet += offsetRequired - } - } - r.currValues = append(r.currValues, values...) - - // Check limit. - limitReached := r.limit > 0 && r.totalSent+len(r.currValues) >= r.limit - if limitReached { - // Limit will be satified with current values. Truncate 'em. - r.currValues = r.currValues[:r.limit-r.totalSent] - } - - // Is chunking in effect? - if r.chunkSize != IgnoredChunkSize { - // Chunking level reached? - for len(r.currValues) >= r.chunkSize { - index := len(r.currValues) - (len(r.currValues) - r.chunkSize) - r.c <- r.processValues(r.currValues[:index]) - r.currValues = r.currValues[index:] - } - - // After values have been sent out by chunking, there may still be some - // values left, if the remainder is less than the chunk size. But if the - // limit has been reached, kick them out. - if len(r.currValues) > 0 && limitReached { - r.c <- r.processValues(r.currValues) - r.currValues = nil - } - } else if limitReached { - // No chunking in effect, but the limit has been reached. - r.c <- r.processValues(r.currValues) - r.currValues = nil - } - - return limitReached -} - -// Flush instructs the limitedRowWriter to emit any pending values as a single row, -// adhering to any limits. Chunking is not enforced. -func (r *limitedRowWriter) Flush() { - if r == nil { - return - } - - // If at least some rows were sent, and no values are pending, then don't - // emit anything, since at least 1 row was previously emitted. This ensures - // that if no rows were ever sent, at least 1 will be emitted, even an empty row. - if r.totalSent != 0 && len(r.currValues) == 0 { - return - } - - if r.limit > 0 && len(r.currValues) > r.limit { - r.currValues = r.currValues[:r.limit] - } - r.c <- r.processValues(r.currValues) - r.currValues = nil -} - -// processValues emits the given values in a single row. -func (r *limitedRowWriter) processValues(values []*MapperValue) *models.Row { - defer func() { - r.totalSent += len(values) - }() - - selectNames := r.selectNames - aliasNames := r.aliasNames - - if r.transformer != nil { - values = r.transformer.Process(values) - } - - // ensure that time is in the select names and in the first position - hasTime := false - for i, n := range selectNames { - if n == "time" { - // Swap time to the first argument for names - if i != 0 { - selectNames[0], selectNames[i] = selectNames[i], selectNames[0] - } - hasTime = true - break - } - } - - // time should always be in the list of names they get back - if !hasTime { - selectNames = append([]string{"time"}, selectNames...) - aliasNames = append([]string{"time"}, aliasNames...) - } - - // since selectNames can contain tags, we need to strip them out - selectFields := make([]string, 0, len(selectNames)) - aliasFields := make([]string, 0, len(selectNames)) - - for i, n := range selectNames { - if _, found := r.tags[n]; !found { - selectFields = append(selectFields, n) - aliasFields = append(aliasFields, aliasNames[i]) - } - } - - row := &models.Row{ - Name: r.name, - Tags: r.tags, - Columns: aliasFields, - } - - // Kick out an empty row it no results available. - if len(values) == 0 { - return row - } - - // if they've selected only a single value we have to handle things a little differently - singleValue := len(selectFields) == SelectColumnCountWithOneValue - - // the results will have all of the raw mapper results, convert into the row - for _, v := range values { - vals := make([]interface{}, len(selectFields)) - - if singleValue { - vals[0] = time.Unix(0, v.Time).UTC() - switch val := v.Value.(type) { - case map[string]interface{}: - vals[1] = val[selectFields[1]] - default: - vals[1] = val - } - } else { - fields := v.Value.(map[string]interface{}) - - // time is always the first value - vals[0] = time.Unix(0, v.Time).UTC() - - // populate the other values - for i := 1; i < len(selectFields); i++ { - f, ok := fields[selectFields[i]] - if ok { - vals[i] = f - continue - } - if v.Tags != nil { - f, ok = v.Tags[selectFields[i]] - if ok { - vals[i] = f - } - } - } - } - - row.Values = append(row.Values, vals) - } - - // Perform any mathematical post-processing. - row.Values = processForMath(r.fields, row.Values) - - return row -} - -type RawQueryDerivativeProcessor struct { - LastValueFromPreviousChunk *MapperValue - IsNonNegative bool // Whether to drop negative differences - DerivativeInterval time.Duration -} - -func (rqdp *RawQueryDerivativeProcessor) canProcess(input *MapperValue) bool { - // Cannot process a nil value - if input == nil { - return false - } - - // See if the field value is numeric, if it's not, we can't process the derivative - validType := false - switch input.Value.(type) { - case int64: - validType = true - case float64: - validType = true - } - - return validType -} - -func (rqdp *RawQueryDerivativeProcessor) Process(input []*MapperValue) []*MapperValue { - if len(input) == 0 { - return input - } - - if len(input) == 1 { - return []*MapperValue{ - &MapperValue{ - Time: input[0].Time, - Value: 0.0, - }, - } - } - - if rqdp.LastValueFromPreviousChunk == nil { - rqdp.LastValueFromPreviousChunk = input[0] - } - - derivativeValues := []*MapperValue{} - for i := 1; i < len(input); i++ { - v := input[i] - - // If we can't use the current or prev value (wrong time, nil), just append - // nil - if !rqdp.canProcess(v) || !rqdp.canProcess(rqdp.LastValueFromPreviousChunk) { - derivativeValues = append(derivativeValues, &MapperValue{ - Time: v.Time, - Value: nil, - }) - continue - } - - // Calculate the derivative of successive points by dividing the difference - // of each value by the elapsed time normalized to the interval - diff := int64toFloat64(v.Value) - int64toFloat64(rqdp.LastValueFromPreviousChunk.Value) - - elapsed := v.Time - rqdp.LastValueFromPreviousChunk.Time - - value := 0.0 - if elapsed > 0 { - value = diff / (float64(elapsed) / float64(rqdp.DerivativeInterval)) - } - - rqdp.LastValueFromPreviousChunk = v - - // Drop negative values for non-negative derivatives - if rqdp.IsNonNegative && diff < 0 { - continue - } - - derivativeValues = append(derivativeValues, &MapperValue{ - Time: v.Time, - Value: value, - }) - } - - return derivativeValues -} - -// processForMath will apply any math that was specified in the select statement -// against the passed in results -func processForMath(fields influxql.Fields, results [][]interface{}) [][]interface{} { - hasMath := false - for _, f := range fields { - if _, ok := f.Expr.(*influxql.BinaryExpr); ok { - hasMath = true - } else if _, ok := f.Expr.(*influxql.ParenExpr); ok { - hasMath = true - } - } - - if !hasMath { - return results - } - - processors := make([]influxql.Processor, len(fields)) - startIndex := 1 - for i, f := range fields { - processors[i], startIndex = influxql.GetProcessor(f.Expr, startIndex) - } - - mathResults := make([][]interface{}, len(results)) - for i, _ := range mathResults { - mathResults[i] = make([]interface{}, len(fields)+1) - // put the time in - mathResults[i][0] = results[i][0] - for j, p := range processors { - mathResults[i][j+1] = p(results[i]) - } - } - - return mathResults -} - -// ProcessAggregateDerivative returns the derivatives of an aggregate result set -func ProcessAggregateDerivative(results [][]interface{}, isNonNegative bool, interval time.Duration) [][]interface{} { - // Return early if we can't calculate derivatives - if len(results) == 0 { - return results - } - - // If we only have 1 value, then the value did not change, so return - // a single row w/ 0.0 - if len(results) == 1 { - return [][]interface{}{ - []interface{}{results[0][0], 0.0}, - } - } - - // Otherwise calculate the derivatives as the difference between consecutive - // points divided by the elapsed time. Then normalize to the requested - // interval. - derivatives := [][]interface{}{} - for i := 1; i < len(results); i++ { - prev := results[i-1] - cur := results[i] - - // If current value is nil, append nil for the value - if prev[1] == nil || cur[1] == nil { - derivatives = append(derivatives, []interface{}{ - cur[0], nil, - }) - continue - } - - // Check the value's type to ensure it's an numeric, if not, return a nil result. We only check the first value - // because derivatives cannot be combined with other aggregates currently. - validType := false - switch cur[1].(type) { - case int64: - validType = true - case float64: - validType = true - } - - if !validType { - derivatives = append(derivatives, []interface{}{ - cur[0], nil, - }) - continue - } - - elapsed := cur[0].(time.Time).Sub(prev[0].(time.Time)) - diff := int64toFloat64(cur[1]) - int64toFloat64(prev[1]) - value := 0.0 - if elapsed > 0 { - value = float64(diff) / (float64(elapsed) / float64(interval)) - } - - // Drop negative values for non-negative derivatives - if isNonNegative && diff < 0 { - continue - } - - val := []interface{}{ - cur[0], - value, - } - derivatives = append(derivatives, val) - } - - return derivatives -} - -// derivativeInterval returns the time interval for the one (and only) derivative func -func derivativeInterval(stmt *influxql.SelectStatement) (time.Duration, error) { - if len(stmt.FunctionCalls()[0].Args) == 2 { - return stmt.FunctionCalls()[0].Args[1].(*influxql.DurationLiteral).Val, nil - } - interval, err := stmt.GroupByInterval() - if err != nil { - return 0, err - } - if interval > 0 { - return interval, nil - } - return time.Second, nil -} - -// resultsEmpty will return true if the all the result values are empty or contain only nulls -func resultsEmpty(resultValues [][]interface{}) bool { - for _, vals := range resultValues { - // start the loop at 1 because we want to skip over the time value - for i := 1; i < len(vals); i++ { - if vals[i] != nil { - return false - } - } - } - return true -} - -func int64toFloat64(v interface{}) float64 { - switch v.(type) { - case int64: - return float64(v.(int64)) - case float64: - return v.(float64) - } - panic(fmt.Sprintf("expected either int64 or float64, got %v", v)) -} - -type int64arr []int64 - -func (a int64arr) Len() int { return len(a) } -func (a int64arr) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a int64arr) Less(i, j int) bool { return a[i] < a[j] } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/functions.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/functions.go index 951382b2b..bc9930518 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/functions.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/functions.go @@ -16,14 +16,17 @@ import ( "sort" "strings" + // "github.com/davecgh/go-spew/spew" "github.com/influxdb/influxdb/influxql" ) +// MapInput represents a collection of values to be processed by the mapper. type MapInput struct { TMin int64 Items []MapItem } +// MapItem represents a single item in a collection that's processed by the mapper. type MapItem struct { Timestamp int64 Value interface{} @@ -205,7 +208,7 @@ func InitializeUnmarshaller(c *influxql.Call) (UnmarshalFunc, error) { }, nil case "distinct": return func(b []byte) (interface{}, error) { - var val interfaceValues + var val InterfaceValues err := json.Unmarshal(b, &val) return val, err }, nil @@ -254,11 +257,11 @@ func MapCount(input *MapInput) interface{} { return nil } -type interfaceValues []interface{} +type InterfaceValues []interface{} -func (d interfaceValues) Len() int { return len(d) } -func (d interfaceValues) Swap(i, j int) { d[i], d[j] = d[j], d[i] } -func (d interfaceValues) Less(i, j int) bool { +func (d InterfaceValues) Len() int { return len(d) } +func (d InterfaceValues) Swap(i, j int) { d[i], d[j] = d[j], d[i] } +func (d InterfaceValues) Less(i, j int) bool { cmpt, a, b := typeCompare(d[i], d[j]) cmpv := valueCompare(a, b) if cmpv == 0 { @@ -278,7 +281,7 @@ func MapDistinct(input *MapInput) interface{} { return nil } - results := make(interfaceValues, len(m)) + results := make(InterfaceValues, len(m)) var i int for value, _ := range m { results[i] = value @@ -296,7 +299,7 @@ func ReduceDistinct(values []interface{}) interface{} { if v == nil { continue } - d, ok := v.(interfaceValues) + d, ok := v.(InterfaceValues) if !ok { msg := fmt.Sprintf("expected distinctValues, got: %T", v) panic(msg) @@ -307,7 +310,7 @@ func ReduceDistinct(values []interface{}) interface{} { } // convert map keys to an array - results := make(interfaceValues, len(index)) + results := make(InterfaceValues, len(index)) var i int for k, _ := range index { results[i] = k @@ -432,9 +435,9 @@ func MapMean(input *MapInput) interface{} { out.Count++ switch v := item.Value.(type) { case float64: - out.Mean += (v - out.Mean) / float64(out.Count) + out.Total += v case int64: - out.Mean += (float64(v) - out.Mean) / float64(out.Count) + out.Total += float64(v) out.ResultType = Int64Type } } @@ -443,27 +446,24 @@ func MapMean(input *MapInput) interface{} { type meanMapOutput struct { Count int - Mean float64 + Total float64 ResultType NumberType } // ReduceMean computes the mean of values for each key. func ReduceMean(values []interface{}) interface{} { - out := &meanMapOutput{} - var countSum int + var total float64 + var count int for _, v := range values { - if v == nil { - continue + if v, _ := v.(*meanMapOutput); v != nil { + count += v.Count + total += v.Total } - val := v.(*meanMapOutput) - countSum = out.Count + val.Count - out.Mean = val.Mean*(float64(val.Count)/float64(countSum)) + out.Mean*(float64(out.Count)/float64(countSum)) - out.Count = countSum } - if out.Count > 0 { - return out.Mean + if count == 0 { + return nil } - return nil + return total / float64(count) } // ReduceMedian computes the median of values @@ -658,6 +658,7 @@ func MapMin(input *MapInput, fieldName string) interface{} { min.Tags = item.Tags pointsYielded = true } + current := min.Val min.Val = math.Min(min.Val, val) @@ -676,55 +677,41 @@ func MapMin(input *MapInput, fieldName string) interface{} { // ReduceMin computes the min of value. func ReduceMin(values []interface{}) interface{} { - min := &minMaxMapOut{} - pointsYielded := false - + var curr *minMaxMapOut for _, value := range values { - if value == nil { + v, _ := value.(*minMaxMapOut) + if v == nil { continue } - v, ok := value.(*minMaxMapOut) - if !ok { - continue + // Replace current if lower value. + if curr == nil || v.Val < curr.Val || (v.Val == curr.Val && v.Time < curr.Time) { + curr = v } + } - // Initialize min - if !pointsYielded { - min.Time = v.Time - min.Val = v.Val - min.Type = v.Type - min.Fields = v.Fields - min.Tags = v.Tags - pointsYielded = true - } - min.Val = math.Min(min.Val, v.Val) - current := min.Val - if current != min.Val { - min.Time = v.Time - min.Fields = v.Fields - min.Tags = v.Tags - } + if curr == nil { + return nil } - if pointsYielded { - switch min.Type { - case Float64Type: - return PositionPoint{ - Time: min.Time, - Value: min.Val, - Fields: min.Fields, - Tags: min.Tags, - } - case Int64Type: - return PositionPoint{ - Time: min.Time, - Value: int64(min.Val), - Fields: min.Fields, - Tags: min.Tags, - } + + switch curr.Type { + case Float64Type: + return PositionPoint{ + Time: curr.Time, + Value: curr.Val, + Fields: curr.Fields, + Tags: curr.Tags, } + case Int64Type: + return PositionPoint{ + Time: curr.Time, + Value: int64(curr.Val), + Fields: curr.Fields, + Tags: curr.Tags, + } + default: + return nil } - return nil } func decodeValueAndNumberType(v interface{}) (float64, NumberType, bool) { @@ -786,55 +773,41 @@ func MapMax(input *MapInput, fieldName string) interface{} { // ReduceMax computes the max of value. func ReduceMax(values []interface{}) interface{} { - max := &minMaxMapOut{} - pointsYielded := false - + var curr *minMaxMapOut for _, value := range values { - if value == nil { + v, _ := value.(*minMaxMapOut) + if v == nil { continue } - v, ok := value.(*minMaxMapOut) - if !ok { - continue + // Replace current if higher value. + if curr == nil || v.Val > curr.Val || (v.Val == curr.Val && v.Time < curr.Time) { + curr = v } + } - // Initialize max - if !pointsYielded { - max.Time = v.Time - max.Val = v.Val - max.Type = v.Type - max.Fields = v.Fields - max.Tags = v.Tags - pointsYielded = true - } - current := max.Val - max.Val = math.Max(max.Val, v.Val) - if current != max.Val { - max.Time = v.Time - max.Fields = v.Fields - max.Tags = v.Tags - } + if curr == nil { + return nil } - if pointsYielded { - switch max.Type { - case Float64Type: - return PositionPoint{ - Time: max.Time, - Value: max.Val, - Fields: max.Fields, - Tags: max.Tags, - } - case Int64Type: - return PositionPoint{ - Time: max.Time, - Value: int64(max.Val), - Fields: max.Fields, - Tags: max.Tags, - } + + switch curr.Type { + case Float64Type: + return PositionPoint{ + Time: curr.Time, + Value: curr.Val, + Fields: curr.Fields, + Tags: curr.Tags, } + case Int64Type: + return PositionPoint{ + Time: curr.Time, + Value: int64(curr.Val), + Fields: curr.Fields, + Tags: curr.Tags, + } + default: + return nil } - return nil } type spreadMapOutput struct { @@ -1180,7 +1153,7 @@ func inferFloat(v reflect.Value) (weight int, value interface{}) { case reflect.String: return stringWeight, v.Interface() } - panic(fmt.Sprintf("interfaceValues.Less - unreachable code; type was %T", v.Interface())) + panic(fmt.Sprintf("InterfaceValues.Less - unreachable code; type was %T", v.Interface())) } func cmpFloat(a, b float64) int { @@ -1492,6 +1465,7 @@ func MapTopBottom(input *MapInput, limit int, fields []string, argCount int, cal // buffer so we don't allocate every time through var pp PositionPoint + if argCount > 2 { // this is a tag aggregating query. // For each unique permutation of the tags given, @@ -1582,7 +1556,6 @@ func MapTopBottom(input *MapInput, limit int, fields []string, argCount int, cal // ReduceTop computes the top values for each key. // This function assumes that its inputs are in sorted ascending order. func ReduceTopBottom(values []interface{}, limit int, fields []string, callName string) interface{} { - out := positionOut{callArgs: fields} minheap := topBottomMapOut{&out, callName == "bottom"} results := make([]PositionPoints, 0, len(values)) @@ -1591,36 +1564,58 @@ func ReduceTopBottom(values []interface{}, limit int, fields []string, callName if v == nil { continue } + o, ok := v.(PositionPoints) - if ok { - results = append(results, o) + if !ok { + continue } + + results = append(results, o) } + // These ranges are all in sorted ascending order // so we can grab the top value out of all of them // to figure out the top X ones. + keys := map[string]struct{}{} for i := 0; i < limit; i++ { var max *PositionPoint whichselected := -1 for iter, v := range results { - if len(v) > 0 && (max == nil || minheap.positionPointLess(max, &v[0])) { - max = &v[0] - whichselected = iter + // ignore if there are no values or if value is less. + if len(v) == 0 { + continue + } else if max != nil && !minheap.positionPointLess(max, &v[0]) { + continue } + + // ignore if we've already appended this key. + if len(fields) > 0 { + tagkey := tagkeytop(fields, nil, v[0].Tags) + if _, ok := keys[tagkey]; ok { + continue + } + } + + max = &v[0] + whichselected = iter } + if whichselected == -1 { - // none of the points have any values - // so we can return what we have now - sort.Sort(topBottomReduceOut{out, callName == "bottom"}) - return out.points + break } + v := results[whichselected] + + tagkey := tagkeytop(fields, nil, v[0].Tags) + keys[tagkey] = struct{}{} + out.points = append(out.points, v[0]) results[whichselected] = v[1:] } // now we need to resort the tops by time sort.Sort(topBottomReduceOut{out, callName == "bottom"}) + return out.points } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/functions_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/functions_test.go index 6e28e9256..9bbf1eff0 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/functions_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/functions_test.go @@ -45,7 +45,7 @@ func TestMapMean(t *testing.T) { {Timestamp: 2, Value: float64(8.0)}, }, }, - output: &meanMapOutput{2, 5.0, Float64Type}, + output: &meanMapOutput{2, 10.0, Float64Type}, }, } @@ -55,7 +55,7 @@ func TestMapMean(t *testing.T) { t.Fatalf("MapMean(%v): output mismatch: exp %v got %v", test.input, test.output, got) } - if got.(*meanMapOutput).Count != test.output.Count || got.(*meanMapOutput).Mean != test.output.Mean { + if got.(*meanMapOutput).Count != test.output.Count || got.(*meanMapOutput).Total != test.output.Total { t.Errorf("output mismatch: exp %v got %v", test.output, got) } } @@ -128,7 +128,7 @@ func TestMapDistinct(t *testing.T) { }, } - values := MapDistinct(input).(interfaceValues) + values := MapDistinct(input).(InterfaceValues) if exp, got := 3, len(values); exp != got { t.Errorf("Wrong number of values. exp %v got %v", exp, got) @@ -136,7 +136,7 @@ func TestMapDistinct(t *testing.T) { sort.Sort(values) - exp := interfaceValues{ + exp := InterfaceValues{ "1", uint64(1), float64(1), @@ -156,7 +156,7 @@ func TestMapDistinctNil(t *testing.T) { } func TestReduceDistinct(t *testing.T) { - v1 := interfaceValues{ + v1 := InterfaceValues{ "2", "1", float64(2.0), @@ -167,7 +167,7 @@ func TestReduceDistinct(t *testing.T) { false, } - expect := interfaceValues{ + expect := InterfaceValues{ "1", "2", false, @@ -204,11 +204,11 @@ func TestReduceDistinctNil(t *testing.T) { }, { name: "empty mappper (len 1)", - values: []interface{}{interfaceValues{}}, + values: []interface{}{InterfaceValues{}}, }, { name: "empty mappper (len 2)", - values: []interface{}{interfaceValues{}, interfaceValues{}}, + values: []interface{}{InterfaceValues{}, InterfaceValues{}}, }, } @@ -222,7 +222,7 @@ func TestReduceDistinctNil(t *testing.T) { } func Test_distinctValues_Sort(t *testing.T) { - values := interfaceValues{ + values := InterfaceValues{ "2", "1", float64(2.0), @@ -233,7 +233,7 @@ func Test_distinctValues_Sort(t *testing.T) { false, } - expect := interfaceValues{ + expect := InterfaceValues{ "1", "2", false, diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/into.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/into.go index 2507015e9..9d7f65917 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/into.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/into.go @@ -2,9 +2,10 @@ package tsdb import ( "errors" + "time" + "github.com/influxdb/influxdb/influxql" "github.com/influxdb/influxdb/models" - "time" ) // convertRowToPoints will convert a query result Row into Points that can be written back in. @@ -35,7 +36,11 @@ func convertRowToPoints(measurementName string, row *models.Row) ([]models.Point } } - p := models.NewPoint(measurementName, row.Tags, vals, v[timeIndex].(time.Time)) + p, err := models.NewPoint(measurementName, row.Tags, vals, v[timeIndex].(time.Time)) + if err != nil { + // Drop points that can't be stored + continue + } points = append(points, p) } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/mapper.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/mapper.go index b49dfefba..8c49bb2f7 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/mapper.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/mapper.go @@ -1,14 +1,7 @@ package tsdb import ( - "container/heap" "encoding/json" - "errors" - "fmt" - "sort" - - "github.com/influxdb/influxdb/influxql" - "github.com/influxdb/influxdb/pkg/slices" ) // Mapper is the interface all Mapper types must implement. @@ -137,514 +130,6 @@ func (mo *MapperOutput) key() string { return mo.cursorKey } -// RawMapper runs the map phase for non-aggregate, raw SELECT queries. -type RawMapper struct { - shard *Shard - stmt *influxql.SelectStatement - qmin, qmax int64 // query time range - - tx Tx - cursors []*TagSetCursor - cursorIndex int - - selectFields []string - selectTags []string - whereFields []string - - ChunkSize int -} - -// NewRawMapper returns a new instance of RawMapper. -func NewRawMapper(sh *Shard, stmt *influxql.SelectStatement) *RawMapper { - return &RawMapper{ - shard: sh, - stmt: stmt, - } -} - -// Open opens and initializes the mapper. -func (m *RawMapper) Open() error { - // Ignore if node has the shard but hasn't written to it yet. - if m.shard == nil { - return nil - } - - // Rewrite statement. - stmt, err := m.shard.index.RewriteSelectStatement(m.stmt) - if err != nil { - return err - } - m.stmt = stmt - - // Set all time-related parameters on the mapper. - m.qmin, m.qmax = influxql.TimeRangeAsEpochNano(m.stmt.Condition) - - // Get a read-only transaction. - tx, err := m.shard.engine.Begin(false) - if err != nil { - return err - } - m.tx = tx - - // Collect measurements. - mms := Measurements(m.shard.index.MeasurementsByName(m.stmt.SourceNames())) - m.selectFields = mms.SelectFields(m.stmt) - m.selectTags = mms.SelectTags(m.stmt) - m.whereFields = mms.WhereFields(m.stmt) - - // Open cursors for each measurement. - for _, mm := range mms { - if err := m.openMeasurement(mm); err != nil { - return err - } - } - - // Remove cursors if there are not SELECT fields. - if len(m.selectFields) == 0 { - m.cursors = nil - } - - return nil -} - -func (m *RawMapper) openMeasurement(mm *Measurement) error { - // Validate that ANY GROUP BY is not a field for the measurement. - if err := mm.ValidateGroupBy(m.stmt); err != nil { - return err - } - - // Validate the fields and tags asked for exist and keep track of which are in the select vs the where - selectFields := mm.SelectFields(m.stmt) - selectTags := mm.SelectTags(m.stmt) - fields := uniqueStrings(m.selectFields, m.whereFields) - - // If we only have tags in our select clause we just return - if len(selectFields) == 0 && len(selectTags) > 0 { - return fmt.Errorf("statement must have at least one field in select clause") - } - - // Calculate tag sets and apply SLIMIT/SOFFSET. - tagSets, err := mm.DimensionTagSets(m.stmt) - if err != nil { - return err - } - tagSets = m.stmt.LimitTagSets(tagSets) - - // Create all cursors for reading the data from this shard. - ascending := m.stmt.TimeAscending() - for _, t := range tagSets { - cursors := []*TagsCursor{} - - for i, key := range t.SeriesKeys { - c := m.tx.Cursor(key, fields, m.shard.FieldCodec(mm.Name), ascending) - if c == nil { - continue - } - - seriesTags := m.shard.index.TagsForSeries(key) - cm := NewTagsCursor(c, t.Filters[i], seriesTags) - cursors = append(cursors, cm) - } - - tsc := NewTagSetCursor(mm.Name, t.Tags, cursors) - tsc.SelectFields = m.selectFields - tsc.SelectWhereFields = fields - if ascending { - tsc.Init(m.qmin) - } else { - tsc.Init(m.qmax) - } - - m.cursors = append(m.cursors, tsc) - } - - sort.Sort(TagSetCursors(m.cursors)) - - return nil -} - -// Close closes the mapper. -func (m *RawMapper) Close() { - if m != nil && m.tx != nil { - m.tx.Rollback() - } -} - -// TagSets returns the list of tag sets for which this mapper has data. -func (m *RawMapper) TagSets() []string { return TagSetCursors(m.cursors).Keys() } - -// Fields returns all SELECT fields. -func (m *RawMapper) Fields() []string { return append(m.selectFields, m.selectTags...) } - -// NextChunk returns the next chunk of data. -// Data is ordered the same as TagSets. Each chunk contains one tag set. -// If there is no more data for any tagset, nil will be returned. -func (m *RawMapper) NextChunk() (interface{}, error) { - var output *MapperOutput - for { - // All tagset cursors processed. NextChunk'ing complete. - if m.cursorIndex == len(m.cursors) { - return nil, nil - } - - cursor := m.cursors[m.cursorIndex] - - k, v := cursor.Next(m.qmin, m.qmax) - if v == nil { - // Tagset cursor is empty, move to next one. - m.cursorIndex++ - if output != nil { - // There is data, so return it and continue when next called. - return output, nil - } else { - // Just go straight to the next cursor. - continue - } - } - - if output == nil { - output = &MapperOutput{ - Name: cursor.measurement, - Tags: cursor.tags, - Fields: m.selectFields, - cursorKey: cursor.key(), - } - } - - output.Values = append(output.Values, &MapperValue{ - Time: k, - Value: v, - Tags: cursor.Tags(), - }) - - if len(output.Values) == m.ChunkSize { - return output, nil - } - } -} - -// AggregateMapper runs the map phase for aggregate SELECT queries. -type AggregateMapper struct { - shard *Shard - stmt *influxql.SelectStatement - qmin, qmax int64 // query time range - - tx Tx - cursors []*TagSetCursor - cursorIndex int - - interval int // Current interval for which data is being fetched. - intervalN int // Maximum number of intervals to return. - intervalSize int64 // Size of each interval. - qminWindow int64 // Minimum time of the query floored to start of interval. - - mapFuncs []mapFunc // The mapping functions. - fieldNames []string // the field name being read for mapping. - - selectFields []string - selectTags []string - whereFields []string -} - -// NewAggregateMapper returns a new instance of AggregateMapper. -func NewAggregateMapper(sh *Shard, stmt *influxql.SelectStatement) *AggregateMapper { - return &AggregateMapper{ - shard: sh, - stmt: stmt, - } -} - -// Open opens and initializes the mapper. -func (m *AggregateMapper) Open() error { - // Ignore if node has the shard but hasn't written to it yet. - if m.shard == nil { - return nil - } - - // Rewrite statement. - stmt, err := m.shard.index.RewriteSelectStatement(m.stmt) - if err != nil { - return err - } - m.stmt = stmt - - // Set all time-related parameters on the mapper. - m.qmin, m.qmax = influxql.TimeRangeAsEpochNano(m.stmt.Condition) - - if err := m.initializeMapFunctions(); err != nil { - return err - } - - // For GROUP BY time queries, limit the number of data points returned by the limit and offset - d, err := m.stmt.GroupByInterval() - if err != nil { - return err - } - - m.intervalSize = d.Nanoseconds() - if m.qmin == 0 || m.intervalSize == 0 { - m.intervalN = 1 - m.intervalSize = m.qmax - m.qmin - } else { - intervalTop := m.qmax/m.intervalSize*m.intervalSize + m.intervalSize - intervalBottom := m.qmin / m.intervalSize * m.intervalSize - m.intervalN = int((intervalTop - intervalBottom) / m.intervalSize) - } - - if m.stmt.Limit > 0 || m.stmt.Offset > 0 { - // ensure that the offset isn't higher than the number of points we'd get - if m.stmt.Offset > m.intervalN { - return nil - } - - // Take the lesser of either the pre computed number of GROUP BY buckets that - // will be in the result or the limit passed in by the user - if m.stmt.Limit < m.intervalN { - m.intervalN = m.stmt.Limit - } - } - - // If we are exceeding our MaxGroupByPoints error out - if m.intervalN > MaxGroupByPoints { - return errors.New("too many points in the group by interval. maybe you forgot to specify a where time clause?") - } - - // Ensure that the start time for the results is on the start of the window. - m.qminWindow = m.qmin - if m.intervalSize > 0 && m.intervalN > 1 { - m.qminWindow = m.qminWindow / m.intervalSize * m.intervalSize - } - - // Get a read-only transaction. - tx, err := m.shard.engine.Begin(false) - if err != nil { - return err - } - m.tx = tx - - // Collect measurements. - mms := Measurements(m.shard.index.MeasurementsByName(m.stmt.SourceNames())) - m.selectFields = mms.SelectFields(m.stmt) - m.selectTags = mms.SelectTags(m.stmt) - m.whereFields = mms.WhereFields(m.stmt) - - // Open cursors for each measurement. - for _, mm := range mms { - if err := m.openMeasurement(mm); err != nil { - return err - } - } - - return nil -} - -func (m *AggregateMapper) openMeasurement(mm *Measurement) error { - // Validate that ANY GROUP BY is not a field for the measurement. - if err := mm.ValidateGroupBy(m.stmt); err != nil { - return err - } - - // Validate the fields and tags asked for exist and keep track of which are in the select vs the where - selectFields := mm.SelectFields(m.stmt) - selectTags := mm.SelectTags(m.stmt) - - // If we only have tags in our select clause we just return - if len(selectFields) == 0 && len(selectTags) > 0 { - return fmt.Errorf("statement must have at least one field in select clause") - } - - // Calculate tag sets and apply SLIMIT/SOFFSET. - tagSets, err := mm.DimensionTagSets(m.stmt) - if err != nil { - return err - } - tagSets = m.stmt.LimitTagSets(tagSets) - - // Create all cursors for reading the data from this shard. - for _, t := range tagSets { - cursors := []*TagsCursor{} - - for i, key := range t.SeriesKeys { - fields := slices.Union(selectFields, m.fieldNames, false) - c := m.tx.Cursor(key, fields, m.shard.FieldCodec(mm.Name), true) - if c == nil { - continue - } - - seriesTags := m.shard.index.TagsForSeries(key) - cursors = append(cursors, NewTagsCursor(c, t.Filters[i], seriesTags)) - } - - tsc := NewTagSetCursor(mm.Name, t.Tags, cursors) - tsc.Init(m.qmin) - m.cursors = append(m.cursors, tsc) - } - - sort.Sort(TagSetCursors(m.cursors)) - - return nil -} - -// initializeMapFunctions initialize the mapping functions for the mapper. -func (m *AggregateMapper) initializeMapFunctions() error { - // Set up each mapping function for this statement. - aggregates := m.stmt.FunctionCalls() - m.mapFuncs = make([]mapFunc, len(aggregates)) - m.fieldNames = make([]string, len(m.mapFuncs)) - - for i, c := range aggregates { - mfn, err := initializeMapFunc(c) - if err != nil { - return err - } - m.mapFuncs[i] = mfn - - // Check for calls like `derivative(lmean(value), 1d)` - var nested *influxql.Call = c - if fn, ok := c.Args[0].(*influxql.Call); ok { - nested = fn - } - switch lit := nested.Args[0].(type) { - case *influxql.VarRef: - m.fieldNames[i] = lit.Val - case *influxql.Distinct: - if c.Name != "count" { - return fmt.Errorf("aggregate call didn't contain a field %s", c.String()) - } - m.fieldNames[i] = lit.Val - default: - return fmt.Errorf("aggregate call didn't contain a field %s", c.String()) - } - } - - return nil -} - -// Close closes the mapper. -func (m *AggregateMapper) Close() { - if m != nil && m.tx != nil { - m.tx.Rollback() - } - return -} - -// TagSets returns the list of tag sets for which this mapper has data. -func (m *AggregateMapper) TagSets() []string { return TagSetCursors(m.cursors).Keys() } - -// Fields returns all SELECT fields. -func (m *AggregateMapper) Fields() []string { return append(m.selectFields, m.selectTags...) } - -// NextChunk returns the next interval of data. -// Tagsets are always processed in the same order as AvailTagsSets(). -// When there is no more data for any tagset nil is returned. -func (m *AggregateMapper) NextChunk() (interface{}, error) { - var tmin, tmax int64 - for { - // All tagset cursors processed. NextChunk'ing complete. - if m.cursorIndex == len(m.cursors) { - return nil, nil - } - - // All intervals complete for this tagset. Move to the next tagset. - tmin, tmax = m.nextInterval() - if tmin < 0 { - m.interval = 0 - m.cursorIndex++ - continue - } - break - } - - // Prep the return data for this tagset. - // This will hold data for a single interval for a single tagset. - tsc := m.cursors[m.cursorIndex] - output := &MapperOutput{ - Name: tsc.measurement, - Tags: tsc.tags, - Fields: m.selectFields, - Values: make([]*MapperValue, 1), - cursorKey: tsc.key(), - } - - // Aggregate values only use the first entry in the Values field. - // Set the time to the start of the interval. - output.Values[0] = &MapperValue{ - Time: tmin, - Value: make([]interface{}, 0), - } - - // Always clamp tmin and tmax. This can happen as bucket-times are bucketed to the nearest - // interval. This is necessary to grab the "partial" buckets at the beginning and end of the time range - qmin, qmax := tmin, tmax - if qmin < m.qmin { - qmin = m.qmin - } - if qmax > m.qmax { - qmax = m.qmax + 1 - } - - tsc.heap = newPointHeap() - for i := range m.mapFuncs { - // Prime the tagset cursor for the start of the interval. This is not ideal, as - // it should really calculate the values all in 1 pass, but that would require - // changes to the mapper functions, which can come later. - // Prime the buffers. - for i := 0; i < len(tsc.cursors); i++ { - k, v := tsc.cursors[i].SeekTo(qmin) - if k == -1 || k > tmax { - continue - } - - heap.Push(tsc.heap, &pointHeapItem{ - timestamp: k, - value: v, - cursor: tsc.cursors[i], - }) - } - - tsc.SelectFields = []string{m.fieldNames[i]} - tsc.SelectWhereFields = uniqueStrings([]string{m.fieldNames[i]}, m.whereFields) - - // Build a map input from the cursor. - input := &MapInput{ - TMin: -1, - } - if len(m.stmt.Dimensions) > 0 && !m.stmt.HasTimeFieldSpecified() { - input.TMin = tmin - } - - for k, v := tsc.Next(qmin, qmax); k != -1; k, v = tsc.Next(qmin, qmax) { - input.Items = append(input.Items, MapItem{ - Timestamp: k, - Value: v, - Fields: tsc.Fields(), - Tags: tsc.Tags(), - }) - } - - // Execute the map function which walks the entire interval, and aggregates the result. - mapValue := m.mapFuncs[i](input) - output.Values[0].Value = append(output.Values[0].Value.([]interface{}), mapValue) - } - - return output, nil -} - -// nextInterval returns the next interval for which to return data. -// If start is less than 0 there are no more intervals. -func (m *AggregateMapper) nextInterval() (start, end int64) { - t := m.qminWindow + int64(m.interval+m.stmt.Offset)*m.intervalSize - - // On to next interval. - m.interval++ - if t > m.qmax || m.interval > m.intervalN { - start, end = -1, 1 - } else { - start, end = t, t+m.intervalSize - } - return -} - // uniqueStrings returns a slice of unique strings from all lists in a. func uniqueStrings(a ...[]string) []string { // Calculate unique set of strings. diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/mapper_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/mapper_test.go index 5b3ac3989..5ad4fdd6b 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/mapper_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/mapper_test.go @@ -23,14 +23,14 @@ func TestShardMapper_RawMapperTagSetsFields(t *testing.T) { shard := mustCreateShard(tmpDir) pt1time := time.Unix(1, 0).UTC() - pt1 := models.NewPoint( + pt1 := models.MustNewPoint( "cpu", map[string]string{"host": "serverA", "region": "us-east"}, map[string]interface{}{"idle": 60}, pt1time, ) pt2time := time.Unix(2, 0).UTC() - pt2 := models.NewPoint( + pt2 := models.MustNewPoint( "cpu", map[string]string{"host": "serverB", "region": "us-east"}, map[string]interface{}{"load": 60}, @@ -113,14 +113,14 @@ func TestShardMapper_WriteAndSingleMapperRawQuerySingleValue(t *testing.T) { shard := mustCreateShard(tmpDir) pt1time := time.Unix(1, 0).UTC() - pt1 := models.NewPoint( + pt1 := models.MustNewPoint( "cpu", map[string]string{"host": "serverA", "region": "us-east"}, map[string]interface{}{"load": 42}, pt1time, ) pt2time := time.Unix(2, 0).UTC() - pt2 := models.NewPoint( + pt2 := models.MustNewPoint( "cpu", map[string]string{"host": "serverB", "region": "us-east"}, map[string]interface{}{"load": 60}, @@ -220,14 +220,14 @@ func TestShardMapper_WriteAndSingleMapperRawQueryMultiValue(t *testing.T) { shard := mustCreateShard(tmpDir) pt1time := time.Unix(1, 0).UTC() - pt1 := models.NewPoint( + pt1 := models.MustNewPoint( "cpu", map[string]string{"host": "serverA", "region": "us-east"}, map[string]interface{}{"foo": 42, "bar": 43}, pt1time, ) pt2time := time.Unix(2, 0).UTC() - pt2 := models.NewPoint( + pt2 := models.MustNewPoint( "cpu", map[string]string{"host": "serverB", "region": "us-east"}, map[string]interface{}{"foo": 60, "bar": 61}, @@ -273,14 +273,14 @@ func TestShardMapper_WriteAndSingleMapperRawQueryMultiSource(t *testing.T) { shard := mustCreateShard(tmpDir) pt1time := time.Unix(1, 0).UTC() - pt1 := models.NewPoint( + pt1 := models.MustNewPoint( "cpu0", map[string]string{"host": "serverA", "region": "us-east"}, map[string]interface{}{"foo": 42}, pt1time, ) pt2time := time.Unix(2, 0).UTC() - pt2 := models.NewPoint( + pt2 := models.MustNewPoint( "cpu1", map[string]string{"host": "serverB", "region": "us-east"}, map[string]interface{}{"bar": 60}, @@ -338,16 +338,16 @@ func TestShardMapper_WriteAndSingleMapperAggregateQuery(t *testing.T) { shard := mustCreateShard(tmpDir) pt1time := time.Unix(10, 0).UTC() - pt1 := models.NewPoint( + pt1 := models.MustNewPoint( "cpu", map[string]string{"host": "serverA", "region": "us-east"}, map[string]interface{}{"value": 1}, pt1time, ) pt2time := time.Unix(20, 0).UTC() - pt2 := models.NewPoint( + pt2 := models.MustNewPoint( "cpu", - map[string]string{"host": "serverB", "region": "us-east"}, + map[string]string{"host": "serverA", "region": "us-east"}, map[string]interface{}{"value": 60}, pt2time, ) @@ -366,13 +366,12 @@ func TestShardMapper_WriteAndSingleMapperAggregateQuery(t *testing.T) { }, { stmt: `SELECT sum(value),mean(value) FROM cpu`, - expected: []string{`{"name":"cpu","fields":["value"],"values":[{"value":[61,{"Count":2,"Mean":30.5,"ResultType":1}]}]}`, `null`}, + expected: []string{`{"name":"cpu","fields":["value"],"values":[{"value":[61,{"Count":2,"Total":61,"ResultType":1}]}]}`, `null`}, }, { stmt: `SELECT sum(value) FROM cpu GROUP BY host`, expected: []string{ - `{"name":"cpu","tags":{"host":"serverA"},"fields":["value"],"values":[{"value":[1]}]}`, - `{"name":"cpu","tags":{"host":"serverB"},"fields":["value"],"values":[{"value":[60]}]}`, + `{"name":"cpu","tags":{"host":"serverA"},"fields":["value"],"values":[{"value":[61]}]}`, `null`}, }, { @@ -384,14 +383,13 @@ func TestShardMapper_WriteAndSingleMapperAggregateQuery(t *testing.T) { { stmt: `SELECT sum(value) FROM cpu GROUP BY region,host`, expected: []string{ - `{"name":"cpu","tags":{"host":"serverA","region":"us-east"},"fields":["value"],"values":[{"value":[1]}]}`, - `{"name":"cpu","tags":{"host":"serverB","region":"us-east"},"fields":["value"],"values":[{"value":[60]}]}`, + `{"name":"cpu","tags":{"host":"serverA","region":"us-east"},"fields":["value"],"values":[{"value":[61]}]}`, `null`}, }, { - stmt: `SELECT sum(value) FROM cpu WHERE host='serverB'`, + stmt: `SELECT sum(value) FROM cpu WHERE host='serverA'`, expected: []string{ - `{"name":"cpu","fields":["value"],"values":[{"value":[60]}]}`, + `{"name":"cpu","fields":["value"],"values":[{"value":[61]}]}`, `null`}, }, { @@ -434,14 +432,14 @@ func TestShardMapper_SelectMapperTagSetsFields(t *testing.T) { shard := mustCreateShard(tmpDir) pt1time := time.Unix(1, 0).UTC() - pt1 := models.NewPoint( + pt1 := models.MustNewPoint( "cpu", map[string]string{"host": "serverA", "region": "us-east"}, map[string]interface{}{"value": 42}, pt1time, ) pt2time := time.Unix(2, 0).UTC() - pt2 := models.NewPoint( + pt2 := models.MustNewPoint( "cpu", map[string]string{"host": "serverB", "region": "us-east"}, map[string]interface{}{"value": 60}, diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/query_executor.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/query_executor.go index f489b218b..3002163b3 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/query_executor.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/query_executor.go @@ -234,6 +234,7 @@ func (q *QueryExecutor) ExecuteQuery(query *influxql.Query, database string, chu // Plan creates an execution plan for the given SelectStatement and returns an Executor. func (q *QueryExecutor) PlanSelect(stmt *influxql.SelectStatement, chunkSize int) (Executor, error) { + var shardIDs []uint64 shards := map[uint64]meta.ShardInfo{} // Shards requiring mappers. // It is important to "stamp" this time so that everywhere we evaluate `now()` in the statement is EXACTLY the same `now` @@ -263,14 +264,22 @@ func (q *QueryExecutor) PlanSelect(stmt *influxql.SelectStatement, chunkSize int } for _, g := range shardGroups { for _, sh := range g.Shards { - shards[sh.ID] = sh + if _, ok := shards[sh.ID]; !ok { + shards[sh.ID] = sh + shardIDs = append(shardIDs, sh.ID) + } } } } + // Sort shard IDs to make testing deterministic. + sort.Sort(uint64Slice(shardIDs)) + // Build the Mappers, one per shard. mappers := []Mapper{} - for _, sh := range shards { + for _, shardID := range shardIDs { + sh := shards[shardID] + m, err := q.ShardMapper.CreateMapper(sh, stmt, chunkSize) if err != nil { return nil, err @@ -282,8 +291,16 @@ func (q *QueryExecutor) PlanSelect(stmt *influxql.SelectStatement, chunkSize int mappers = append(mappers, m) } - executor := NewSelectExecutor(stmt, mappers, chunkSize) - return executor, nil + // Certain operations on the SELECT statement can be performed by the AggregateExecutor without + // assistance from the Mappers. This allows the AggregateExecutor to prepare aggregation functions + // and mathematical functions. + stmt.RewriteDistinct() + + if (stmt.IsRawQuery && !stmt.HasDistinct()) || stmt.IsSimpleDerivative() { + return NewRawExecutor(stmt, mappers, chunkSize), nil + } else { + return NewAggregateExecutor(stmt, mappers), nil + } } // expandSources expands regex sources and removes duplicates. @@ -353,6 +370,9 @@ func (q *QueryExecutor) executeDropDatabaseStatement(stmt *influxql.DropDatabase if err != nil { return &influxql.Result{Err: err} } else if dbi == nil { + if stmt.IfExists { + return &influxql.Result{} + } return &influxql.Result{Err: ErrDatabaseNotFound(stmt.Name)} } @@ -365,12 +385,17 @@ func (q *QueryExecutor) executeDropDatabaseStatement(stmt *influxql.DropDatabase } } + // Remove database from meta-store first so that in-flight writes can complete without error, but new ones will + // be rejected. + res := q.MetaStatementExecutor.ExecuteStatement(stmt) + + // Remove the database from the local store err = q.Store.DeleteDatabase(stmt.Name, shardIDs) if err != nil { return &influxql.Result{Err: err} } - return q.MetaStatementExecutor.ExecuteStatement(stmt) + return res } // executeDropMeasurementStatement removes the measurement and all series data from the local store for the given measurement @@ -1023,3 +1048,9 @@ var ( func ErrDatabaseNotFound(name string) error { return fmt.Errorf("database not found: %s", name) } func ErrMeasurementNotFound(name string) error { return fmt.Errorf("measurement not found: %s", name) } + +type uint64Slice []uint64 + +func (a uint64Slice) Len() int { return len(a) } +func (a uint64Slice) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a uint64Slice) Less(i, j int) bool { return a[i] < a[j] } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/query_executor_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/query_executor_test.go index b694ad1ad..47bf0bdec 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/query_executor_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/query_executor_test.go @@ -23,7 +23,7 @@ func TestWritePointsAndExecuteQuery(t *testing.T) { defer os.RemoveAll(store.Path()) // Write first point. - if err := store.WriteToShard(shardID, []models.Point{models.NewPoint( + if err := store.WriteToShard(shardID, []models.Point{models.MustNewPoint( "cpu", map[string]string{"host": "server"}, map[string]interface{}{"value": 1.0}, @@ -33,7 +33,7 @@ func TestWritePointsAndExecuteQuery(t *testing.T) { } // Write second point. - if err := store.WriteToShard(shardID, []models.Point{models.NewPoint( + if err := store.WriteToShard(shardID, []models.Point{models.MustNewPoint( "cpu", map[string]string{"host": "server"}, map[string]interface{}{"value": 1.0}, @@ -76,7 +76,7 @@ func TestWritePointsAndExecuteQuery_Update(t *testing.T) { defer os.RemoveAll(store.Path()) // Write original point. - if err := store.WriteToShard(1, []models.Point{models.NewPoint( + if err := store.WriteToShard(1, []models.Point{models.MustNewPoint( "temperature", map[string]string{}, map[string]interface{}{"value": 100.0}, @@ -97,7 +97,7 @@ func TestWritePointsAndExecuteQuery_Update(t *testing.T) { executor.ShardMapper = &testShardMapper{store: store} // Rewrite point with new value. - if err := store.WriteToShard(1, []models.Point{models.NewPoint( + if err := store.WriteToShard(1, []models.Point{models.MustNewPoint( "temperature", map[string]string{}, map[string]interface{}{"value": 200.0}, @@ -117,7 +117,7 @@ func TestDropSeriesStatement(t *testing.T) { store, executor := testStoreAndExecutor("") defer os.RemoveAll(store.Path()) - pt := models.NewPoint( + pt := models.MustNewPoint( "cpu", map[string]string{"host": "server"}, map[string]interface{}{"value": 1.0}, @@ -173,13 +173,13 @@ func TestDropMeasurementStatement(t *testing.T) { store, executor := testStoreAndExecutor("") defer os.RemoveAll(store.Path()) - pt := models.NewPoint( + pt := models.MustNewPoint( "cpu", map[string]string{"host": "server"}, map[string]interface{}{"value": 1.0}, time.Unix(1, 2), ) - pt2 := models.NewPoint( + pt2 := models.MustNewPoint( "memory", map[string]string{"host": "server"}, map[string]interface{}{"value": 1.0}, @@ -239,7 +239,7 @@ func TestDropDatabase(t *testing.T) { store, executor := testStoreAndExecutor("") defer os.RemoveAll(store.Path()) - pt := models.NewPoint( + pt := models.MustNewPoint( "cpu", map[string]string{"host": "server"}, map[string]interface{}{"value": 1.0}, diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/raw.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/raw.go new file mode 100644 index 000000000..dc13b9737 --- /dev/null +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/raw.go @@ -0,0 +1,950 @@ +package tsdb + +import ( + "fmt" + "math" + "sort" + "time" + + "github.com/influxdb/influxdb/influxql" + "github.com/influxdb/influxdb/models" +) + +const ( + // Return an error if the user is trying to select more than this number of points in a group by statement. + // Most likely they specified a group by interval without time boundaries. + MaxGroupByPoints = 100000 + + // Since time is always selected, the column count when selecting only a single other value will be 2 + SelectColumnCountWithOneValue = 2 + + // IgnoredChunkSize is what gets passed into Mapper.Begin for aggregate queries as they don't chunk points out + IgnoredChunkSize = 0 +) + +type RawExecutor struct { + stmt *influxql.SelectStatement + mappers []*StatefulMapper + chunkSize int + limitedTagSets map[string]struct{} // Set tagsets for which data has reached the LIMIT. +} + +// NewRawExecutor returns a new RawExecutor. +func NewRawExecutor(stmt *influxql.SelectStatement, mappers []Mapper, chunkSize int) *RawExecutor { + a := []*StatefulMapper{} + for _, m := range mappers { + a = append(a, &StatefulMapper{m, nil, false}) + } + return &RawExecutor{ + stmt: stmt, + mappers: a, + chunkSize: chunkSize, + limitedTagSets: make(map[string]struct{}), + } +} + +// Close closes the executor such that all resources are released. +// Once closed, an executor may not be re-used. +func (e *RawExecutor) close() { + if e != nil { + for _, m := range e.mappers { + m.Close() + } + } +} + +// Execute begins execution of the query and returns a channel to receive rows. +func (e *RawExecutor) Execute() <-chan *models.Row { + out := make(chan *models.Row, 0) + go e.execute(out) + return out +} + +func (e *RawExecutor) execute(out chan *models.Row) { + // It's important that all resources are released when execution completes. + defer e.close() + + // Open the mappers. + for _, m := range e.mappers { + if err := m.Open(); err != nil { + out <- &models.Row{Err: err} + return + } + } + + // Get the distinct fields across all mappers. + var selectFields, aliasFields []string + if e.stmt.HasWildcard() { + sf := newStringSet() + for _, m := range e.mappers { + sf.add(m.Fields()...) + } + selectFields = sf.list() + aliasFields = selectFields + } else { + selectFields = e.stmt.Fields.Names() + aliasFields = e.stmt.Fields.AliasNames() + } + + // Used to read ahead chunks from mappers. + var rowWriter *limitedRowWriter + var currTagset string + + // Keep looping until all mappers drained. + var err error + for { + // Get the next chunk from each Mapper. + for _, m := range e.mappers { + if m.drained { + continue + } + + // Set the next buffered chunk on the mapper, or mark it drained. + for { + if m.bufferedChunk == nil { + m.bufferedChunk, err = m.NextChunk() + if err != nil { + out <- &models.Row{Err: err} + return + } + if m.bufferedChunk == nil { + // Mapper can do no more for us. + m.drained = true + break + } + + // If the SELECT query is on more than 1 field, but the chunks values from the Mappers + // only contain a single value, create k-v pairs using the field name of the chunk + // and the value of the chunk. If there is only 1 SELECT field across all mappers then + // there is no need to create k-v pairs, and there is no need to distinguish field data, + // as it is all for the *same* field. + if len(selectFields) > 1 && len(m.bufferedChunk.Fields) == 1 { + fieldKey := m.bufferedChunk.Fields[0] + + for i := range m.bufferedChunk.Values { + field := map[string]interface{}{fieldKey: m.bufferedChunk.Values[i].Value} + m.bufferedChunk.Values[i].Value = field + } + } + } + + if e.tagSetIsLimited(m.bufferedChunk.Name) { + // chunk's tagset is limited, so no good. Try again. + m.bufferedChunk = nil + continue + } + // This mapper has a chunk available, and it is not limited. + break + } + } + + // All Mappers done? + if e.mappersDrained() { + rowWriter.Flush() + break + } + + // Send out data for the next alphabetically-lowest tagset. All Mappers emit data in this order, + // so by always continuing with the lowest tagset until it is finished, we process all data in + // the required order, and don't "miss" any. + tagset := e.nextMapperTagSet() + if tagset != currTagset { + currTagset = tagset + // Tagset has changed, time for a new rowWriter. Be sure to kick out any residual values. + rowWriter.Flush() + rowWriter = nil + } + + ascending := true + if len(e.stmt.SortFields) > 0 { + ascending = e.stmt.SortFields[0].Ascending + } + + var timeBoundary int64 + + if ascending { + // Process the mapper outputs. We can send out everything up to the min of the last time + // of the chunks for the next tagset. + timeBoundary = e.nextMapperLowestTime(tagset) + } else { + timeBoundary = e.nextMapperHighestTime(tagset) + } + + // Now empty out all the chunks up to the min time. Create new output struct for this data. + var chunkedOutput *MapperOutput + for _, m := range e.mappers { + if m.drained { + continue + } + + chunkBoundary := false + if ascending { + chunkBoundary = m.bufferedChunk.Values[0].Time > timeBoundary + } else { + chunkBoundary = m.bufferedChunk.Values[0].Time < timeBoundary + } + + // This mapper's next chunk is not for the next tagset, or the very first value of + // the chunk is at a higher acceptable timestamp. Skip it. + if m.bufferedChunk.key() != tagset || chunkBoundary { + continue + } + + // Find the index of the point up to the min. + ind := len(m.bufferedChunk.Values) + for i, mo := range m.bufferedChunk.Values { + if ascending && mo.Time > timeBoundary { + ind = i + break + } else if !ascending && mo.Time < timeBoundary { + ind = i + break + } + + } + + // Add up to the index to the values + if chunkedOutput == nil { + chunkedOutput = &MapperOutput{ + Name: m.bufferedChunk.Name, + Tags: m.bufferedChunk.Tags, + cursorKey: m.bufferedChunk.key(), + } + chunkedOutput.Values = m.bufferedChunk.Values[:ind] + } else { + chunkedOutput.Values = append(chunkedOutput.Values, m.bufferedChunk.Values[:ind]...) + } + + // Clear out the values being sent out, keep the remainder. + m.bufferedChunk.Values = m.bufferedChunk.Values[ind:] + + // If we emptied out all the values, clear the mapper's buffered chunk. + if len(m.bufferedChunk.Values) == 0 { + m.bufferedChunk = nil + } + } + + if ascending { + // Sort the values by time first so we can then handle offset and limit + sort.Sort(MapperValues(chunkedOutput.Values)) + } else { + sort.Sort(sort.Reverse(MapperValues(chunkedOutput.Values))) + } + + // Now that we have full name and tag details, initialize the rowWriter. + // The Name and Tags will be the same for all mappers. + if rowWriter == nil { + rowWriter = &limitedRowWriter{ + limit: e.stmt.Limit, + offset: e.stmt.Offset, + chunkSize: e.chunkSize, + name: chunkedOutput.Name, + tags: chunkedOutput.Tags, + selectNames: selectFields, + aliasNames: aliasFields, + fields: e.stmt.Fields, + c: out, + } + } + if e.stmt.HasDerivative() { + interval, err := derivativeInterval(e.stmt) + if err != nil { + out <- &models.Row{Err: err} + return + } + rowWriter.transformer = &RawQueryDerivativeProcessor{ + IsNonNegative: e.stmt.FunctionCalls()[0].Name == "non_negative_derivative", + DerivativeInterval: interval, + } + } + + // Emit the data via the limiter. + if limited := rowWriter.Add(chunkedOutput.Values); limited { + // Limit for this tagset was reached, mark it and start draining a new tagset. + e.limitTagSet(chunkedOutput.key()) + continue + } + } + + close(out) +} + +// mappersDrained returns whether all the executors Mappers have been drained of data. +func (e *RawExecutor) mappersDrained() bool { + for _, m := range e.mappers { + if !m.drained { + return false + } + } + return true +} + +// nextMapperTagset returns the alphabetically lowest tagset across all Mappers. +func (e *RawExecutor) nextMapperTagSet() string { + tagset := "" + for _, m := range e.mappers { + if m.bufferedChunk != nil { + if tagset == "" { + tagset = m.bufferedChunk.key() + } else if m.bufferedChunk.key() < tagset { + tagset = m.bufferedChunk.key() + } + } + } + return tagset +} + +// nextMapperLowestTime returns the lowest minimum time across all Mappers, for the given tagset. +func (e *RawExecutor) nextMapperLowestTime(tagset string) int64 { + minTime := int64(math.MaxInt64) + for _, m := range e.mappers { + if !m.drained && m.bufferedChunk != nil { + if m.bufferedChunk.key() != tagset { + continue + } + t := m.bufferedChunk.Values[len(m.bufferedChunk.Values)-1].Time + if t < minTime { + minTime = t + } + } + } + return minTime +} + +// nextMapperHighestTime returns the highest time across all Mappers, for the given tagset. +func (e *RawExecutor) nextMapperHighestTime(tagset string) int64 { + maxTime := int64(math.MinInt64) + for _, m := range e.mappers { + if !m.drained && m.bufferedChunk != nil { + if m.bufferedChunk.key() != tagset { + continue + } + t := m.bufferedChunk.Values[0].Time + if t > maxTime { + maxTime = t + } + } + } + return maxTime +} + +// tagSetIsLimited returns whether data for the given tagset has been LIMITed. +func (e *RawExecutor) tagSetIsLimited(tagset string) bool { + _, ok := e.limitedTagSets[tagset] + return ok +} + +// limitTagSet marks the given taset as LIMITed. +func (e *RawExecutor) limitTagSet(tagset string) { + e.limitedTagSets[tagset] = struct{}{} +} + +// limitedRowWriter accepts raw mapper values, and will emit those values as rows in chunks +// of the given size. If the chunk size is 0, no chunking will be performed. In addition if +// limit is reached, outstanding values will be emitted. If limit is zero, no limit is enforced. +type limitedRowWriter struct { + chunkSize int + limit int + offset int + name string + tags map[string]string + fields influxql.Fields + selectNames []string + aliasNames []string + c chan *models.Row + + currValues []*MapperValue + totalOffSet int + totalSent int + + transformer interface { + Process(input []*MapperValue) []*MapperValue + } +} + +// Add accepts a slice of values, and will emit those values as per chunking requirements. +// If limited is returned as true, the limit was also reached and no more values should be +// added. In that case only up the limit of values are emitted. +func (r *limitedRowWriter) Add(values []*MapperValue) (limited bool) { + if r.currValues == nil { + r.currValues = make([]*MapperValue, 0, r.chunkSize) + } + + // Enforce offset. + if r.totalOffSet < r.offset { + // Still some offsetting to do. + offsetRequired := r.offset - r.totalOffSet + if offsetRequired >= len(values) { + r.totalOffSet += len(values) + return false + } else { + // Drop leading values and keep going. + values = values[offsetRequired:] + r.totalOffSet += offsetRequired + } + } + r.currValues = append(r.currValues, values...) + + // Check limit. + limitReached := r.limit > 0 && r.totalSent+len(r.currValues) >= r.limit + if limitReached { + // Limit will be satified with current values. Truncate 'em. + r.currValues = r.currValues[:r.limit-r.totalSent] + } + + // Is chunking in effect? + if r.chunkSize != IgnoredChunkSize { + // Chunking level reached? + for len(r.currValues) >= r.chunkSize { + index := len(r.currValues) - (len(r.currValues) - r.chunkSize) + r.c <- r.processValues(r.currValues[:index]) + r.currValues = r.currValues[index:] + } + + // After values have been sent out by chunking, there may still be some + // values left, if the remainder is less than the chunk size. But if the + // limit has been reached, kick them out. + if len(r.currValues) > 0 && limitReached { + r.c <- r.processValues(r.currValues) + r.currValues = nil + } + } else if limitReached { + // No chunking in effect, but the limit has been reached. + r.c <- r.processValues(r.currValues) + r.currValues = nil + } + + return limitReached +} + +// Flush instructs the limitedRowWriter to emit any pending values as a single row, +// adhering to any limits. Chunking is not enforced. +func (r *limitedRowWriter) Flush() { + if r == nil { + return + } + + // If at least some rows were sent, and no values are pending, then don't + // emit anything, since at least 1 row was previously emitted. This ensures + // that if no rows were ever sent, at least 1 will be emitted, even an empty row. + if r.totalSent != 0 && len(r.currValues) == 0 { + return + } + + if r.limit > 0 && len(r.currValues) > r.limit { + r.currValues = r.currValues[:r.limit] + } + r.c <- r.processValues(r.currValues) + r.currValues = nil +} + +// processValues emits the given values in a single row. +func (r *limitedRowWriter) processValues(values []*MapperValue) *models.Row { + defer func() { + r.totalSent += len(values) + }() + + selectNames := r.selectNames + aliasNames := r.aliasNames + + if r.transformer != nil { + values = r.transformer.Process(values) + } + + // ensure that time is in the select names and in the first position + hasTime := false + for i, n := range selectNames { + if n == "time" { + // Swap time to the first argument for names + if i != 0 { + selectNames[0], selectNames[i] = selectNames[i], selectNames[0] + } + hasTime = true + break + } + } + + // time should always be in the list of names they get back + if !hasTime { + selectNames = append([]string{"time"}, selectNames...) + aliasNames = append([]string{"time"}, aliasNames...) + } + + // since selectNames can contain tags, we need to strip them out + selectFields := make([]string, 0, len(selectNames)) + aliasFields := make([]string, 0, len(selectNames)) + + for i, n := range selectNames { + if _, found := r.tags[n]; !found { + selectFields = append(selectFields, n) + aliasFields = append(aliasFields, aliasNames[i]) + } + } + + row := &models.Row{ + Name: r.name, + Tags: r.tags, + Columns: aliasFields, + } + + // Kick out an empty row it no results available. + if len(values) == 0 { + return row + } + + // if they've selected only a single value we have to handle things a little differently + singleValue := len(selectFields) == SelectColumnCountWithOneValue + + // the results will have all of the raw mapper results, convert into the row + for _, v := range values { + vals := make([]interface{}, len(selectFields)) + + if singleValue { + vals[0] = time.Unix(0, v.Time).UTC() + switch val := v.Value.(type) { + case map[string]interface{}: + vals[1] = val[selectFields[1]] + default: + vals[1] = val + } + } else { + fields := v.Value.(map[string]interface{}) + + // time is always the first value + vals[0] = time.Unix(0, v.Time).UTC() + + // populate the other values + for i := 1; i < len(selectFields); i++ { + f, ok := fields[selectFields[i]] + if ok { + vals[i] = f + continue + } + if v.Tags != nil { + f, ok = v.Tags[selectFields[i]] + if ok { + vals[i] = f + } + } + } + } + + row.Values = append(row.Values, vals) + } + + // Perform any mathematical post-processing. + row.Values = processForMath(r.fields, row.Values) + + return row +} + +type RawQueryDerivativeProcessor struct { + LastValueFromPreviousChunk *MapperValue + IsNonNegative bool // Whether to drop negative differences + DerivativeInterval time.Duration +} + +func (rqdp *RawQueryDerivativeProcessor) canProcess(input *MapperValue) bool { + // Cannot process a nil value + if input == nil { + return false + } + + // See if the field value is numeric, if it's not, we can't process the derivative + validType := false + switch input.Value.(type) { + case int64: + validType = true + case float64: + validType = true + } + + return validType +} + +func (rqdp *RawQueryDerivativeProcessor) Process(input []*MapperValue) []*MapperValue { + if len(input) == 0 { + return input + } + + if len(input) == 1 { + return []*MapperValue{ + &MapperValue{ + Time: input[0].Time, + Value: 0.0, + }, + } + } + + if rqdp.LastValueFromPreviousChunk == nil { + rqdp.LastValueFromPreviousChunk = input[0] + } + + derivativeValues := []*MapperValue{} + for i := 1; i < len(input); i++ { + v := input[i] + + // If we can't use the current or prev value (wrong time, nil), just append + // nil + if !rqdp.canProcess(v) || !rqdp.canProcess(rqdp.LastValueFromPreviousChunk) { + derivativeValues = append(derivativeValues, &MapperValue{ + Time: v.Time, + Value: nil, + }) + continue + } + + // Calculate the derivative of successive points by dividing the difference + // of each value by the elapsed time normalized to the interval + diff := int64toFloat64(v.Value) - int64toFloat64(rqdp.LastValueFromPreviousChunk.Value) + + elapsed := v.Time - rqdp.LastValueFromPreviousChunk.Time + + value := 0.0 + if elapsed > 0 { + value = diff / (float64(elapsed) / float64(rqdp.DerivativeInterval)) + } + + rqdp.LastValueFromPreviousChunk = v + + // Drop negative values for non-negative derivatives + if rqdp.IsNonNegative && diff < 0 { + continue + } + + derivativeValues = append(derivativeValues, &MapperValue{ + Time: v.Time, + Value: value, + }) + } + + return derivativeValues +} + +// processForMath will apply any math that was specified in the select statement +// against the passed in results +func processForMath(fields influxql.Fields, results [][]interface{}) [][]interface{} { + hasMath := false + for _, f := range fields { + if _, ok := f.Expr.(*influxql.BinaryExpr); ok { + hasMath = true + } else if _, ok := f.Expr.(*influxql.ParenExpr); ok { + hasMath = true + } + } + + if !hasMath { + return results + } + + processors := make([]influxql.Processor, len(fields)) + startIndex := 1 + for i, f := range fields { + processors[i], startIndex = influxql.GetProcessor(f.Expr, startIndex) + } + + mathResults := make([][]interface{}, len(results)) + for i, _ := range mathResults { + mathResults[i] = make([]interface{}, len(fields)+1) + // put the time in + mathResults[i][0] = results[i][0] + for j, p := range processors { + mathResults[i][j+1] = p(results[i]) + } + } + + return mathResults +} + +// ProcessAggregateDerivative returns the derivatives of an aggregate result set +func ProcessAggregateDerivative(results [][]interface{}, isNonNegative bool, interval time.Duration) [][]interface{} { + // Return early if we can't calculate derivatives + if len(results) == 0 { + return results + } + + // If we only have 1 value, then the value did not change, so return + // a single row w/ 0.0 + if len(results) == 1 { + return [][]interface{}{ + []interface{}{results[0][0], 0.0}, + } + } + + // Otherwise calculate the derivatives as the difference between consecutive + // points divided by the elapsed time. Then normalize to the requested + // interval. + derivatives := [][]interface{}{} + for i := 1; i < len(results); i++ { + prev := results[i-1] + cur := results[i] + + // If current value is nil, append nil for the value + if prev[1] == nil || cur[1] == nil { + derivatives = append(derivatives, []interface{}{ + cur[0], nil, + }) + continue + } + + // Check the value's type to ensure it's an numeric, if not, return a nil result. We only check the first value + // because derivatives cannot be combined with other aggregates currently. + validType := false + switch cur[1].(type) { + case int64: + validType = true + case float64: + validType = true + } + + if !validType { + derivatives = append(derivatives, []interface{}{ + cur[0], nil, + }) + continue + } + + elapsed := cur[0].(time.Time).Sub(prev[0].(time.Time)) + diff := int64toFloat64(cur[1]) - int64toFloat64(prev[1]) + value := 0.0 + if elapsed > 0 { + value = float64(diff) / (float64(elapsed) / float64(interval)) + } + + // Drop negative values for non-negative derivatives + if isNonNegative && diff < 0 { + continue + } + + val := []interface{}{ + cur[0], + value, + } + derivatives = append(derivatives, val) + } + + return derivatives +} + +// derivativeInterval returns the time interval for the one (and only) derivative func +func derivativeInterval(stmt *influxql.SelectStatement) (time.Duration, error) { + if len(stmt.FunctionCalls()[0].Args) == 2 { + return stmt.FunctionCalls()[0].Args[1].(*influxql.DurationLiteral).Val, nil + } + interval, err := stmt.GroupByInterval() + if err != nil { + return 0, err + } + if interval > 0 { + return interval, nil + } + return time.Second, nil +} + +// resultsEmpty will return true if the all the result values are empty or contain only nulls +func resultsEmpty(resultValues [][]interface{}) bool { + for _, vals := range resultValues { + // start the loop at 1 because we want to skip over the time value + for i := 1; i < len(vals); i++ { + if vals[i] != nil { + return false + } + } + } + return true +} + +func int64toFloat64(v interface{}) float64 { + switch v.(type) { + case int64: + return float64(v.(int64)) + case float64: + return v.(float64) + } + panic(fmt.Sprintf("expected either int64 or float64, got %v", v)) +} + +// RawMapper runs the map phase for non-aggregate, raw SELECT queries. +type RawMapper struct { + shard *Shard + stmt *influxql.SelectStatement + qmin, qmax int64 // query time range + + tx Tx + cursors []*TagSetCursor + cursorIndex int + + selectFields []string + selectTags []string + whereFields []string + + ChunkSize int +} + +// NewRawMapper returns a new instance of RawMapper. +func NewRawMapper(sh *Shard, stmt *influxql.SelectStatement) *RawMapper { + return &RawMapper{ + shard: sh, + stmt: stmt, + } +} + +// Open opens and initializes the mapper. +func (m *RawMapper) Open() error { + // Ignore if node has the shard but hasn't written to it yet. + if m.shard == nil { + return nil + } + + // Rewrite statement. + stmt, err := m.shard.index.RewriteSelectStatement(m.stmt) + if err != nil { + return err + } + m.stmt = stmt + + // Set all time-related parameters on the mapper. + m.qmin, m.qmax = influxql.TimeRangeAsEpochNano(m.stmt.Condition) + + // Get a read-only transaction. + tx, err := m.shard.engine.Begin(false) + if err != nil { + return err + } + m.tx = tx + + // Collect measurements. + mms := Measurements(m.shard.index.MeasurementsByName(m.stmt.SourceNames())) + m.selectFields = mms.SelectFields(m.stmt) + m.selectTags = mms.SelectTags(m.stmt) + m.whereFields = mms.WhereFields(m.stmt) + + // Open cursors for each measurement. + for _, mm := range mms { + if err := m.openMeasurement(mm); err != nil { + return err + } + } + + // Remove cursors if there are not SELECT fields. + if len(m.selectFields) == 0 { + m.cursors = nil + } + + return nil +} + +func (m *RawMapper) openMeasurement(mm *Measurement) error { + // Validate that ANY GROUP BY is not a field for the measurement. + if err := mm.ValidateGroupBy(m.stmt); err != nil { + return err + } + + // Validate the fields and tags asked for exist and keep track of which are in the select vs the where + selectFields := mm.SelectFields(m.stmt) + selectTags := mm.SelectTags(m.stmt) + fields := uniqueStrings(m.selectFields, m.whereFields) + + // If we only have tags in our select clause we just return + if len(selectFields) == 0 && len(selectTags) > 0 { + return fmt.Errorf("statement must have at least one field in select clause") + } + + // Calculate tag sets and apply SLIMIT/SOFFSET. + tagSets, err := mm.DimensionTagSets(m.stmt) + if err != nil { + return err + } + tagSets = m.stmt.LimitTagSets(tagSets) + + // Create all cursors for reading the data from this shard. + ascending := m.stmt.TimeAscending() + for _, t := range tagSets { + cursors := []*TagsCursor{} + + for i, key := range t.SeriesKeys { + c := m.tx.Cursor(key, fields, m.shard.FieldCodec(mm.Name), ascending) + if c == nil { + continue + } + + seriesTags := m.shard.index.TagsForSeries(key) + cm := NewTagsCursor(c, t.Filters[i], seriesTags) + cursors = append(cursors, cm) + } + + tsc := NewTagSetCursor(mm.Name, t.Tags, cursors) + tsc.SelectFields = m.selectFields + if ascending { + tsc.Init(m.qmin) + } else { + tsc.Init(m.qmax) + } + + m.cursors = append(m.cursors, tsc) + } + + sort.Sort(TagSetCursors(m.cursors)) + + return nil +} + +// Close closes the mapper. +func (m *RawMapper) Close() { + if m != nil && m.tx != nil { + m.tx.Rollback() + } +} + +// TagSets returns the list of tag sets for which this mapper has data. +func (m *RawMapper) TagSets() []string { return TagSetCursors(m.cursors).Keys() } + +// Fields returns all SELECT fields. +func (m *RawMapper) Fields() []string { return append(m.selectFields, m.selectTags...) } + +// NextChunk returns the next chunk of data. +// Data is ordered the same as TagSets. Each chunk contains one tag set. +// If there is no more data for any tagset, nil will be returned. +func (m *RawMapper) NextChunk() (interface{}, error) { + var output *MapperOutput + for { + // All tagset cursors processed. NextChunk'ing complete. + if m.cursorIndex == len(m.cursors) { + return nil, nil + } + + cursor := m.cursors[m.cursorIndex] + + k, v := cursor.Next(m.qmin, m.qmax) + if v == nil { + // Tagset cursor is empty, move to next one. + m.cursorIndex++ + if output != nil { + // There is data, so return it and continue when next called. + return output, nil + } else { + // Just go straight to the next cursor. + continue + } + } + + if output == nil { + output = &MapperOutput{ + Name: cursor.measurement, + Tags: cursor.tags, + Fields: m.selectFields, + cursorKey: cursor.key(), + } + } + + output.Values = append(output.Values, &MapperValue{ + Time: k, + Value: v, + Tags: cursor.Tags(), + }) + + if len(output.Values) == m.ChunkSize { + return output, nil + } + } +} diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/executor_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/raw_test.go similarity index 61% rename from Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/executor_test.go rename to Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/raw_test.go index 2f30e3392..2515ff4fc 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/executor_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/raw_test.go @@ -3,12 +3,13 @@ package tsdb_test import ( "encoding/json" "io/ioutil" - "math" "os" "path/filepath" + "reflect" "testing" "time" + "github.com/davecgh/go-spew/spew" "github.com/influxdb/influxdb/influxql" "github.com/influxdb/influxdb/meta" "github.com/influxdb/influxdb/models" @@ -57,7 +58,7 @@ func TestWritePointsAndExecuteTwoShards(t *testing.T) { // Write two points across shards. pt1time := time.Unix(1, 0).UTC() - if err := store.WriteToShard(sID0, []models.Point{models.NewPoint( + if err := store.WriteToShard(sID0, []models.Point{models.MustNewPoint( "cpu", map[string]string{"host": "serverA", "region": "us-east"}, map[string]interface{}{"value": 100}, @@ -66,7 +67,7 @@ func TestWritePointsAndExecuteTwoShards(t *testing.T) { t.Fatalf(err.Error()) } pt2time := time.Unix(2, 0).UTC() - if err := store.WriteToShard(sID1, []models.Point{models.NewPoint( + if err := store.WriteToShard(sID1, []models.Point{models.MustNewPoint( "cpu", map[string]string{"host": "serverB", "region": "us-east"}, map[string]interface{}{"value": 200}, @@ -140,6 +141,7 @@ func TestWritePointsAndExecuteTwoShards(t *testing.T) { t.Logf("Skipping test %s", tt.stmt) continue } + executor, err := query_executor.PlanSelect(mustParseSelectStatement(tt.stmt), tt.chunkSize) if err != nil { t.Fatalf("failed to plan query: %s", err.Error()) @@ -186,7 +188,7 @@ func TestWritePointsAndExecuteTwoShardsAlign(t *testing.T) { } // Write interleaving, by time, chunks to the shards. - if err := store.WriteToShard(sID0, []models.Point{models.NewPoint( + if err := store.WriteToShard(sID0, []models.Point{models.MustNewPoint( "cpu", map[string]string{"host": "serverA"}, map[string]interface{}{"value": 100}, @@ -194,7 +196,7 @@ func TestWritePointsAndExecuteTwoShardsAlign(t *testing.T) { )}); err != nil { t.Fatalf(err.Error()) } - if err := store.WriteToShard(sID1, []models.Point{models.NewPoint( + if err := store.WriteToShard(sID1, []models.Point{models.MustNewPoint( "cpu", map[string]string{"host": "serverB"}, map[string]interface{}{"value": 200}, @@ -202,7 +204,7 @@ func TestWritePointsAndExecuteTwoShardsAlign(t *testing.T) { )}); err != nil { t.Fatalf(err.Error()) } - if err := store.WriteToShard(sID1, []models.Point{models.NewPoint( + if err := store.WriteToShard(sID1, []models.Point{models.MustNewPoint( "cpu", map[string]string{"host": "serverA"}, map[string]interface{}{"value": 300}, @@ -266,7 +268,7 @@ func TestWritePointsAndExecuteTwoShardsQueryRewrite(t *testing.T) { // Write two points across shards. pt1time := time.Unix(1, 0).UTC() - if err := store0.WriteToShard(sID0, []models.Point{models.NewPoint( + if err := store0.WriteToShard(sID0, []models.Point{models.MustNewPoint( "cpu", map[string]string{"host": "serverA"}, map[string]interface{}{"value1": 100}, @@ -275,7 +277,7 @@ func TestWritePointsAndExecuteTwoShardsQueryRewrite(t *testing.T) { t.Fatalf(err.Error()) } pt2time := time.Unix(2, 0).UTC() - if err := store1.WriteToShard(sID1, []models.Point{models.NewPoint( + if err := store1.WriteToShard(sID1, []models.Point{models.MustNewPoint( "cpu", map[string]string{"host": "serverB"}, map[string]interface{}{"value2": 200}, @@ -315,7 +317,7 @@ func TestWritePointsAndExecuteTwoShardsQueryRewrite(t *testing.T) { if err != nil { t.Fatalf("failed to create mapper1: %s", err.Error()) } - executor := tsdb.NewSelectExecutor(parsedSelectStmt, []tsdb.Mapper{mapper0, mapper1}, tt.chunkSize) + executor := tsdb.NewRawExecutor(parsedSelectStmt, []tsdb.Mapper{mapper0, mapper1}, tt.chunkSize) // Check the results. got := executeAndGetResults(executor) @@ -358,7 +360,7 @@ func TestWritePointsAndExecuteTwoShardsTagSetOrdering(t *testing.T) { } // Write tagsets "y" and "z" to first shard. - if err := store.WriteToShard(sID0, []models.Point{models.NewPoint( + if err := store.WriteToShard(sID0, []models.Point{models.MustNewPoint( "cpu", map[string]string{"host": "y"}, map[string]interface{}{"value": 100}, @@ -366,7 +368,7 @@ func TestWritePointsAndExecuteTwoShardsTagSetOrdering(t *testing.T) { )}); err != nil { t.Fatalf(err.Error()) } - if err := store.WriteToShard(sID0, []models.Point{models.NewPoint( + if err := store.WriteToShard(sID0, []models.Point{models.MustNewPoint( "cpu", map[string]string{"host": "z"}, map[string]interface{}{"value": 200}, @@ -376,7 +378,7 @@ func TestWritePointsAndExecuteTwoShardsTagSetOrdering(t *testing.T) { } // Write tagsets "x", y" and "z" to second shard. - if err := store.WriteToShard(sID1, []models.Point{models.NewPoint( + if err := store.WriteToShard(sID1, []models.Point{models.MustNewPoint( "cpu", map[string]string{"host": "x"}, map[string]interface{}{"value": 300}, @@ -384,7 +386,7 @@ func TestWritePointsAndExecuteTwoShardsTagSetOrdering(t *testing.T) { )}); err != nil { t.Fatalf(err.Error()) } - if err := store.WriteToShard(sID1, []models.Point{models.NewPoint( + if err := store.WriteToShard(sID1, []models.Point{models.MustNewPoint( "cpu", map[string]string{"host": "y"}, map[string]interface{}{"value": 400}, @@ -392,7 +394,7 @@ func TestWritePointsAndExecuteTwoShardsTagSetOrdering(t *testing.T) { )}); err != nil { t.Fatalf(err.Error()) } - if err := store.WriteToShard(sID1, []models.Point{models.NewPoint( + if err := store.WriteToShard(sID1, []models.Point{models.MustNewPoint( "cpu", map[string]string{"host": "z"}, map[string]interface{}{"value": 500}, @@ -450,13 +452,13 @@ func TestShowMeasurementsMultipleShards(t *testing.T) { // Write two points across shards. pt1time := time.Unix(1, 0).UTC() if err := store0.WriteToShard(sID0, []models.Point{ - models.NewPoint( + models.MustNewPoint( "cpu_user", map[string]string{"host": "serverA", "region": "east", "cpuid": "cpu0"}, map[string]interface{}{"value1": 100}, pt1time, ), - models.NewPoint( + models.MustNewPoint( "mem_free", map[string]string{"host": "serverA", "region": "east"}, map[string]interface{}{"value2": 200}, @@ -466,13 +468,13 @@ func TestShowMeasurementsMultipleShards(t *testing.T) { t.Fatalf(err.Error()) } pt2time := time.Unix(2, 0).UTC() - if err := store1.WriteToShard(sID1, []models.Point{models.NewPoint( + if err := store1.WriteToShard(sID1, []models.Point{models.MustNewPoint( "mem_used", map[string]string{"host": "serverB", "region": "west"}, map[string]interface{}{"value3": 300}, pt2time, ), - models.NewPoint( + models.MustNewPoint( "cpu_sys", map[string]string{"host": "serverB", "region": "west", "cpuid": "cpu0"}, map[string]interface{}{"value4": 400}, @@ -549,13 +551,13 @@ func TestShowShowTagKeysMultipleShards(t *testing.T) { // Write two points across shards. pt1time := time.Unix(1, 0).UTC() if err := store0.WriteToShard(sID0, []models.Point{ - models.NewPoint( + models.MustNewPoint( "cpu", map[string]string{"host": "serverA", "region": "uswest"}, map[string]interface{}{"value1": 100}, pt1time, ), - models.NewPoint( + models.MustNewPoint( "cpu", map[string]string{"host": "serverB", "region": "useast"}, map[string]interface{}{"value1": 100}, @@ -566,13 +568,13 @@ func TestShowShowTagKeysMultipleShards(t *testing.T) { } pt2time := time.Unix(2, 0).UTC() if err := store1.WriteToShard(sID1, []models.Point{ - models.NewPoint( + models.MustNewPoint( "cpu", map[string]string{"host": "serverB", "region": "useast", "rack": "12"}, map[string]interface{}{"value1": 100}, pt1time, ), - models.NewPoint( + models.MustNewPoint( "mem", map[string]string{"host": "serverB"}, map[string]interface{}{"value2": 200}, @@ -659,580 +661,314 @@ func TestShowShowTagKeysMultipleShards(t *testing.T) { } } -// TestProccessAggregateDerivative tests the RawQueryDerivativeProcessor transformation function on the engine. -// The is called for a query with a GROUP BY. -func TestProcessAggregateDerivative(t *testing.T) { - tests := []struct { - name string - fn string - interval time.Duration - in [][]interface{} - exp [][]interface{} - }{ - { - name: "empty input", - fn: "derivative", - interval: 24 * time.Hour, - in: [][]interface{}{}, - exp: [][]interface{}{}, - }, - - { - name: "single row returns 0.0", - fn: "derivative", - interval: 24 * time.Hour, - in: [][]interface{}{ - []interface{}{ - time.Unix(0, 0), 1.0, - }, - }, - exp: [][]interface{}{ - []interface{}{ - time.Unix(0, 0), 0.0, - }, - }, - }, - { - name: "basic derivative", - fn: "derivative", - interval: 24 * time.Hour, - in: [][]interface{}{ - []interface{}{ - time.Unix(0, 0), 1.0, - }, - []interface{}{ - time.Unix(0, 0).Add(24 * time.Hour), 3.0, - }, - []interface{}{ - time.Unix(0, 0).Add(48 * time.Hour), 5.0, - }, - []interface{}{ - time.Unix(0, 0).Add(72 * time.Hour), 9.0, - }, - }, - exp: [][]interface{}{ - []interface{}{ - time.Unix(0, 0).Add(24 * time.Hour), 2.0, - }, - []interface{}{ - time.Unix(0, 0).Add(48 * time.Hour), 2.0, - }, - []interface{}{ - time.Unix(0, 0).Add(72 * time.Hour), 4.0, - }, - }, - }, - { - name: "12h interval", - fn: "derivative", - interval: 12 * time.Hour, - in: [][]interface{}{ - []interface{}{ - time.Unix(0, 0), 1.0, - }, - []interface{}{ - time.Unix(0, 0).Add(24 * time.Hour), 2.0, - }, - []interface{}{ - time.Unix(0, 0).Add(48 * time.Hour), 3.0, - }, - []interface{}{ - time.Unix(0, 0).Add(72 * time.Hour), 4.0, - }, - }, - exp: [][]interface{}{ - []interface{}{ - time.Unix(0, 0).Add(24 * time.Hour), 0.5, - }, - []interface{}{ - time.Unix(0, 0).Add(48 * time.Hour), 0.5, - }, - []interface{}{ - time.Unix(0, 0).Add(72 * time.Hour), 0.5, - }, - }, - }, - { - name: "negative derivatives", - fn: "derivative", - interval: 24 * time.Hour, - in: [][]interface{}{ - []interface{}{ - time.Unix(0, 0), 1.0, - }, - []interface{}{ - time.Unix(0, 0).Add(24 * time.Hour), 2.0, - }, - []interface{}{ - time.Unix(0, 0).Add(48 * time.Hour), 0.0, - }, - []interface{}{ - time.Unix(0, 0).Add(72 * time.Hour), 4.0, - }, - }, - exp: [][]interface{}{ - []interface{}{ - time.Unix(0, 0).Add(24 * time.Hour), 1.0, - }, - []interface{}{ - time.Unix(0, 0).Add(48 * time.Hour), -2.0, - }, - []interface{}{ - time.Unix(0, 0).Add(72 * time.Hour), 4.0, - }, - }, - }, - { - name: "negative derivatives", - fn: "non_negative_derivative", - interval: 24 * time.Hour, - in: [][]interface{}{ - []interface{}{ - time.Unix(0, 0), 1.0, - }, - []interface{}{ - time.Unix(0, 0).Add(24 * time.Hour), 2.0, - }, - // Show resultes in negative derivative - []interface{}{ - time.Unix(0, 0).Add(48 * time.Hour), 0.0, - }, - []interface{}{ - time.Unix(0, 0).Add(72 * time.Hour), 4.0, - }, - }, - exp: [][]interface{}{ - []interface{}{ - time.Unix(0, 0).Add(24 * time.Hour), 1.0, - }, - []interface{}{ - time.Unix(0, 0).Add(72 * time.Hour), 4.0, - }, - }, - }, - { - name: "integer derivatives", - fn: "derivative", - interval: 24 * time.Hour, - in: [][]interface{}{ - []interface{}{ - time.Unix(0, 0), 1.0, - }, - []interface{}{ - time.Unix(0, 0).Add(24 * time.Hour), int64(3), - }, - []interface{}{ - time.Unix(0, 0).Add(48 * time.Hour), int64(5), - }, - []interface{}{ - time.Unix(0, 0).Add(72 * time.Hour), int64(9), - }, - }, - exp: [][]interface{}{ - []interface{}{ - time.Unix(0, 0).Add(24 * time.Hour), 2.0, - }, - []interface{}{ - time.Unix(0, 0).Add(48 * time.Hour), 2.0, - }, - []interface{}{ - time.Unix(0, 0).Add(72 * time.Hour), 4.0, - }, - }, - }, - { - name: "string derivatives", - fn: "derivative", - interval: 24 * time.Hour, - in: [][]interface{}{ - []interface{}{ - time.Unix(0, 0), "1.0", - }, - []interface{}{ - time.Unix(0, 0).Add(24 * time.Hour), "2.0", - }, - []interface{}{ - time.Unix(0, 0).Add(48 * time.Hour), "3.0", - }, - []interface{}{ - time.Unix(0, 0).Add(72 * time.Hour), "4.0", - }, - }, - exp: [][]interface{}{ - []interface{}{ - time.Unix(0, 0).Add(24 * time.Hour), nil, - }, - []interface{}{ - time.Unix(0, 0).Add(48 * time.Hour), nil, - }, - []interface{}{ - time.Unix(0, 0).Add(72 * time.Hour), nil, - }, - }, - }, - { - name: "bool derivatives", - fn: "derivative", - interval: 24 * time.Hour, - in: [][]interface{}{ - []interface{}{ - time.Unix(0, 0), "1.0", - }, - []interface{}{ - time.Unix(0, 0).Add(24 * time.Hour), true, - }, - []interface{}{ - time.Unix(0, 0).Add(48 * time.Hour), true, - }, - []interface{}{ - time.Unix(0, 0).Add(72 * time.Hour), true, - }, - }, - exp: [][]interface{}{ - []interface{}{ - time.Unix(0, 0).Add(24 * time.Hour), nil, - }, - []interface{}{ - time.Unix(0, 0).Add(48 * time.Hour), nil, - }, - []interface{}{ - time.Unix(0, 0).Add(72 * time.Hour), nil, - }, - }, - }, - } - - for _, test := range tests { - got := tsdb.ProcessAggregateDerivative(test.in, test.fn == "non_negative_derivative", test.interval) - - if len(got) != len(test.exp) { - t.Fatalf("ProcessAggregateDerivative(%s) - %s\nlen mismatch: got %d, exp %d", test.fn, test.name, len(got), len(test.exp)) - } - - for i := 0; i < len(test.exp); i++ { - if test.exp[i][0] != got[i][0] || test.exp[i][1] != got[i][1] { - t.Fatalf("ProcessAggregateDerivative - %s results mismatch:\ngot %v\nexp %v", test.name, got, test.exp) - } - } +func TestProcessAggregateDerivative_Empty(t *testing.T) { + results := tsdb.ProcessAggregateDerivative([][]interface{}{}, false, 24*time.Hour) + if !reflect.DeepEqual(results, [][]interface{}{}) { + t.Fatalf("unexpected results: %s", spew.Sdump(results)) } } -// TestProcessRawQueryDerivative tests the RawQueryDerivativeProcessor transformation function on the engine. -// The is called for a queries that do not have a group by. -func TestProcessRawQueryDerivative(t *testing.T) { - tests := []struct { - name string - fn string - interval time.Duration - in []*tsdb.MapperValue - exp []*tsdb.MapperValue - }{ - { - name: "empty input", - fn: "derivative", - interval: 24 * time.Hour, - in: []*tsdb.MapperValue{}, - exp: []*tsdb.MapperValue{}, - }, +func TestProcessAggregateDerivative_SingleRow(t *testing.T) { + results := tsdb.ProcessAggregateDerivative([][]interface{}{ + []interface{}{time.Unix(0, 0), 1.0}, + }, false, 24*time.Hour) - { - name: "single row returns 0.0", - fn: "derivative", - interval: 24 * time.Hour, - in: []*tsdb.MapperValue{ - { - Time: time.Unix(0, 0).Unix(), - Value: 1.0, - }, - }, - exp: []*tsdb.MapperValue{ - { - Time: time.Unix(0, 0).Unix(), - Value: 0.0, - }, - }, - }, - { - name: "basic derivative", - fn: "derivative", - interval: 24 * time.Hour, - in: []*tsdb.MapperValue{ - { - Time: time.Unix(0, 0).Unix(), - Value: 0.0, - }, - { - Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), - Value: 3.0, - }, - { - Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), - Value: 5.0, - }, - { - Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), - Value: 9.0, - }, - }, - exp: []*tsdb.MapperValue{ - { - Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), - Value: 3.0, - }, - { - Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), - Value: 2.0, - }, - { - Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), - Value: 4.0, - }, - }, - }, - { - name: "integer derivative", - fn: "derivative", - interval: 24 * time.Hour, - in: []*tsdb.MapperValue{ - { - Time: time.Unix(0, 0).Unix(), - Value: int64(0), - }, - { - Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), - Value: int64(3), - }, - { - Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), - Value: int64(5), - }, - { - Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), - Value: int64(9), - }, - }, - exp: []*tsdb.MapperValue{ - { - Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), - Value: 3.0, - }, - { - Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), - Value: 2.0, - }, - { - Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), - Value: 4.0, - }, - }, - }, - { - name: "12h interval", - fn: "derivative", - interval: 12 * time.Hour, - in: []*tsdb.MapperValue{ - { - Time: time.Unix(0, 0).UnixNano(), - Value: 1.0, - }, - { - Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), - Value: 2.0, - }, - { - Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), - Value: 3.0, - }, - { - Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), - Value: 4.0, - }, - }, - exp: []*tsdb.MapperValue{ - { - Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), - Value: 0.5, - }, - { - Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), - Value: 0.5, - }, - { - Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), - Value: 0.5, - }, - }, - }, - { - name: "negative derivatives", - fn: "derivative", - interval: 24 * time.Hour, - in: []*tsdb.MapperValue{ - { - Time: time.Unix(0, 0).Unix(), - Value: 1.0, - }, - { - Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), - Value: 2.0, - }, - // should go negative - { - Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), - Value: 0.0, - }, - { - Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), - Value: 4.0, - }, - }, - exp: []*tsdb.MapperValue{ - { - Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), - Value: 1.0, - }, - { - Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), - Value: -2.0, - }, - { - Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), - Value: 4.0, - }, - }, - }, - { - name: "negative derivatives", - fn: "non_negative_derivative", - interval: 24 * time.Hour, - in: []*tsdb.MapperValue{ - { - Time: time.Unix(0, 0).Unix(), - Value: 1.0, - }, - { - Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), - Value: 2.0, - }, - // should go negative - { - Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), - Value: 0.0, - }, - { - Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), - Value: 4.0, - }, - }, - exp: []*tsdb.MapperValue{ - { - Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), - Value: 1.0, - }, - { - Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), - Value: 4.0, - }, - }, - }, - { - name: "string derivatives", - fn: "derivative", - interval: 24 * time.Hour, - in: []*tsdb.MapperValue{ - { - Time: time.Unix(0, 0).Unix(), - Value: "1.0", - }, - { - Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), - Value: "2.0", - }, - { - Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), - Value: "3.0", - }, - { - Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), - Value: "4.0", - }, - }, - exp: []*tsdb.MapperValue{ - { - Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), - Value: nil, - }, - { - Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), - Value: nil, - }, - { - Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), - Value: nil, - }, - }, - }, - { - name: "bool derivatives", - fn: "derivative", - interval: 24 * time.Hour, - in: []*tsdb.MapperValue{ - { - Time: time.Unix(0, 0).Unix(), - Value: true, - }, - { - Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), - Value: true, - }, - { - Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), - Value: false, - }, - { - Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), - Value: false, - }, - }, - exp: []*tsdb.MapperValue{ - { - Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), - Value: nil, - }, - { - Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), - Value: nil, - }, - { - Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), - Value: nil, - }, - }, - }, + if !reflect.DeepEqual(results, [][]interface{}{ + []interface{}{time.Unix(0, 0), 0.0}, + }) { + t.Fatalf("unexpected results: %s", spew.Sdump(results)) + } +} + +func TestProcessAggregateDerivative_Basic_24h(t *testing.T) { + results := tsdb.ProcessAggregateDerivative([][]interface{}{ + []interface{}{time.Unix(0, 0), 1.0}, + []interface{}{time.Unix(0, 0).Add(24 * time.Hour), 3.0}, + []interface{}{time.Unix(0, 0).Add(48 * time.Hour), 5.0}, + []interface{}{time.Unix(0, 0).Add(72 * time.Hour), 9.0}, + }, false, 24*time.Hour) + + if !reflect.DeepEqual(results, [][]interface{}{ + []interface{}{time.Unix(0, 0).Add(24 * time.Hour), 2.0}, + []interface{}{time.Unix(0, 0).Add(48 * time.Hour), 2.0}, + []interface{}{time.Unix(0, 0).Add(72 * time.Hour), 4.0}, + }) { + t.Fatalf("unexpected results: %s", spew.Sdump(results)) + } +} + +func TestProcessAggregateDerivative_Basic_12h(t *testing.T) { + results := tsdb.ProcessAggregateDerivative([][]interface{}{ + []interface{}{time.Unix(0, 0), 1.0}, + []interface{}{time.Unix(0, 0).Add(24 * time.Hour), 2.0}, + []interface{}{time.Unix(0, 0).Add(48 * time.Hour), 3.0}, + []interface{}{time.Unix(0, 0).Add(72 * time.Hour), 4.0}, + }, false, 12*time.Hour) + + if !reflect.DeepEqual(results, [][]interface{}{ + []interface{}{time.Unix(0, 0).Add(24 * time.Hour), 0.5}, + []interface{}{time.Unix(0, 0).Add(48 * time.Hour), 0.5}, + []interface{}{time.Unix(0, 0).Add(72 * time.Hour), 0.5}, + }) { + t.Fatalf("unexpected results: %s", spew.Sdump(results)) + } +} + +func TestProcessAggregateDerivative_Negative(t *testing.T) { + results := tsdb.ProcessAggregateDerivative([][]interface{}{ + []interface{}{time.Unix(0, 0), 1.0}, + []interface{}{time.Unix(0, 0).Add(24 * time.Hour), 2.0}, + []interface{}{time.Unix(0, 0).Add(48 * time.Hour), 0.0}, + []interface{}{time.Unix(0, 0).Add(72 * time.Hour), 4.0}, + }, false, 24*time.Hour) + + if !reflect.DeepEqual(results, [][]interface{}{ + []interface{}{time.Unix(0, 0).Add(24 * time.Hour), 1.0}, + []interface{}{time.Unix(0, 0).Add(48 * time.Hour), -2.0}, + []interface{}{time.Unix(0, 0).Add(72 * time.Hour), 4.0}, + }) { + t.Fatalf("unexpected results: %s", spew.Sdump(results)) + } +} + +func TestProcessAggregateDerivative_Negative_NonNegative(t *testing.T) { + results := tsdb.ProcessAggregateDerivative([][]interface{}{ + []interface{}{time.Unix(0, 0), 1.0}, + []interface{}{time.Unix(0, 0).Add(24 * time.Hour), 2.0}, + []interface{}{time.Unix(0, 0).Add(48 * time.Hour), 0.0}, + []interface{}{time.Unix(0, 0).Add(72 * time.Hour), 4.0}, + }, true, 24*time.Hour) + + if !reflect.DeepEqual(results, [][]interface{}{ + []interface{}{time.Unix(0, 0).Add(24 * time.Hour), 1.0}, + []interface{}{time.Unix(0, 0).Add(72 * time.Hour), 4.0}, + }) { + t.Fatalf("unexpected results: %s", spew.Sdump(results)) + } +} + +func TestProcessAggregateDerivative_Integer(t *testing.T) { + results := tsdb.ProcessAggregateDerivative([][]interface{}{ + []interface{}{time.Unix(0, 0), 1.0}, + []interface{}{time.Unix(0, 0).Add(24 * time.Hour), int64(3)}, + []interface{}{time.Unix(0, 0).Add(48 * time.Hour), int64(5)}, + []interface{}{time.Unix(0, 0).Add(72 * time.Hour), int64(9)}, + }, false, 24*time.Hour) + + if !reflect.DeepEqual(results, [][]interface{}{ + []interface{}{time.Unix(0, 0).Add(24 * time.Hour), 2.0}, + []interface{}{time.Unix(0, 0).Add(48 * time.Hour), 2.0}, + []interface{}{time.Unix(0, 0).Add(72 * time.Hour), 4.0}, + }) { + t.Fatalf("unexpected results: %s", spew.Sdump(results)) + } +} + +func TestProcessAggregateDerivative_String(t *testing.T) { + results := tsdb.ProcessAggregateDerivative([][]interface{}{ + []interface{}{time.Unix(0, 0), "1.0"}, + []interface{}{time.Unix(0, 0).Add(24 * time.Hour), "2.0"}, + []interface{}{time.Unix(0, 0).Add(48 * time.Hour), "3.0"}, + []interface{}{time.Unix(0, 0).Add(72 * time.Hour), "4.0"}, + }, false, 24*time.Hour) + + if !reflect.DeepEqual(results, [][]interface{}{ + []interface{}{time.Unix(0, 0).Add(24 * time.Hour), nil}, + []interface{}{time.Unix(0, 0).Add(48 * time.Hour), nil}, + []interface{}{time.Unix(0, 0).Add(72 * time.Hour), nil}, + }) { + t.Fatalf("unexpected results: %s", spew.Sdump(results)) + } +} + +func TestProcessAggregateDerivative_Bool(t *testing.T) { + results := tsdb.ProcessAggregateDerivative([][]interface{}{ + []interface{}{time.Unix(0, 0), "1.0"}, + []interface{}{time.Unix(0, 0).Add(24 * time.Hour), true}, + []interface{}{time.Unix(0, 0).Add(48 * time.Hour), true}, + []interface{}{time.Unix(0, 0).Add(72 * time.Hour), true}, + }, false, 24*time.Hour) + + if !reflect.DeepEqual(results, [][]interface{}{ + []interface{}{time.Unix(0, 0).Add(24 * time.Hour), nil}, + []interface{}{time.Unix(0, 0).Add(48 * time.Hour), nil}, + []interface{}{time.Unix(0, 0).Add(72 * time.Hour), nil}, + }) { + t.Fatalf("unexpected results: %s", spew.Sdump(results)) + } +} + +func TestRawQueryDerivative_Process_Empty(t *testing.T) { + p := tsdb.RawQueryDerivativeProcessor{ + IsNonNegative: false, + DerivativeInterval: 24 * time.Hour, } - for _, test := range tests { - p := tsdb.RawQueryDerivativeProcessor{ - IsNonNegative: test.fn == "non_negative_derivative", - DerivativeInterval: test.interval, - } - got := p.Process(test.in) + results := p.Process([]*tsdb.MapperValue{}) + if !reflect.DeepEqual(results, []*tsdb.MapperValue{}) { + t.Fatalf("unexpected results: %s", spew.Sdump(results)) + } +} - if len(got) != len(test.exp) { - t.Fatalf("RawQueryDerivativeProcessor(%s) - %s\nlen mismatch: got %d, exp %d", test.fn, test.name, len(got), len(test.exp)) - } +func TestRawQueryDerivative_Process_Single(t *testing.T) { + p := tsdb.RawQueryDerivativeProcessor{ + IsNonNegative: false, + DerivativeInterval: 24 * time.Hour, + } - for i := 0; i < len(test.exp); i++ { - if v, ok := test.exp[i].Value.(float64); ok { - if test.exp[i].Time != got[i].Time || math.Abs((v-got[i].Value.(float64))) > 0.0000001 { - t.Fatalf("RawQueryDerivativeProcessor - %s results mismatch:\ngot %v\nexp %v", test.name, got, test.exp) - } - } else { - if test.exp[i].Time != got[i].Time || test.exp[i].Value != got[i].Value { - t.Fatalf("RawQueryDerivativeProcessor - %s results mismatch:\ngot %v\nexp %v", test.name, got, test.exp) - } - } - } + results := p.Process([]*tsdb.MapperValue{ + {Time: time.Unix(0, 0).Unix(), Value: 1.0}, + }) + if !reflect.DeepEqual(results, []*tsdb.MapperValue{ + {Time: time.Unix(0, 0).Unix(), Value: 0.0}, + }) { + t.Fatalf("unexpected results: %s", spew.Sdump(results)) + } +} + +func TestRawQueryDerivative_Process_Basic_24h(t *testing.T) { + p := tsdb.RawQueryDerivativeProcessor{ + IsNonNegative: false, + DerivativeInterval: 24 * time.Hour, + } + + results := p.Process([]*tsdb.MapperValue{ + {Time: time.Unix(0, 0).Unix(), Value: 0.0}, + {Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), Value: 3.0}, + {Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), Value: 5.0}, + {Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), Value: 9.0}, + }) + if !reflect.DeepEqual(results, []*tsdb.MapperValue{ + {Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), Value: 3.0}, + {Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), Value: 2.0}, + {Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), Value: 4.0}, + }) { + t.Fatalf("unexpected results: %s", spew.Sdump(results)) + } +} + +func TestRawQueryDerivative_Process_Basic_12h(t *testing.T) { + p := tsdb.RawQueryDerivativeProcessor{ + IsNonNegative: false, + DerivativeInterval: 12 * time.Hour, + } + + results := p.Process([]*tsdb.MapperValue{ + {Time: time.Unix(0, 0).UnixNano(), Value: 1.0}, + {Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), Value: 2.0}, + {Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), Value: 3.0}, + {Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), Value: 4.0}, + }) + if !reflect.DeepEqual(results, []*tsdb.MapperValue{ + {Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), Value: 0.5}, + {Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), Value: 0.5}, + {Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), Value: 0.5}, + }) { + t.Fatalf("unexpected results: %s", spew.Sdump(results)) + } +} + +func TestRawQueryDerivative_Process_Integer(t *testing.T) { + p := tsdb.RawQueryDerivativeProcessor{ + IsNonNegative: false, + DerivativeInterval: 24 * time.Hour, + } + + results := p.Process([]*tsdb.MapperValue{ + {Time: time.Unix(0, 0).Unix(), Value: int64(0)}, + {Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), Value: int64(3)}, + {Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), Value: int64(5)}, + {Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), Value: int64(9)}, + }) + if !reflect.DeepEqual(results, []*tsdb.MapperValue{ + {Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), Value: 3.0}, + {Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), Value: 2.0}, + {Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), Value: 4.0}, + }) { + t.Fatalf("unexpected results: %s", spew.Sdump(results)) + } +} + +func TestRawQueryDerivative_Process_Negative(t *testing.T) { + p := tsdb.RawQueryDerivativeProcessor{ + IsNonNegative: false, + DerivativeInterval: 24 * time.Hour, + } + + results := p.Process([]*tsdb.MapperValue{ + {Time: time.Unix(0, 0).Unix(), Value: 1.0}, + {Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), Value: 2.0}, + {Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), Value: 0.0}, + {Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), Value: 4.0}, + }) + if !reflect.DeepEqual(results, []*tsdb.MapperValue{ + {Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), Value: 1.0}, + {Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), Value: -2.0}, + {Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), Value: 4.0}, + }) { + t.Fatalf("unexpected results: %s", spew.Sdump(results)) + } +} + +func TestRawQueryDerivative_Process_Negative_NonNegative(t *testing.T) { + p := tsdb.RawQueryDerivativeProcessor{ + IsNonNegative: true, + DerivativeInterval: 24 * time.Hour, + } + + results := p.Process([]*tsdb.MapperValue{ + {Time: time.Unix(0, 0).Unix(), Value: 1.0}, + {Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), Value: 2.0}, + {Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), Value: 0.0}, + {Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), Value: 4.0}, + }) + if !reflect.DeepEqual(results, []*tsdb.MapperValue{ + {Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), Value: 1.0}, + {Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), Value: 4.0}, + }) { + t.Fatalf("unexpected results: %s", spew.Sdump(results)) + } +} + +func TestRawQueryDerivative_Process_String(t *testing.T) { + p := tsdb.RawQueryDerivativeProcessor{ + IsNonNegative: false, + DerivativeInterval: 24 * time.Hour, + } + + results := p.Process([]*tsdb.MapperValue{ + {Time: time.Unix(0, 0).Unix(), Value: "1.0"}, + {Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), Value: "2.0"}, + {Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), Value: "3.0"}, + {Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), Value: "4.0"}, + }) + if !reflect.DeepEqual(results, []*tsdb.MapperValue{ + {Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), Value: nil}, + {Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), Value: nil}, + {Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), Value: nil}, + }) { + t.Fatalf("unexpected results: %s", spew.Sdump(results)) + } +} + +func TestRawQueryDerivative_Process_Bool(t *testing.T) { + p := tsdb.RawQueryDerivativeProcessor{ + IsNonNegative: false, + DerivativeInterval: 24 * time.Hour, + } + + results := p.Process([]*tsdb.MapperValue{ + {Time: time.Unix(0, 0).Unix(), Value: true}, + {Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), Value: true}, + {Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), Value: false}, + {Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), Value: false}, + }) + if !reflect.DeepEqual(results, []*tsdb.MapperValue{ + {Time: time.Unix(0, 0).Add(24 * time.Hour).UnixNano(), Value: nil}, + {Time: time.Unix(0, 0).Add(48 * time.Hour).UnixNano(), Value: nil}, + {Time: time.Unix(0, 0).Add(72 * time.Hour).UnixNano(), Value: nil}, + }) { + t.Fatalf("unexpected results: %s", spew.Sdump(results)) } } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/shard.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/shard.go index 3a7215e08..37bdfad36 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/shard.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/shard.go @@ -20,12 +20,12 @@ import ( ) const ( - statWriteReq = "write_req" - statSeriesCreate = "series_create" - statFieldsCreate = "fields_create" - statWritePointsFail = "write_points_fail" - statWritePointsOK = "write_points_ok" - statWriteBytes = "write_bytes" + statWriteReq = "writeReq" + statSeriesCreate = "seriesCreate" + statFieldsCreate = "fieldsCreate" + statWritePointsFail = "writePointsFail" + statWritePointsOK = "writePointsOk" + statWriteBytes = "writeBytes" ) var ( diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/shard_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/shard_test.go index b913b52e5..e6732985e 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/shard_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/shard_test.go @@ -30,7 +30,7 @@ func TestShardWriteAndIndex(t *testing.T) { t.Fatalf("error openeing shard: %s", err.Error()) } - pt := models.NewPoint( + pt := models.MustNewPoint( "cpu", map[string]string{"host": "server"}, map[string]interface{}{"value": 1.0}, @@ -99,7 +99,7 @@ func TestShardWriteAddNewField(t *testing.T) { } defer sh.Close() - pt := models.NewPoint( + pt := models.MustNewPoint( "cpu", map[string]string{"host": "server"}, map[string]interface{}{"value": 1.0}, @@ -111,7 +111,7 @@ func TestShardWriteAddNewField(t *testing.T) { t.Fatalf(err.Error()) } - pt = models.NewPoint( + pt = models.MustNewPoint( "cpu", map[string]string{"host": "server"}, map[string]interface{}{"value": 1.0, "value2": 2.0}, @@ -159,7 +159,7 @@ func TestShard_Autoflush(t *testing.T) { // Write a bunch of points. for i := 0; i < 100; i++ { - if err := sh.WritePoints([]models.Point{models.NewPoint( + if err := sh.WritePoints([]models.Point{models.MustNewPoint( fmt.Sprintf("cpu%d", i), map[string]string{"host": "server"}, map[string]interface{}{"value": 1.0}, @@ -199,7 +199,7 @@ func TestShard_Autoflush_FlushInterval(t *testing.T) { // Write some points. for i := 0; i < 100; i++ { - if err := sh.WritePoints([]models.Point{models.NewPoint( + if err := sh.WritePoints([]models.Point{models.MustNewPoint( fmt.Sprintf("cpu%d", i), map[string]string{"host": "server"}, map[string]interface{}{"value": 1.0}, @@ -256,7 +256,7 @@ func benchmarkWritePoints(b *testing.B, mCnt, tkCnt, tvCnt, pntCnt int) { points := []models.Point{} for _, s := range series { for val := 0.0; val < float64(pntCnt); val++ { - p := models.NewPoint(s.Measurement, s.Series.Tags, map[string]interface{}{"value": val}, time.Now()) + p := models.MustNewPoint(s.Measurement, s.Series.Tags, map[string]interface{}{"value": val}, time.Now()) points = append(points, p) } } @@ -297,7 +297,7 @@ func benchmarkWritePointsExistingSeries(b *testing.B, mCnt, tkCnt, tvCnt, pntCnt points := []models.Point{} for _, s := range series { for val := 0.0; val < float64(pntCnt); val++ { - p := models.NewPoint(s.Measurement, s.Series.Tags, map[string]interface{}{"value": val}, time.Now()) + p := models.MustNewPoint(s.Measurement, s.Series.Tags, map[string]interface{}{"value": val}, time.Now()) points = append(points, p) } } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/store.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/store.go index 6ba318bfd..e2b52a19f 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/store.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/store.go @@ -155,7 +155,9 @@ func (s *Store) DeleteDatabase(name string, shardIDs []uint64) error { if shard != nil { shard.Close() } + delete(s.shards, id) } + if err := os.RemoveAll(filepath.Join(s.path, name)); err != nil { return err } diff --git a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/store_test.go b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/store_test.go index 93c1fb565..696c275eb 100644 --- a/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/store_test.go +++ b/Godeps/_workspace/src/github.com/influxdb/influxdb/tsdb/store_test.go @@ -286,7 +286,7 @@ func benchmarkStoreOpen(b *testing.B, mCnt, tkCnt, tvCnt, pntCnt, shardCnt int) points := []models.Point{} for _, s := range series { for val := 0.0; val < float64(pntCnt); val++ { - p := models.NewPoint(s.Measurement, s.Series.Tags, map[string]interface{}{"value": val}, time.Now()) + p := models.MustNewPoint(s.Measurement, s.Series.Tags, map[string]interface{}{"value": val}, time.Now()) points = append(points, p) } }