Compare commits

...

1049 commits

Author SHA1 Message Date
jaten-molecula
f7afd5d2e1
Merge pull request #875 from molecula/remove_debug_tooling
turn on row cache. tx: remove debug tooling
2020-09-22 15:56:02 -05:00
Jason Aten
55d4c29933 turn off debug machinery on tx backends
- enable row cache again. Was off for tx perf measurement.
 - centralize UseRowCache choice to just one point, in rbf.EnableRowCache
2020-09-22 15:47:52 -05:00
Kuba Podgórski
886318931d
Merge pull request #882 from kuba--/fix-869
Fix holes in grpc response for inspect
2020-09-22 14:20:10 +02:00
Kuba Podgórski
b54a289cd8 Fix holes in grpc response for inspect 2020-09-22 12:02:26 +02:00
jaten-molecula
ba2851229d
Merge pull request #871 from molecula/bluegreen_verify
blue_green verify accepts empty fragments
2020-09-18 11:55:22 -04:00
Jason Aten
57be5392cf blue_green verify accepts empty fragments 2020-09-18 10:49:36 -05:00
tgruben
8ec10ec1e8
Merge pull request #868 from molecula/with_primary_instead_owner
Translate only on coordinator/primary
2020-09-17 17:12:56 -05:00
Kuba Podgórski
650244214d Translate only on coordinator/primary
- This is the commit message #3:
2020-09-17 17:03:29 -05:00
jaten-molecula
7dc59c4d9c
Merge pull request #831 from molecula/rbf-tx-cursor-stack-allocate
Stack allocate return from rbf.Tx.cursor()
2020-09-16 23:40:32 -04:00
Jason Aten
9b99009871 allow rbf-tx-cursor-stack-allocate to merge; fix conflict with the new HasData code 2020-09-16 22:35:30 -05:00
jaten-molecula
c04642f34b
Merge branch 'master' into rbf-tx-cursor-stack-allocate 2020-09-16 23:21:35 -04:00
jaten-molecula
0f5838816b
Merge pull request #857 from molecula/rbf-fix-direct-write-corruption
Fix RBF write corruption during direct write.
2020-09-16 23:07:57 -04:00
jaten-molecula
71033f78b5
Merge branch 'master' into rbf-fix-direct-write-corruption 2020-09-16 22:32:47 -04:00
Nia
16c5212e2d
Merge pull request #858 from niaow/deprecate-inspect
Add a deprecation warning to Inspect
2020-09-16 15:31:57 -04:00
Nia Weiss
f03ba0e682
add a deprecation warning to Inspect 2020-09-16 12:30:12 -04:00
Ben Johnson
6918fe2f60
Merge branch 'master' into rbf-fix-direct-write-corruption 2020-09-16 09:55:26 -06:00
jaten-molecula
75b01ba87d
Merge pull request #850 from molecula/db_has_data_rb
blue_green verification and migration capabilities.
2020-09-16 11:22:34 -04:00
Ben Johnson
458984c756 Fix RBF write corruption during direct write.
This commit fixes an issue where direct writes would overwrite the
source page where data was being copied from because writes are
immediate (instead of going to the WAL first).
2020-09-16 08:59:53 -06:00
Jason Aten
31d54010f8 blue_green verification and migration capabilities.
- on startup in blue_green mode, we will migrate
   blue to green if blue is empty.
 - otherwise, when blue has data, we verify
   against green before proceeding with the
   blue_green run.
 - small optimization in the rbf cursorx.go to
   short-circuit processing on a nil bitmap.
   This avoids a roaringparanoia tag panic.
 - back out holdbkg.go, was too slow.
   add a distinct Holder.imu lock instead.
2020-09-16 09:41:05 -05:00
Kuba Podgórski
dd4ffff704
Merge pull request #853 from kuba--/fix-translate_index_keys
Fix translation index keys
2020-09-16 15:21:31 +02:00
Kuba Podgórski
fa159c9a17
Merge branch 'master' into fix-translate_index_keys 2020-09-16 15:09:49 +02:00
jaten-molecula
47926092ee
Merge pull request #855 from molecula/cleanup_cherrypick
cleanup lmdb tests that were leaving test directories behind
2020-09-16 07:51:23 -04:00
Jason Aten
c093aa9d0e cleanup lmdb tests 2020-09-16 06:46:12 -05:00
Kuba Podgórski
ab26134992
Merge branch 'master' into fix-translate_index_keys 2020-09-16 10:39:32 +02:00
Kuba Podgórski
a8a9a73b6c Fix translation index keys 2020-09-16 10:36:44 +02:00
tgruben
793cf934b8
Merge pull request #848 from molecula/fix_843
don't panic if dbs is reopened under roaring only. fixes #843
2020-09-15 16:54:52 -05:00
Jason Aten
439ac243b4 don't panic if dbs is reopened under roaring only. fixes #843 2020-09-15 15:52:05 -05:00
Kuba Podgórski
e83e18832a
Merge pull request #847 from kuba--/fix-writable
Fix TranslateStore writable
2020-09-15 20:36:27 +02:00
Kuba Podgórski
dcc237413e
Merge branch 'master' into fix-writable 2020-09-15 19:14:24 +02:00
Nia
c7a0b17600
Merge pull request #846 from niaow/typenames
Fix type names in PQL Extract and SQL Show
2020-09-15 13:13:32 -04:00
Kuba Podgórski
76715a3f9d Fix TranslateStore writable 2020-09-15 19:09:59 +02:00
Nia Weiss
19e0f3601b
fix type names in PQL Extract and SQL Show 2020-09-15 12:53:21 -04:00
tgruben
9649819c09
Merge pull request #845 from molecula/rebal_together_rb
fine tune Tx placement, make it lazier so we don't create extra shards.
2020-09-14 18:34:15 -05:00
jaten-molecula
28fa1d036e
Merge branch 'master' into rebal_together_rb 2020-09-14 19:21:31 -04:00
Jason Aten
748f6a61bc fine tune Tx placement, isolate Tx backends more.
- Tx creation is lazier so we don't create xtra shards.

 - Then the dir scan for blue-green state checking finds only the right shards.
2020-09-14 18:03:25 -05:00
Travis Turner
37196b5e03
Merge pull request #834 from travisturner/metric-typos
Fix typos in transaction metric names
2020-09-14 17:32:53 -05:00
Travis Turner
d2c6a8ddde
Merge branch 'master' into metric-typos 2020-09-14 17:20:49 -05:00
seebs
72b1eec2e9
Merge pull request #829 from seebs/storeDistinct
Test cases for Store(Distinct)
2020-09-14 14:28:05 -05:00
Seebs
e7b239d2d8 Test cases for Store(Distinct)
This adds testing for Store(Distinct(...)) with and without filters, to verify that
we can, in fact, store the results of a Distinct() query directly. This was at one
point unsupported, now we think it should work so we're testing it.

The change to the testdata is because the specific structure used for this test doesn't
work with a keyed index, and changing things to be "foreign keys" seems annoying and
more complicated, but possibly that should become part of a future test.

There was talk of testing this with non-BSI fields, but they don't seem to
actually work with Distinct right now, so that will be later.
2020-09-14 14:15:29 -05:00
seebs
88d0f541aa
Merge pull request #841 from seebs/setMapped
export SetMapped from roaring, use it in Tx stores
2020-09-14 14:15:11 -05:00
Seebs
8ab9174a09 export SetMapped from roaring, use it in Tx stores
Thaw() is supposed to always provide writable storage, which it does
by ensuring that containers aren't frozen, but also by cloning or
copying their data if the data is marked as being memory-mapped.

But only the roaring backend had the ability to mark data as memory-mapped,
because that wasn't exported. Fixed this, and added corresponding code
to badger, lmdb, and rbf.
2020-09-14 14:08:00 -05:00
Ben Johnson
343c446b7e
Merge pull request #839 from molecula/rbf-fix-checkpoint
Remove tx before issuing checkpoint.
2020-09-14 12:56:03 -06:00
jaten-molecula
d0ce2e1207
Merge branch 'master' into rbf-fix-checkpoint 2020-09-14 13:58:24 -04:00
alanbernstein
1e40af7e25
Merge pull request #830 from alanbernstein/metrics-json-fix
Fix panicking metrics.json endpoint
2020-09-14 12:28:39 -05:00
Ben Johnson
77661a891d Remove tx before issuing checkpoint. 2020-09-14 10:36:59 -06:00
Alan Bernstein
18a593008a Add simple tests for metrics endpoints 2020-09-14 11:34:59 -05:00
Alan Bernstein
acbd6ec37c New channel per node 2020-09-14 11:34:59 -05:00
jaten-molecula
fc2518e2d1
Merge pull request #838 from molecula/fix_rr_leaks
fix TestImportClearRestart resource leaks under roaring, better skipForRoaring func
2020-09-14 11:38:26 -04:00
Jason Aten
7028bcfc9d fix resource leaks in fragment_internal_test.go under roaring, better skipForRoaring func
- add tournament.sh to do all pair-wise comparisons of blue-green backends.
- isolate txstores away from roaring index/ directories with indexname.index.txstores@@@ dirs.
2020-09-13 22:45:39 -05:00
Travis Turner
a0db828701
Merge pull request #835 from travisturner/port-vdsm-metrics
Port over VDSM metrics
2020-09-11 16:28:36 -05:00
Travis
41e0465eda
Port over VDSM metrics 2020-09-11 16:03:50 -05:00
Travis
75644af64d
Fix typos in transaction metric names 2020-09-11 15:18:39 -05:00
tgruben
5e00dbadc2
Merge pull request #812 from molecula/bgdev_rb
blue_green migration; holdbkg.go holder goroutine.
2020-09-11 14:32:09 -05:00
Jason Aten
2eb097c14d blue_green migration. holdbkg.go holder goroutine.
- blue_green for doing migration. Called before Holder.Open finishes.
- holdbkg.go added for index lookup. Less wedging between a deadlock and a race.
- fix fault under read-only map under lmdb at
  TestExecutor_Execute_Row_Range/RowIDColumnID by doing cow in roaring.
- roaring -tags gofuzz builds again
- roaringparanoia build tag added to make test targets in Makefile
- add rbf.NewDBWithAllocZero for out-of-bounds memory checks
- .circleci/config.yml test-shardwidth-22 with large run container, kept OOM-ing we suspect.

Fixes #819
2020-09-11 14:24:15 -05:00
Antonio Navarro Perez
a68ee73f00
Merge pull request #782 from ajnavarro/remove-blake3-duplicated-code 2020-09-11 11:23:25 +02:00
jaten-molecula
2cc7c829cd
Merge branch 'master' into remove-blake3-duplicated-code 2020-09-11 01:14:02 -04:00
Ben Johnson
5b72a68628 Stack allocate return from rbf.Tx.cursor() 2020-09-10 14:58:22 -06:00
Nia
be944a6247
Merge pull request #822 from niaow/inspect-panic
Fix inspect panic from incorrect handling of the many types of empty argument
2020-09-09 09:39:20 -04:00
Nia Weiss
6ff224308d
fix inspect panic from incorrect handling of the many types of empty argument 2020-09-09 09:11:21 -04:00
seebs
db578423bb
Merge pull request #818 from seebs/roaringDoc
Roaring documentation updates and fixes resulting from them
2020-09-08 22:41:48 -05:00
Seebs
c079d4764b Check for possibly-dirty N values in containers modified in-place
After documenting the semantics, I noticed an arguable hole in them,
which is that you could Freeze() a dirty container, and then Repair()
wouldn't work on it. On further study, I added a roaringparanoia
check for attempts to access the N of dirty containers.

It turns out there's several such. But also, it turns out, there's
circumstances where unionInPlace is relying on the assumption that
N is valid, which it isn't always for dirty containers. Also, there's
at least one case where we rely on the assumption that forcibly
thawing a container, then calling unionInPlace on it, always modifies
that container. But that's not supposed to be true for an empty
container -- an empty container might be better handled by just
returning the container it's being unioned with. So, we drop the
unnecessary thaw (all the *InPlace ops are already thawing if/when
they need to), but we use the return from unionInPlace.
2020-09-08 13:16:54 -05:00
Seebs
17ba2e35a9 call helper functions every time to get new run slices
If you just stash the results of the function when defining the test cases, the
outcome is in part that you are reusing the same slices for multiple things. So,
for instance, if you perform a union on the OddBitsSet slice, with the EvenBitsSet
slice, the result is to overwrite the first entry in that slice with the 0-ffff
run... But the original slice still exists, and then we reuse it and get a slice
with a bit count of around 98,000. The underlying issue is that doContainer()
is calling NewContainerRun(), which is simply using the provided slice, not
copying it -- which is intentional, but the test has to be careful about it.

We call repair on the one we think should be a bitmap. Theoretically
maybe we should also repair the other one in case unionRunRun some day
starts returning unrepaired bitmaps, which in principle it's allowed to
do...
2020-09-08 12:38:49 -05:00
Seebs
ecacbf65d4 Document copy-on-write semantics, at all.
The copy-on-write semantics were previously documented only in
the 125-line commit log from the patch which introduced them. Add
documentation for them in a few likely places.
2020-09-08 11:44:56 -05:00
Ben Johnson
8397696a59
Merge pull request #813 from molecula/import-roaring-direct
Add direct import option; remove tx from fragment.Open()
2020-09-08 08:19:24 -06:00
Ben Johnson
a51c530763 Add direct import option; remove tx from fragment.Open() 2020-09-08 08:13:16 -06:00
Ben Johnson
62635e7668
Merge pull request #811 from molecula/fix-wal-replay
Fix RBF WAL replay/truncation
2020-09-08 08:11:00 -06:00
Ben Johnson
f2bde49d25 Fix RBF WAL replay/truncation 2020-09-07 09:39:26 -06:00
Antonio Navarro Perez
ca340f7e62 [hash] Remove duplicated blake3 code
blake3 code is used in several places on the code. The file was
duplicated on root and rbf package.

To avoid cyclic dependencies, I moved it to hash package. Some methods
must be public to use them in different places.

HashOfDir method was removed. Not used.

Signed-off-by: Antonio Navarro Perez <antnavper@gmail.com>
2020-09-07 12:24:22 +02:00
jaten-molecula
1994560cf6
Merge pull request #803 from molecula/fgen_refined
refine our f.gen nil handling to support blue/green
2020-09-04 18:52:32 -04:00
Jason Aten
63187f59ee refine our f.gen nil handling to support blue/green 2020-09-04 17:28:38 -05:00
Ben Johnson
278d518f03
Merge pull request #800 from molecula/rbf-exclusive-write
Add RBF exclusive lock mode
2020-09-04 13:41:52 -06:00
Ben Johnson
90663e070e
Merge branch 'master' into rbf-exclusive-write 2020-09-04 13:10:02 -06:00
jaten-molecula
bb101ebef2
Merge pull request #802 from molecula/fgen
restore former f.gen nil behavior
2020-09-04 14:56:45 -04:00
Ben Johnson
159d01b55d Add exclusive write option for RBF.
This commit adds the ability to start a transaction with an exclusive
lock for the entire database. This ensures no other read or write
transactions can run at the same time. Writes in this mode write
directly to the database and skip the WAL entirely.
2020-09-04 12:46:42 -06:00
Jason Aten
4b6e773c7c panic if f.gen is nil because it means the storage wasn't open 2020-09-04 13:19:46 -05:00
jaten-molecula
aebe028854
Merge pull request #777 from molecula/dbshard2
database per shard, HolderConfig, rbf bit-wise import speedups.
2020-09-04 14:04:13 -04:00
Ben Johnson
150c8a5b06 database per shard, HolderConfig, rbf bit-wise import speedups.
- introduce Query Context (Qcx) for managing database-per-shard.
- replaces the MultiTx, so mtx.go is retired and removed.
- introduces the HolderConfig struct and all Holders now have
  a path from birth.
- rbf speedups on bitwise writes
- badgerdb is removed due to unresolvable write conflicts.

fixes #703 #676
2020-09-04 13:00:33 -05:00
Nia
60da12a8e5
Merge pull request #784 from niaow/remove-sql-limit
Remove SQL artificial limit
2020-09-04 08:46:33 -04:00
Nia
ec41481523
Merge branch 'master' into remove-sql-limit 2020-09-04 08:11:48 -04:00
jaten-molecula
921f15991b
Merge pull request #788 from molecula/fldschk
pilosa-chk: show col, row translation, -v for fragment details
2020-09-03 19:20:11 -04:00
Jason Aten
1e87d113f1 pilosa-chk: col, row translation reported, -v shows fragment checksum 2020-09-03 18:14:49 -05:00
tgruben
17ddcacbd4
Merge pull request #787 from molecula/chktrans
cmd/translatorchk checksums and summarizes key counts from column key translators
2020-09-03 15:29:31 -05:00
Jason Aten
56803e6632 pilosa/cmd/translatorchk checksums and summarizes key counts from column key translators.
opens ~/.pilosa/index/_keys boltdbs and hashes the StringKey->ID mappings.
2020-09-03 15:23:05 -05:00
Nia Weiss
b676292d67
add named returns to clarify extractLimitOffset 2020-09-03 10:37:09 -04:00
Nia Weiss
daa784319c
remove SQL artificial limit 2020-09-03 09:14:11 -04:00
jaten-molecula
d3485dbdb3
Merge pull request #776 from niaow/configure-slurp
Configure slurp
2020-09-03 02:33:58 -04:00
jaten-molecula
0a548a9009
Merge branch 'master' into configure-slurp 2020-09-03 01:56:30 -04:00
Nia
82e5f09cb3
Merge pull request #779 from niaow/pg-race
Fix race condition in pg cancellation test
2020-09-02 19:07:28 -04:00
Nia Weiss
c9995bfae1
fix race condition in pg cancellation test 2020-09-02 19:01:47 -04:00
Nia
bd1b76e25e
Merge pull request #760 from niaow/pg-local-cancel
Add support for local cancellation to postgres endpoint
2020-09-02 15:05:31 -04:00
Nia
219e22cc64
Merge branch 'master' into pg-local-cancel 2020-09-02 14:02:18 -04:00
Nia Weiss
95d261b4d7
configure slurp 2020-09-02 12:53:16 -04:00
jaten-molecula
c213cb5457
Merge pull request #762 from molecula/optimize-import
rbf bitwise import optimizations
2020-09-01 21:44:46 -04:00
jaten-molecula
55cb448c37
Merge branch 'master' into optimize-import 2020-09-01 21:18:57 -04:00
Travis Turner
836d055978
Merge pull request #759 from travisturner/join-bug
fix bug on left/right join mapping
2020-09-01 19:06:49 -05:00
Kuba Podgórski
6255c3f07f
Merge branch 'master' into join-bug 2020-09-02 01:51:48 +02:00
Travis Turner
72c068a64f
Merge pull request #773 from travisturner/row-pointer
return *Row instead of Row on empty key result
2020-09-01 18:31:29 -05:00
Travis
32b5826d1a
fix bug on left/right join mapping 2020-09-01 18:22:55 -05:00
Travis
d02cb10687
return *Row instead of Row on empty key result 2020-09-01 17:34:29 -05:00
alanbernstein
58007ab7ef
Merge pull request #765 from alanbernstein/lattice-release
Add embedded UI to default release process
2020-09-01 13:27:08 -05:00
Alan Bernstein
007b3ffc3c re-re-arrange error checks 2020-09-01 11:32:58 -05:00
Alan Bernstein
4ce3879e2c Revert UI->Lattice name change, correct the ordering of error checks in statikHandler 2020-09-01 10:16:31 -05:00
Alan Bernstein
a597a79e2d Add embedded UI to default release process 2020-09-01 10:16:31 -05:00
Kuba Podgórski
2d202d6aa3
Merge pull request #766 from kuba--/fix-null
support null results
2020-09-01 15:33:02 +02:00
Kuba Podgórski
ceb72fc3a9 support null results 2020-09-01 13:00:30 +02:00
Nia
1e5f0ecc5d
Merge pull request #763 from niaow/sql-leak
Fix SQL memory leak
2020-08-31 14:38:23 -04:00
Nia Weiss
d421558f58
fix SQL memory leak 2020-08-31 13:10:49 -04:00
Nia
96a8fd5cb7
Merge pull request #724 from niaow/pg-primitive
Add primitive types to pg encoder
2020-08-31 13:04:39 -04:00
Nia Weiss
9f368b06bd
add licesnse header to pg formatter test 2020-08-31 12:42:37 -04:00
Nia Weiss
542eb2dba3
pg formatter tests 2020-08-31 12:39:55 -04:00
Nia Weiss
cfcc1da0bb
add primitive types to pg encoder 2020-08-31 12:27:10 -04:00
Nia
9b12c01c2b
Merge pull request #736 from niaow/dirtycache
Force ranked cache recalculation in Top after a skipped invalidation
2020-08-31 11:56:50 -04:00
Nia
ac57a0bd42
Merge branch 'master' into dirtycache 2020-08-31 11:28:52 -04:00
Ben Johnson
1672158e65
Merge branch 'master' into optimize-import 2020-08-31 08:58:12 -06:00
Ben Johnson
627b50d89d misc import optimizations 2020-08-31 08:46:53 -06:00
alanbernstein
b2cad11c99
Merge pull request #733 from alanbernstein/enlattice
Embed lattice via statik
2020-08-31 09:40:02 -05:00
Alan Bernstein
b14ebcadae Unexport statik filesystem 2020-08-31 09:02:46 -05:00
Cody Soyland
a3e122dc5e Fix CORS support by applying middleware to router. 2020-08-31 09:02:46 -05:00
Alan Bernstein
f9d3040827 Update gitignore and makefile 2020-08-31 09:02:46 -05:00
Alan Bernstein
384b6511cd Replace null with [] in /schema field response 2020-08-31 09:02:46 -05:00
Alan Bernstein
659a2bb560 Silence stderr in makefile 2020-08-31 09:02:46 -05:00
Alan Bernstein
6e917b6a9a Log lattice version info 2020-08-31 09:02:46 -05:00
Alan Bernstein
99869ce792 Minor fixes 2020-08-31 09:02:46 -05:00
Alan Bernstein
b039bd50a1 Switch to mux PathPrefix matcher entirely 2020-08-31 09:02:46 -05:00
Alan Bernstein
19ee27fdb6 Use SPA handler to serve from filesystem, to test routing behavior 2020-08-31 09:02:46 -05:00
Alan Bernstein
d912403eb2 Add missing file 2020-08-31 09:02:46 -05:00
Alan Bernstein
88a288e775 Embed lattice via statik 2020-08-31 09:02:46 -05:00
Kuba Podgórski
b88c5cd5ea
Merge pull request #753 from kuba--/grpc-errcode
Add rich error types to gRPC interface
2020-08-31 15:45:24 +02:00
Kuba Podgórski
bfff643f34 Fix error code for PostVDS 2020-08-31 12:16:07 +02:00
Kuba Podgórski
f7a0e5f536 Fix error code for DeleteVDS 2020-08-31 12:14:58 +02:00
Kuba Podgórski
bf17385409
Merge branch 'master' into grpc-errcode 2020-08-31 10:25:35 +02:00
Nia Weiss
bf7e4b5583
add support for local cancellation to postgres endpoint 2020-08-29 14:56:49 -04:00
Nia
9c0d45a2d9
Merge pull request #752 from niaow/batch-translate
Batch the translation of field keys in results
2020-08-28 15:25:19 -04:00
Nia
5bc5991bd7
Merge branch 'master' into batch-translate 2020-08-28 15:09:05 -04:00
Kuba Podgórski
25ead95967
Merge branch 'master' into grpc-errcode 2020-08-28 20:42:28 +02:00
Nia Weiss
8549b74421
merge groupcount translations check into an else-if 2020-08-28 14:22:35 -04:00
Nia
a872548b01
Merge pull request #756 from niaow/postgres-sql-empty
Write empty column headers in postgres when there is no response
2020-08-28 14:20:22 -04:00
Nia Weiss
57fdabfe40
write empty column headers in postgres when there is no response 2020-08-28 13:28:50 -04:00
Travis Turner
81d32a6eeb
Merge pull request #751 from travisturner/sql-mapper-stuff
add sql mapper routes for count(*) on joins
2020-08-28 11:55:49 -05:00
Travis Turner
27c2368b9a
Merge branch 'master' into sql-mapper-stuff 2020-08-28 11:43:16 -05:00
Nia Weiss
0a9699490b
batch the translation of field keys in results 2020-08-28 11:56:37 -04:00
Kuba Podgórski
2b1c9950f4 Add rich error types to gRPC interface 2020-08-28 17:24:49 +02:00
jaten-molecula
f2fb2c98f8
Merge pull request #737 from molecula/bulk-import-value
Implement TxBitmap to cache up many bit changes in a Tx
2020-08-28 00:43:46 -04:00
Travis
ae07aacd51
add sql mapper routes for count(*) on joins 2020-08-27 22:49:54 -05:00
jaten-molecula
4883424205
Merge branch 'master' into bulk-import-value 2020-08-27 21:43:32 -04:00
Nia
9c621d7622
Merge pull request #742 from niaow/pg-require-tls
Fix postgres configuration
2020-08-27 12:49:45 -04:00
Nia Weiss
f0f8336f57
rename postgres.addr to postgres.bind for consistency 2020-08-27 11:35:54 -04:00
Ben Johnson
e3db6c9cec Implement bulk value import 2020-08-27 09:20:23 -06:00
Nia Weiss
68542ddc35
force ranked cache recalculation in Top after a skipped invalidation 2020-08-27 11:05:18 -04:00
Nia Weiss
288593231f
require TLS when configured 2020-08-27 10:59:40 -04:00
Nia Weiss
47dd6b5b8f
fix postgres endpoint config 2020-08-27 10:49:43 -04:00
Nia Weiss
c2823a933b
add a config option to require TLS on postgres 2020-08-27 10:49:43 -04:00
Kuba Podgórski
475a9e3910
Merge pull request #732 from kuba--/translatekey-writable
Add writable argument to TranslateKey functions.
2020-08-27 16:15:49 +02:00
Kuba Podgórski
248ec3a296
Merge branch 'master' into translatekey-writable 2020-08-27 16:10:29 +02:00
Kuba Podgórski
6b9eed3af4
Merge pull request #743 from kuba--/fix-info
Pass name to newNotFoundError
2020-08-27 16:08:40 +02:00
Kuba Podgórski
2ca8ca0605 Pass name to newNotFoundError 2020-08-27 15:35:23 +02:00
Nia
c3174efdeb
Merge pull request #740 from niaow/xor-array-array
Speed up xorArrayArray
2020-08-26 17:30:57 -04:00
Nia Weiss
6e06cc0c30
speed up xorArrayArray 2020-08-26 17:14:24 -04:00
Kuba Podgórski
9cf58ddb1c Add writable argument to TranslateKey functions. 2020-08-26 09:57:50 +02:00
Kuba Podgórski
ca5e27ad8e
Merge pull request #721 from kuba--/invalid-query/fix-706
Fix https://github.com/molecula/pilosa/issues/706
2020-08-25 21:33:54 +02:00
Kuba Podgórski
b67148ccfc
Merge branch 'master' into invalid-query/fix-706 2020-08-25 20:38:30 +02:00
Nia
6db4c8bb3a
Merge pull request #725 from niaow/storekeyed
Fix Store() into a keyed set
2020-08-25 14:08:46 -04:00
Kuba Podgórski
24980c7af8
Merge branch 'master' into invalid-query/fix-706 2020-08-25 19:30:03 +02:00
Nia Weiss
a8e7c2d0ef
fix Store() into a keyed set 2020-08-25 11:39:03 -04:00
jaten-molecula
8ad3dd2de6
Merge pull request #728 from molecula/skip_cluster_tests_bg
TestClusterResize_AddNode,AddNodeConcurrentIndex skipped blue-green roaring
2020-08-25 04:06:49 -04:00
tgruben
06281fc0c8
Merge branch 'master' into skip_cluster_tests_bg 2020-08-25 02:30:00 -05:00
tgruben
15315e47ee
Merge pull request #729 from molecula/fix683
test roaring.Container.UnionInPlace does not overflow
2020-08-25 02:29:29 -05:00
jaten-molecula
bc18e4dc42
Merge branch 'master' into skip_cluster_tests_bg 2020-08-25 03:28:00 -04:00
Jason Aten
ca7552b4a8 test that roaring.Container.UnionInPlace does
not overflow two big arrays into an invalid array.

recreate badloader from git history, at 85fa67e8. Could not
reproduce this, but lots of container usage
also got updated in the meantime.

Fixes #683
2020-08-24 17:06:41 -05:00
jaten-molecula
c5660ed712
Merge pull request #701 from molecula/fix-rbf-race
Remove duplicate checkpoint() invocation
2020-08-24 16:19:20 -04:00
jaten-molecula
ec02d5a00e
Merge branch 'master' into fix-rbf-race 2020-08-24 16:06:25 -04:00
jaten-molecula
0e6b65d3c2
Merge branch 'master' into skip_cluster_tests_bg 2020-08-24 16:02:13 -04:00
Jason Aten
48f31ddce7 TestClusterResize_AddNode and TestClusterResize_AddNodeConcurrentIndex skipped under blue-green test with roaring 2020-08-24 14:57:08 -05:00
alanbernstein
14a9a838eb
Merge pull request #690 from alanbernstein/prometheus-json
Add metrics.json endpoint using prometheus/prom2json
2020-08-24 14:29:27 -05:00
Alan Bernstein
9b027a89b9 Return dict of all node metrics 2020-08-24 14:04:02 -05:00
Alan Bernstein
d9b670c83c Add metrics.json endpoint using prometheus/prom2json 2020-08-24 14:04:02 -05:00
Sarah
6c4e8aeff5
Merge branch 'master' into fix-rbf-race 2020-08-24 13:23:56 -05:00
jaten-molecula
0e4144c302
Merge pull request #709 from molecula/audit3rb
testhook: Leak auditing infrastructure
2020-08-24 12:32:20 -04:00
Seebs
cecaf99ee4 testhook: leak auditing infrastructure
The testhook/ package provides an easy way to set up multiple
hooks to run before/after tests are run.

The audit hooks track open and closes of storage backends,
files, indexes, and holders, for example. A tempdir wrapper
creates temporary directories which are automatically cleaned up
when the test ends. Any kind of resource creation that
should be closed at test conclusion can be tracked. We
will complain at the end of the TestMain if resources are
leaking.

Leaks under go1.13:

We use a wrapper function which is a no-op for go 1.13, but actually
calls testing.TB.Cleanup in go1.14, so we can still build with 1.13 even though
tests will leak files all over the place there. Because of this,
don't run the testhook tests when using 1.13, as they'll always fail.

- the test/pilosa.go http client now times out after 10 seconds
to help diagnose hung server situations.

- Makefile targets added to get better progress reports.
2020-08-24 11:26:39 -05:00
jaten-molecula
7e326fb833
Merge pull request #722 from tgruben/rbf-add
copied optimized add/remove to rbf
2020-08-24 11:42:57 -04:00
Todd Gruben
edf6608129 copied optimized add/remove to rbf
use large test container for CI
2020-08-24 10:37:25 -05:00
Kuba Podgórski
397d37a129 Fix https://github.com/molecula/pilosa/issues/706 2020-08-24 15:34:09 +02:00
Kuba Podgórski
bc3f329e8f
Merge pull request #720 from kuba--/sql-drop
Add support for SQL 'drop table...'
2020-08-24 14:52:49 +02:00
Kuba Podgórski
ceda4ab61b Add support for drop table 2020-08-24 13:24:18 +02:00
tgruben
9caa670c90
Merge pull request #719 from tgruben/lmdb-add
copied badger addRemove implementation to lmdb
2020-08-23 08:50:07 -05:00
tgruben
9e94c86932
Merge branch 'master' into lmdb-add 2020-08-23 08:40:16 -05:00
Todd Gruben
4dd4b441c4 copied badger addRemove implementation to lmdb 2020-08-23 08:39:03 -05:00
jaten-molecula
4a6d8bc5df
Merge branch 'master' into fix-rbf-race 2020-08-22 23:23:57 -04:00
Nia
0ce9062588
Merge pull request #716 from kuba--/sql-show
Add support for SHOW queries
2020-08-21 19:46:47 -04:00
Kuba Podgórski
ad9e3338b5 Add support for SHOW queries 2020-08-22 01:25:35 +02:00
Nia
c137efbe44
Merge pull request #707 from niaow/pg-sql
Add SQL to postgres endpoint
2020-08-21 19:13:26 -04:00
Kuba Podgórski
cb89a4a831
Merge branch 'master' into pg-sql 2020-08-22 01:01:22 +02:00
Kuba Podgórski
37a4cda454
Merge pull request #563 from kuba--/fix-210/translate-entry
Fix 210/translate entry
2020-08-22 00:52:53 +02:00
Kuba Podgórski
7c13fa71d7
Merge branch 'master' into fix-210/translate-entry 2020-08-22 00:18:34 +02:00
Kuba Podgórski
6d0baa82b3
Update http/translator_test.go
Co-authored-by: Travis Turner <travis@pilosa.com>
2020-08-22 00:18:25 +02:00
Kuba Podgórski
cc66775ea4
Update http/translator_test.go
Co-authored-by: Travis Turner <travis@pilosa.com>
2020-08-22 00:18:19 +02:00
Nia
842377ba17
Merge pull request #713 from niaow/dont-log-100000-times
Stop logging in TestStartupInvalidLength
2020-08-21 17:29:52 -04:00
Nia Weiss
dd73bf3960
stop logging in TestStartupInvalidLength 2020-08-21 17:20:10 -04:00
jaten-molecula
8d397babdf
Merge pull request #710 from molecula/clear_importvals
ImportRequest.Clear and ImportValuesRequest.Clear respected by api.Import() and api.ImportValues()
2020-08-21 15:42:24 -05:00
tgruben
056d5990da
Merge branch 'master' into clear_importvals 2020-08-21 15:33:04 -05:00
Jason Aten
40a9d01f46 ImportRequest.Clear and ImportValuesRequest.Clear respected by api.Import() and api.ImportValues()
- tested in TestAPI_ClearFlagForImportAndImportValues api_test.go
2020-08-21 15:17:44 -05:00
Nia Weiss
49c3bd9701
add SQL to postgres endpoint 2020-08-21 14:47:59 -04:00
Ben Johnson
91c8c5b5e9
Merge branch 'master' into fix-rbf-race 2020-08-21 10:33:43 -06:00
Kuba Podgórski
a2f24bc115
Merge pull request #663 from kuba--/sql-mapper
SQL mapper
2020-08-21 17:24:59 +02:00
Ben Johnson
162bd0303a Remove duplicate checkpoint() invocation 2020-08-21 08:25:04 -06:00
Kuba Podgórski
17fa1e578b Add benchmark for translation reader 2020-08-21 15:15:41 +02:00
Kuba Podgórski
bbbeb22d31
Merge branch 'master' into sql-mapper 2020-08-21 03:17:46 +02:00
tgruben
57babb3269
Merge pull request #700 from molecula/atomicrecord
AtomicRecord allows the client to request atomic updates.
2020-08-20 17:42:15 -05:00
Jason Aten
df5dd1557e AtomicRecord allows the client to request atomic updates.
- Atomic record contains multiple ImportRequest and
   ImportValueRequest, plus ability to Clear individual requests.
 - adds http handlers for importing AtomicRecord.
2020-08-20 17:33:52 -05:00
Kuba Podgórski
0e0a405184
Merge branch 'master' into sql-mapper 2020-08-20 21:53:59 +02:00
Nia
272f6708a3
Merge pull request #679 from niaow/pg
Add a postgres endpoint to pilosa
2020-08-20 14:24:56 -04:00
Kuba Podgórski
6fa69fb81a
Merge branch 'master' into sql-mapper 2020-08-20 20:17:53 +02:00
Nia Weiss
14676c0713
remove postgres debugging types 2020-08-20 14:17:50 -04:00
Nia
44061923c7
Update pg/message/io.go
Co-authored-by: Travis Turner <travis@pilosa.com>
2020-08-20 14:14:14 -04:00
Nia Weiss
128e02046a
add a postgres endpoint to pilosa 2020-08-20 11:29:16 -04:00
alanbernstein
68b17312e0
Merge pull request #687 from alanbernstein/unicode
Use _buffer instead of buffer
2020-08-19 17:12:26 -05:00
Alan Bernstein
6fc5465aaa Use rune slice in all cases, add tests 2020-08-19 16:32:27 -05:00
Alan Bernstein
0a96043c23 Use _buffer instead of buffer 2020-08-19 16:32:27 -05:00
Kuba Podgórski
0be436e18d
Merge branch 'master' into sql-mapper 2020-08-19 21:33:58 +02:00
Ben Johnson
0a37f396f8
Merge pull request #671 from molecula/wal-write-cache
Add RBF WAL write cache
2020-08-19 09:54:55 -06:00
Ben Johnson
9dbb82cf3e WAL mutex fixes 2020-08-19 08:42:53 -06:00
Ben Johnson
51504f4fe4 Add WAL write cache mutex; update name; add benchmarks 2020-08-19 08:33:19 -06:00
Ben Johnson
5849794a1b Add RBF WAL write cache 2020-08-19 08:33:19 -06:00
Cody Soyland
776de471db
Merge pull request #693 from molecula/ubuntu_dockerfile
ubuntu 20:10 image instead of alpine, for cgo support
2020-08-18 20:40:26 -05:00
Jason Aten
ef156f6172 ubuntu 20:10 image instead of alpine, for cgo support 2020-08-18 16:55:44 -05:00
Kuba Podgórski
2b34976c22 porting sqlmapper from vdsm 2020-08-18 15:53:26 +02:00
Cody Soyland
039780a85e
Merge pull request #685 from molecula/discard-test-386
no more test-386
2020-08-17 20:15:14 -05:00
Jason Aten
aea7d4a812 no more test-386
- lmdb won't build under 386
 - still have to keep lmdb_other.go for arm/arm64
2020-08-17 19:54:50 -05:00
jaten-molecula
ba1d5e9f9f
Merge pull request #681 from molecula/fix-rbf-reopen
Fix RBF checkpoint on reopen. Fixes #673
2020-08-17 19:16:17 -05:00
jaten-molecula
3e4da916e8
Merge branch 'master' into fix-rbf-reopen 2020-08-17 19:09:55 -05:00
tgruben
d84ae88944
Merge pull request #677 from molecula/lmdb_as_backend
add lmdb, Tx call stats, and prep for db/shard.
2020-08-17 18:43:57 -05:00
Jason Aten
123ce41840 add lmdb, Tx call stats, and prep for db/shard.
- lmdb as a backend (lmdb.go)
   (lmdb is the fastest known transactional storage backend)
 - per Tx call statics report enabled with PILOSA_CALLSTAT=true (stattx.go)
 - framework for per-shard db (dbshard.go)
 - txfactory handles any pair under blue-green testing (txfactory.go)
 - enable CGO in Dockerfiles for lmdb
2020-08-17 18:26:58 -05:00
seebs
4f7e4feaf8
Merge pull request #680 from seebs/cachefix
when updating a container, drop the single-container cache
2020-08-17 15:03:10 -05:00
Ben Johnson
b1e806a518 Fix RBF checkpoint on reopen.
Previously, the `rbf.DB.opened` flag was set after `checkpoint()`
when reopening, however, this flag is checked by `checkpoint()` so
it was not properly executing.
2020-08-17 13:28:28 -06:00
Seebs
16ba54293a when updating a container, drop the single-container cache
This can cause incredibly weird and hard-to-debug problems if the previous
container value is still in the cache after an update, and in particular,
can result in having a stale container value cached after a roaring import
that modified the container. Coupled with another bug which could corrupt
containers on a delete, this produces a very strange bug where a value is
present in a fragment, but an attempt to delete it reports failure.
2020-08-17 11:23:47 -05:00
Travis Turner
c5e9b8d917
Merge pull request #674 from travisturner/container-corrupt
stop setting N on a frozen container
2020-08-16 15:59:58 -05:00
Travis
ff5925aa8b
stop setting N on a frozen container 2020-08-14 17:25:14 -05:00
Matthew Jaffee
cd83f7965c
Merge pull request #672 from molecula/remove-transaction.md
remove old transactions doc
2020-08-14 14:40:10 -05:00
Matt Jaffee
b11bdeab87
remove old transactions doc 2020-08-14 11:57:28 -05:00
jaten-molecula
05ca83c2f5
Merge pull request #669 from molecula/autocommit2
build-tag out lmdb-go from builds; lower autocommit limits to avoid ErrTxnTooBig; improve txn size estimation
2020-08-13 11:52:42 -05:00
Jason Aten
c90c4c275d lower autocommit limits to avoid ErrTxnTooBig; improve txn size estimation 2020-08-13 16:42:47 +00:00
Travis Turner
1f53e86e47
Merge pull request #662 from travisturner/transaction-list
transactions as a list endpoint; added Transaction.CreatedAt
2020-08-13 08:40:57 -05:00
Travis
1837811ce1
transactions as a list endpoint; added Transaction.CreatedAt 2020-08-12 23:11:54 -05:00
tgruben
0dea018fb7
Merge pull request #665 from molecula/autocommit
tested working autocommit approach
2020-08-12 21:56:03 -05:00
Jason Aten
5e49e10cda tested working autocommit approach 2020-08-13 01:12:14 +00:00
jaten-molecula
c7d6229380
Merge pull request #664 from molecula/bigtx
PutContainer() handles large txn with autocommit
2020-08-12 19:18:19 -05:00
Jason Aten
6dc9727796 PutContainer() handles large txn with autocommit 2020-08-12 23:15:23 +00:00
jaten-molecula
973e9a7955
Merge pull request #659 from molecula/rbf_99pct
all test green on rbf. WOOT.
2020-08-12 16:13:47 -05:00
Todd Gruben
547ee14f5b all test green on rbf. WOOT.
- rbf had races around the new rootRecords cache in tx
 - rbf tx needed a write lock on the db now that rootRecords are written
 - added a global registry for rbfDB to correctly dedup instances
 - implement DeleteFragment, DeleteIndex for rbf
 - use badger style keys for rbf to allow content checksumming to be list
    containers in the same order
 - lots of other integration of rbf into pilosa layer.
2020-08-12 21:08:30 +00:00
Cody Soyland
123e0b6376
Merge pull request #644 from codysoyland/grpc-crd-sql
Add gRPC methods to support the functionality of VDSM
2020-08-12 14:55:24 -05:00
Cody Soyland
21456827ae
Merge branch 'master' into grpc-crd-sql 2020-08-12 14:21:53 -05:00
Cody Soyland
3361033c5b Add PostVDS and DeleteVDS 2020-08-12 14:21:22 -05:00
Cody Soyland
3771bd43b5 Add note about gRPC unary methods and futures 2020-08-12 14:08:30 -05:00
Nia
50a2e3c5fa
Merge pull request #650 from jaddr2line/limit
Add a "Limit" query
2020-08-12 14:04:12 -04:00
Nia
911c039991
Fix incorrect negative in Limit query documentation
Co-authored-by: Travis Turner <travis@pilosa.com>
2020-08-12 13:53:57 -04:00
Jaden Weiss
40a6fbd79e
add a "Limit" query 2020-08-12 12:27:23 -04:00
Nia
2a1fafb227
Merge pull request #652 from jaddr2line/constrow
Add ConstRow query
2020-08-12 12:22:06 -04:00
Nia
a19ca16810
Merge branch 'master' into constrow 2020-08-12 11:47:46 -04:00
Travis Turner
683b8dfe31
Merge pull request #660 from travisturner/error-rows-int
error on Rows(field=<int>)
2020-08-11 19:44:46 -05:00
Travis
eae038a07e
error on Rows(field=<int>) 2020-08-11 18:07:03 -05:00
Jaden Weiss
0e16192aeb
add ConstRow query 2020-08-11 08:25:32 -04:00
Jaden Weiss
96c2364c1b
Merge pull request #642 from jaddr2line/query-extract
Add an "Extract" query
2020-08-10 19:32:36 -04:00
Jaden Weiss
590bd07995
add an "Extract" query 2020-08-10 09:33:55 -04:00
Cody Soyland
559a2c6864 Add GetVDS and GetVDSs gRPC implementations 2020-08-07 16:28:06 -05:00
Ben Johnson
2bc3442ae8
Merge pull request #649 from molecula/fix-rbf-rollback
Fix RBF checkpoint high water mark
2020-08-07 14:29:31 -06:00
Ben Johnson
faa1662bf6 Fix RBF checkpoint high water mark
Previously, the `checkpoint()` function determined the segments to drop
based on the current active transactions' WAL ID references. However, if
no transactions are active then the checkpoint would drop segments too
aggressively.

This changes the determination by using the highest WAL ID that is
actually checkpointed to disk to determine the high water mark. If no
page are checkpointed then no segments can be dropped.
2020-08-07 10:07:33 -06:00
Ben Johnson
5644867507
Merge pull request #643 from molecula/fix-rbf-hanging-tests
Fix RBF/pilosa hanging tests
2020-08-07 10:06:57 -06:00
Cody Soyland
35675a3e59 Add stubbed gRPC types to support python-molecula
This adds the following rpc calls: GetVDS, GetVDSs, PostVDS, DeleteVDS, QuerySQL, QuerySQLUnary

Currently, they are not implemented.
2020-08-06 14:19:16 -05:00
Ben Johnson
78f9fbe332 Fix RBF/pilosa hanging tests 2020-08-06 11:43:41 -06:00
alanbernstein
5306426b19
Merge pull request #641 from alanbernstein/more-docs-updates
Fix various docs issues
2020-08-05 12:34:57 -05:00
Alan Bernstein
5f26290f92 Fix various docs issues 2020-08-05 12:26:22 -05:00
Ben Johnson
3fb2cccb00
Merge pull request #637 from molecula/rbf-fixes
Multiple RBF test fixes
2020-08-05 09:02:06 -06:00
Ben Johnson
f8cacd8081 Multiple RBF test fixes 2020-08-05 08:20:34 -06:00
Travis Turner
df709af10e
Merge pull request #638 from travisturner/fix-executeclearrow
safe cast of bool in executeClearRow
2020-08-04 15:52:48 -05:00
Travis
ddee2cb0a8
safe cast of bool in executeClearRow 2020-08-04 15:16:23 -05:00
tgruben
7f6c6956d8
Merge pull request #635 from molecula/efence_off
DetectMemAccessPastTx flag added, default false.
2020-08-03 22:03:36 -05:00
tgruben
86b5d89bc9
Merge branch 'master' into efence_off 2020-08-03 21:24:23 -05:00
alanbernstein
dede69f04d
Merge pull request #590 from alanbernstein/improve-tx-error-messages
Improve 'shouldn't ever happen' error messages
2020-08-03 12:44:30 -05:00
alanbernstein
27edff60b0
Merge branch 'master' into improve-tx-error-messages 2020-08-03 12:34:22 -05:00
Jason Aten
b806322c5a DetectMemAccessPastTx flag added, default false. Allow badger to run at full speed rather than with debugging code on by default 2020-08-01 21:40:54 -04:00
tgruben
0820babc44
Merge pull request #634 from molecula/unionfix
Follow roaring.Union() with optimize() to avoid overly large containers.
2020-08-01 08:58:47 -05:00
Jason Aten
394b8522d1 Follow roaring.Union() with optimize() to avoid overly large containers.
The cmd/loader is a preliminary sketch of the load testing tool.
2020-07-31 20:10:43 -04:00
tgruben
d0c2b80021
Merge pull request #631 from molecula/rbf_thurs
rbf: OffsetRange, ImportRoaringBits, CountRange work
2020-07-30 19:30:04 -05:00
Jason Aten
a3d802f8a3 rbf: OffsetRange, ImportRoaringBits, CountRange work
green:
TestFragment_RowsIteration/combinations
TestFragment_RoaringImportTopN

red: (needs Ben's attention)
PILOSA_TXSRC=rbf go test -v -run TestFragment_TopN_IDs  -tags=' shardwidth20'  "-gcflags=all=-d=checkptr=0"

also red: (one for Ben)
TestCursor_FirstNext_Quick/9 is throwing
  panic: cannot find segment containing WAL page: 1
  as we check the error back from checkpoint() in Rollback().
2020-07-30 20:20:38 -04:00
jaten-molecula
2fb76ba919
Merge pull request #626 from molecula/rbf_dump
rbf Dump() and DumpString() debug methods.
2020-07-30 13:57:30 -04:00
Jason Aten
8903d8c117 rbf Dump() and DumpString() debug methods. 2020-07-30 13:50:36 -04:00
jaten-molecula
d2586210a2
Merge pull request #621 from molecula/bluegreen_atg
blueGreenTx roaring vs badger is all tests green (atg)
2020-07-30 12:04:11 -04:00
Jason Aten
72c893a3d1 blueGreenTx roaring vs badger is all tests green (atg).
back to github.com/dgraph-io/badger/v2 v2.0.1-rc1.0.20200709123515-8e896a7af361  b/c github.com/dgraph-io/badger/v2 v2.0.1-rc1.0.20200718033852-37ee16d8ad1c had issues with CI on 386 and arm
2020-07-30 11:50:25 -04:00
Cody Soyland
5c5cd1e1fd
Merge pull request #620 from codysoyland/go-mod-tidy
Add CI job to ensure go mod files are tidy
2020-07-29 18:18:37 -05:00
Cody Soyland
55313bd69d Add CI job to ensure go mod files are tidy. 2020-07-29 15:28:58 -05:00
Ben Johnson
55c0785215
Merge pull request #597 from molecula/rbf-tx
Implement pilosa.Tx for RBF
2020-07-29 11:42:20 -06:00
Ben Johnson
64de208170 Implement pilosa.Tx for RBF 2020-07-29 11:25:41 -06:00
jaten-molecula
4cdf62ab89
Merge pull request #615 from molecula/parallelized_open_frag
Parallelize view.OpenFragmentsInTx
2020-07-28 11:18:13 -04:00
Jason E. Aten
71eccd121d fix race in view.openFragmentsInTx 2020-07-28 07:55:28 -04:00
Jason Aten
38eea9b4a7 reparallelize view.go openFragmentsInTx() 2020-07-28 07:55:28 -04:00
jaten-molecula
1a89fc27a1
Merge pull request #605 from molecula/badger_atg
Tx integration milestone
2020-07-27 20:44:12 -04:00
Jason Aten
ac7be132ef Tx integration milestone
a) All tests green under -race for both PILOSA_TXSRC=roaring and PILOSA_TXSRC=badger.

b) Distinct is merged back into mainline pilosa.

Seebs notes on the Distinct work:

merge Distinct plugin back into main source tree, convert to Tx

We drop all references to the Preemptively Deprecated Don't You Dare
Use This extension interface, and move the one and only extension we had
(Distinct) into the main executor.

Also this fixes an arguable bug, which is that Container.AsBitmap()
would panic on a nil parameter, but it should have returned an empty
bitmap, because a nil *Ccontainer is a valid empty container. This
simplifies logic significantly in Distinct.

Fixes #569 #570 #571 #572 #573 #584 #585
2020-07-27 19:29:46 -04:00
Ben Johnson
efc90a6d36
Merge pull request #596 from molecula/rm-rbf-fun-dot
Remove generation of rbf/fun.dot in tests
2020-07-23 10:12:17 -06:00
Ben Johnson
75930ed82f Remove generation of rbf/fun.dot in tests 2020-07-23 09:37:50 -06:00
alanbernstein
7c94828a3c
Merge pull request #589 from alanbernstein/broken-index-translation
Fix broken index key translation in state DEGRADED
2020-07-22 21:22:06 -05:00
Alan Bernstein
5ffc7d7b59 Move checkClusterStatus to test package 2020-07-22 19:54:44 -05:00
Travis
339b76a091 use c.Topology, when available, to determine partitionNodes 2020-07-22 16:41:53 -05:00
Alan Bernstein
a727c74d35 Use nodes from topology to calculate partitionNodes 2020-07-22 16:41:53 -05:00
Alan Bernstein
6c8e2e9450 Add test 2020-07-22 16:41:53 -05:00
Alan Bernstein
fa77a36e83 Improve 'shouldn't ever happen' error messages 2020-07-22 16:36:02 -05:00
Jaden Weiss
fd65384faa
Merge pull request #583 from jaddr2line/bsi-test
Test every possible BSI comparison up to 6 bits
2020-07-21 15:58:31 -04:00
Jaden Weiss
9072b4c290
test every possible BSI comparison up to 6 bits 2020-07-21 13:56:36 -04:00
seebs
f5a228e56d
Merge pull request #579 from seebs/shutdownresize
Handle cluster shutdown during a resize
2020-07-21 12:45:18 -05:00
seebs
aae1d25152
Merge branch 'master' into shutdownresize 2020-07-21 12:27:33 -05:00
Jaden Weiss
adde0fa0bc
Merge pull request #564 from jaddr2line/fix-between-common-bits
Fix BSI range queries with nonzero common upper bits and oversized BSI queries
2020-07-21 13:24:00 -04:00
Jaden Weiss
9fb5f8b349
Merge branch 'master' into fix-between-common-bits 2020-07-21 13:19:15 -04:00
Seebs
99420b564e Handle cluster shutdown during a resize
The new logic to send resize instructions more makes it easier
to hit this, but it's probably always been a theoretically possible
bug to hit: If you are shutting a cluster down, then you stop accepting
connections, which means that if you have an existing resize job, you
can't get responses for it. Which means that the other nodes will
fail to notify you of the success or failure of resize instructions,
so the code waiting on the resize job's status waits forever.

When closing, we bail immediately on that; we don't need to wait for
those notifications. We still have a buffer, and a reasonable confidence
that we'll never write more than one result status, so if one of them
*does* somehow show up and cause the job to have a status,
writing the status won't block.
2020-07-21 12:16:38 -05:00
jaten-molecula
4c0d66703e
Merge pull request #582 from molecula/fix575b
remove premature configuration of Txsrc in test config
2020-07-20 21:04:46 -04:00
Jaden Weiss
e6b4cc2f32
fix oversized rangeEQ 2020-07-20 20:43:50 -04:00
Jaden Weiss
b9b0dd293f
fix rangeBetween when there are nonzero common upper bits and oversized rangeGT 2020-07-20 20:38:32 -04:00
Jason Aten
f5688fa700 remove premature configuration of Txsrc in test config 2020-07-20 20:22:47 -04:00
jaten-molecula
d23ea94ec0
Merge pull request #581 from molecula/fix575
pilosa server --tx compatible with PILOSA_TXSRC. fixes #575
2020-07-20 18:48:40 -04:00
Jason Aten
7fe7ed907f env PILOSA_TXSRC is reinjected into the env if pilosa server --tx overrides it. fixes #575
1. Tests can choose the Tx engine desired by setting the PILOSA_TXSRC
      env variable. For example:

         PILOSA_TXSRC=badger go test -v -run TestImportClearRestart

   2. pilosa server --tx is enabled now.

     Examples:
      pilosa server --tx roaring    # gives the legacy approach.
      pilosa server --tx rbf        # will activate RBF
      pilosa server --tx badger     # will activate BadgerDB
      pilosa server --tx badger_rbf # will run Blue-Green badger to RBF comparisons.

      and so forth. See pilosa server -h or txfactory.go for all valid --tx choices.

   3. Mechanism that makes both tests(1) and pilosa server(2) work at once:
       pilosa/server/server.go injects PILOSA_TXSRC into env to
       communicate with NewIndex in pilosa/index.go.
2020-07-20 17:56:12 -04:00
jaten-molecula
22fd24566a
Merge pull request #580 from molecula/fix568
green TestImportClearRestart on PILOSA_TXSRC=badger. fixes #568
2020-07-20 17:24:29 -04:00
Jason Aten
8a1dabb3ba 60m timeout on race 2020-07-20 17:09:32 -04:00
Jason Aten
f59f8a369e green TestImportClearRestart on PILOSA_TXSRC=badger. fixes #568 2020-07-20 16:32:34 -04:00
jaten-molecula
1c9ef3d321
Merge pull request #565 from molecula/tx_roaring_badger
integration of Tx, RoaringTx and BadgerTx implementations.
2020-07-20 16:01:45 -04:00
Jason Aten
97b530ca78 integration of Tx, RoaringTx and BadgerTx implementations.
- all tests green on RoaringTx
  - RoaringTx on by default
  - blueGreenTx testing framework available for A-vs-B comparison
    of Tx implementations
  - flag -tx added to server command line but not wired to
    change NewIndex() selection yet.
  - 918 green tests, 14 tests red on BadgerTx.

    A full list of the 14 red tests on BadgerTx follows.
    Note that these red tests represent not defects in BadgerDB
    or BadgerTx but rather failures of the pre-existing pilosa infrastructure to yet
    be fully adapted from files to using a transactional storage engine.

    As such these are tests that RBF should not be expected to
    pass yet either.

    Fixing the pilosa infrastructure to allow these tests
    to go green under Badger is the next and highest priority
    order of business, but RBF can get much testing benefit
    from the 918 green tests we do have, and hence we merge
    as much as we have today.

    The 14 red tests when NewIndex() is set to use
    BadgerTx are as follows. Note in particular
    that pilosa cluster resizing is not working yet under a
    transactional store.

     TestCluster_ResizeStates/Multiple_nodes,_with_data
     TestImportClearRestart/0MaxOpN10000
     TestImportClearRestart/1MaxOpN10000
     TestImportClearRestart/2MaxOpN10000
     TestImportClearRestart/3MaxOpN10000
     TestExecutor_Execute_Existence/Row
     TestExecutor_ForeignIndex
     TestExecutor_Execute_CountDistinct/Distinct
     TestExecutor_Execute_CountDistinct/Count(Distinct)
     TestExecutor_Execute_CountDistinct/GroupBy(Distinct)
     TestExecutor_BareDistinct
     TestExecutor_Execute_TopNDistinct/TopN
     TestHolderSyncer_IntField/BasicSync
     TestHolderSyncer_IntField/MultiShard
2020-07-20 15:50:08 -04:00
seebs
ab6a3aff10
Merge pull request #458 from seebs/eaddrinuse
Fix very-sporadic EADDRINUSE failures in CI testing (and related cluster test issues)
2020-07-20 10:58:48 -05:00
Seebs
364b533ead various cluster test fixups/cleanups
Some cluster tests failed sporadically. In order to fix them, I
introduced some debugging-related functionality, which revealed
several new bugs that were actually existing bugs we just happened
not to hit in testing. This combines various fixes.

We start with "make the nodes used in testing have distinct names
based on the test case name", which lets us discover that we are
leaking clusters, which continue to sit around talking with each
other. That in turn causes significantly higher load on access to
ephemeral ports, which causes sporadic failures when we shut a
node down and try to restart it, but something else has gotten assigned
its ephemeral port number since then.

Part of the fix is to try to rebind on port 0 if an attempt to
bind to a specified port over 32k fails. This is a guess; the
actual ephemeral port range could be 16k+, 32k+, or 48k+, or just
about anything else really, but it seems reasonable in
practice.

There were bugs in the oft-repeated loops to await the cluster
achieving a given state, and it could hang forever if it didn't,
so we add a timeout and a standard function on the test.Cluster
type to handle that. Note that the timeout seems irrelevant; in
every case I've tried, a timeout of 0 is fine because the node
start doesn't complete until the cluster state has changed.

Add a method to test.Command to run a query, expecting a specific
result. Also clean up some of the formatting and generation of
queries, and allow parameterized (badly) queries. This lets us fix
a subtle bug, which is that test cases were depending on assumptions
about shardwidths. Also improve the diagnostic output from some of
these functions so test failures are more comprehensible.

But actually that dependency on shardwidths was ALSO revealing a
genuine underlying bug, which is that a node resize did not correctly
propagate the schema to a new node if there was no data present
on shards that node would own. We now also have a test case that
hits that (or would, if we hadn't fixed it).

Add comments explaining the server options parameters for MustNewCluster
and MustRunCluster.

Also, we implement the ReadFrom and WriteTo behaviors for
InMemTranslateStore, without which some of the cluster resize tests
fail. Props to the comment for specifically stating that they wouldn't
work if that happened, which probably saved me several hours of
debugging. The implementations may not be robust, but
InMemTranslateStore is intended to be used only in lightweight
and transient testing.
2020-07-20 10:45:59 -05:00
Seebs
ef8b054367 move Cluster type and methods into existing almost-empty cluster.go 2020-07-20 10:45:47 -05:00
tgruben
1067d29784
Merge pull request #547 from tgruben/refactor-putleaf
refactor putleaf
2020-07-17 13:39:35 -05:00
Todd Gruben
cd4bd016f9 skipping a test for ci issue 2020-07-17 09:30:07 -05:00
Todd Gruben
848359a36b skipped test for race wip 2020-07-17 09:15:20 -05:00
Todd Gruben
e61f0f6b1f missing liscense 2020-07-17 09:09:42 -05:00
tgruben
88bf29ea40
Merge branch 'master' into refactor-putleaf 2020-07-17 09:06:31 -05:00
Todd Gruben
9bbceb931a missing license 2020-07-17 09:05:53 -05:00
Todd Gruben
7c5c693fcb linter fix 2020-07-17 09:03:32 -05:00
Todd Gruben
59d2d89a5c removed leafArg and added conatinertypebitmapptr; lint fixes 2020-07-17 02:16:35 -05:00
Jaden Weiss
3f45a90582
Merge pull request #549 from jaddr2line/microseconds
Add microseconds to log
2020-07-16 17:13:10 -04:00
Jaden Weiss
de061ff21d
Merge branch 'master' into microseconds 2020-07-16 17:01:50 -04:00
Todd Gruben
bbd83e4618 . 2020-07-16 08:31:09 -05:00
Kuba Podgórski
2ee5e3d8e5
Merge pull request #538 from kuba--/translation-coordinator
Translation coordinator
2020-07-15 22:53:18 +02:00
Kuba Podgórski
ce628c96cd
Merge branch 'master' into translation-coordinator 2020-07-15 22:45:43 +02:00
Kuba Podgórski
1d6a4dea88
Merge pull request #554 from kuba--/i0-i1
Remove test leftovers
2020-07-15 14:06:23 +02:00
Kuba Podgórski
559e71f62a Remove test leftovers (holder's path). 2020-07-15 13:50:59 +02:00
Jaden Weiss
5579d632c7
Merge pull request #539 from jaddr2line/remove-arm64-binary
remove an arm64 pilosa binary that was committed for some reason
2020-07-14 20:08:23 -04:00
Kuba Podgórski
1edd773e90
Merge branch 'master' into remove-arm64-binary 2020-07-15 01:42:26 +02:00
Jaden Weiss
c99d35a793
Merge pull request #552 from jaddr2line/queries-docs
Add /queries endpoint to API reference
2020-07-14 18:26:27 -04:00
Jaden Weiss
b442ff8336
add /queries endpoint to API reference 2020-07-14 15:58:59 -04:00
Jaden Weiss
9036a16b08
add microseconds to log 2020-07-14 11:44:55 -04:00
Todd Gruben
e68f0a0d8f missing license 2020-07-14 09:00:09 -05:00
Todd Gruben
b61bdbd7a0 fix linter errors 2020-07-14 08:54:30 -05:00
Todd Gruben
374a4ec9ce fixed missing refactor test;refactor GetBitmap 2020-07-14 07:57:02 -05:00
Todd Gruben
b8fe08c765 simplify Walker interface 2020-07-13 20:34:33 -05:00
Todd Gruben
d6986b78a5 . 2020-07-13 20:08:22 -05:00
Todd Gruben
b2b614a686 refactor cursor.GetBitmap->tx.GetBitmap;rename to WalkRootRecordPages;err check 2020-07-13 19:32:26 -05:00
Todd Gruben
b11b43af43 refactor putleaf 2020-07-13 10:48:25 -05:00
Travis Turner
3c619fa2ef
Merge pull request #543 from travisturner/foreign-index-todos
Get ForeignIndex keys in GroupBy
2020-07-11 19:45:31 -05:00
Travis Turner
9f80619f19
Merge branch 'master' into foreign-index-todos 2020-07-11 18:24:55 -05:00
Kuba Podgórski
92ab76b621
Merge pull request #399 from molecula/alisharawal-patch-1
Update README.md
2020-07-11 21:28:14 +02:00
Kuba Podgórski
51ba2492e9
Merge branch 'master' into translation-coordinator 2020-07-11 19:22:48 +02:00
Kuba Podgórski
51891040d0
Merge branch 'master' into alisharawal-patch-1 2020-07-11 19:17:30 +02:00
Travis
a9dc8b8add
translate GroupBy previous value from foreign index 2020-07-11 10:18:45 -05:00
Travis
8f6b876af0
get ForeignIndex keys in GroupBy 2020-07-10 22:48:35 -05:00
alanbernstein
07d3cfe23b
Merge pull request #542 from alanbernstein/validate-transaction-id
Restrict allowed characters in transaction IDs
2020-07-10 20:18:42 -05:00
Kuba Podgórski
0fc7b89b48
Merge branch 'master' into remove-arm64-binary 2020-07-11 01:59:35 +02:00
Kuba Podgórski
010ab5ef8b Extend translate coordinator test 2020-07-11 01:51:03 +02:00
Alan Bernstein
65febc37fd Allow '' in ID regex 2020-07-10 18:07:17 -05:00
Alan Bernstein
7c37ececfd Restrict allowed characters in transaction IDs 2020-07-10 17:58:47 -05:00
Jaden Weiss
a947bc8ad9
Merge pull request #540 from jaddr2line/fix-inspect-query-validate
Fix incorrect validation of query specification in Inspect
2020-07-10 15:56:07 -04:00
Jaden Weiss
2f8d8345c1
fix incorrect validation of query specification in Inspect 2020-07-10 14:35:30 -04:00
Jaden Weiss
8940402e76
remove an arm64 pilosa binary that was committed for some reason 2020-07-10 13:28:27 -04:00
Travis Turner
c7ccd62b3b
Merge pull request #537 from travisturner/grpc-port-0
Replace grpc port code that I removed for some reason
2020-07-10 11:06:40 -05:00
Travis
85bd306eb5
replace grpc port code that i removed for some reason 2020-07-10 10:57:20 -05:00
Jaden Weiss
bb9b3d4e6f
Merge pull request #527 from jaddr2line/like
Add `Rows(like=...)` and `UnionRows` queries
2020-07-09 19:10:29 -04:00
Jaden Weiss
239aacc160
Merge branch 'master' into like 2020-07-09 19:04:03 -04:00
Jaden Weiss
62ce26e7b8
Merge pull request #534 from jaddr2line/inspect-query-v2
Add support for inspecting with a query
2020-07-09 14:09:47 -04:00
Jaden Weiss
f4e9e1688f
add support for inspecting with a query 2020-07-09 13:29:15 -04:00
Travis Turner
85d1fa30dc
Merge pull request #530 from travisturner/advertise-grpc
add --advertise-grpc configuration option
2020-07-09 11:38:18 -05:00
Travis
944a6dca78
add --advertise-grpc configuration option 2020-07-09 11:17:12 -05:00
Ben Johnson
3c021fc2e3
Merge pull request #526 from molecula/rbf
Roaring Bitmap Format
2020-07-08 13:39:37 -06:00
Ben Johnson
de51b538f3 rbf: roaring bitmap format
Co-authored-by: Todd Gruben <todd@pilosa.com>
2020-07-08 13:27:18 -06:00
Jaden Weiss
4bacab6524
update query language docs to include like and UnionRows 2020-07-08 13:08:11 -04:00
Jaden Weiss
c9edccb267
add executor tests and license headers for like & UnionRows 2020-07-08 12:55:18 -04:00
Jaden Weiss
7a16ffff68
add UnionRows query 2020-07-08 12:55:18 -04:00
Jaden Weiss
227881cb67
simplify suffix matching 2020-07-08 12:55:17 -04:00
Jaden Weiss
0611ef3418
initial implementation of Rows like 2020-07-08 12:55:17 -04:00
Jaden Weiss
ed8e5a933e
optimize & document & test like matcher
optimize suffix matching
add some descriptive comments to the like matcher
test all paths in the like tokenizer and matcher
2020-07-08 12:55:17 -04:00
Jaden Weiss
9eb8fcb37f
initial impl of like 2020-07-08 12:55:17 -04:00
alanbernstein
7f1b1f63ef
Merge pull request #506 from alanbernstein/docs-updates
Docs updates
2020-07-07 15:27:07 -05:00
alanbernstein
0aae064310
Merge branch 'master' into docs-updates 2020-07-07 15:17:42 -05:00
Alan Bernstein
94038f4446 Update wording 2020-07-07 03:48:37 -05:00
Ben Johnson
d53d8ef010
Merge pull request #508 from molecula/tx
Tx Interface
2020-07-02 17:20:23 -06:00
Ben Johnson
bf55bbc717 Tx Interface
This commit adds a transaction interface which will be used in the
future to add support to RBF (Roaring B-tree Format).
2020-07-02 10:43:15 -06:00
Kuba Podgórski
549c98dc64
Merge pull request #447 from kuba--/union-run-run
Add unionRunRunInPlace
2020-07-02 11:05:27 +02:00
Kuba Podgórski
751383ecb1
Merge branch 'master' into union-run-run 2020-07-02 10:52:59 +02:00
Jaden Weiss
a52c5d803d
Merge pull request #510 from jaddr2line/roaring-cleanup-3
Roaring cleanup
2020-07-01 17:21:14 -04:00
Jaden Weiss
6d692487ed
Merge branch 'master' into roaring-cleanup-3 2020-07-01 17:15:10 -04:00
Kuba Podgórski
ec73bf5906
Merge branch 'master' into union-run-run 2020-07-01 22:25:06 +02:00
Kuba Podgórski
e777a28283
Merge pull request #513 from kuba--/todo-501
Address the overflow issue with values outside the int64 range
2020-07-01 21:09:09 +02:00
Kuba Podgórski
4bed1df101 Address the overflow issue with values outside the int64 range 2020-07-01 20:40:47 +02:00
Kuba Podgórski
d6caf34c02
Merge pull request #509 from kuba--/todo-503
Add test for Rows on bool
2020-07-01 16:32:24 +02:00
Kuba Podgórski
f0abb5e8b4
Merge branch 'master' into todo-503 2020-07-01 16:23:38 +02:00
Kuba Podgórski
e52b1c04fc
Merge pull request #512 from kuba--/todo-504
FieldValue - check if column arg exists
2020-07-01 16:06:16 +02:00
Kuba Podgórski
5bbb3e2065 FieldValue - check if column arg exists 2020-07-01 11:36:42 +02:00
Kuba Podgórski
58964947d4
Update executor_internal_test.go
Co-authored-by: Travis Turner <travis@pilosa.com>
2020-07-01 10:17:30 +02:00
Kuba Podgórski
f921c5ded0 Add test for Rows on bool 2020-07-01 01:30:36 +02:00
Jaden Weiss
3e0ce32d00
roaring cleanup 2020-06-30 16:38:16 -04:00
Alan Bernstein
3841419907 Use more believable words 2020-06-30 12:23:36 -05:00
Alan Bernstein
31ae30f703 Replace smart quotes 2020-06-30 11:26:25 -05:00
Alan Bernstein
5a6db1cc2b Document topn heuristic behavior 2020-06-30 11:26:16 -05:00
Jaden Weiss
ad2390444a
Merge pull request #500 from molecula/seebs-big-inspect-pr
Improve inspect output, switch roaring over to using new unmarshal, handle inspecting whole holders
2020-06-29 15:31:01 -04:00
Jaden Weiss
31014d11e5
remove unnecesary slice operation when processing holder 2020-06-29 15:22:27 -04:00
Seebs
4d494f6699
shared/generic functionality for iterating holders
This is sort of large, but it's annoyingly difficult to
separate out.

The basic idea is to allow us to have a single holder-iterating
block of code, which is associated with the holder, that can be used
for various things, like the snapshot queue background scan, or
for inspect operations.

We invent the concept of a HolderFilter, which is a thing that
can decide what things in a holder it cares about, and a HolderOperator,
which can also process those things selectively.

In the process, we fix up a couple of subtle bugs in the
inspect logic; specifically, the assumption that the mapped flag could
tell you whether a container was modified by the ops log doesn't
work with mmap, so we have a shiny new flag which is used to track
that, internal to the roaring/container code.

All of this leads to the actual *point* of this exercise, which is
making it easier to create an /inspect endpoint which produces almost
the same data we'd have gotten from `pilosa inspect` on a data directory;
the distinction is that it doesn't try to identify the distinction
between data from disk and data from operations since the file was
loaded. Possibly it should, but it doesn't yet.

The snapshot queue is now implemented using the HolderOperator
design, which requires some subtle changes to how it works, but
overall makes it easier to follow the snapshot queue logic,
and also shares that logic with the way Inspect works.

The holder's snapshot queue is now provided by the server, in
a default environment.

The queueless snapshot queue no longer triggers snapshots on
enqueue -- it turns out that breaks badly, because a key
point about enqueueing a snapshot is that it's safe to do it
*during* a transaction on that fragment, and triggering a
snapshot during a transaction actually causes horrible errors
as the ops log ends up being the old file, which we close.
Related to this, we also need to prevent closed fragments from
trying to snapshot, so we track fragment openness when opening
or closing, and bail on trying to snapshot a fragment which is closed.

We also stop using the queueless snapshot queue during tests,
because that's a horrible idea.

We copy a little bit of the partition logic from the cluster code so
we don't have to expose it all, this lets us check whether the node
we're looking at is the one which should be primary for a given shard,
and if not, identify which node would be. This works only when
pointed at a data directory, for now.

The test cases for the holder have to be internal, because pilosa
doesn't export view/fragment, just Index/Field. This means that the
holder test cases can't just use the test/* package, so they duplicate
some of its logic, approximately.
2020-06-29 15:18:47 -04:00
Seebs
2826ecc0b7
track retries correctly in truncation case 2020-06-29 15:13:51 -04:00
Seebs
176d49e4b5
use syswrap to close file after opening it with syswrap 2020-06-29 15:13:50 -04:00
Seebs
3e7f0b32e9
drop old Call data while processing a list of calls
We don't need the Calls anymore, and especially Precomputed calls
(like Distinct) could be a significant memory load that's increased
as we process additional calls, so we drop the Precomputed references.
We can't drop the calls entirely -- translation can require lookups of
call arguments.
2020-06-29 15:13:50 -04:00
Seebs
7e7051d387
don't try to truncate files when invoked read-only 2020-06-29 15:13:50 -04:00
Seebs
ceaf5c15d1
thread the holder through things, and improve snapshot queue logic
This is logically two separate things, but the individual changes
are thoroughly intertwined in the code.

The first change is a logical change to the design of the snapshot
queue, which is that it now adjusts the maxOpN the background scan
targets, allowing it to lower that value over time when things are
quiet. We do this because it turns out that on large data sets,
this can make a factor-of-four difference in memory usage!

So, in general, on a quiet system, each pass through the holder
aims for about 1/4 of the existing fragments to get snapshotted.
When there's more load, we adjust those values up.

We also make the snapshot queue a bit less chatty, to make testing
less annoying -- we only print stats if the queue enqueues at least
two snapshots, or skips any.

The second change is threading the holder through things. We've
always threaded the logger through, and then added the snapshot
queue, and some of the Inspect-related work led to wanting to
have a way to thread options through, so what if we just threaded
the holder itself through, and removed the direct copying around
of the logger, snapshot queue, and so on. Similarly, everything
can now use holder.PartitionN instead of having to get its own
copy of PartitionN handed out to each index.

This does imply ensuring that test cases always get a reasonable
default holder.

This is a precursor to adding additional information to the holder,
such as whether it's in a special read-only mode, which would imply
not modifying on-disk files. This is already semi-supported for
the specific case of the background snapshot queue and cache flushing,
which are attached to the (created in a previous commit) new
holder Activate method, instead of being automatic on holder Open.

The change to a snapshot queue can also cause races in tests, because
the fragment.Clean method's "sanity check" accesses a fragment without
a lock. Fix that. Since there's a couple of t.Fatalf(), but we need
to release the lock before closing, we use an anonymous function
with a defer to handle that. Whee!
2020-06-29 15:13:50 -04:00
Seebs
121717594b
improve inspect output, switch roaring over to using new unmarshal
At some point the code changeover to use roaring iterators for
unmarshal got dropped, but the old unmarshal code is way harder to
make work for inspect, so this change is back.

This exports some of the names from the things returned by Info,
but also adds a roaring function to use the unmarshalling logic on
arbitrary data, allowing us to get more insight into a file -- in
particular, letting us distinguish between the bitmaps specified by
the roaring data and the bitmaps resulting from applying the ops log.
2020-06-29 15:13:47 -04:00
Jaden Weiss
c1612851af
Merge pull request #498 from jaddr2line/transaction-test-timeout
TestTransactionManager: raise transaction timeouts to avoid sporadic failures
2020-06-29 14:42:21 -04:00
Jaden Weiss
0d05e413d3
TestTransactionManager: raise transaction timeouts to avoid sporadic failures 2020-06-29 13:44:20 -04:00
Jaden Weiss
a175858375
Merge pull request #496 from jaddr2line/molecula-rebrand
Rebrand pilosa binaries
2020-06-26 17:51:21 -04:00
Jaden Weiss
fedb1f7dbd
set version to match Molecula convention 2020-06-26 17:41:19 -04:00
Jaden Weiss
4a4252bb28
Merge pull request #1 from codysoyland/enterprise-removal
Remove a few more enterprise references
2020-06-26 17:24:02 -04:00
Cody Soyland
4f22c388b3 Remove a few more enterprise references 2020-06-26 15:57:21 -05:00
Jaden Weiss
cd317d7e91
tweak VERSION_ID 2020-06-26 16:49:06 -04:00
Jaden Weiss
aa1995073f
rebrand pilosa binaries
This change rebrands the Pilosa binaries from "Pilosa Enterprise" to "Molecula Pilosa" and simplifies the version info string.
2020-06-26 16:49:05 -04:00
Kuba Podgórski
d99971a6c2
Merge branch 'master' into union-run-run 2020-06-26 22:32:07 +02:00
Kuba Podgórski
8b33a073b4
Merge pull request #495 from kuba--/int-eq
Support '=' condition for int/decimal fields
2020-06-26 22:30:03 +02:00
Kuba Podgórski
4351464f84
Merge branch 'master' into int-eq 2020-06-26 21:26:44 +02:00
Jaden Weiss
a78b1cfe19
Merge pull request #446 from jaddr2line/fastrank
Remove allocations from ranked cache when possible
2020-06-26 12:46:58 -04:00
Kuba Podgórski
3782c3ac14 Support '=' condition for int/decimal fields 2020-06-26 18:14:59 +02:00
Jaden Weiss
94b55f8556
remove allocations from ranked cache when possible 2020-06-26 12:12:51 -04:00
Jaden Weiss
079bee5711
Merge pull request #489 from jaddr2line/field-cleanup
Remove unused field code
2020-06-26 11:37:15 -04:00
Jaden Weiss
5523587435
pilosa: remove unused field code 2020-06-26 10:14:56 -04:00
Kuba Podgórski
76324f1498
Merge branch 'master' into union-run-run 2020-06-26 01:28:25 +02:00
Jaden Weiss
0a030e9a77
Merge pull request #482 from jaddr2line/rm-invalid-unsafe
Remove invalid uses of unsafe from roaring containers
2020-06-25 15:02:33 -04:00
Jaden Weiss
934048bb02
roaring: remove invalid uses of unsafe 2020-06-25 11:48:21 -04:00
Kuba Podgórski
47481432ef
Merge pull request #468 from kuba--/fix-DEGRADED
Lets the remote node to proceed
2020-06-25 17:05:30 +02:00
Kuba Podgórski
80cfb5c182
Merge branch 'master' into fix-DEGRADED 2020-06-25 16:41:41 +02:00
Kuba Podgórski
0bdb420b4c
Merge pull request #475 from kuba--/fix-iface-conv
Make a safe cast
2020-06-25 15:36:53 +02:00
Kuba Podgórski
ed86f6ea5d Make a safe cast 2020-06-25 14:44:14 +02:00
Cody Soyland
bc9d13206f
Merge pull request #490 from codysoyland/ci-fixes
Fix/refactor CircleCI config
2020-06-24 13:52:45 -05:00
Cody Soyland
2024228153 Fix/refactor CircleCI config
- Fix incorrect usage of workspaces (vendor dir in current directory no
  longer primary cache of go modules)
- Refactor checkout, github-auth, and mod cache into a reusable command
- Fix issue with dockerhub upload and github authentication
2020-06-24 13:45:17 -05:00
Kuba Podgórski
413ce4f1ab
Merge branch 'master' into fix-DEGRADED 2020-06-24 19:56:24 +02:00
Jaden Weiss
2925101b09
Merge pull request #487 from jaddr2line/cluster-message-error
Differentiate between cluster message request errors and cluster message processing errors
2020-06-24 09:44:03 -04:00
Jaden Weiss
b5b60b8912
http: differentiate between cluster message request errors and cluster message processing errors 2020-06-24 09:23:08 -04:00
Kuba Podgórski
905cda7f08 Add benchmarks 2020-06-23 22:18:13 +02:00
Kuba Podgórski
443f2d8f7c The third attempt to implement unionRunRunInPlace 2020-06-23 22:18:13 +02:00
Kuba Podgórski
eddbb7d0b0 Optimize run intervals by combnining neighbours 2020-06-23 22:18:13 +02:00
Kuba Podgórski
2b0cd2f234 Add unionRunRunInPlace 2020-06-23 22:18:13 +02:00
Jaden Weiss
89ac3f4737
Merge pull request #486 from jaddr2line/handlerfixes
Fix incorrect error handling in HTTP handler and output raw error text when JSON is not selected
2020-06-23 12:57:55 -04:00
Jaden Weiss
66aef92cfa
http: fix incorrect error handling and output raw error text when JSON is not selected 2020-06-23 12:26:37 -04:00
Cody Soyland
16ebdf290d
Merge pull request #479 from codysoyland/ci-dockerhub-fix
Fix and simplify CI DockerHub integration
2020-06-23 09:13:41 -05:00
Cody Soyland
7cb9548097 Fix and simplify CI DockerHub integration 2020-06-19 11:41:54 -05:00
Jaden Weiss
5dc02a3998
Merge pull request #474 from jaddr2line/timeclr
Fix Clear() returning `false` when it should return `true`
2020-06-19 09:03:42 -04:00
Jaden Weiss
a270bff67b
fix Clear() returning false when clearing a bit with no time views 2020-06-19 08:51:08 -04:00
Kuba Podgórski
739935c3f6
Merge branch 'master' into fix-DEGRADED 2020-06-18 16:13:58 +02:00
tgruben
21888703e5
Merge pull request #469 from tgruben/splat-init
Functionalize  fillerBitmap initialization
2020-06-17 09:39:16 -05:00
tgruben
3103da2c19
Update roaring/container_stash.go
Co-authored-by: Jaden Weiss <jaden@jadendw.dev>
2020-06-17 08:51:25 -05:00
Todd Gruben
f47888989a shorten fillerBitmap initialization 2020-06-17 07:12:24 -05:00
Kuba Podgórski
87306d3145 Lets the remote node to proceed, instead of waiting in DOWN state because cluster is in STARTING state. 2020-06-17 11:37:30 +02:00
Jaden Weiss
7f4142497e
Merge pull request #467 from jaddr2line/splat
Optimize `splatRun`
2020-06-16 13:50:27 -04:00
Jaden Weiss
8fc7861148
roaring: optimize splat 2020-06-16 13:04:50 -04:00
Travis Turner
cf24ee161f
Merge pull request #465 from travisturner/document-shift
Add comments warning that Shift() is unsupported
2020-06-15 17:53:38 -05:00
Travis
c5786e1d78
Add comments warning that Shift() is unsupported 2020-06-15 17:16:00 -05:00
alisharawal
4a68b46597
Merge branch 'master' into alisharawal-patch-1 2020-06-15 11:15:14 -05:00
Jaden Weiss
d9baea83f4
Merge pull request #454 from jaddr2line/groupbyoffset
Apply base in GroupBy on BSI
2020-06-11 17:24:39 -04:00
Jaden Weiss
837d0a1465
Merge branch 'master' into groupbyoffset 2020-06-11 12:20:35 -04:00
Jaden Weiss
32e47642ae
address review of "Apply base in GroupBy on BSI" 2020-06-10 17:49:00 -04:00
tgruben
8bc303793c
Merge pull request #453 from tgruben/includes-perf
Optimized performance of row.Includes
2020-06-10 15:50:50 -05:00
Jaden Weiss
535257af75
apply base in GroupBy 2020-06-10 16:16:53 -04:00
Todd Gruben
416f332070 Optimize row.Includes 2020-06-10 13:12:38 -05:00
Jaden Weiss
ec9474114a
Merge pull request #449 from jaddr2line/fixbsioffbyone
Fix BSI comparison match-all-but-one operation
2020-06-10 10:41:35 -04:00
Jaden Weiss
8887927dbd
add regression test for BSI match-all-but-one operations 2020-06-10 10:09:33 -04:00
Jaden Weiss
d478dd9d94
fix BSI comparison match-all-but-one operation 2020-06-10 09:23:39 -04:00
Kuba Podgórski
058ed747c7
Merge pull request #437 from kuba--/err-check
Return error instead of panicking on Store(Distinct())
2020-06-09 14:53:23 +02:00
Kuba Podgórski
9c3b080bf0 Check result before return 2020-06-09 11:39:16 +02:00
Cody Soyland
a519907822
Merge pull request #440 from codysoyland/ci-size
Use xlarge executor in CircleCI
2020-06-08 15:24:31 -05:00
Cody Soyland
8352f5d273 Add configurable resource class, enable only for test-race. 2020-06-08 15:20:31 -05:00
Cody Soyland
ecbb5b2a0d Use xlarge executor in CircleCI 2020-06-08 15:20:31 -05:00
Jaden Weiss
12e6534244
Merge pull request #426 from jaddr2line/simplebsi
Simplify BSI comparisons
2020-06-08 16:14:41 -04:00
Jaden Weiss
ee8036d376
Merge branch 'master' into simplebsi 2020-06-08 16:07:29 -04:00
Jaden Weiss
120cc02536
process BSI ops more efficiently 2020-06-08 14:52:19 -04:00
Jaden Weiss
721a968d63
Merge pull request #438 from jaddr2line/cpumhz
fix CPU speed on non-Intel platforms
2020-06-08 13:34:27 -04:00
Jaden Weiss
a4e53b4bc1
Merge branch 'master' into cpumhz 2020-06-08 13:26:49 -04:00
seebs
c0c027ef67
Merge pull request #429 from seebs/racetime
Improve time requirements for tests with race detector on
2020-06-08 12:25:07 -05:00
Seebs
44569fa210 Reduce iterations in TestFragment_RowsIteration
We don't really learn more from trying every multiple of 10,000 than we do
from trying maybe 32 values, and it's worse at larger shard widths.
2020-06-08 12:10:40 -05:00
Seebs
52aa3e2e23 Improve container/bitmap comparison logic for testing
We have a "deadcode" bitmapsEqual which is actually used in testing but
probably shouldn't be, and we don't have a good container equality test.

Problem is, equality tests are sort of slow in the things-are-equal case,
which is the most common case, so we've got some moderately-specialized
code here; specifically, special comparison code that takes advantage
of knowing that if two containers have the same number of bits, you only
have to check whether all the bits from one are present in the other,
because that can't be true for differing containers with the same number
of bits. This reduces the runtime for the ContainerCombinations case
from about 24 seconds to a bit under 2 on my laptop, or from around
10 minutes to about 37 seconds with the race detector on.

Also simplify the InPlaceWrapper functions not to invoke bitmaps, because
it's not really necessary.
2020-06-08 12:10:40 -05:00
Seebs
3f0c9925f4 Don't test quite so many values for BtreeSeek and BtreeDelete
BtreeSeek is O(N^2) on its N, and there's not a ton of extra utility
to testing a larger range of values, so we reduce N by a bit, cutting
runtime from ~10s to <1s on my laptop. Also reduce the scale of the
BtreeDelete1/BtreeDelete2 tests a bit because, again, lots of runtime
for little marginal information.
2020-06-08 12:10:40 -05:00
Seebs
e223c79ace Don't use a whole shard of values for Execute_All test.
This is pretty expensive even for default shard width, and very expensive
for ShardWidth = 1<<22, and we don't really get much extra benefit from
having a million values instead of a hundred or so.
2020-06-08 12:10:40 -05:00
Seebs
1484674a1c Add and use bitmap-to-slice-or-set comparison functions
The generation of slices from things, and use of reflect.DeepEqual to compare
the slices, is a lot more expensive than it needs to be. Omitting it removes most
of the runtime of the marshal tests.
2020-06-08 12:10:40 -05:00
Seebs
1952a43ed4 Write fewer bits to test the rowcache behavior
The failure mode in question was pretty predictable and tied to number of
snapshots, not to number of bits written, so we can probably use a lot fewer
bits and still get good results, but this is really slow under -race testing.
2020-06-08 12:10:40 -05:00
Seebs
1460756b3f Provide option for adjusting node timeouts, set it for tests.
There's no reason to have 10-20 seconds of delays for testing this,
because in testing, we're running things on the local machine and don't
need to worry about significant network lag. Make retry count and delay
settable options, and set them lower. Moves the Replica2 test in
server/server_test.go from ~21s to ~2s.
2020-06-08 12:10:40 -05:00
Seebs
55ff03a2d6 Lower scale of some random-value tests
The random-value tests can be pathological, and in particular, the
test of arbitrarily-spaced values is in effect O(N^2), and with race
testing on, that test *alone* can take ten minutes to run, but
it's not really all that exciting. We just reduce a bunch of values
and/or test fewer things for these, which doesn't significantly alter
coverage, but reduces test runtime on my laptop with `-race` from
21 minutes to a bit under 5.
2020-06-08 12:10:40 -05:00
Jaden Weiss
7695e63bc5
fix CPU speed on non-Intel platforms 2020-06-08 11:40:53 -04:00
Jaden Weiss
0a17b3713c
Merge pull request #428 from jaddr2line/cleanshutdown
Cleanly shut down the executor
2020-06-05 18:32:57 -04:00
Jaden Weiss
b0a0524ffe
cleanly shut down the executor 2020-06-05 15:25:35 -04:00
Jaden Weiss
0ce5d92407
simplify BSI comparisons 2020-06-05 14:13:29 -04:00
seebs
33728e65d9
Merge pull request #408 from seebs/execontext
thread contexts better through executor
2020-06-04 15:18:12 -05:00
seebs
610d72dbfc
Merge branch 'master' into execontext 2020-06-04 14:57:16 -05:00
Jaden Weiss
0095810e4d
Merge pull request #419 from jaddr2line/trackqueries
track active queries
2020-06-04 14:58:22 -04:00
Jaden Weiss
06517075bf
add unit test to active query tracker 2020-06-04 14:05:06 -04:00
Jaden Weiss
1099a57945
fix pretty printing of active queries list to handle special characters and multiline queries 2020-06-04 14:04:46 -04:00
Jaden Weiss
023efcaba6
track active queries 2020-06-04 10:50:15 -04:00
Seebs
439c710ca9 thread contexts better through executor
When a mapper hits an error, we want it to immediately tell the
other things in that same mapper that they can stop now. But we
don't want to propagate that all the way back up; if a specific
node has a failure executing a query, we will in some cases want
to send a new query to other backup nodes, so the overall
context isn't cancelled yet.

In general, mapFn and reduceFn have been closures that inherit
a context from the function defining them -- but we don't want
that! We want them to be stopped if their specific mapper gets
cancelled, too, because otherwise they can consume a lot of
resources long after the mapper has stopped being interested
in them.  So now those are parameters passed into them,
and mapperLocal puts *those* contexts in the jobs shoved into
the job queue, and the workers pass the context in to the
mapFn/reduceFn.

We also check responses from reduceFn now; both mapReduce
and mapperLocal check for a possible error, and return that,
and reduce functions doing anything nontrivial check their
context.

We also add a few more explicit checks for context cancellation
in various places, especially in the GroupByIterator which is
what bit us that one time. The explicit check against ctx.Err
is officially safe as of Go 1.9 or so. (It was previously
unspecified, but on further study, the Go team concluded that
no actual implementation did anything else, and existing code
was already depending on that.) This also affects the rows
function, because that could potentially take quite a while to
run for a large fragment.
2020-06-03 16:09:01 -05:00
seebs
33a90fd328
Merge pull request #297 from seebs/nofreeze
Don't automatically freeze the results of RowSegment ops
2020-06-03 14:54:12 -05:00
Seebs
6abe7dc12f Don't automatically freeze the results of RowSegment ops
I think when this code was written, I thought "freeze" would be
really cheap. It's not actually that cheap. As a result, freezing
things preemptively when it may be that nothing ever tries to write
to them anyway is possibly disadvantageous, to the tune of being
roughly 20% of a sample profile we were shown. Instead, we don't
mark the components "writable", so if anything wants to write to
them, it'll end up freezing itself new copies of their bitmaps
later. But in practice that probably doesn't happen.
2020-06-03 13:21:41 -05:00
Kuba Podgórski
0bba9c81e8
Merge pull request #417 from kuba--/rev-mtx
Change order of cluster/index locks
2020-06-03 15:37:20 +02:00
Kuba Podgórski
0320228b99 Change order of cluster/index locks 2020-06-03 15:09:56 +02:00
Kuba Podgórski
a8e6846e78
Merge pull request #401 from kuba--/etag
Add (in memory) CreatedAt to index and fields
2020-06-03 15:09:22 +02:00
Kuba Podgórski
bb048da241
Update docs/api-reference.md
Co-authored-by: Matthew Jaffee <matthew.jaffee@gmail.com>
2020-06-03 14:47:51 +02:00
Kuba Podgórski
6a892102e4 Update api-reference.md 2020-06-03 13:26:16 +02:00
Kuba Podgórski
d8a417f657 Move applyCreatedAt from mergeClusterStatus directly to ClusterStatus message, to avoid deadlocks 2020-06-03 13:26:16 +02:00
Kuba Podgórski
0c98c887ac Pass Schema in ClusterStatus message 2020-06-03 13:26:16 +02:00
Kuba Podgórski
ba7f039dd1 Rename etag to createdAt 2020-06-03 13:26:16 +02:00
Kuba Podgórski
3d270f45d2 Add (in memory) ETag to index and fields 2020-06-03 13:26:16 +02:00
tgruben
43195ae2dd
Merge pull request #416 from tgruben/fix-grpc-address
add address for listening
2020-06-02 16:25:53 -05:00
Todd Gruben
fdaae31c2c add address for listening 2020-06-02 15:39:28 -05:00
Matthew Jaffee
31c5e7363d
Merge pull request #412 from jaffee/escape-query-strings-411
modify PQL parser to handle escapes in string values
2020-05-29 08:51:54 -05:00
Matt Jaffee
1c1204fc77
modify PQL parser to handle escapes in string values
This modifies the parser to properly "unquote" incoming strings. So if
a string comes in double or single quoted, we approximately follow Go
rules for removing the quotes and processing escape sequences.

The differences from Go are:
1. we only support backslash, quote, tab and newline escape
sequenences.
2. Single quoted strings are supported and work just like double
quoted strings.
3. The peg parser won't actually accept backquoted strings (I don't
think)

Fixes: #411
2020-05-29 07:58:50 -05:00
tgruben
c177bf831d
Merge pull request #409 from tgruben/transaction-doc-update
correction to endpoint
2020-05-29 07:22:18 -05:00
tgruben
242b3d4b14
Merge branch 'master' into transaction-doc-update 2020-05-29 07:02:02 -05:00
seebs
6aa5041de6
Merge pull request #313 from seebs/roaring4g
Handle file sizes over 4GB
2020-05-28 22:55:24 -05:00
Seebs
1ca9435af9 Handle file sizes over 4GB
We only have 4 bytes for offsets, but what if a file is
over 4GB? Someone came to us with a file with 265 *million* containers,
in a single fragment, which means that over 3GB of their 4.7GB file
is actually just the container headers alone. But we can't easily make
the offsets larger, or change the file format.

So we don't. We just track how many 4GB hunks of the file we've
been through and bump that every time the 32-bit offset wraps. And this
appears to... just work.

This is fixed for both the roaring iterator and the old unmarshalBinary
logic. The logic to handle this will work on 32-bit hosts in the sense
that it will correctly error out for excessively large file sizes or
container counts, but it doesn't actually handle the large files since
it can't.
2020-05-28 17:08:32 -05:00
Todd Gruben
4d1ce90b32 correction to endpoint 2020-05-28 15:36:00 -05:00
tgruben
a777eddfcf
Merge pull request #405 from tgruben/trace-tagging
added some context to tracing
2020-05-28 13:34:19 -05:00
tgruben
5cd15b250a
Merge branch 'master' into trace-tagging 2020-05-27 15:46:51 -05:00
Todd Gruben
4274d2d141 convert to camelCase 2020-05-27 15:23:13 -05:00
Kuba Podgórski
6ed3bde54e
Merge pull request #407 from kuba--/status
Add grpc uri to status
2020-05-27 17:26:32 +02:00
Kuba Podgórski
604f3b3373 Add grpc uri to status 2020-05-27 16:41:38 +02:00
Jaden Weiss
e0291e9d25
Merge pull request #398 from jaddr2line/transactionmetrics
add metrics for transactions
2020-05-27 08:52:53 -04:00
Jaden Weiss
efef42ae97
Merge branch 'master' into transactionmetrics 2020-05-27 08:36:23 -04:00
Todd Gruben
022019c6cc removed shard level tracing tag 2020-05-26 23:23:57 -05:00
Travis Turner
ffa40b1bdb
Merge pull request #387 from travisturner/int-eq-null
Add support for `intfield == null`
2020-05-26 21:05:42 -05:00
Todd Gruben
a2f825a32e added some context to tracing 2020-05-26 17:18:23 -05:00
Jaden Weiss
a4643084bd
Merge branch 'master' into transactionmetrics 2020-05-26 17:55:12 -04:00
Jaden Weiss
5644fb2275
add metrics for transactions 2020-05-26 17:51:28 -04:00
Travis
e4b9293f26
Add support for int == null 2020-05-22 12:29:52 -05:00
Travis Turner
d36f397a35
Merge pull request #404 from travisturner/row-todos
clean up the TODOs and some comments
2020-05-22 12:05:58 -05:00
Travis
041726fbf7
clean up the TODOs and some comments 2020-05-22 11:00:22 -05:00
Travis Turner
2586661812
Merge pull request #402 from travisturner/roaring-tests
Address TODOs in roaring tests
2020-05-21 19:50:38 -05:00
Travis
0a94f8393f
Address TODOs in roaring tests
In addition to adding some tests, this commit moves the
`GenerateUint64Slice()` helper function into  a new `generator` package
so that it can be used in both internal and non-internal tests.
2020-05-21 13:28:54 -05:00
Jaden Weiss
c04143fd61
Merge pull request #394 from jaddr2line/nextdelete
fix use-after-free in b-tree bitmap update
2020-05-20 13:39:05 -04:00
alisharawal
aa0cfecdb3
Update README.md 2020-05-20 11:37:22 -05:00
Jaden Weiss
6e3e513425
roaring: fix use-after-free in b-tree bitmap update 2020-05-20 12:01:21 -04:00
Travis Turner
8eae053fe6
Merge pull request #395 from travisturner/roaring-todos
clarify a few of the TODO comments
2020-05-19 15:16:05 -05:00
Travis
631d3deeed
clarify a few of the TODO comments 2020-05-19 13:50:48 -05:00
Travis Turner
4b4fd590ca
Merge pull request #389 from travisturner/proto-todos
finish implementing PairField proto encoding
2020-05-19 08:54:58 -05:00
Travis
4fb3820afb
finish implementing PairField proto encoding 2020-05-18 20:19:22 -05:00
Travis Turner
231fef27eb
Merge pull request #390 from travisturner/deadline-skew
increase test deadlineSkew to 1s
2020-05-18 20:18:51 -05:00
Travis
42814bb70c
increase test deadlineSkew to 1s 2020-05-18 19:57:44 -05:00
Travis Turner
63dd5e1174
Merge pull request #386 from travisturner/bsigroup-edges
Fix edge case bugs in range queries
2020-05-18 15:02:50 -05:00
Travis
d0de49ef39
handle edge cases in range queries 2020-05-16 10:35:35 -05:00
seebs
c6d61391d2
Merge pull request #249 from seebs/roaringrow
use roaring row support internally
2020-05-15 17:00:17 -05:00
Seebs
0ddc968001 cache result of marshalling import-was-OK message
This message gets generated millions of times and it's unchanging
for the life of the program, and small.
2020-05-15 16:22:08 -05:00
Seebs
79940cf077 encoding/proto: allow distinct serializers
We want to be able to control whether or not we use roaring to
serialize Rows, which means serializers have to be able to be
distinct.

We also make corresponding changes to http/handler.go to have
it use the exported serializers directly rather than the API's
serializer (which is always the base protobuf serializer
right now, and if it weren't, that would be bad because we
were assuming it was).

When we're accepting protobuf from a pilosa server, flag that
we'll accept roaring bitmaps as opposed to the naive column
representation.
2020-05-15 16:22:08 -05:00
Seebs
ce0761050e less spammy snapshotqueue
During testing we spawn a lot of tiny snapshot queues. Make the message
less spammy by printing it only if any enqueues were skipped (shouldn't
ever happen) or more than one thing got enqueued (likely in real usage,
but doesn't happen in testing usually).
2020-05-15 16:22:08 -05:00
Kuba Podgórski
3151d83136
Merge pull request #383 from kuba--/grpc-err/356
Wrap grpc resp.Err
2020-05-15 23:14:41 +02:00
Kuba Podgórski
3fe85d0f91 Move grpc_internal_test to grpc_test 2020-05-15 19:52:03 +02:00
Kuba Podgórski
ea08cce8fe
Update api.go
Co-authored-by: Travis Turner <travis@pilosa.com>
2020-05-15 19:39:36 +02:00
Kuba Podgórski
f40601f6f5 Wrap grpc resp.Err 2020-05-15 18:16:31 +02:00
Kuba Podgórski
e39334ca92
Merge pull request #377 from kuba--/opt-indiagnostics/310
Disable diagnostics by default
2020-05-15 02:09:31 +02:00
Kuba Podgórski
7ee8f80012
Merge branch 'master' into opt-indiagnostics/310 2020-05-15 00:53:33 +02:00
Kuba Podgórski
fc9e75edc2
Merge pull request #378 from kuba--/off-ae/340
Turn anti-entropy off by default
2020-05-13 14:26:13 +02:00
Kuba Podgórski
fd27cbb886
Update server/config.go
Co-authored-by: Matthew Jaffee <matthew.jaffee@gmail.com>
2020-05-12 18:51:53 +02:00
Kuba Podgórski
889f79d11a Turn anti-entropy off by default 2020-05-12 16:49:43 +02:00
Kuba Podgórski
368cb8cb53 opt-in diagnostics 2020-05-12 15:37:47 +02:00
Travis Turner
2f44a16755
Merge pull request #374 from travisturner/todo-fixes
tidy up some of the TODO comments
2020-05-10 21:45:13 -05:00
Travis
bc8244a581
well, put the TODO back, just in a different place 2020-05-10 19:18:54 -05:00
Travis
d546b8ac01
use pilosa.ErrNotImplemented for unused interface implementations 2020-05-10 18:50:05 -05:00
Travis
9255d43e9a
tidy up some of the TODO comments 2020-05-09 22:26:12 -05:00
Cody Soyland
16f0b5eaba
Merge pull request #343 from codysoyland/branch-rename
Rename enterprise branch to master
2020-05-08 17:50:02 -05:00
Cody Soyland
b96666d06a Rename enterprise branch to master 2020-05-08 16:29:17 -05:00
Kuba Podgórski
5578eb8daa
Merge pull request #258 from kuba--/intersect-inplace
The first implementation of intersect in place
2020-05-07 11:23:40 +02:00
Kuba Podgórski
c802caeddd The first implementation of intersect in place 2020-05-06 23:59:07 +02:00
Kuba Podgórski
1a00008dbd
Merge pull request #339 from kuba--/getridof-rowid
get rid of rowID from groupby on ints response
2020-05-06 23:43:56 +02:00
Kuba Podgórski
19df3211f9 get rid of rowID from groupby on ints response 2020-05-06 23:23:43 +02:00
Travis Turner
04f09870ec
Merge pull request #338 from travisturner/remove-lookup
remove extra index lookup
2020-05-06 13:54:18 -05:00
Travis
2ca4e971f1
remove extra index lookup 2020-05-06 13:04:46 -05:00
Travis Turner
994ac5aad8
Merge pull request #337 from travisturner/fieldvalue
add FieldValue call
2020-05-06 12:29:39 -05:00
Travis
a91014c7bb
add FieldValue call 2020-05-06 11:52:27 -05:00
Travis Turner
a72de2c68a
Merge pull request #329 from travisturner/cluster-startup
avoid deadlock on translationSync.Reset during startup
2020-05-05 08:56:23 -05:00
Travis
92a94c9ec1
remove noSleep option 2020-05-05 08:31:43 -05:00
Travis
7b36f417d8
avoid deadlock on translationSync.Reset during startup 2020-05-05 08:31:42 -05:00
Travis Turner
f9cb7abccf
Merge pull request #334 from travisturner/block-limits
Fix off-by-one maxRowID in block limits
2020-05-04 14:02:30 -05:00
Travis
de0785d305 add a test for the "clears" bug 2020-05-01 16:15:42 -05:00
Travis
1f308066a7 fix test error messages 2020-05-01 15:54:56 -05:00
Travis
b25796f532 Fix off-by-one maxRowID in block limits
In the case where a block merge needed to occur
on a replica containing a row on the edge of the block,
the existing logic would inadvertently clear the first
row in the next block. This PR fixes that.
2020-05-01 15:15:47 -05:00
Matthew Jaffee
8e0a787ec0
Merge pull request #332 from jaffee/tracing-off
change default tracing config to 'off'
2020-04-30 21:25:02 -05:00
Matt Jaffee
8aa7a76d31
change default tracing config to 'off'
also fix a typo
2020-04-30 15:02:15 -05:00
Cody Soyland
ea99f32d36
Merge pull request #327 from codysoyland/docker-bind-grpc
Bind to public interface in docker image
2020-04-28 14:45:06 -05:00
Cody Soyland
0c7a83dc0b Bind to public interface in docker image 2020-04-28 14:20:22 -05:00
Travis Turner
95d44dce28
Merge pull request #318 from travisturner/config-cmd
suppress test config arguments from pilosa config output
2020-04-24 13:23:36 -05:00
Travis
9405463911 suppress test config arguments from pilosa config output 2020-04-24 11:02:42 -05:00
Travis Turner
022b135538
Merge pull request #316 from travisturner/json-header
Ensure content-type header is application/json where appropriate
2020-04-23 22:10:44 -05:00
Travis
af6eb94c1d increase deadlineSkew so TestTransactionsAPI doesn't fail during race test 2020-04-23 17:26:35 -05:00
Travis
5da907d469 ensure content-type header is application/json where appropriate 2020-04-23 16:24:40 -05:00
Travis Turner
8cf06bf5d9
Merge pull request #314 from travisturner/decimal-adjust-precision
Adjust decimal precision if we have decimal places to sacrifice.
2020-04-23 08:43:23 -05:00
Travis
432e18d571 return early on mantissa=0 2020-04-22 19:11:23 -05:00
Travis
809a02d986 Adjust decimal precision if we have decimal places to sacrifice. 2020-04-22 17:10:05 -05:00
Travis Turner
2795a3f7e2
Merge pull request #309 from molecula/transactions
Transactions
2020-04-22 15:52:42 -05:00
Matt Jaffee
97ae8e0db7
add kuba testcase, fix race
we fix the race by not returning pointers to the things which we're
keeping in the in-memory store
2020-04-22 15:17:25 -05:00
Matt Jaffee
85f57f9975
fix lint 2020-04-22 14:30:14 -05:00
Matt Jaffee
7c7836f16f
convert transactions to be pointers everywhere
I think this will improve the transaction response messages Kuba
mentioned where it was an empty transaction instead of a nil or not
there... if not it should make it easier to do that anyhow.
2020-04-22 14:01:24 -05:00
Matt Jaffee
9dbc6f89db
address minor feedback from previous PR 2020-04-22 12:31:25 -05:00
Matt Jaffee
7da137277c
tweak comment, add validation TODO 2020-04-22 10:42:59 -05:00
Matthew Jaffee
afcb3e7f96
Merge pull request #2 from travisturner/backups
minor code adjustments during review
2020-04-22 07:35:05 -05:00
Travis Turner
c9b7ed51aa
Update deadline comment
Co-Authored-By: Matthew Jaffee <matthew.jaffee@gmail.com>
2020-04-21 17:49:57 -05:00
Travis
08583cf2d0 minor code adjustments during review 2020-04-21 16:54:17 -05:00
Matt Jaffee
a3c5f4822e
keep the zone info back in deadline strings (but output in UTC)
instead of defining them as being in UTC, but not including the zone
info, we will keep the standard format with zone info, but always
output the time in UTC. This means that we can parse incoming
deadlines that happen to have zone information, though I don't think
we ever need to.
2020-04-21 12:29:19 -05:00
Matt Jaffee
5e29effa93
don't wrap error, dedup compare transactions code 2020-04-20 22:30:14 -05:00
Matt Jaffee
b23d27f507
add cluster state validation to API methods for transactions 2020-04-20 22:22:40 -05:00
Matt Jaffee
662ed4f324
invert if statements and fix typos 2020-04-20 22:12:07 -05:00
Matt Jaffee
432ab57822
add license headers 2020-04-20 14:41:05 -05:00
Matt Jaffee
89c1d48a0f
transaction deadline format UTC, lint
also change "deadlineSkew" comparison in tests to account for race tests in CI
seeing false differences
2020-04-20 14:37:28 -05:00
Matt Jaffee
c36952a0f1
propagate context throughout transaction stuff 2020-04-20 13:30:48 -05:00
Matt Jaffee
41975de6b8
add transactions external documentation
- make sure client reads/closes all bodies
- support blank transaction ID in http handler
2020-04-20 13:30:48 -05:00
Matt Jaffee
210c7239ab
add HTTP handlers and client for transactions 2020-04-20 13:30:48 -05:00
Matt Jaffee
9ad1106647
implement transaction API layer and intra-cluster messaging
also adds a "noSleep" option to the server command to avoid the 5
second sleep we introduced on startup for non-coordinator cluster
nodes. The sleep doesn't seem to be needed in the tests and makes them
much slower.
2020-04-20 13:30:48 -05:00
Matt Jaffee
088e60b830
better defer that Unlock 2020-04-20 13:30:48 -05:00
Matt Jaffee
38cec6f20e
add TransactionManager and TransactionStore for transactions/backups
This all needs to be wired into API/Server/Cluster/Holder etc. but I
think the TransactionManager will be a pretty good building block for
managing transaction state at the coordinator level.
2020-04-20 13:30:48 -05:00
Seebs
c3ef9a1768
draft outline of transaction API 2020-04-20 13:30:47 -05:00
Cody Soyland
a353527705
Merge pull request #307 from codysoyland/copy-reopen
Copy reopen.FileWriter into pilosa
2020-04-20 12:41:09 -05:00
Cody Soyland
761b090f3c Fix linter error 2020-04-20 12:10:44 -05:00
Cody Soyland
5e1c72e6f2 Copy reopen.FileWriter into pilosa 2020-04-20 11:58:09 -05:00
Travis Turner
53500e5ad5
Merge pull request #304 from travisturner/grpc-listener
move gRPC listener creation outside of grpcServer
2020-04-18 13:14:12 -05:00
Travis
7de8399b17 move gRPC listener creation outside of grpcServer
For tests, we need to create the grpc listener with port 0 in order to
automatically assign a port. This PR moves the lister creation outside
of the grcpServer itself so that we can access that auto-created port.
2020-04-18 11:38:15 -05:00
Travis Turner
72d496f5c7
Merge pull request #302 from travisturner/grpc-client-queryunary
add QueryUnary to the grpc client api
2020-04-17 16:32:41 -05:00
Travis
2b98ef5edb add QueryUnary to the grpc client api 2020-04-17 16:12:10 -05:00
seebs
26091bd3ed
Merge pull request #301 from codysoyland/makefile-checkptr
Use makefile variable for NOCHECKPTR
2020-04-17 15:57:33 -05:00
Cody Soyland
3b5908332d Use makefile variable for NOCHECKPTR 2020-04-17 15:15:43 -05:00
seebs
f8f924e972
Merge pull request #300 from seebs/groupby
GroupBy should terminate even if the last result is empty
2020-04-17 15:13:49 -05:00
Seebs
3a7ab3b8eb GroupBy should terminate even if the last result is empty
If you have two criteria, and the last result you generate is
empty, the nextAtIdx iterator for i==1 will try to continue
poking the i==0 iterator. That one produces a nil result, and
declares the entire group-by iterator done... But the nextAtIdx
call above it isn't checking that, and just loops forever.
This causes some queries to become stuck permanently, consuming
ridiculous amounts of resources almost entirely focused on
calling Intersect millions of times to get empty results.
2020-04-17 14:39:03 -05:00
Cody Soyland
398ce117ec
Merge pull request #287 from codysoyland/logger
Reopen log file on SIGHUP
2020-04-17 12:40:25 -05:00
Cody Soyland
1058440cfe Do not reuse error object (data race) 2020-04-16 17:08:50 -05:00
Cody Soyland
e4cd1b8871 Add -v flag to test-race 2020-04-16 17:08:50 -05:00
Cody Soyland
a00e93f699 Add TODO about fork 2020-04-16 17:08:50 -05:00
Cody Soyland
2155d6cab8 Move some things around, fix linter warnings. 2020-04-16 17:08:50 -05:00
Cody Soyland
a66ee26a6b Reopen log file on SIGHUP 2020-04-16 17:08:50 -05:00
Cody Soyland
08fa90e0f3 Reduce duplication of setupLogger arch-specific code 2020-04-16 17:08:50 -05:00
Travis Turner
46fb6859ed
Merge pull request #296 from travisturner/grpc-tabler-rowser
gRPC tabler rowser
2020-04-16 15:19:47 -05:00
Travis
c45a4bf3dc ToTable and ToRows interface for gRPC 2020-04-16 14:16:54 -05:00
Travis Turner
0691b6c015
Merge pull request #294 from travisturner/overflow-fix
error on potential overflow
2020-04-15 16:24:37 -05:00
Travis
80f1bdebd7 error on potential overflow 2020-04-15 15:50:03 -05:00
Travis Turner
468b69fba9
Merge pull request #291 from travisturner/range-problems
fix some range query problems
2020-04-15 15:27:12 -05:00
Travis
8a22a3ede3 fix some range query problems 2020-04-15 14:42:16 -05:00
Travis Turner
37f05fc3ea
Merge pull request #288 from travisturner/upgrade-min-max
upgrade decimal min/max with scale
2020-04-15 08:08:00 -05:00
Travis
79a6c1e5ab upgrade decimal min/max with scale 2020-04-15 00:14:07 -05:00
seebs
f41bced2ec
Merge pull request #286 from seebs/storeauto
in Store/SetRow, create field if it doesn't already exist
2020-04-14 15:33:10 -05:00
Seebs
70bfe86f75 in Store/SetRow, create field if it doesn't already exist
If you try to Store to a nonexistent field, we create an automatic
Set field with no cache for it, assuming it won't be used for TopN
queries. If you want TopN to work, you need to actually create it
yourself.
2020-04-14 15:12:36 -05:00
alanbernstein
18d01dd62a
Merge pull request #280 from alanbernstein/snake-case-node
Snakify
2020-04-11 13:25:58 -05:00
Alan Bernstein
615c1bd186 Snakify 2020-04-10 22:15:43 -05:00
Matthew Jaffee
76404142ba
Merge pull request #246 from molecula/prometheus-improvements
Prometheus improvements
2020-04-10 21:55:35 -05:00
Matt Jaffee
68276159ca
don't access req.Query before knowing req is a QueryRequest 2020-04-10 21:07:43 -05:00
Alan Bernstein
18c6d8f76f
Update a few metric names 2020-04-10 20:59:20 -05:00
Alan Bernstein
71b9762501
Address review feedback again 2020-04-10 20:59:20 -05:00
Alan Bernstein
9947c92e8e
Add stats labels and slow-query log in GRPC endpoints 2020-04-10 20:59:20 -05:00
Alan Bernstein
eaf21eb19b
Add metrics for GRPC request timing 2020-04-10 20:59:19 -05:00
Alan Bernstein
5df680fb9f
Remove old metric from tests 2020-04-10 20:59:19 -05:00
Alan Bernstein
d79f04b7b3
Remove MetricMaximumRow 2020-04-10 20:59:19 -05:00
Alan Bernstein
a7bfacbee2
Revert "Add tags to MaxRow metric"
This reverts commit 6013e7211b3aef93d0880401c8ef74b34a620328.
2020-04-10 20:59:19 -05:00
Alan Bernstein
b37a0addb3
Add tags to MaxRow metric 2020-04-10 20:59:19 -05:00
Alan Bernstein
b1838159f2
Minor fixes 2020-04-10 20:59:19 -05:00
Alan Bernstein
df2503dfa6
Switch to Timing helper function 2020-04-10 20:59:19 -05:00
Alan Bernstein
2423ecf8d5
Update some metric names to follow conventions better 2020-04-10 20:59:19 -05:00
Alan Bernstein
f883d61c28
Consolidate BlockRepair metrics with tags 2020-04-10 20:59:18 -05:00
Alan Bernstein
389acfc8ed
Fix minor issues with metric labels and tests 2020-04-10 20:59:18 -05:00
Alan Bernstein
c2c0a5c32f
Address review feedback 2020-04-10 20:59:18 -05:00
Alan Bernstein
8b405c226c
Add 'prometheus' option in other help text/comments 2020-04-10 20:59:18 -05:00
Alan Bernstein
a3fb1c022b
Use metrics consts in tests 2020-04-10 20:59:18 -05:00
Alan Bernstein
8c9db373d0
Fix some metrics names 2020-04-10 20:59:18 -05:00
Alan Bernstein
5137f56f9c
Use const from package 2020-04-10 20:59:18 -05:00
Alan Bernstein
34c6d42063
Fix broken metrics label and log when others are encountered 2020-04-10 20:59:17 -05:00
Alan Bernstein
3c275681d2
Profile -> Column 2020-04-10 20:59:17 -05:00
Alan Bernstein
b1adcd91fc
Use consistent metric name convention 2020-04-10 20:59:17 -05:00
Alan Bernstein
70111b5604
Define metrics names as constants 2020-04-10 20:59:17 -05:00
Alan Bernstein
84e6a25bad
Update help message 2020-04-10 20:59:17 -05:00
Alan Bernstein
857ddf73c2
Reduce snapshot verbosity 2020-04-10 20:59:17 -05:00
Alan Bernstein
eceef6b42b
Use 'query_' prefix to identify query metrics 2020-04-10 20:59:16 -05:00
tgruben
550fcec9ee
Merge pull request #279 from tgruben/fix-closers
Closed all post request bodies and optimized available shard with new view capabilities
2020-04-10 20:29:04 -05:00
Todd Gruben
ccc7ca3aa5 close not needed 2020-04-10 20:12:32 -05:00
Todd Gruben
5590f1d954 lint fix 2020-04-10 19:07:01 -05:00
Todd Gruben
ef5a8cefef Added request Close and optimized availble shard with new view capabilities 2020-04-10 17:59:44 -05:00
seebs
4325d62fe7
Merge pull request #277 from seebs/between
Return empty rows for impossible ranges
2020-04-10 16:21:09 -05:00
Seebs
017e65cd99 Return empty rows for impossible ranges
If the high end of a range is below the low end of the range, there's
no values in it, so we can short-circuit that. If we don't, if the
low end is zero or higher, and the high end is below zero, we can
get very surprising behaviors, such as accepting values up to the
inverse of the high end. Add a test case for this and treat it the
same as a low range end above the field's maximum or a high end
below the field's minimum, returning an empty row immediately.
2020-04-10 13:24:08 -05:00
seebs
9b002bcc24
Merge pull request #271 from seebs/decimalfault
Handle nonexistent shards in min/max decimal queries.
2020-04-09 22:01:07 -05:00
Seebs
8e662d33a5
Handle nonexistent shards in min/max decimal queries.
If a shard has never had any decimal values in it at all for a
field, the ValCount object returned has no DecimalVal, which could
cause a segfault if we don't check for it. Add a test case which
sporadically triggers that behavior (it's timing/luck related,
unfortunately), and then also fix it.
2020-04-09 21:30:34 -05:00
Matthew Jaffee
179cab91e7
Merge pull request #275 from travisturner/serialize-null
serialize null operation (!= null)
2020-04-09 21:25:05 -05:00
Travis
88b2d79812 serialize null operation (!= null) 2020-04-09 17:56:27 -05:00
alanbernstein
0e6ed80bd4
Merge pull request #269 from alanbernstein/long-query-full
WIP Show full query string when logging slow queries
2020-04-09 16:12:11 -05:00
Alan Bernstein
92416018d5
Use more general log message 2020-04-09 15:53:40 -05:00
Alan Bernstein
5aebb242f8
Fix off-by-one error 2020-04-09 15:53:40 -05:00
Alan Bernstein
e3ace544ca
Show full query string when logging slow queries 2020-04-09 15:53:39 -05:00
tgruben
57b2f32fa1
Merge pull request #260 from tgruben/sync-start
delay start for non-coordinator
2020-04-09 14:42:41 -05:00
Todd Gruben
e7bbc3a0a8 Added delay to allow cooridinator a head start in launch on multi node clusters.
considered using Cluster.Disabled to identify if pilosa was stand alone but settled
on using an empty gossip seeds list
2020-04-09 14:20:01 -05:00
alanbernstein
bdfad719df
Merge pull request #263 from alanbernstein/grpc-multi
Support multiple dialTargets and cycle through on connection reset
2020-04-08 13:39:58 -05:00
alanbernstein
1846d21765
Merge pull request #1 from travisturner/dialtargets
cycle through dial targets regardless of error
2020-04-08 12:55:17 -05:00
Travis
8f0ae1b6f5 cycle through dial targets regardless of error 2020-04-08 12:29:15 -05:00
Alan Bernstein
adb21589f0 Support multiple dialTargets and cycle through on connection reset 2020-04-08 10:09:49 -05:00
seebs
1b5d86c8f0
Merge pull request #240 from seebs/q2perf
improve performance of difference/not
2020-04-07 20:24:51 -05:00
Seebs
be379e7806 cache AvailableShards
The computation of available shards is cheap, because realistically, virtually
no one has enough shards that the resulting bitmap is more than one container.

We don't try to fix this at the field/index levels because it's significantly
harder to do there, but I think the creation of these bitmaps is probably
the most expensive part, and switching the unions to union-in-place probably
reduces cost significantly.

Note that the bitmaps being unioned almost certainly have exactly one small
container in them.
2020-04-07 19:33:21 -05:00
Seebs
6c797e0c4e TODO => TODONE: use masks for runToBitmap
Had the code lying around from mad science elsewhere, backported.
2020-04-07 19:33:21 -05:00
Seebs
d4e496887b make differenceRunBitmap smarter
We avoid using bitmapContains so often because that turns out to be expensive.
Also, if we produce more than runMaxSize runs, we're going to convert to
a bitmap container (or possibly an array container if there were over
2048 items, but they're all singletons), and we can streamline that by just
converting the source to bitmap and returning differenceBitmapBitmap, which
is faster in this case.

This appears to overall take about half as long in the workload I was
looking at.
2020-04-07 19:33:20 -05:00
Kuba Podgórski
7868188670
Merge pull request #200 from kuba--/groupby-int/124
Group by on ints
2020-04-08 01:47:17 +02:00
Kuba Podgórski
1217deee1c Rebase 2020-04-08 01:06:18 +02:00
tgruben
64caffebe8
Merge pull request #196 from tgruben/async-available-shards
limit frequency of writes for available shards broadcast
2020-04-06 09:08:04 -05:00
Todd Gruben
6712f8cf06 removed debug log 2020-04-06 08:43:30 -05:00
Todd Gruben
5470753cb5 fix conflict 2020-04-05 18:37:11 -05:00
Travis
0e2bb550db add deleted (rebalanced) shards to remoteAvailableShards 2020-04-05 18:37:11 -05:00
Todd Gruben
6bd81b87eb unexport availableShardFileFlushDuration 2020-04-05 18:37:11 -05:00
Todd Gruben
000ea90877 unbuffer channel 2020-04-05 18:34:43 -05:00
Todd Gruben
af068e08e7 cleanup and comments 2020-04-05 18:34:43 -05:00
Todd Gruben
40a3dce93c Co-authored-by: Travis Turner <github@calfrope.com> 2020-04-05 18:34:43 -05:00
Todd Gruben
bb04f7f6ac limit frequency of writes for available shards 2020-04-05 18:31:28 -05:00
Cody Soyland
0a86f6a97b Increase gRPC maximum message length 2020-04-03 16:35:57 -05:00
Travis
b7fcfb41d9 add proto OldMin/OldMax for backward compatibility 2020-04-03 12:25:07 -05:00
Travis Turner
48879107b0
Merge pull request #244 from travisturner/fix-race
make sure frag.maxRow() call is lock-protected
2020-04-02 23:52:38 -05:00
Travis
b4f1781d30 make sure frag.maxRow() call is lock-protected 2020-04-02 23:25:57 -05:00
seebs
632f6856ab
Merge pull request #229 from seebs/checkptr
disable checkptr with go 1.14
2020-04-02 22:19:45 -05:00
seebs
a20532073b
Merge branch 'enterprise' into checkptr 2020-04-02 20:48:49 -05:00
alanbernstein
4faacad4cf
Merge pull request #242 from molecula/prometheus-options
Add options to prometheus client to support setting namespace
2020-04-02 20:24:15 -05:00
Alan Bernstein
dad4ccf103 Propagate namespace to tags client 2020-04-02 19:29:46 -05:00
Alan Bernstein
110d2b6024 Restore lost comment 2020-04-02 19:07:43 -05:00
Alan Bernstein
f15d031c55 Add options to prometheus client to support setting namespace 2020-04-02 19:05:55 -05:00
Travis Turner
7432546af8
Merge pull request #241 from travisturner/shardwidth22-tests
alter tests to allow for shardwidth22
2020-04-02 18:25:29 -05:00
Travis
182c1d3c42 alter tests to allow for shardwidth22
also, reset BitDepth on field and bsiGroup during
importRoaringOverwrite
2020-04-02 17:32:49 -05:00
Cody Soyland
da503776e2
Merge pull request #237 from codysoyland/ci-tweaks
CI: Modify Docker Hub rules and use "make test-race" for running race detector
2020-04-02 12:25:40 -05:00
Cody Soyland
6a639efdba Modify Docker Hub deployment filter rules.
The documentation is unclear/incorrect, and these filters aren't
behaving correctly. This is an attempt at fixing that. More info at:
https://discuss.circleci.com/t/job-runs-even-when-tags-ignore-filter-is-triggered-when-combined-with-branches-only/20664/11
2020-04-02 12:14:37 -05:00
Cody Soyland
14319cc54b Use "make test-race" instead of custom test flags in CI 2020-04-02 12:14:37 -05:00
Seebs
c6799bd604 disable checkptr with go 1.14
The checkptr feature is actually probably right about a few
things in roaring and boltdb, but we can ignore them for now, and
that prevents checking for races, so we disable that temporarily.

Also supply NOCHECKPTR in non-race tests because CI uses "make test"
with -race in $TESTFLAGS and we might do that on other occasions.
2020-04-02 11:37:06 -05:00
Matthew Jaffee
44fcbee447
Merge pull request #235 from jaffee/nil-check-indexmeta
add nil check for index meta
2020-04-02 11:33:25 -05:00
Matt Jaffee
35fa26918d
add nil check for index meta 2020-04-02 11:09:07 -05:00
Travis Turner
794bc5b168
Merge pull request #232 from travisturner/decimal-grcp
convert grpc response to use pql.Decimal
2020-04-02 07:55:26 -05:00
Travis
4d985653ae convert grpc response to use pql.Decimal 2020-04-01 23:59:50 -05:00
Travis Turner
0b480f8405
Merge pull request #231 from travisturner/reintroduce-decimal
Reintroduce decimal
2020-04-01 21:44:26 -05:00
Travis
22cca67d6a Revert "back out the pql.Decimal changes"
This reverts commit 741ba9b268.
2020-04-01 17:46:46 -05:00
Matthew Jaffee
e77bf45643
Merge pull request #227 from travisturner/backout-decimal
back out the pql.Decimal changes
2020-04-01 11:32:02 -05:00
Travis
741ba9b268 back out the pql.Decimal changes 2020-04-01 11:10:33 -05:00
Cody Soyland
1284791d22
Merge pull request #224 from codysoyland/ci-fixes
CI fixes: quote TESTFLAGS and fix stable release filter
2020-04-01 09:41:38 -05:00
Cody Soyland
dc5a471939 Downgrade golangci-lint to 1.23.8 (attempt fix for OOM failures) 2020-04-01 09:07:11 -05:00
Cody Soyland
39d83e7b6f Add no_output_timeout for the race detector 2020-04-01 09:07:11 -05:00
Cody Soyland
01033d4fff CI fixes: quote TESTFLAGS and fix stable release filter 2020-04-01 09:07:11 -05:00
Kuba Podgórski
b0f1ee3fce
. (#225) 2020-04-01 15:19:05 +02:00
seebs
c6083d6816
Merge pull request #163 from seebs/distinctshards
Address issues with Distinct failures in testing, or across shards, or in cases where the range of Distinct results is not the same as the range of shards available in any index.
2020-03-31 21:06:16 -05:00
Travis
0374bda45f Adjust bare-distinct logic.
If an index is provided to a bare distinct which happens
to be the index handling the query, then the query needs
to behave as if no index argument was provided.

For example:

When querying against index `i`,
```
Distinct(index="i", field="ints")`
```
should behave exactly like
```
Distinct(field="ints")
```
2020-03-31 19:51:06 -05:00
Seebs
a495b6c227 make Distinct work across nodes, probably
Problem: A top-level bare "Distinct" call returns results only
for shards on the current node.

Analysis: We don't actually want to limit Distinct calls to "available"
shards at all. We just want to run them on everything. But we already
did that in generating the precomputed results; all we need to do is,
if we get a non-shard-specific request for precomputed values, just
return all the values.

It's pretty hard to create logic for this using our fancy mapReduce,
but also we could just... not do that.
2020-03-31 19:51:06 -05:00
seebs
12ba11a437
Merge pull request #215 from seebs/mmap-v-cache
clear container lookup cache when updating every container, handle nils with differenceInPlace, use transaction/ops log for mergeBlock.
2020-03-31 19:49:43 -05:00
Seebs
76e7470559 make mergeBlock use transactions
mergeBlock was bypassing the transaction setup stuff, which means that
if we ran out of open files, mergeBlock wouldn't generate ops log
entries (!), also it didn't update the cache (!). This came up because
it also didn't enjoy the "catch your segfaults and issue a diagnostic"
behavior offered by the generation code.

Switch to computing positions directly and calling importPositions,
which does a transaction.
2020-03-31 16:22:41 -05:00
Seebs
28b9d6d7fc ditch lastKey cache on UpdateEvery
UpdateEvery can change every key, and I think it strongly suggests no
reasonable expectation of repeated access to a previously-accessed key,
but also it can change the containers and replace them.

We were avoiding caching mapped containers in some but not all cases,
and that was causing segfaults. But really, the *problem* is that
the remap operation wasn't clearing (or updating) the cache. Cleaning
that up allows us to take advantage of the caching performance advantage
even when working with read-only/mapped bitmaps.

The only way to hit this:

* Have mmapped containers to begin with.
* Do reads so those containers get frozen.
* Access, either reading or writing, a specific container with key K.
* Snapshot, so the bitmap gets its containers replaced.
* Remember, they have to be frozen -- if they aren't frozen,
  we'll update the containers in place.
* Now have GC run so it actually unmaps the data.
* Now try to write to the container with key K *before reading or
  writing any other key*. You have to get through the whole snapshot
  and GC process without any other reads or writes.
* You get the cached value. You try to use it. You explode.

The sliceContainers code was also setting lastKey to 0 in some cases,
but also setting lastContainer to nil, so this wouldn't have caused
problems, but just to be careful, I've standardized on ^uint64(0)
for everything.
2020-03-31 16:20:05 -05:00
Seebs
1ac00291f3 Add test for the weird remapping/cache interaction.
This test is really a test of a very specific bit of the internals
of containers_btree/containers_slice, but we can't easily test it from
there because they don't have all the logic for remapping files.

The underlying issue is that they maintain a single-item "most recent
container" cache, and this wasn't getting updated during the remap
operations, happening through containers.UpdateEvery. The fix is
probably just to make sure that UpdateEvery invalidates the cache.
2020-03-31 16:20:05 -05:00
Seebs
d26e221a91 don't call isArray on a nil *Container
differenceInPlace wasn't checking for nil containers, which are
theoretically valid empty containers. Also added a couple of other
N==0 checks to streamline the higher-level operation.
2020-03-31 16:20:05 -05:00
Cody Soyland
3b6a26e5c6
Merge pull request #219 from codysoyland/ci-updates
Add updated CircleCI config
2020-03-31 16:18:10 -05:00
Cody Soyland
3a7f385a01 Add updated CircleCI config
- Remove YAML magic
- Remove a lot of duplication
- Update linter
- Use parameterized jobs and matrix build
- Update Docker Hub CD to produce versioned and "latest" images
- Add custom shard width test to workflow
2020-03-31 15:17:11 -05:00
Travis Turner
b5aa280ced
Merge pull request #220 from travisturner/decimal-yaml
yaml marshal/unmarshal for pql.Decimal
2020-03-31 13:53:00 -05:00
Travis
7cb265098f yaml marshal/unmarshal for pql.Decimal 2020-03-31 12:43:42 -05:00
Kuba Podgórski
6dc3837c9a
WIP: fix 'unknown call: Distinct' error (#213) 2020-03-31 16:29:14 +02:00
Travis Turner
dca2120c06
Merge pull request #199 from travisturner/cluster-resize-translation-partitions
include translate partitions in cluster resize instructions
2020-03-30 22:01:09 -05:00
Kuba Podgórski
ac76f6227d
Make internal.IndexMeta.TrackExistence true 2020-03-30 21:24:16 -05:00
Travis
98c5603965
close reader. include all replias in translation partition rebalance 2020-03-30 21:24:16 -05:00
Travis
4c311aa1a7
write to temp partition file. use io.Copy 2020-03-30 21:24:16 -05:00
Travis
2724ecfd5f
WIP: include translate partitions in cluster resize instructions
This commit adds `TranslationSources` to the cluster
`ResizeInstruction`. These are the sources of translation
partitions which the receiving node needs in order to support
partition distribution in the new, resized cluster.

This also fixes a bug where index options were not being
encode in the proto Index object. That meant that the schema
transferred via protobuf was not correct. The reason why
things normally worked is because index creation typically
happens on the CreateIndex message, which does include the
options.

TODO:

- [ ] implement the TranslateStore interface for `InMemTranslateStore`
and `mock.TranslateStore`
- [ ] surely need some more tests around the `ReadFrom` and `WriteTo`
2020-03-30 21:24:16 -05:00
Travis Turner
2ad06f9423
Merge pull request #212 from travisturner/decimal-min-max-args
support pql.Decimal for decimal field min/max arguments
2020-03-30 14:32:02 -05:00
Travis
80da129861 require scale argument for decimal fields 2020-03-30 13:38:50 -05:00
Travis
1da7cf09bb support pql.Decimal for decimal field min/max arguments 2020-03-27 16:02:47 -05:00
Cory LaNou
2a4088e0ec
Merge pull request #206 from corylanou/test-helpers
Make better use of t.Helper
2020-03-25 07:35:35 -05:00
Cory LaNou
391fda9849
Merge branch 'enterprise' into test-helpers 2020-03-24 15:51:01 -05:00
corylanou
f84185230d
make use of t.Helper 2020-03-24 15:00:32 -05:00
Cody Soyland
7ba5c0e3d3
Merge pull request #201 from codysoyland/queryunary
Add QueryPQLUnary gRPC call
2020-03-23 16:58:27 -05:00
Cody Soyland
1aa1ec51aa Add QueryPQLUnary gRPC call 2020-03-23 16:24:31 -05:00
Cory LaNou
69a1365bf0
Merge pull request #202 from corylanou/sum/194
Fixed sum for negative values
2020-03-23 16:22:11 -05:00
corylanou
f32f9f64a4
fix sum for negative values 2020-03-23 14:02:46 -05:00
Travis Turner
532f746f25
Merge pull request #195 from travisturner/sync-mutex-bool
support mutex/bool fields in anti-entropy
2020-03-19 11:47:37 -05:00
Travis Turner
5cfb29beb2
Merge branch 'enterprise' into sync-mutex-bool 2020-03-19 11:28:56 -05:00
Travis Turner
564eee0bdf
Merge pull request #190 from travisturner/decimal-between
serialize decimal between pql for internode queries
2020-03-19 11:26:12 -05:00
Travis
57c30b9dae support mutex/bool fields in anti-entropy 2020-03-19 11:11:33 -05:00
Travis
4d38723fd4 serialize decimal between pql for internode queries 2020-03-19 07:50:43 -05:00
Travis Turner
419c2179b5
Merge pull request #185 from travisturner/decimal-min-max-overflow
Avoid overflow on decimal min/max default values
2020-03-18 08:01:40 -05:00
Travis
298f290e86
Avoid overflow on decimal min/max default values
If the min/max provided are already on the boundary of int64,
then we don't want to operate on them and cause overflow.
there are still overflow scenarios where a user provides a
min/max which is not on the boundary, but overflow once the
scale is applied. This does not address those cases, but at
least it addresses the default case (where a min/max is not
provided)
2020-03-18 07:41:35 -05:00
Kuba Podgórski
73ca124944
Fix runCountRange when range start == interval start (#181)
When the interval is a proper superset of the range with start equal to
interval start, the range must be considered a superset or it will be
completly ignored (since it neither a subset nor it overlaps)

Co-authored-by: Pierre Fersing <pierre.fersing@bleemeo.com>
2020-03-17 20:31:35 +01:00
Travis Turner
ba5b133e1e
Merge pull request #180 from travisturner/mu-anti-entropy
add mutex for anti-entropy and node join/leave
2020-03-17 13:05:53 -05:00
Travis
fbbd474978 add mutex for anti-entropy and node join/leave 2020-03-17 12:46:09 -05:00
Travis Turner
22ae1139d1
Merge pull request #172 from travisturner/float-to-decimal
use pql.Decimal instead of float64
2020-03-16 08:35:19 -05:00
Travis
c60241b5a9 Get rid of Sign from pql.Decimal struct
It turns out that it's not very useful to keep the sign
value as a separate argument in the pql.Decimal struct.
This commit incorporates it into Value, and makes Value
an `int64` (for some bone-headed reason I had made it a
`uint32` before which is just dumb).
2020-03-15 23:00:00 -05:00
Travis
30e08eb532 add error conditions to tests 2020-03-15 16:18:59 -05:00
Travis
513edeae9c fix min/max bug for decimal fields 2020-03-14 22:35:24 -05:00
Travis
963affcc30 WIP: use pql.Decimal instead of float64
This commit introduces a new type: pql.Decimal
We use that instead of float64 in order to ensure
that the string representation is consistent.

One unfortunate discovery during implementation is
that the RowAttrs and ColAttrs support floats, and
the PEG file was treating them as such. So I had
to split the PEG definitions into float-specific
items and decimal-specific items.
2020-03-14 22:33:52 -05:00
Travis Turner
334eb3cd08
Merge pull request #175 from travisturner/int-min-max-with-offset
Fixes the min/max bug for `int` fields with offset.
2020-03-14 15:18:48 -05:00
Travis
3863ab4b41 Fixes the min/max bug for int fields with offset.
Methods `MinForShard` and `MaxForShard` were not adjusting
their return value by the offset.
2020-03-14 14:01:40 -05:00
Travis Turner
0d55d6eab2
Merge pull request #160 from travisturner/int-fragment-sync-better-fix
support fragment sync for int and decimal fields
2020-03-13 18:42:12 -05:00
Travis
a5cba75855 fix typos in comment 2020-03-13 12:15:54 -05:00
Travis
3b676a7cbd fix linter warnings 2020-03-13 12:15:54 -05:00
Travis
cbf80370cb support fragment sync for int and decimal fields
This PR adds support for anti-entropy syncing for integer
and decimal fields. It differs from the logic for other
field types in that it does not rely on a consensus to determine
what the value should be; instead, it considers the correct
values to be those of the primary replica. From there, data
is pushed to all non-primary replicas.
2020-03-13 12:15:54 -05:00
Cody Soyland
8b5848f615
Merge pull request #164 from codysoyland/dockerfile-env-config
Use env variables instead of flags in Dockerfile
2020-03-12 14:44:45 -05:00
Cody Soyland
c6a293f818 Use env variables instead of flags in Dockerfile
This allows you to override those attributes without overriding the
entire command.
2020-03-12 11:22:31 -05:00
Travis Turner
93a804f7ab
Merge pull request #165 from travisturner/range-ast-fix
fix Call stringer for range conditional
2020-03-12 08:29:38 -05:00
Travis
0c6f0e2ba5 fix Call stringer for range conditional 2020-03-11 22:53:37 -05:00
Travis Turner
d76e8fc17c
Merge pull request #162 from travisturner/makefile-helpers
add linter and test-race targets to makefile
2020-03-11 10:10:41 -05:00
Travis
dde1e26baa add linter and test-race targets to makefile 2020-03-11 09:24:00 -05:00
Travis Turner
cdbb274a2b
Merge pull request #157 from travisturner/int-fragment-sync-quick-fix
temporary fix for int field replica sync bug
2020-03-09 15:22:41 -05:00
Travis Turner
f747501473
Merge branch 'enterprise' into int-fragment-sync-quick-fix 2020-03-09 14:56:06 -05:00
Matthew Jaffee
d0aad872e2
Merge pull request #144 from jaffee/min-con-reuse
Min con reuse
2020-03-09 14:54:56 -05:00
Matt Jaffee
f0f86500c7
add minimal fix for connection reuse issue - @tgruben has a more complete fix 2020-03-07 09:46:47 -06:00
Travis
70c3cf1775 include a basic test which covers the temp fix 2020-03-05 21:16:10 -06:00
Travis
86da5c7adf temporary fix for int field replica sync bug 2020-03-05 20:57:16 -06:00
Travis Turner
b99bdb8169
Merge pull request #156 from travisturner/forward-translation-to-coordinator
forward field translation request to coordinator
2020-03-05 17:53:35 -06:00
Travis
3e4f7dd3f3 change translateFieldKeys to variadic function 2020-03-05 15:38:34 -06:00
Travis
d06ffd207f forward field translation request to coordinator 2020-03-05 14:54:23 -06:00
seebs
bbeacbe3c3
Merge pull request #147 from seebs/emptylog
don't fill up empty space with non-functional ops logs
2020-03-04 18:28:55 -06:00
Seebs
eb263b7666 don't fill up empty space with non-functional ops logs
Two changes:

1. Don't write batch/roaring adds or removes when N is 0, because
a write of no bits is not a meaningful write.
2. When unmarshalling roaring things, if a roaring bitmap didn't
change many bits, treat it as having changed at least 1 bit per 8 bytes,
so an 8KB hunk of roaring data counts as 1K changes, which will
nudge us towards snapshotting. This should keep us from having
Large Files show up so much.

This was particularly noticeable on the existence field, which
tends to a steady state of "completely full" very quickly in a lot
of cases.
2020-03-04 18:10:10 -06:00
Kuba Podgórski
53c486fce0
Remove not needed translationSyncer from holder. (#152) 2020-03-05 00:19:58 +01:00
Travis Turner
78327904ff
Merge pull request #145 from travisturner/translate-partition-better-fix
add translationSyncer interface
2020-03-03 19:42:50 -06:00
Travis
842c820366 add translationSyncer interface
This PR adds a translationSyncer interface; I tried to include
comments in the code explaining what's going on. This is taken
from those comments:

translationSyncer provides an interface allowing a function
to notify the server that an action has occurred which requires
the translation sync process to be reset. In general, this
includes anything which modifies schema (add/remove index, etc),
or anything that changes the cluster topology (add/remove node).
I originally considered leveraging the broadcaster since that was
already in place and provides similar event messages, but the
broadcaster is really meant for notifiying other nodes, while
this is more akin to an internal message bus. In fact, I think
a future iteration on this may be to make it more generic so
it can act as an internal message bus where one of the messages
being published is "translationSyncReset".
2020-03-03 14:21:45 -06:00
Travis Turner
92eae8e715
Merge pull request #146 from travisturner/remove-errant-print
remove errant println from test
2020-03-03 12:39:28 -06:00
Travis
7aea54936e remove errant println from test 2020-03-03 11:27:36 -06:00
Travis Turner
13e7679fd9
Merge pull request #138 from travisturner/translate-partition-quick-fix
very crude fix for the translate key read-only bug
2020-02-29 16:18:50 -06:00
Travis
717bd09e97 include the test which covers this scenario 2020-02-29 08:43:10 -06:00
Travis
fb8f612afe very crude fix for the translate key read-only bug
This PR forces the non-coordinator nodes to reset their translation
sync (and therefore their own cosideration of read-only partitions)
any time they receive a `ClusterStatus` message. So basically, as the
cluster grows during the startup process, each node will reset their
translation sync.

This is NOT a good solution log term, but it should address the
immediate problem.

Things to note:
- the coordinator sync isn't getting reset, but that's ok, because the
immediate problem is a partition marked as read-only when it shouldn't
be; i.e. it's ok to have the inverse (a partition not marked as
read-only when it should be) because that partition won't receive
requests anyway.
- the last node to start is already correct and doesn't really need to
reset its sync.
- there are many other scenarios not covered by this fix.

Based on this theory:

```
i have another theory that i’m going to try to test.
this one would only apply in the case where a multi-node cluster is restarted with an existing, keyed index.
- start node0: it thinks it’s responsible for all partitions (nothing is read-only)
- start node1: it thinks it’s responsible for ~1/2 of the partitions and marks the other 1/2 as read-only
- start node2: it thinks it’s responsible for ~1/3 of the partitions and marks the other 2/3 as read-only
now if node0 is the coordinator receiving all translation requests, that still might not explain what’s happening, because in that case it would just do all the translating. i think. but either way, i should make sure that scenario is not happening, but i think it may be.

actually, that might explain it, because what would happen when the coordinator received a translation request, is that it would handle the 1/3 that it owned (now that the cluster is 3 nodes), and it would send the other 2/3 out to the other 2 nodes. but where it sent the requests wouldn’t line up with what the nodes thought they were responsible for based on the restart order
in this example, node 1 would receive requests for the wrong partitions
```
2020-02-29 08:43:05 -06:00
Matthew Jaffee
fcbebcf1b6
Merge pull request #103 from seebs/gencrash
don't mark a source as changed before we've finished remapping
2020-02-21 17:00:19 -06:00
Seebs
d742c67317
avoid race on max count reads and writes 2020-02-21 16:38:36 -06:00
Seebs
ba7db3028b
sanity-check: check whether containers are flagged as mapped before mapping
In the old unmarshal code, the decision to mark a thing as mapped (always
yes) happens separately from setting the mapping. What if this could ever
somehow possibly go wrong? Let's sanity-check that to be extra careful.
2020-02-21 16:38:35 -06:00
Seebs
7841a660a8
make sure setArray isn't copying mapped data addresses by accident in unionInPlace 2020-02-21 16:38:35 -06:00
Seebs
99d865c2ea
ensure that we've unrequested mapping when applying empty storage 2020-02-21 16:38:35 -06:00
Seebs
337e451cc7
lint and review changes
Log an error in the probably-irrelevant case where we ended up with
a file, but Stat failed, which shouldn't ever happen we hope anyway.

Also explicitly discard the status from RemapRoaringStorage in a case
where we don't care.
2020-02-21 16:38:35 -06:00
Seebs
cb686dcad0
make mmap test experiment with different amounts of mapping
This is sort of prototype-ish, but the idea is that we use SetMaxMapCount
from syswrap, which already exists, to let us test edge cases like
"what happens if you only sometimes have mapped data".
2020-02-21 16:38:35 -06:00
Seebs
63fb2f8539
generation testing and paranoia features
We might have a problem with a stale mmap, and to try to narrow it down
a bit, we add some sanity-checking features and panic recovery to the
generation Transaction code.

This is pretty experimental.
2020-02-21 16:38:35 -06:00
Seebs
372389fd30
don't corrupt files when mmap fails
In some cases, after a snapshot, if mmap fails, we could write
a duplicate of the bitmap to the file, creating cryptic "unknown
op type: 60" messages. This doesn't fix those files, but it stops
making them.
2020-02-21 16:38:35 -06:00
Seebs
f9e7fee47d
don't mark a source as changed before we've finished remapping
Also, check the remap operation for errors, and if an error occurs,
try to remap to nil (which shouldn't be able to fail).
2020-02-21 16:38:34 -06:00
Matthew Jaffee
ebab831a43
Merge pull request #113 from molecula/minMaxFloatHandling
min and max should properly scale their output for decimal fields
2020-02-21 16:38:11 -06:00
Matthew Jaffee
8c9b717b05
fix "worhtless" typo in comment 2020-02-21 16:00:31 -06:00
Matt Jaffee
fe46c84d19
also fix Sum query, but don't convert to float until the last step
this avoids compounding floating point errors while summing up the
numbers, and means less logic needs to change. Should probably convert
min and max to use this approach as well, though they don't suffer
from the compounding error issue, it is simpler.
2020-02-21 14:11:30 -06:00
Matt Jaffee
7321f9427c
min and max should properly scale their output for decimal fields
this involved adding an optional float value to the ValCount struct
which complicated result types, necessitated grpc changes, and needed
quite a few tests at different layers.
2020-02-21 14:11:29 -06:00
Matthew Jaffee
d369e3b4bf
Merge pull request #125 from travisturner/time-panic
send nil time value to prevent downstream panic
2020-02-21 14:08:53 -06:00
Travis Turner
52d450cbaf
Merge branch 'enterprise' into time-panic 2020-02-21 10:31:24 -06:00
Travis
13a1b3a2c1 send nil time value to prevent downstream panic 2020-02-18 15:55:40 -06:00
Kuba Podgórski
f3f11f4a44
Let translate keys as empty strings (#120) 2020-02-18 12:54:24 +01:00
Kuba Podgórski
57eb741c24
Don't allow an int and decimal fields to be created with keys=true (#118) 2020-02-14 08:47:31 +01:00
Kuba Podgórski
59f5d4f7d6
Do not clear existence column (#117) 2020-02-13 07:08:45 +01:00
Travis Turner
4713ccd0c8
Merge pull request #110 from travisturner/translatestore-fixes
WIP: Thread OpenTranslateStore through Holder to Index
2020-02-12 11:56:49 -06:00
Ben Johnson
c8cefea897
Fix test performance 2020-02-12 10:25:26 -06:00
Travis
d9ef4c0986
use OpenInMemTranslateStore by default in tests 2020-02-12 10:25:25 -06:00
Travis
4dd530e956
open bolt translate store partitions asynchronously 2020-02-12 10:25:25 -06:00
Travis
49c8bf01a0
WIP: Thread OpenTranslateStore through Holder to Index 2020-02-12 10:25:25 -06:00
Cody Soyland
f9f6fce6b4
Merge pull request #111 from codysoyland/go-1.14-rc
Add Go 1.14-rc to CI
2020-02-10 07:59:48 -06:00
Cody Soyland
88c3477010 Add Go 1.14-rc to CI 2020-02-06 19:46:01 -06:00
Kuba Podgórski
e77c69d212
Update handler.go (#108) 2020-02-05 22:14:25 +01:00
tgruben
fc6fd150ba
Merge pull request #106 from tgruben/bug-105
handle missing index in join properly
2020-02-05 10:29:56 -06:00
Todd Gruben
0acac34fee travis suggetions 2020-02-04 15:21:55 -06:00
Todd Gruben
3140b2d8cb handle missing index in join properly 2020-02-04 11:55:49 -06:00
Kuba Podgórski
3bb45ea2c0
Fix Set operation for float numbers on decimal fields. (#101) 2020-02-03 19:29:15 +01:00
Kuba Podgórski
7c395ac4d1
Simplify Holder's logic for CreateIndex (#104) 2020-02-03 17:41:06 +01:00
Travis Turner
b693688677
Merge pull request #60 from molecula/translation-sharding
Translation sharding
2020-01-31 10:44:51 -06:00
Travis
9b4c7610e6 Merge branch 'enterprise' into translation-sharding 2020-01-31 10:13:44 -06:00
tgruben
eca14d8608
Merge pull request #97 from tgruben/row-difference-in-place
Difference in place at row level
2020-01-30 15:10:23 -06:00
Travis Turner
8626a2a710
Merge pull request #99 from travisturner/translation-sharding
Ensure ForeignIndex key translation happens in API.
2020-01-30 14:42:41 -06:00
tgruben
1be22f13e8
Merge pull request #2 from travisturner/row-difference-in-place
fix differenceInPlace test
2020-01-30 14:31:30 -06:00
Todd Gruben
f498a40d97 go mod tidy 2020-01-30 14:28:44 -06:00
Travis
0bdcb5ab0a fix differenceInPlace test 2020-01-30 13:38:31 -06:00
Travis
61e527251a fix some comments 2020-01-30 10:56:03 -06:00
Todd Gruben
8de32fd62a high level support for difference in place 2020-01-30 08:11:43 -06:00
Travis
e40400b130 Ensure ForeignIndex key translation happens in API.
For Fields with ForeignIndex (which have keys), the API was missing
the logic to do that translation against the translateStore of
the foreign index. This commit adds that logic, as well as some
missing translateStore-related logic in the gRPC code.
2020-01-28 22:46:12 -06:00
tgruben
32b92abd6e
Merge pull request #94 from tgruben/diff-inplace
Adding roaring in place difference support
2020-01-23 08:23:47 -06:00
tgruben
014c3a98a5
Merge pull request #1 from travisturner/diffinplace
tidy comments and remove pilosa dependency
2020-01-22 12:09:50 -06:00
Travis
bb80d1ed2d tidy comments and remove pilosa dependency 2020-01-22 11:59:32 -06:00
Todd Gruben
f40ced47fa included previous patterns for in place testing 2020-01-21 16:56:33 -06:00
Travis Turner
43b8d7827a
Merge pull request #90 from travisturner/translation-sharding
Field.ForeignIndex translation on ImportValue()
2020-01-17 17:25:14 -06:00
Travis
d41ee99052 linter fix 2020-01-17 14:46:43 -06:00
Travis
b620e37e51 move the foreign index key check into applyTranslateStore() 2020-01-17 14:41:41 -06:00
Travis
f001ad199f use translateIndexKeys instead of translateIndexKeySet in ImportValue() 2020-01-17 11:58:15 -06:00
Travis
efffa39c2c check foreign index on field open 2020-01-17 11:42:21 -06:00
Travis
90a2e116a7 update translateResult to translate foreign index keys on SignedRow results 2020-01-17 11:10:42 -06:00
Travis
0ba5b48fca Field.ForeignIndex translation on ImportValue() 2020-01-16 22:23:29 -06:00
Ben Johnson
10ccb6d523 translate foreign index 2020-01-16 13:53:12 -06:00
Ben Johnson
4020f8c73e fix cross-index translation 2020-01-15 14:28:06 -06:00
Travis
54679c12c4 post merge, needs review of TODOs 2020-01-14 22:10:12 -06:00
Travis
df51f07f96 Merge branch 'enterprise' into translation-sharding 2020-01-14 20:05:12 -06:00
Travis Turner
0459285101
Merge pull request #87 from travisturner/grpc-connection-reset
reset grpc connection after TransientFailure
2020-01-14 16:26:02 -06:00
Travis
757df0d284 reset grpc connection after TransientFailure 2020-01-14 15:18:25 -06:00
Travis
f232ec4277 Merge remote-tracking branch 'upstream/enterprise' into enterprise 2020-01-14 15:18:09 -06:00
Matthew Jaffee
12c6cd1c4e
Merge pull request #88 from jaffee/grpc-mutex-string-bug
mutex field data type should be string not []string
2020-01-12 18:27:47 -06:00
Matt Jaffee
ff9ad9a4fe
mutex field data type should be string not []string 2020-01-12 17:38:43 -06:00
tgruben
ba70bf3079
Merge pull request #89 from tgruben/bug-q2-double-delete
bit remove leaves internals corrupt on empty edge case
2020-01-12 16:25:31 -06:00
tgruben
3261ec4dc4
Merge branch 'enterprise' into bug-q2-double-delete 2020-01-12 16:09:12 -06:00
Todd Gruben
99108a1c63 bit remove leaves internals corrupt on empty edge case 2020-01-12 12:00:00 -06:00
Travis
89f6429dac Merge remote-tracking branch 'upstream/enterprise' into enterprise 2020-01-10 15:45:25 -06:00
Travis Turner
9015c00da9
Merge pull request #84 from travisturner/foreign-index
Add FieldOption.ForeignIndex
2020-01-10 15:45:02 -06:00
Travis
a6a2f84bd5 During Holder.Open, apply foreign index after all indexes open
In the case where a field with a foreign index opens before the
foreign index has opened (and is available as a reference in the
holder), push the field into a queue to have its foreign index
applied once all indexes have opened.
2020-01-10 12:28:28 -06:00
Travis
3d3286a9ca fix an issue caused by empty column list defaulting to IDs 2020-01-10 12:28:28 -06:00
Travis
f79fde43e3 exclude internal fields (i.e. _exists) from Inspect output 2020-01-10 12:28:28 -06:00
Travis
742135dc10 Get ForeignIndex string value when reading BSI field.
In the `Inspect` function in `server/grpc.go`, getting
the value of an `int` field with a foreign index to
an index with `Keys()`, we need to return the string
key value instead of the BSI int value for the field.

This commit also changes the method `Field.keys()` to be
exported as `Field.Keys()` so that it's accessible in
the server package.
2020-01-10 12:28:28 -06:00
Travis
35f9dfa374 remove write portion of extension data race 2020-01-10 12:28:28 -06:00
Travis
881d3bef06 Adjust the FieldOption logic to be in place prior to field.Open().
This commit changes the order of FieldOption application so that
it's always set before field.Open() is called.

This was required because field.Open() now uses some of the values
from FieldOptions to determine if/when to use a particular
translateStore. For example, when FieldOptions.ForeignIndex is set,
the translateStore from the foreign index is retrieved during
field.Open().
2020-01-10 12:28:28 -06:00
Matt Jaffee
4d8f307c5e handle string values in ImportValueRequest sorting 2020-01-10 12:28:28 -06:00
Matt Jaffee
132cf7cc1c add StringValues to proto ImportValueRequest, update proto versions
I ran:

brew upgrade protobuf
GO111MODULE=off go get -u github.com/gogo/protobuf/protoc-gen-gofast

I'm not sure if everything is still going to work, but I'm excited to
find out!
2020-01-10 12:28:27 -06:00
Travis
b22d0143e4 loadNewExtensions is unused, but included for completeness 2020-01-10 12:28:27 -06:00
Travis
1542cbefc0 Add FieldOption.ForeignIndex
This allows a BSI field to have an option indicating
that it is a foreign key to another index. If the foreign
index has column keys, then this field handles string values
by using the foreign index's translate store.
2020-01-10 12:28:27 -06:00
Travis
5ca37bf3f3 reset grpc connection after TransientFailure 2020-01-10 11:49:33 -06:00
Matthew Jaffee
7aef0743e1
Merge pull request #86 from jaffee/field-char-limit
allow field and index names up to 230 characters
2020-01-09 14:43:55 -06:00
Matt Jaffee
0a1de81441
allow field and index names up to 230 characters
The lowest limitation I've seen on any filesystem we care about is 255
characters. 230 leaves enough space that an index or field could be
backed up and have a timestamp and file extension appended while
still allowing for much longer index and field names.
2020-01-09 13:02:22 -06:00
Ben Johnson
cda2729935 fix bitdepth race 2020-01-08 09:47:43 -07:00
Ben Johnson
1f6910b0b8 fix group by test 2020-01-08 09:47:43 -07:00
Ben Johnson
a043490996 add multi-shard translation 2020-01-08 09:47:43 -07:00
Ben Johnson
bdfdeb1291 fix lint 2020-01-08 09:47:43 -07:00
Ben Johnson
a189477ba3 rebase & fix test const 2020-01-08 09:47:43 -07:00
Ben Johnson
1409ab5664 fix data race 2020-01-08 09:47:43 -07:00
Ben Johnson
1b068f75a8 Fix inmem read only translation bug 2020-01-08 09:47:43 -07:00
Ben Johnson
2f76283f03 fix replication errors & test races 2020-01-08 09:47:43 -07:00
Ben Johnson
9647d9b4bb fixing additional tests 2020-01-08 09:47:43 -07:00
Ben Johnson
c0a129979e fix tests 2020-01-08 09:47:43 -07:00
Ben Johnson
e3606d6615 fix id generation 2020-01-08 09:47:43 -07:00
Ben Johnson
82910911dd refactoring id partitioning 2020-01-08 09:47:43 -07:00
Ben Johnson
f31d68739e holder syncer translate implementation 2020-01-08 09:47:43 -07:00
Ben Johnson
b3e86e8394 refactoring stores back into index/field 2020-01-08 09:47:43 -07:00
Ben Johnson
7215bfd16c Implement translator store sharding 2020-01-08 09:47:43 -07:00
Travis Turner
cabfa3c456 add a test for pilosa/#2084 2020-01-08 09:47:43 -07:00
tgruben
457789effd
Merge pull request #62 from tgruben/clearvalue
add support to clear value for column
2020-01-07 13:12:21 -06:00
Todd Gruben
643884aeb3 refactored clear to fix q2;removed unused comment 2020-01-07 12:55:32 -06:00
Todd Gruben
26e3460413 removed view creation from ClearValue 2020-01-07 10:55:45 -06:00
Todd Gruben
d843959904 fixed comments; removed create fragment 2020-01-07 10:39:56 -06:00
Todd Gruben
75e017cf4f Merge remote-tracking branch 'upstream/enterprise' into clearvalue 2020-01-07 10:28:47 -06:00
Cody Soyland
29f7448b89
Merge pull request #83 from codysoyland/expvar-compatibility
Initialize expvar lazily to prevent panic if importing both Pilosa v1 and v2
2020-01-02 17:56:42 -06:00
Cody Soyland
8d32005ede Initialize expvar lazily to prevent panic if importing both Pilosa v1 and v2. 2020-01-02 15:43:34 -06:00
Travis Turner
5643afac47
Merge pull request #82 from travisturner/row-response-sorter
WIP: RowResponseSorter for sorting a list of RowResponse based on sort params
2019-12-31 13:18:05 -06:00
Travis
0fffd9a0cb RowResponseSorter for sorting a list of RowResponse based on sort paraters 2019-12-31 11:52:01 -06:00
Travis Turner
c247805d96
Merge pull request #81 from travisturner/inspect-field-output
Remove empty field check in Inspect()
2019-12-27 13:42:36 -06:00
Travis Turner
98df5672e9
Merge branch 'enterprise' into inspect-field-output 2019-12-27 13:26:38 -06:00
Travis Turner
768de9dc3a
Merge pull request #80 from travisturner/row-response-error
Add StatusError to RowResponse for better error handling.
2019-12-27 13:25:38 -06:00
Travis
3be4141382 Return the correct data type label in grpc header
Based on the pilosa field type, return the correct data type
label in the gRPC column header.
2019-12-27 12:43:45 -06:00
Travis
73090e05c8 Add StatusError to RowResponse for better error handling.
This PR adds a `StatusError` to the `pproto.RowResponse` type, which
allows a stream to pass an error on the stream (encoded into
the `RowResponse.StatusError`). This can be checked downstream
for matching `EOF` or `err != nil` and handled appropriately.

This is helpful mainly with the `RowResponse` reducers which run in
goroutines. Instead of trying to manage a separate channel of errors
from those goroutines, we just follow the grpc model and send the
error with the stream.
2019-12-27 12:43:45 -06:00
Travis Turner
94f97297eb
Merge pull request #77 from travisturner/all-shard
Allow All() to be called at the shard level
2019-12-27 12:35:41 -06:00
Travis
d34e38f134 Remove empty field check in Inspect()
The check for field existence is not necessary; since we
add the `_id` field to every response then at the very
least that field will be returned.

This check was preventin a query like `select _id from ...`
from returning any results.
2019-12-26 22:27:46 -06:00
Travis
586a13e942 Allow All() to be called at the shard level 2019-12-20 22:44:46 -06:00
Cody Soyland
7b30b91448
Merge pull request #74 from codysoyland/docker-build-vendor
Vendor modules before building docker image so private modules can be downloaded
2019-12-20 17:52:26 -06:00
Cody Soyland
d4117f3137 Vendor modules before building docker image so private modules can be downloaded 2019-12-20 16:05:18 -06:00
seebs
47316d9f2f
Merge pull request #70 from seebs/unionAA
Simplify unionArrayArray
2019-12-20 13:49:51 -06:00
seebs
d4d3d75e28
Merge branch 'enterprise' into unionAA 2019-12-20 13:28:57 -06:00
Matthew Jaffee
d86a3c3f2f
Merge pull request #55 from seebs/pluginfix
Pluginfix
2019-12-20 12:53:09 -06:00
Matt Jaffee
fe0f57651e
build with distinct by default 2019-12-20 12:21:47 -06:00
Cody Soyland
49b2029656
Run "go mod vendor" outside of Docker so authenticated modules may use system credentials 2019-12-20 12:21:47 -06:00
Seebs
7e1fd8392f
go.mod/go.sum changes for using molecula/ext
This pins us to the initial external release of molecula/ext, which
with any luck will be the only one. (Narrator: It was not to be the
only one.) We also use GOPRIVATE so we don't need a replace directive.
2019-12-20 12:21:47 -06:00
Seebs
0eba050054
stop using pkg/plugin, start using build tags
After a few experiments with pkg/plugin, I'm ready to concede that the
people warning me it was unsuitable for production use were in fact
correct.

In the brave new world, the "ext" package is moved to its own module
outside pilosa. This means that importing it doesn't imply any need to
version-check against pilosa; we can just use versioned copies of the
ext package, which can be public because it doesn't contain anything
we need to care about keeping proprietary.

Then we can, conditional on build tags, import modules from a
neighboring repo which contains the actual implementations, and if
they're imported, their init functions register them.
2019-12-20 12:21:47 -06:00
Seebs
f51c2dbc42
use extensions through build tags 2019-12-20 12:21:47 -06:00
Travis Turner
179fb910f7
Merge pull request #71 from travisturner/all-limit-offset
Add All() support to PQL, including limit and offset
2019-12-18 22:10:42 -06:00
Travis
361e51cb41 Add All() support to PQL, including limit and offset
This PR is meant to get all columns from an index
based on the TrackExistence row.

`All()` is a PQL function that can be used as a typical
row object. Optional arguments are `limit` and `offset`.
2019-12-18 18:00:15 -06:00
Seebs
83aa505673 Simplify unionArrayArray
Also short-circuit it in some cases.
2019-12-17 15:30:56 -06:00
Travis Turner
1af016df84
Merge pull request #68 from travisturner/linter-fix
Fix an impossible code path raised by the linter
2019-12-17 09:04:37 -06:00
Travis
532caa0fbf fix an impossible code path raised by the linter 2019-12-16 21:56:57 -06:00
Travis Turner
6f556fb880
Merge pull request #63 from travisturner/row-field-label
Wrap return types: RowIdentifiers, Pair, and []Pair
2019-12-16 07:42:20 -06:00
Travis
4f7f4f58b1 add field to SignedRow, and implement its grpc response 2019-12-14 15:56:06 -06:00
Travis
3b7b54094a update clustertests to use v2 (and go 1.13) 2019-12-13 18:45:43 -06:00
Travis
5cb37834a0 Wrap return types: RowIdentifiers, Pair, and []Pair
This PR adds a field name (string) to the return types
which represent the values from a specific field. For example,
a TopN query on field `x` would be `TopN(x)` and have results
like:
```
[]Pair{
  {ID: 14, Count: 10},
  {ID: 3, Count: 8},
  {ID: 7, Count: 3},
}
```
In order to know what field this result type refers to, we wrap
`[]Pair` in a new struct called `PairsField` which contains an
addition `Field` string where `x` is stored.

This is useful for informing the gRPC server how to construct
more appropriate headers for the result stream (in this case,
the column headers can now be "x" and "count").

Similar logic was applied to `RowIdentifiers` and `Pair` as well.
2019-12-13 15:36:51 -06:00
Travis Turner
c5aeed0715
Merge pull request #57 from tgruben/fix-minmax-count
return total match counts for either min or max
2019-12-13 15:28:27 -06:00
Todd Gruben
4523a4d693
removed uneeded test run 2019-12-13 15:10:28 -06:00
Todd Gruben
16171b3e65
return total match counts for either min or max 2019-12-13 15:10:28 -06:00
Todd Gruben
95f2864abe applied suggestions by @travisturner 2019-12-11 12:13:57 -06:00
Todd Gruben
7a2d90ade8 add support to clear value for column 2019-12-09 16:28:03 -06:00
Travis Turner
c4e339f72b
Merge pull request #59 from travisturner/not-found-code
Add grpc NotFound code where applicable
2019-12-03 17:44:30 -06:00
Travis
a5652a182c add grpc NotFound code where applicable 2019-12-03 11:53:20 -06:00
Matthew Jaffee
521ea603d0
Merge pull request #52 from molecula/import-col-attrs
Import col attrs
2019-12-02 01:25:08 -06:00
Matt Jaffee
87ee83f4cb
check errors in test to fix lint 2019-12-01 07:33:25 -06:00
Matt Jaffee
9e3029b969
re run generate-protoc 2019-12-01 07:33:24 -06:00
Alan Bernstein
e470b3276e
Add generated proto 2019-12-01 07:33:24 -06:00
Alan Bernstein
cf7d668b49
Support import column attrs in client 2019-12-01 07:33:24 -06:00
Alan Bernstein
c3e8284f6c
Test for presence of column attrs 2019-12-01 07:33:24 -06:00
Alan Bernstein
628def3db7
Clarify some error messages 2019-12-01 07:33:24 -06:00
Alan Bernstein
aba67364b1
Add support for importing column attrs 2019-12-01 07:33:23 -06:00
Travis Turner
76bb3985f0
Merge pull request #53 from travisturner/having-between
Add support for BETWEEN type conditions in the having clause.
2019-11-30 21:58:45 -06:00
Travis
52debbc389 Add support for BETWEEN type conditions in the having clause.
There is a TODO in the `StringWithSubj` method because the value
types really depend on the subject type (for example, `count` uses
uint64, while `sum` uses int64). I'm waiting to address this
until we decide how to handle sums of floats (Decimal), because
that will affect this logic as well.
2019-11-30 11:59:09 -06:00
Travis Turner
0247a9073c
Merge pull request #51 from travisturner/groupby-having
Add "having" support to GroupBy() queries
2019-11-29 23:01:15 -06:00
Travis
24d02c1920 Add "having" support to GroupBy() queries
This PR adds support for a `having` argument in a `GroupBy` query.
Usage looks like this:
```
GroupBy(Rows(a), having=Condition(count > 10))
GroupBy(Rows(a), aggregate=Sum(field=b), having=Condition(sum > 100))
```
2019-11-28 18:40:57 -06:00
Travis Turner
bc0018b67b
Merge pull request #50 from travisturner/datatype-fixes
Add StreamClient and StreamServer interfaces
2019-11-27 17:21:55 -06:00
Travis
3bc0dc28f0 Add StreamClient and StreamServer interfaces
In order to standardize results as streams of RowResponse,
this PR introduces two interfaces `StreamClient` and
`StreamServer`) which mirror the grpc stream interfaces.
Upstream users (sqlmapper, vdsm, etc) can implement
instances of these interfaces to ensure that results can
stream through the entire sytem in an expected way.

This PR also fixes a couple of missing data types.
2019-11-27 16:10:46 -06:00
Matthew Jaffee
210676c927
Merge pull request #39 from molecula/groupby-sum
Groupby sum
2019-11-27 16:04:42 -06:00
Matt Jaffee
114c1a9df8
remove unecessary span from executor tracing 2019-11-27 11:09:06 -06:00
Matt Jaffee
60397d3e8f
add more tracing around groupBy 2019-11-27 11:00:13 -06:00
Matt Jaffee
f2f9ea01dc
get group by aggregates working with PQL validation and grpc streaming 2019-11-27 11:00:13 -06:00
Ben Johnson
ea9914dba0
Add optional GroupBy() 'aggregate' field.
This commit adds an `aggregate` field that allows a `Sum()` call
to be executed for every returned group.
2019-11-27 11:00:13 -06:00
Matt Jaffee
81a4d32cdd
allow IDs to be passed even when keys enabled
This change allows one to query Pilosa fields and indexes directly
with integer row and column ids even when key translation is
enabled. This was previously disallowed during query
translation... I'm not sure why, but it can be quite useful for
debugging and testing to be able to use IDs directly. I have a test in
go-pilosa which uses this functionality.

I also simplified a bunch of the test code which was of the form:
```
else {
    if blah {
    }
}
```

to be:

```
else if blah {
```

which I think is pretty harmless.

I also changed a snapshot log line that has been bugging me to be
Debug level so that it isn't generating lots of useless logs for long
running Pilosa instances.
2019-11-27 08:41:40 -06:00
tgruben
16d5db97a6
Merge pull request #47 from tgruben/inspect-fix
fixed bug in slice container seek
2019-11-26 11:35:51 -06:00
Todd Gruben
ca0247e35b fixed bug in slice container seek 2019-11-26 09:21:52 -06:00
seebs
407c309640
Merge pull request #46 from seebs/distinctfix
Distinctfix
2019-11-22 17:29:24 -06:00
Seebs
d79ecbad86 recompute shards for cross-index calls
It turns out that we need to recompute the set of shards whenever
a query is cross-index. Otherwise we get partial results in unexpected
ways sometimes.
2019-11-22 16:02:47 -06:00
Seebs
36ef82d7ac Zero bitmap storage when reusing it for container-as-bitmap
If you don't do this, it works fine the first time you use a given
storage, but after that you start seeing spurious bits.
2019-11-22 16:02:42 -06:00
Seebs
397d93e84b provide an empty filter when a filter was empty 2019-11-22 16:02:31 -06:00
Seebs
26326ac74c recompute shards for cross-index queries
When computing results on another index, recompute list of
shards for that index.
2019-11-22 16:02:26 -06:00
Seebs
6ad39a376e handle precalls and cross-index queries better
There's two actual changes here, but they're closely related.

First, handle named parameters for precalls, not just indexed parameters.
Second, when doing translation for a call, check whether it specifies an
index, and if it does, use that index instead of the current index for
the translation.
2019-11-22 16:02:20 -06:00
seebs
16c3cfa727
Merge pull request #42 from seebs/sqdeadlock
use the right lock for Enqueue
2019-11-18 21:53:00 -06:00
seebs
01309e4fc5
Merge branch 'enterprise' into sqdeadlock 2019-11-18 20:51:40 -06:00
Cody Soyland
d3c8728821
Merge pull request #19 from codysoyland/ci-updates
Update CI to support Pilosa Enterprise (private dependencies)
2019-11-18 13:52:20 -06:00
Cody Soyland
58182ff563 Pilosa Enterprise private CI 2019-11-18 13:30:32 -06:00
Travis Turner
295adbfe67
Merge pull request #44 from travisturner/cache-threshold-deletes
Fixed ranked cache logic to support reducing cached values below the threshold
2019-11-18 12:10:46 -06:00
Travis
ed82a535e5 Fix ranked cache logic to support reducing cached values below
the threshold.

Prior to this commit, if a cache value was reduced to a value
that fell below the threshold, the operation would be ignored
and the cached value would remain at the old, higher value.

This commit also fixes logic which reduces a cached value within
the framework of uint64 values by subracting the absolute value
of the negative value (since adding a negitive doesn't work with
unsigned integers).
2019-11-18 11:39:50 -06:00
Travis Turner
32f754899e
Merge pull request #43 from travisturner/translate-race-in-test
allow for translate store race in test
2019-11-15 16:33:30 -06:00
Travis
0a69fca657 allow for translate store race in test (by using retry)
In this case, the test is reading from the translateStore
replica before the translateStore replication has had time to
deliver its log to the replica. The only way to truly address
this in the translate store would be to route all key misses
that happen on a read-only replica to the primary translate
store (or somehow know when the primary is done sending to
replicas) for actual verification that the key does not exist.
That's more involved than we want to do here; this PR just
addresses the problem in the test.
2019-11-15 07:57:47 -06:00
Seebs
f204b37760 use atomics instead of locking for stats 2019-11-14 17:40:14 -06:00
Seebs
bab077199c use the right lock for Enqueue
The request for a non-read lock blocks until all existing read
locks exit, meaning that if an Immediate operation is already
going for a fragment, an Enqueue operation will hang forever
holding the fragment's lock, while the Immediate operation has
probably relinquished the fragment's lock to wait for the
queue worker to process it. But the queue worker can't process
it, because the incoming Enqueue still holds the fragment's
lock. Solution: Don't block the Enqueue operation like that.
It shouldn't coexist with things that actually change the sq
channels, like Stop(), but it is fine for it to coexist with
other queue operations.
2019-11-14 17:00:34 -06:00
seebs
9cec40e69d
Merge pull request #41 from seebs/roaringProto
Stop using Roaring in protobuf messages until it's supported elsewhere
2019-11-14 12:45:35 -06:00
Seebs
6fc6cc4350 Stop using Roaring in protobuf messages until it's supported elsewhere
go-pilosa uses protobuf to talk to us but doesn't support the roaring
format. Conveniently, there's a kill switch.
2019-11-14 10:54:40 -06:00
Matthew Jaffee
fa9c911860
Merge pull request #37 from travisturner/rename-bool-label
Rename bool label from changed to result
2019-11-13 14:30:14 -06:00
Travis
e8cd48155a
Rename bool lable from changed to result 2019-11-13 14:12:52 -06:00
Matthew Jaffee
4dfeb89b43
Merge pull request #36 from travisturner/includescolumn-keys
Add column keys support to IncludeColumn
2019-11-13 14:09:24 -06:00
Travis
625125bac6 add column keys support to IncludeColumn 2019-11-13 11:34:21 -06:00
Matthew Jaffee
013ee21621
Merge pull request #28 from molecula/pql-float-values
Pql float values
2019-11-13 11:04:36 -06:00
Matt Jaffee
0e445db7ff
improve error message checking field type in import roaring 2019-11-13 10:07:24 -06:00
Travis
26fc621f09
Support integer predicates in Decimal field range queries. 2019-11-13 10:07:24 -06:00
Matt Jaffee
0ac778516e
check error, make linter happy 2019-11-13 10:07:24 -06:00
Matt Jaffee
14dfc9e31d
fix comment typo for BTWN_LTE_LT 2019-11-13 10:07:24 -06:00
Matt Jaffee
9c8ad727b5
allow floats in PQL queries for decimal fields
had to workaround some cruft in the parser that was trying to only
support a BETWEEN query as LTE, LTE. Now we have operations for all
combinations of LT and LTE.

unrelated - changed the port a test was binding to as it conflicted
with a port I was using locally.
2019-11-13 10:07:24 -06:00
Matt Jaffee
0fec16a141
fix integer bug on less than queries.
this was introduced recently to fix another bug. the comment above it
is correct, just the logic was off-by-one. The test shows the issue
and was confirmed to reproduce it and then fix it.
2019-11-13 10:07:23 -06:00
Travis Turner
7401fd1333
Merge pull request #33 from travisturner/grpc-logger
Fixed gRPC server logger; pass logger through from main
2019-11-13 09:56:04 -06:00
Travis Turner
afd1c004f9
Merge branch 'enterprise' into grpc-logger 2019-11-13 08:31:26 -06:00
Travis Turner
d3432478f9
Merge pull request #34 from travisturner/bool-returns
Fix makeRows in gRPC hander to handle a bool result
2019-11-13 08:24:43 -06:00
Travis
7fd5248d98 makeRows in gRPC hander now handles a bool result
This PR adds bool support to the makeRows function
in the gRPC handler.
2019-11-12 16:13:52 -06:00
seebs
1fea1ea375
Merge pull request #22 from seebs/fsckSnapshotExtension
This is a collection of changes that have been pending forever. It improves the snapshot queue performance, adds some amount of recovery for corrupt filles, reduces memory usage in the rowcache, and adds an extension interface. Yes, they should probably have happened separately over time, things happened.
2019-11-12 14:50:34 -06:00
Seebs
1e0873c70b lock BufferLogger for reads/writes
With the new addition of the holder background scan, it's possible
for an open holder to write log messages at arbitrary times. The
TestHolder_Open/ErrIndexName test checks the contents of the output
buffer, but those contents could be changing if the background task
happens to run at the right time. Use trivial locking around that
so that this shouldn't happen.
2019-11-12 12:15:13 -06:00
Seebs
03f3f424aa don't lint PEG files
I was pretty sure I'd done this, but I guess not: Skip linting
the PEG files.
2019-11-12 12:15:13 -06:00
Seebs
8cc7a176b5 license header fixups
Fix up license headers for the extension code, and add the proto
file to the list of things we don't check license headers for.
2019-11-12 12:15:13 -06:00
Seebs
c5136b14db ensmarten snapshot queue
The snapshot queue needs a bit more subtlety. In some cases,
we really do want to do a snapshot right now -- these shouldn't
have to wait for possibly a hundred or more other snapshots
to complete.

In other cases, we don't really care that much whether we do
a snapshot, and just dropping it is probably fine.

To accommodate this, we distinguish between "urgent" and
"normal" snapshots, and between "Immediate" (does an urgent
snapshot, waits for it) and "Enqueue" (might enqueue a snapshot
but *also might not* if we're already busy). There's a
corresponding "Await" to wait for a snapshot, if one is
pending, but not if one isn't.

We also have a background scan that checks the holder. It will
scan pretty actively when it's finding fragments that need
snapshots (no enqueued snapshot, opN > MaxOpN). It pauses
for a second after every hundred fragments that didn't need
snapshots, and for a minute after each holder scan that didn't
find any. So, if you don't need snapshots, it does basically
nothing, if you do, it'll be moderately aggressive about
submitting tasks -- but it always waits if there's *any*
requested snapshots in the queues.

Updates since initial draft:

Check results from Await more consistently, and in one case, use Immediate
instead and then check its error.

Fix a race condition.  The race condition comes about if:

1. You have a limited enough worker pool that this can happen.
(In testing we tend to have a worker pool of 1.)
2. A fragment is in the normal, non-urgent, queue already.
3. An immediate request comes in for that fragment. This always
happens *with the fragment lock held*.
4. A worker thread grabs that fragment from the queue.
5. The worker thread now waits on the lock. Meanwhile, the
immediate request blocks on sending the fragment to the urgent
queue.
6. The worker can't read the urgent queue, and the immediate
request can't send it, so the immediate request can't proceed.

What's supposed to happen is that the immediate request sends
the thing, and gets into Await(), which sleeps on a condition
variable using the lock, which is to say, releases the lock.

The obvious resolution is to let go of the lock, send the
message, and then reclaim the lock. But then we have the
possibility that the message sent ends up with a timestamp
right after a snapshot that happened *after* the Immediate
request was started. Oops. So we create the request, then let
go of the lock, then send the request, then reclaim the lock
and go into the Await state. All is well.

This is on top of more general use of wait groups, etcetera,
to allow us to ensure that any holder scans terminate *before*
we close the channels they might otherwise be trying to write to.
So, shutdown process is now:

* grab lock on queue (workers and scanners don't use the lock)
* mark snapshotqueue done
* wait for holder scans to complete/exit
* close and nil out all the channels
* release lock

Anything trying to submit to this needs to hold the lock, unless
it's a holder scan, so either it got the lock before we did and already
submitted the thing, or it will get the lock after this and not find
a channel to write to; it's just the holder scanner that has an
ongoing thing that might have started a write to the channel *without*
a lock held, because it's expected that it might have to wait minutes
or hours before the write will complete because it's a background task.

Also, rework the background holder scan to grab lists of
indexes/fields/views/fragments, then scan the grabbed/copied lists,
rather than iterating over maps, allowing us to grab the lock when
we're about to access a thing and let it go when done.

There might be a simpler/cleaner way to do this but opinions on how
safe it is are very mixed, so in the mean time, I'm making the range
behavior not depend at all on there being no writes to the various tiers
of holder/index/view/fragment during the background scans.
2019-11-12 12:15:13 -06:00
Seebs
3b696da34a plugins and precomputed data
So in some cases, when we do a query, the results of one
part of the query are innately shared-across-nodes; for
instance, a hypothetical Distinct query. More generally,
we allow cross-index queries; calls can have "index=foo"
in them.

This patch lets us handle that without duplicating that
query all over. Before we actually start doing the
separate calls, we run the query once from the coordinating
node, then patch the results in, and send relevant subsets
over to each client, etcetera. Also provides slightly
friendlier (and I hope faster) support for converting
bitmaps to/from sets of rows.

We also add an extension interface, and some fancy stuff
to let us define new calls, which use this. They're sort
of tied together because the first extension I wanted to
implement needed precomputed calls. The extension API
lets us create extensions using `pkg/plugin` (with all its
associated limitations, unfortunately), then query them
at load time for functionality.

This also implies some revamping of the argument
validation for PQL, like verifying that functions exist
and knowing things about their argument types.

So basically this is an overly intrusive patch, and would
be better as separate patches, but they're hard to detangle.

add trivial execution-time profiling

What if you could ?profile=true on a query and get some
numbers back? That'd be really cool.

We already have tracing/spans, but right now, those only generate
any data if you have something set up for them to trace to. Add a
fancy wrapper that lets us generate our own tracing data, and dump
it into the request response, if ?profile=true.

add a sample extension, add missing features to extension interface

Implement a naive probabilistic filter extension as an example of
what an extension looks like. In the process, discover multiple
omissions in the bitmap API. Well, I did *say* it was experimental.
2019-11-12 12:14:29 -06:00
Seebs
b25eb8f596 Sources and Generations: tracking mmapped files
This code represents an attempt at providing reliable tracking
of whether any bitmaps still in use have access to a given block
of mmapped data, allowing us to unmap the data when nothing is using
it anymore.

The basic approach is as follows: Each mmap is associated with
a new object, called a "generation". A generation reflects
a particular instance of a given file being mapped. When a
bitmap is built from an mmapped data source, the bitmap is
given a pointer to the generation as its Source. When bitmap
operations combine containers from other bitmaps, they
produce new bitmaps that are tagged with the combined set of
sources.

When we snapshot a file, or for some other reason wish to remap
it, the corresponding bitmap has all its containers updated to
use the new storage, and the bitmap's source is changed. However,
previously-handed-out containers might still have references to the
old storage. Those containers would be in bitmaps with the old
source.

After a bunch of study of trying to reference-count and track
this, I realized: We don't actually need to do that, because we
already have something suitable for determining whether anything
can reach a given object. It's the garbage collector.

So we set a finalizer on the generation object, which handles
unmapping. There's additional sanity-checks here to confirm things
like "we thought this generation should be expiring", and we
track timestamps. We could also have things check whether a
given bitmap's source was marked as obsolete "a while ago", but
that isn't implemented yet.

There's a debug version of this which tracks finalization, creation,
and ending timestamps, and has a call to provide diagnostics for
this. Identical generation IDs get separated out with random
suffixes in this case -- there's sometimes a second or third
instance of the same name due to a holder closing and reopening,
but this basically only happens in testing.

Note that generations are still used even when there's no mmapping,
but unless debugging is turned on, they shouldn't propagate much --
we don't consider a generation to be the source of a bitmap unless
the bitmap actually mapped things from that generation's mmapped
storage, or debugging is on.

There's a couple of other, possibly more subtle, changes and
bug fixes that got caught by the testing on this:
* If a fragment is partially opened and then opening some later
  part fails, we close the earlier parts before returning the
  error so we aren't leaving it partially open.
* Several operations on segments which were requesting that a
  frozen copy of a bitmap be created are now actually *replacing*
  their bitmap with the frozen bitmap, rather than discarding it.
* intersectRunRun, if it decides to create an array or bitmap,
  will yield that container instead of discarding it.

And why all of this? Why, so we can actually implement the thing
where when a fragment has a valid roaring bitmap, but the ops log
is corrupt, we can truncate the corrupt part of the ops log and
reopen it. Which I did.

When the generationdebug build tag is in use, every generation
has a finalizer all the time. When it's not, they only get finalizers
when we expect them to be done -- say, when closing a fragment.
This is because finalizers appear to be possibly-expensive.

There's some logical cleanup to openStorage here, dividing part
of its work into applyStorage and importStorage, which have a common
case for handling "there's no data in this file".
2019-11-12 12:14:29 -06:00
Seebs
6654466033 partially implement truncation of fragments for corrupt ops log
Which is to say don't actually implement it, because openStorage
is too messy right now, but this is the rest of the framework,
and now I'm going to digress into fixing openStorage.
2019-11-12 12:14:29 -06:00
Seebs
538768ea9d handle truncated/damaged .available.shards
The available shards file is just a hint to save us a bit
of time later; we don't need it to run and it can get updated
pretty easily later. If we have problems reading it, we
should just report the error, nuke the file, and continue
without it.
2019-11-12 12:14:29 -06:00
Travis
b8f665db1a pass logger through to the grpc server and handler 2019-11-12 11:08:25 -06:00
seebs
1262cd18e0
Merge pull request #30 from seebs/intfixes
Fix an error that could cause imported values to keep high-order bits from previously imported values, and another that could cause BSI fields to store extra bits they don't need.
2019-11-12 00:02:04 -06:00
seebs
b3adf2d4f6
Merge branch 'enterprise' into q2-11-7 2019-11-11 21:39:45 -06:00
Cody Soyland
5194ede82c
Merge pull request #31 from codysoyland/proto-pkg-name
Change proto package name to "pilosa" to not conflict with molecula
2019-11-11 20:51:52 -06:00
Cody Soyland
69e5e523c1
Merge branch 'enterprise' into proto-pkg-name 2019-11-11 17:26:44 -06:00
Seebs
a804a0dfb1 always treat BSI fields as having at least their depth
If you imported only small values, BSI fields could end up
not bothering to clear higher bits in existing values, which
produced strange behaviors.

We also move the computation of requiredDepth, and the change
to the field, down, combining it with the other checks of the
values for min/max being in range.

Without this, a data set with a ludicrously large value in it
could break a BSI field's depth even though the import would then
reject it.
2019-11-11 16:55:26 -06:00
Travis Turner
1a44f02e3c reset fragment.rowCache after importValue 2019-11-11 16:54:51 -06:00
Travis Turner
10514f7ced
Merge pull request #25 from travisturner/includes-column
Add an IncludesColumn() function to PQL
2019-11-11 12:55:23 -06:00
Cody Soyland
b9335c9f5c Change proto package name to "pilosa" to not conflict with molecula. Upgrade protoc to 3.10.1 2019-11-11 12:24:38 -06:00
Travis
ed37ef5dcf Add an IncludesColumn() function to PQL
Usage:
`IncludesColumn(Intersect(Row(a=1), Row(b=2)), column=10)`

The above query will return a `bool` indicating whether the
intersection of rows a-1 and b-2 contains column 10. Because
a single column is specified, this executes on a single shard
(shard=0 in this example).
2019-11-11 08:17:28 -06:00
Travis Turner
198626e657
Merge pull request #29 from travisturner/linter-fixes
fix golangci-lint complaints
2019-11-11 08:06:35 -06:00
Travis
87b8edc4c5 fix golangci-lint complaints 2019-11-10 17:58:46 -06:00
Travis Turner
2a5d79ad83
Merge pull request #26 from travisturner/proto-licence-exception
add proto/pilosa.pb.go to license.exceptions list
2019-11-10 16:31:00 -06:00
Travis
3129b1c841 add proto/pilosa.pb.go to license.exceptions list 2019-11-10 11:33:29 -06:00
Travis Turner
6632821617
Merge pull request #24 from travisturner/cache-size-none
fix cacheSize when cacheType is none (and cacheSize is 0)
2019-11-08 22:30:17 -06:00
Travis
a842dd521c fix cacheSize when cacheType is none (and cacheSize is 0)
There was an edge case where setting cacheType to none
wouldn't zero out its cacheSize. This fixes that edge case.
2019-11-08 15:58:49 -06:00
Matthew Jaffee
d6f2196bf1
Merge pull request #16 from pilosa/tls-grpc
use TLS settings when setting up GRPC server or client
2019-10-30 19:35:56 -05:00
Matt Jaffee
6f21887259
use TLS settings when setting up GRPC server or client 2019-10-30 17:38:48 -05:00
Matthew Jaffee
468cf98811
Merge pull request #15 from pilosa/decimal-to-grpc
add decimal field support to Inspect
2019-10-30 13:33:55 -05:00
Matt Jaffee
a414cada4f
add decimal field support to Inspect
I tested this manually with BloomRPC and curl, but need to write real
tests. Also need to get floats for decimal fields coming out of QueryPQL.
2019-10-30 11:13:48 -05:00
Matthew Jaffee
8f64a4f585
Merge pull request #11 from pilosa/decimal-support
support for decimal fields
2019-10-29 16:49:20 -05:00
Matt Jaffee
a9a4d244ef
fix a bug in the "less than" logic 2019-10-29 16:36:15 -05:00
Matt Jaffee
5dcabfcc7f
support for decimal fields
This commit adds a Decimal field type which is implemented mostly with
the Int field. It adds an optional "Scale" value to the Int field
which means that the values stored in that field are actually meant to
be divided by 10^Scale before being interpreted.

In order to make use of this functionality, we extend the importValue
request to allow a slice of floats rather than just int64. If the
slice of floats is present, each float in the slice is multiplied by
10^Scale and converted to an int64 before being imported. If a slice
of int64 is imported to a Decimal field, it is treated normally, and
scale is ignored. This allows the conversion to be handled at the
client side if desired.

Currently there are Field level methods for querying Float values out
of a decimal field, but no support in PQL or the executor for getting
float values. Going to wait until I can use the generic result type
before doing that, so for now, any values queried will be the scaled
integer values.

needed to add client support for importing float values, and did this
by adding a more general and simplified client method for value
imports.

rewrote api.ImportValue to use the new method which should be more
performant and efficient.

allow floats to be "pilosa import"ed into decimal fields
2019-10-29 16:36:14 -05:00
seebs
4ef7f7e26b
Merge pull request #12 from seebs/profile
Profiling and a couple of minor fixes
2019-10-29 15:24:51 -05:00
Seebs
616ed39771 Skip longest tests when running -short
The cluster timeout/down tests are way more than half the total
time for "go test", and are very unlikely to be of interest in regular
usage, although they matter for CI. Skip them when doing short
tests.
2019-10-29 15:24:09 -05:00
Seebs
7a381f7eaf allow years other than 2017 in licenses
Also clean up the license hash checking a bit. We trim vendor early
in find so we don't have to walk the whole vendor tree only to grep
the files out, and we don't check the license hashes of the exceptions,
and the exceptions are now a plain text file of non-regex strings
we match exactly. Also the license hash code is only written once.

This will help us a lot if development on Pilosa continues through
2018 or later.
2019-10-29 15:24:09 -05:00
Seebs
820c5ce220 add trivial execution-time profiling
What if you could ?profile=true on a query and get some
numbers back? That'd be really cool.

We already have tracing/spans, but right now, those only generate
any data if you have something set up for them to trace to. Add a
fancy wrapper that lets us generate our own tracing data, and dump
it into the request response, if ?profile=true.

We track wall-clock execution time, plus possible arbitrary K/V
pairs. Memory stats are not included, because obtaining them is
surprisingly expensive.
2019-10-29 15:23:37 -05:00
Travis Turner
9a2f5b3b4c
Merge pull request #10 from travisturner/grpc
initial gRPC server implementation
2019-10-29 14:39:16 -05:00
Travis
a7bb90fcd0 initial gRPC server implementation
add makeRows() tests
register the gRPC server
use api.Index() instead of api.Schema()

support most field types in Inspect() query

currently, there's no support for `time` fields.
those will be dependent upon the output format
and the ability to materialize the timestamp from
the time views.

this commit also changes the response type of the
`Inspect()` query to be a tabular `RowResponse`.
2019-10-29 14:27:07 -05:00
Travis
77a81eb2e1 fix bug preventing a Rows() query on a bool field 2019-10-29 14:27:07 -05:00
274 changed files with 91238 additions and 12276 deletions

View file

@ -1,106 +1,118 @@
version: 2 version: 2.1
defaults: &defaults
working_directory: /go/src/github.com/pilosa/pilosa executors:
docker: golang:
- image: circleci/golang:1.13 parameters:
environment: version:
GO111MODULE: "on" type: string
fast-checkout: &fast-checkout default: "1.14"
attach_workspace: resource_class:
at: . type: string
jobs: default: medium
setup: docker:
<<: *defaults - image: circleci/golang:<< parameters.version >>
resource_class: << parameters.resource_class >>
working_directory: /go/src/github.com/pilosa/pilosa
commands:
add-github-auth:
steps:
- run: git config --global url."https://moleculacorp:${GITHUB_PERSONAL_ACCESS_TOKEN}@github.com".insteadOf "https://github.com"
restore-mod-cache:
steps: steps:
- checkout
- restore_cache: - restore_cache:
keys: key: mod-cache-{{ checksum "go.sum" }}
- mod-cache-{{ checksum "go.sum" }} save-mod-cache:
- run: "go mod download" steps:
- save_cache: - save_cache:
key: mod-cache-{{ checksum "go.sum" }} key: mod-cache-{{ checksum "go.sum" }}
paths: paths:
- /go/pkg/mod/ - /go/pkg/mod/
- persist_to_workspace: checkout-plus:
root: .
paths: "*"
check-license-headers:
<<: *defaults
steps: steps:
- *fast-checkout - add-github-auth
- checkout
- restore-mod-cache
jobs:
setup:
executor:
name: golang
steps:
- checkout-plus
- run: go mod download
- save-mod-cache
check-license-headers:
executor:
name: golang
steps:
- checkout-plus
- run: make check-license-headers - run: make check-license-headers
linter: linter:
<<: *defaults executor:
name: golang
steps: steps:
- *fast-checkout - checkout-plus
- run: curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s v1.20.0 - run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sudo sh -s -- -b /usr/local/bin v1.23.8
- run: sudo cp bin/golangci-lint /usr/local/bin/
- run: make golangci-lint - run: make golangci-lint
test-build-arm: go-mod-tidy:
<<: *defaults executor:
name: golang
steps: steps:
- *fast-checkout - checkout-plus
- run: go mod tidy
- run: git diff --exit-code -- go.mod go.sum
test-build-arm:
executor:
name: golang
steps:
- checkout-plus
- run: make build GOOS=linux GOARCH=arm GOARM=5 - run: make build GOOS=linux GOARCH=arm GOARM=5
- run: make build GOOS=linux GOARCH=arm GOARM=6 - run: make build GOOS=linux GOARCH=arm GOARM=6
- run: make build GOOS=linux GOARCH=arm GOARM=7 - run: make build GOOS=linux GOARCH=arm GOARM=7
- run: make build GOOS=linux GOARCH=arm64 - run: make build GOOS=linux GOARCH=arm64
test-golang-1.13: &base-test test:
<<: *defaults parameters:
resource_class:
type: string
default: medium
golang_version:
type: string
default: "1.14"
shard_width:
type: string
default: "20"
test_make_target:
type: string
default: "test"
test_flags:
type: string
default: ""
goarch:
type: string
default: amd64
executor:
name: golang
version: << parameters.golang_version >>
resource_class: << parameters.resource_class >>
steps: steps:
- *fast-checkout - checkout-plus
- run: sudo apt-get install lsof
- run: make test
test-golang-1.13-shard22:
<<: *defaults
steps:
- *fast-checkout
- run: sudo apt-get install lsof
- run: make test SHARD_WIDTH=22
test-golang-1.13-race:
<<: *defaults
steps:
- *fast-checkout
- run: sudo apt-get install lsof - run: sudo apt-get install lsof
- run: - run:
command: make test TESTFLAGS="-race -v -timeout=30m" command: make << parameters.test_make_target >> SHARD_WIDTH=<< parameters.shard_width >> GOARCH=<< parameters.goarch >>
no_output_timeout: 30m no_output_timeout: 30m
test-golang-1.13-386:
<<: *base-test
environment:
GO111MODULE: "on"
GOARCH: 386
test-golang-1.13-enterprise:
<<: *defaults
steps:
- *fast-checkout
- run: sudo apt-get install lsof
- run: make test ENTERPRISE=1
test-golang-1.12:
<<: *defaults
docker:
- image: circleci/golang:1.12
steps:
- *fast-checkout
- run: sudo apt-get install lsof
- run: make test
test-golang-1.11:
<<: *defaults
docker:
- image: circleci/golang:1.11
steps:
- *fast-checkout
- run: sudo apt-get install lsof
- run: make test
cluster-tests: cluster-tests:
<<: *defaults executor:
name: golang
steps: steps:
- *fast-checkout - checkout-plus
- setup_remote_docker - setup_remote_docker
- run: make clustertests-build - run: make clustertests-build
prerelease: prerelease:
<<: *base-test executor:
name: golang
steps: steps:
- *fast-checkout - checkout-plus
- run: make prerelease - run: make prerelease
- store_artifacts: - store_artifacts:
path: build path: build
@ -108,9 +120,12 @@ jobs:
root: . root: .
paths: build paths: build
release: release:
<<: *defaults executor:
name: golang
steps: steps:
- *fast-checkout - checkout-plus
- attach_workspace:
at: .
- run: make release - run: make release
- store_artifacts: - store_artifacts:
path: build path: build
@ -121,50 +136,72 @@ jobs:
docker: docker:
- image: circleci/python:2.7-jessie - image: circleci/python:2.7-jessie
steps: steps:
- run: '[[ -v CIRCLE_PR_NUMBER ]] && circleci step halt || true' # Skip job if this is a PR - checkout-plus
- *fast-checkout - attach_workspace:
at: .
- run: sudo pip install awscli - run: sudo pip install awscli
- run: make prerelease-upload - run: make prerelease-upload
dockerhub-upload: dockerhub-upload-unstable:
<<: *defaults executor:
name: golang
steps: steps:
- run: '[[ -v CIRCLE_PR_NUMBER ]] && circleci step halt || true' # Skip job if this is a PR - checkout-plus
- *fast-checkout
- setup_remote_docker - setup_remote_docker
- run: make docker - run: make docker
- run: docker tag pilosa:$(git describe --tags) pilosa/pilosa:master
- run: docker login -u $DOCKER_USER -p $DOCKER_PASS - run: docker login -u $DOCKER_USER -p $DOCKER_PASS
- run: docker push pilosa/pilosa:master - run: make docker-tag-push DOCKER_TARGET=moleculacorp/pilosa:<< pipeline.git.branch >>
dockerhub-upload-stable:
executor:
name: golang
steps:
- checkout-plus
- setup_remote_docker
- run: make docker
- run: docker login -u $DOCKER_USER -p $DOCKER_PASS
- run: make docker-tag-push DOCKER_TARGET=moleculacorp/pilosa:<< pipeline.git.tag >>
- run: make docker-tag-push DOCKER_TARGET=moleculacorp/pilosa:latest
workflows: workflows:
version: 2 build:
test:
jobs: jobs:
- setup - setup:
context: molecula
filters:
tags:
only: /^v.*/
- linter: - linter:
requires: requires:
- setup - setup
- check-license-headers: - check-license-headers:
requires: requires:
- setup - setup
- go-mod-tidy:
requires:
- setup
- test-build-arm: - test-build-arm:
requires: requires:
- setup - setup
- test-golang-1.13-enterprise: - test:
name: test-golang-<< matrix.golang_version >>
matrix:
parameters:
golang_version: ["1.14", "1.13"]
resource_class: xlarge
requires: requires:
- setup - setup
- test-golang-1.13-race: filters:
tags:
only: /^v.*/
- test:
name: test-race
test_make_target: test-race
resource_class: xlarge
requires: requires:
- setup - setup
- test-golang-1.13-386: - test:
requires: name: test-shardwidth-22
- setup shard_width: "22"
- test-golang-1.13: resource_class: large
requires:
- setup
- test-golang-1.12:
requires:
- setup
- test-golang-1.11:
requires: requires:
- setup - setup
- cluster-tests: - cluster-tests:
@ -174,22 +211,20 @@ workflows:
requires: requires:
- linter - linter
- check-license-headers - check-license-headers
- test-golang-1.13 - test-golang-1.14
- release: - dockerhub-upload-unstable:
context: molecula
requires: requires:
- linter - setup
- check-license-headers filters:
- test-golang-1.13 branches:
only: master
- dockerhub-upload-stable:
context: molecula
requires:
- setup
filters: filters:
tags: tags:
only: /^v.*/ only: /^v.*/
branches: branches:
ignore: /.*/ ignore: /.*/
- prerelease-upload:
requires:
- prerelease
- dockerhub-upload:
requires:
- linter
- check-license-headers
- test-golang-1.13

2
.gitignore vendored
View file

@ -4,3 +4,5 @@ vendor
.protoc-gen-gofast .protoc-gen-gofast
.DS_Store .DS_Store
build build
*~
lattice

3
.golangci.yml Normal file
View file

@ -0,0 +1,3 @@
run:
skip-files:
- pql/pql.peg.go

View file

@ -1,14 +1,19 @@
FROM golang:1.13.0 as builder FROM golang:1.13.0 as builder
ARG BUILD_FLAGS
ARG MAKE_FLAGS
COPY . pilosa COPY . pilosa
RUN cd pilosa && CGO_ENABLED=0 make install FLAGS="-a" RUN cd pilosa && make install FLAGS="-a -mod=vendor ${BUILD_FLAGS}" ${MAKE_FLAGS}
FROM alpine:3.9.4 FROM ubuntu:20.10
LABEL maintainer "dev@pilosa.com" LABEL maintainer "dev@pilosa.com"
RUN apk add --no-cache curl jq RUN apt-get update
## debug image: RUN apt-get install -y curl htop vim golang tree jq netcat
RUN apt-get install -y curl jq
COPY --from=builder /go/bin/pilosa /pilosa COPY --from=builder /go/bin/pilosa /pilosa
@ -18,5 +23,9 @@ COPY NOTICE /NOTICE
EXPOSE 10101 EXPOSE 10101
VOLUME /data VOLUME /data
ENV PILOSA_DATA_DIR /data
ENV PILOSA_BIND 0.0.0.0:10101
ENV PILOSA_BIND_GRPC 0.0.0.0:20101
ENTRYPOINT ["/pilosa"] ENTRYPOINT ["/pilosa"]
CMD ["server", "--data-dir", "/data", "--bind", "http://0.0.0.0:10101"] CMD ["server"]

View file

@ -1,17 +1,14 @@
# This Dockerfile is used for cluster testing - it produces a much larger image # This Dockerfile is used for cluster testing - it produces a much larger image
# and includes all of Go as well as some utilities. # and includes all of Go as well as some utilities.
FROM golang:1.11 FROM golang:1.13
LABEL maintainer "dev@pilosa.com" LABEL maintainer "dev@pilosa.com"
COPY . /go/src/github.com/pilosa/pilosa/ COPY . /go/src/github.com/pilosa/pilosa/
RUN cd /go/src/github.com/pilosa/pilosa \ RUN cd /go/src/github.com/pilosa/pilosa \
&& GO111MODULE=on make vendor && make install FLAGS="-a -mod=vendor"
RUN cd /go/src/github.com/pilosa/pilosa \
&& CGO_ENABLED=0 make install FLAGS="-a"
# download pumba for fault injection # download pumba for fault injection
ADD https://github.com/alexei-led/pumba/releases/download/0.6.0/pumba_linux_amd64 /pumba ADD https://github.com/alexei-led/pumba/releases/download/0.6.0/pumba_linux_amd64 /pumba

257
Makefile
View file

@ -1,23 +1,30 @@
.PHONY: build check-clean clean cover cover-viz default docker docker-build docker-test generate generate-protoc generate-pql gometalinter install install-build-deps install-golangci-lint install-gometalinter install-protoc install-protoc-gen-gofast install-peg prerelease prerelease-upload release release-build test .PHONY: build check-clean clean build-lattice cover cover-viz default docker docker-build docker-test docker-tag-push generate generate-protoc generate-pql generate-statik gometalinter install install-build-deps install-golangci-lint install-gometalinter install-protoc install-protoc-gen-gofast install-peg install-statik prerelease prerelease-upload release release-build test testv testv-race testvsub testvsub-race
CLONE_URL=github.com/pilosa/pilosa CLONE_URL=github.com/pilosa/pilosa
VERSION := $(shell git describe --tags 2> /dev/null || echo unknown) VERSION := $(shell git describe --tags 2> /dev/null || echo unknown)
VERSION_ID = $(if $(ENTERPRISE_ENABLED),enterprise-)$(VERSION)-$(GOOS)-$(GOARCH) LATTICE_COMMIT := $(shell git -C lattice rev-parse --short HEAD 2>/dev/null)
VARIANT = Molecula
VERSION_ID = $(VERSION)-$(GOOS)-$(GOARCH)
BRANCH := $(if $(TRAVIS_BRANCH),$(TRAVIS_BRANCH),$(if $(CIRCLE_BRANCH),$(CIRCLE_BRANCH),$(shell git rev-parse --abbrev-ref HEAD))) BRANCH := $(if $(TRAVIS_BRANCH),$(TRAVIS_BRANCH),$(if $(CIRCLE_BRANCH),$(CIRCLE_BRANCH),$(shell git rev-parse --abbrev-ref HEAD)))
BRANCH_ID := $(BRANCH)-$(GOOS)-$(GOARCH) BRANCH_ID := $(BRANCH)-$(GOOS)-$(GOARCH)
BUILD_TIME := $(shell date -u +%FT%T%z) BUILD_TIME := $(shell date -u +%FT%T%z)
SHARD_WIDTH = 20 SHARD_WIDTH = 20
LDFLAGS="-X github.com/pilosa/pilosa/v2.Version=$(VERSION) -X github.com/pilosa/pilosa/v2.BuildTime=$(BUILD_TIME) -X github.com/pilosa/pilosa/v2.Enterprise=$(if $(ENTERPRISE_ENABLED),1)" COMMIT := $(shell git describe --exact-match >/dev/null 2>&1 || git rev-parse --short HEAD)
LDFLAGS="-X github.com/pilosa/pilosa/v2.Version=$(VERSION) -X github.com/pilosa/pilosa/v2.BuildTime=$(BUILD_TIME) -X github.com/pilosa/pilosa/v2.Variant=$(VARIANT) -X github.com/pilosa/pilosa/v2.Commit=$(COMMIT) -X github.com/pilosa/pilosa/v2.LatticeCommit=$(LATTICE_COMMIT)"
GO_VERSION=latest GO_VERSION=latest
ENTERPRISE ?= 0
ENTERPRISE_ENABLED = $(subst 0,,$(ENTERPRISE))
RELEASE ?= 0 RELEASE ?= 0
RELEASE_ENABLED = $(subst 0,,$(RELEASE)) RELEASE_ENABLED = $(subst 0,,$(RELEASE))
BUILD_TAGS += $(if $(ENTERPRISE_ENABLED),enterprise) NOCHECKPTR=$(shell go version | grep -q 'go1.1[4,5,6,7]' && echo \"-gcflags=all=-d=checkptr=0\" )
BUILD_TAGS += $(if $(RELEASE_ENABLED),release) BUILD_TAGS += $(if $(RELEASE_ENABLED),release)
BUILD_TAGS += shardwidth$(SHARD_WIDTH) BUILD_TAGS += shardwidth$(SHARD_WIDTH)
LICENSE_HASH=$(shell head -13 pilosa.go | shasum | cut -f 1 -d " ") TEST_TAGS = roaringparanoia
define LICENSE_HASH_CODE
head -13 $1 | sed -e 's/Copyright 20[0-9][0-9]/Copyright 20XX/g' | shasum | cut -f 1 -d " "
endef
LICENSE_HASH=$(shell $(call LICENSE_HASH_CODE, pilosa.go))
export GO111MODULE=on export GO111MODULE=on
export GOPRIVATE=github.com/molecula
# Run tests and compile Pilosa # Run tests and compile Pilosa
default: test build default: test build
@ -32,7 +39,41 @@ vendor: go.mod
# Run test suite # Run test suite
test: test:
go test ./... -tags='$(BUILD_TAGS)' $(TESTFLAGS) go test ./... -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) -v
# Run test suite with race flag
test-race:
go test ./... -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) -race $(NOCHECKPTR) -timeout 60m -v
testv: topt testvsub
testv-race: topt-race testvsub-race
# testvsub: run go test -v in sub-directories in "local mode" with incremental output,
# avoiding go -test ./... "package list mode" which doesn't give output
# until the test run finishes. Package list mode makes it hard to
# find which test is hung/deadlocked.
#
testvsub:
set -e; for i in ctl http pg pql rbf roaring server sql txkey; do \
echo; echo "___ testing subpkg $$i"; \
cd $$i; pwd; \
go test -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) -v -timeout 60m || break; \
echo; echo "999 done testing subpkg $$i"; \
cd ..; \
done
testvsub-race:
set -e; for i in ctl http pg pql rbf roaring server sql txkey; do \
echo; echo "___ testing subpkg $$i -race"; \
cd $$i; pwd; \
go test -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) -v -race -timeout 60m || break; \
echo; echo "999 done testing subpkg $$i -race"; \
cd ..; \
done
tour:
./tournament.sh
bench: bench:
go test ./... -bench=. -run=NoneZ -timeout=127m $(TESTFLAGS) go test ./... -bench=. -run=NoneZ -timeout=127m $(TESTFLAGS)
@ -53,8 +94,7 @@ build:
# Create a single release build under the build directory # Create a single release build under the build directory
release-build: release-build:
$(MAKE) $(if $(DOCKER_BUILD),docker-)build FLAGS="-o build/pilosa-$(VERSION_ID)/pilosa" RELEASE=1 $(MAKE) $(if $(DOCKER_BUILD),docker-)build FLAGS="-o build/pilosa-$(VERSION_ID)/pilosa" RELEASE=1
cp NOTICE README.md build/pilosa-$(VERSION_ID) cp NOTICE README.md LICENSE build/pilosa-$(VERSION_ID)
$(if $(ENTERPRISE_ENABLED),cp enterprise/COPYING build/pilosa-$(VERSION_ID),cp LICENSE build/pilosa-$(VERSION_ID))
tar -cvz -C build -f build/pilosa-$(VERSION_ID).tar.gz pilosa-$(VERSION_ID)/ tar -cvz -C build -f build/pilosa-$(VERSION_ID).tar.gz pilosa-$(VERSION_ID)/
@echo Created release build: build/pilosa-$(VERSION_ID).tar.gz @echo Created release build: build/pilosa-$(VERSION_ID).tar.gz
@ -65,14 +105,15 @@ ifndef SKIP_CHECK_CLEAN
endif endif
# Create release build tarballs for all supported platforms. Linux compilation happens under Docker. # Create release build tarballs for all supported platforms. Linux compilation happens under Docker.
release: check-clean release: check-clean generate-statik
$(MAKE) release-build GOOS=darwin GOARCH=amd64 $(MAKE) release-build GOOS=darwin GOARCH=amd64
$(MAKE) release-build GOOS=darwin GOARCH=amd64 ENTERPRISE=1
$(MAKE) release-build GOOS=linux GOARCH=amd64 $(MAKE) release-build GOOS=linux GOARCH=amd64
$(MAKE) release-build GOOS=linux GOARCH=amd64 ENTERPRISE=1
$(MAKE) release-build GOOS=linux GOARCH=386
$(MAKE) release-build GOOS=linux GOARCH=386 ENTERPRISE=1
# Create release build tarballs for all supported platforms. Same as `release`, but without embedded Lattice UI.
release-sans-ui: check-clean
rm -f statik/statik.go
$(MAKE) release-build GOOS=darwin GOARCH=amd64
$(MAKE) release-build GOOS=linux GOARCH=amd64
# try (e.g.) internal/clustertests/docker-compose-replication2.yml # try (e.g.) internal/clustertests/docker-compose-replication2.yml
DOCKER_COMPOSE=internal/clustertests/docker-compose.yml DOCKER_COMPOSE=internal/clustertests/docker-compose.yml
@ -81,15 +122,15 @@ DOCKER_COMPOSE=internal/clustertests/docker-compose.yml
# running. This will catch changes to internal/clustertests/*.go, but if you # running. This will catch changes to internal/clustertests/*.go, but if you
# make changes to Pilosa, you'll want to run clustertests-build to rebuild the # make changes to Pilosa, you'll want to run clustertests-build to rebuild the
# pilosa image. # pilosa image.
clustertests: clustertests: vendor
docker-compose -f $(DOCKER_COMPOSE) down docker-compose -f $(DOCKER_COMPOSE) down
docker-compose -f $(DOCKER_COMPOSE) build client1 docker-compose -f $(DOCKER_COMPOSE) build client1
docker-compose -f $(DOCKER_COMPOSE) up --exit-code-from=client1 docker-compose -f $(DOCKER_COMPOSE) up --exit-code-from=client1
# Like clustertests, but rebuilds all images. # Like clustertests, but rebuilds all images.
clustertests-build: clustertests-build: vendor
docker-compose -f $(DOCKER_COMPOSE) down docker-compose -f $(DOCKER_COMPOSE) down -v
docker-compose -f $(DOCKER_COMPOSE) up --exit-code-from=client1 --build docker-compose -f $(DOCKER_COMPOSE) up --exit-code-from=client1 --build
# Create prerelease builds # Create prerelease builds
@ -104,10 +145,20 @@ prerelease-upload:
install: install:
go install -tags='$(BUILD_TAGS)' -ldflags $(LDFLAGS) $(FLAGS) ./cmd/pilosa go install -tags='$(BUILD_TAGS)' -ldflags $(LDFLAGS) $(FLAGS) ./cmd/pilosa
lattice:
git clone git@github.com:molecula/lattice.git
build-lattice: lattice require-yarn
cd lattice && git pull && yarn install && yarn build
# `go generate` protocol buffers # `go generate` protocol buffers
generate-protoc: require-protoc require-protoc-gen-gofast generate-protoc: require-protoc require-protoc-gen-gofast
go generate github.com/pilosa/pilosa/v2/internal go generate github.com/pilosa/pilosa/v2/internal
# `go generate` statik assets (lattice UI)
generate-statik: build-lattice require-statik
go generate github.com/pilosa/pilosa/v2/statik
# `go generate` stringers # `go generate` stringers
generate-stringer: generate-stringer:
go generate github.com/pilosa/pilosa/v2 go generate github.com/pilosa/pilosa/v2
@ -115,25 +166,175 @@ generate-stringer:
generate-pql: require-peg generate-pql: require-peg
cd pql && peg -inline pql.peg && cd .. cd pql && peg -inline pql.peg && cd ..
# dunno if protoc-gen-gofast is actually needed here
generate-proto-grpc: require-protoc require-protoc-gen-gofast
protoc -I proto proto/pilosa.proto --go_out=plugins=grpc:proto
# `go generate` all needed packages # `go generate` all needed packages
generate: generate-protoc generate-stringer generate-pql generate: generate-protoc generate-statik generate-stringer generate-pql
# Create Docker image from Dockerfile # Create Docker image from Dockerfile
docker: docker: vendor
docker build -t "pilosa:$(VERSION)" . docker build --build-arg BUILD_FLAGS="${FLAGS}" -t "pilosa:$(VERSION)" .
@echo Created docker image: pilosa:$(VERSION) @echo Created docker image: pilosa:$(VERSION)
# Tag and push a Docker image
docker-tag-push: vendor
docker tag "pilosa:$(VERSION)" $(DOCKER_TARGET)
docker push $(DOCKER_TARGET)
@echo Pushed docker image: $(DOCKER_TARGET)
# Compile Pilosa inside Docker container # Compile Pilosa inside Docker container
docker-build: docker-build:
docker run --rm -v $(PWD):/go/src/$(CLONE_URL) -w /go/src/$(CLONE_URL) -e GOOS=$(GOOS) -e GOARCH=$(GOARCH) golang:$(GO_VERSION) go build -tags='$(BUILD_TAGS)' -ldflags $(LDFLAGS) $(FLAGS) $(CLONE_URL)/cmd/pilosa docker run --rm -v $(PWD):/go/src/$(CLONE_URL) -w /go/src/$(CLONE_URL) -e GOOS=$(GOOS) -e GOARCH=$(GOARCH) golang:$(GO_VERSION) go build -tags='$(BUILD_TAGS)' -ldflags $(LDFLAGS) $(FLAGS) $(CLONE_URL)/cmd/pilosa
# Install diagnostic pilosa-keydump tool. Allows viewing the keys in a transaction-engine directory.
pilosa-keydump:
go install -tags='$(BUILD_TAGS)' -ldflags $(LDFLAGS) $(FLAGS) ./cmd/pilosa-keydump
# Install diagnostic pilosa-chk tool for string translations and fragment checksums.
pilosa-chk:
go install -tags='$(BUILD_TAGS)' -ldflags $(LDFLAGS) $(FLAGS) ./cmd/pilosa-chk
# Run Pilosa tests inside Docker container # Run Pilosa tests inside Docker container
docker-test: docker-test:
docker run --rm -v $(PWD):/go/src/$(CLONE_URL) -w /go/src/$(CLONE_URL) golang:$(GO_VERSION) go test -tags='$(BUILD_TAGS)' $(TESTFLAGS) ./... docker run --rm -v $(PWD):/go/src/$(CLONE_URL) -w /go/src/$(CLONE_URL) golang:$(GO_VERSION) go test -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) ./...
# run top tests, not subdirs. print summary red/green after.
# The \-\-\- FAIL avoids counting the extra two FAIL strings at then bottom of log.topt.
topt:
mv log.topt.roar log.topt.roar.prev || true
go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.roar
@echo " log.topt.roar green: \c"; cat log.topt.roar | grep PASS |wc -l
@echo " log.topt.roar red: \c"; cat log.topt.roar | grep '\-\-\- FAIL' |wc -l
topt-badger:
mv log.topt.badger log.topt.badger.prev || true
PILOSA_TXSRC=badger go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.badger
@echo " log.topt.badger green: \c"; cat log.topt.badger | grep PASS |wc -l
@echo " log.topt.badger red: \c"; cat log.topt.badger | grep '\-\-\- FAIL' |wc -l
topt-badger-race:
mv log.topt.badger-race log.topt.badger-race.prev || true
PILOSA_TXSRC=badger go test -race -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.badger-race
@echo " log.topt.badger-race green: \c"; cat log.topt.badger-race | grep PASS |wc -l
@echo " log.topt.badger-race red: \c"; cat log.topt.badger-race | grep '\-\-\- FAIL' |wc -l
topt-rbf:
mv log.topt.rbf log.topt.rbf.prev || true
PILOSA_TXSRC=rbf go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.rbf
@echo " log.topt.rbf green: \c"; cat log.topt.rbf | grep PASS |wc -l
@echo " log.topt.rbf red: \c"; cat log.topt.rbf | grep '\-\-\- FAIL' |wc -l
topt-rbf-race:
mv log.topt.rbf-race log.topt.rbf-race.prev || true
PILOSA_TXSRC=rbf go test -race -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) -timeout 120m 2>&1 | tee log.topt.rbf-race
@echo " log.topt.rbf-race green: \c"; cat log.topt.rbf-race | grep PASS |wc -l
@echo " log.topt.rbf-race red: \c"; cat log.topt.rbf-race | grep '\-\-\- FAIL' |wc -l
topt-lmdb:
mv log.topt.lmdb log.topt.lmdb.prev || true
PILOSA_TXSRC=lmdb go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.lmdb
@echo " log.topt.lmdb green: \c"; cat log.topt.lmdb | grep PASS |wc -l
@echo " log.topt.lmdb red: \c"; cat log.topt.lmdb | grep '\-\-\- FAIL' |wc -l
topt-lmdb-race:
mv log.topt.lmdb log.topt.lmdb.prev || true
PILOSA_TXSRC=lmdb go test -race -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.lmdb-race
@echo " log.topt.lmdb-race green: \c"; cat log.topt.lmdb-race | grep PASS |wc -l
@echo " log.topt.lmdb-race red: \c"; cat log.topt.lmdb-race | grep '\-\-\- FAIL' |wc -l
topt-race:
mv log.topt.race log.topt.race.prev || true
go test -race -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.race
@echo " log.topt.race green: \c"; cat log.topt.race | grep PASS |wc -l
@echo " log.topt.race red: \c"; cat log.topt.race | grep '\-\-\- FAIL' |wc -l
# blue-green checks. These run two different storage engines (rbf, roaring, or badger)
# and compare each transaction for a result.
bg-rr: # shorthand for bluegreen test with A:badger; B:roaring
mv log.bg-rr log.bg-rr.prev || true
PILOSA_TXSRC=badger_roaring go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.bg-rr
@echo " log.bg-rr green: \c"; cat log.bg-rr | grep PASS |wc -l
@echo " log.bg-rr red: \c"; cat log.bg-rr | grep '\-\-\- FAIL' |wc -l
rr-bg: # bluegreen with A:roaring; B:badger (B's values are returned).
mv log.bg.roar_bg log.bg.roar_bg.prev || true
PILOSA_TXSRC=roaring_badger go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rr-bg
@echo " log.rr-bg green: \c"; cat log.rr-bg | grep PASS |wc -l
@echo " log.rr-bg red: \c"; cat log.rr-bg | grep '\-\-\- FAIL' |wc -l
rbf-rr:
mv log.rbf-rr log.rbf-rr.prev || true
PILOSA_TXSRC=rbf_roaring go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rbf-rr
@echo " log.rbf-rr green: \c"; cat log.rbf-rr | grep PASS |wc -l
@echo " log.rbf-rr red: \c"; cat log.rbf-rr | grep '\-\-\- FAIL' |wc -l
rr-rbf:
mv log.rr-rbf log.rr-rbf.prev || true
PILOSA_TXSRC=roaring_rbf go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rr-rbf
@echo " log.rr-rbf green: \c"; cat log.rr-rbf | grep PASS |wc -l
@echo " log.rr-rbf red: \c"; cat log.rr-rbf | grep '\-\-\- FAIL' |wc -l
rbf-bg:
mv log.rbf-bg log.rbf-bg.prev || true
PILOSA_TXSRC=rbf_badger go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rbf-bg
@echo " log.rbf-bg green: \c"; cat log.rbf-bg | grep PASS |wc -l
@echo " log.rbf-bg red: \c"; cat log.rbf-bg | grep '\-\-\- FAIL' |wc -l
bg-rbf:
mv log.bg-rbf log.bg-rbf.prev || true
PILOSA_TXSRC=badger_rbf go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.bg-rbf
@echo " log.bg-rbf green: \c"; cat log.bg-rbf | grep PASS |wc -l
@echo " log.bg-rbf red: \c"; cat log.bg-rbf | grep '\-\-\- FAIL' |wc -l
rbf-lm:
mv log.rbf-lm log.rbf-lm.prev || true
PILOSA_TXSRC=rbf_lmdb go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rbf-lm
@echo " log.rbf-lm green: \c"; cat log.rbf-lm | grep PASS |wc -l
@echo " log.rbf-lm red: \c"; cat log.rbf-lm | grep '\-\-\- FAIL' |wc -l
lm-rbf:
mv log.lm-rbf log.lm-rbf.prev || true
PILOSA_TXSRC=lmdb_rbf go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.lm-rbf
@echo " log.lm-rbf green: \c"; cat log.lm-rbf | grep PASS |wc -l
@echo " log.lm-rbf red: \c"; cat log.lm-rbf | grep '\-\-\- FAIL' |wc -l
lm-rr:
mv log.lm-rr log.lm-rr.prev || true
PILOSA_TXSRC=lmdb_roaring go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.lm-rr
@echo " log.lm-rr green: \c"; cat log.lm-rr | grep PASS |wc -l
@echo " log.lm-rr red: \c"; cat log.lm-rr | grep '\-\-\- FAIL' |wc -l
rr-lm:
mv log.rr-lm log.rr-lm.prev || true
PILOSA_TXSRC=roaring_lmdb go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rr-lm
@echo " log.rr-lm green: \c"; cat log.rr-lm | grep PASS |wc -l
@echo " log.rr-lm red: \c"; cat log.rr-lm | grep '\-\-\- FAIL' |wc -l
bg-lm:
mv log.topt.bg-lm log.topt.bg-lm.prev || true
PILOSA_TXSRC=badger_lmdb go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.bg-lm
@echo " log.topt.bg-lm green: \c"; cat log.topt.bg-lm | grep PASS |wc -l
@echo " log.topt.bg-lm red: \c"; cat log.topt.bg-lm | grep '\-\-\- FAIL' |wc -l
lm-bg:
mv log.topt.lm-bg log.topt.lm-bg.prev || true
PILOSA_TXSRC=lmdb_badger go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.lm-bg
@echo " log.topt.lm-bg green: \c"; cat log.topt.lm-bg | grep PASS |wc -l
@echo " log.topt.lm-bg red: \c"; cat log.topt.lm-bg | grep '\-\-\- FAIL' |wc -l
# Run golangci-lint # Run golangci-lint
golangci-lint: require-golangci-lint golangci-lint: require-golangci-lint
golangci-lint run golangci-lint run --timeout 3m --skip-files '.*\.peg\.go'
# Alias
linter: golangci-lint
# Better alias
ocd: golangci-lint
# Run gometalinter with custom flags # Run gometalinter with custom flags
gometalinter: require-gometalinter vendor gometalinter: require-gometalinter vendor
@ -161,9 +362,8 @@ gometalinter: require-gometalinter vendor
# Verify that all Go files have license header # Verify that all Go files have license header
check-license-headers: SHELL:=/bin/bash check-license-headers: SHELL:=/bin/bash
check-license-headers: check-license-headers:
@! find . -name '*.go' | grep -v '^./vendor' | while read fn;\ @! find . -path ./vendor -prune -o -name '*.go' -print | grep -v -F -f license.exceptions | while read fn;\
do [[ `head -13 $$fn | shasum | cut -f 1 -d " "` == $(LICENSE_HASH) ]] || echo $$fn; done | \ do [[ `$(call LICENSE_HASH_CODE, $$fn)` == $(LICENSE_HASH) ]] || echo $$fn; done | grep '.'
grep -v apimethod_string.go | grep -v pb.go | grep -v peg.go | grep -v lru.go | grep -v btree | grep -v enterprise
###################### ######################
# Build dependencies # # Build dependencies #
@ -175,7 +375,10 @@ require-%:
$(info Verified build dependency "$*" is installed.),\ $(info Verified build dependency "$*" is installed.),\
$(error Build dependency "$*" not installed. To install, try `make install-$*`)) $(error Build dependency "$*" not installed. To install, try `make install-$*`))
install-build-deps: install-protoc-gen-gofast install-protoc install-stringer install-peg install-build-deps: install-protoc-gen-gofast install-protoc install-statik install-stringer install-peg
install-statik:
go get -u github.com/rakyll/statik
install-stringer: install-stringer:
GO111MODULE=off go get -u golang.org/x/tools/cmd/stringer GO111MODULE=off go get -u golang.org/x/tools/cmd/stringer

45
NOTICE
View file

@ -14,27 +14,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
Enterprise Edition software license
===================================
Files contained under the directory `enterprise` are subject to the following
license notice (Full license included in the file `COPYING`):
Copyright (C) 2018 Pilosa Corp. All rights reserved.
Pilosa Enterprise Edition is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Pilosa Enterprise Edition is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Pilosa Enterprise Edition. If not, see <http://www.gnu.org/licenses/>.
Third-party software licenses Third-party software licenses
============================= =============================
@ -115,3 +94,27 @@ The file /server/tlsconfig.go contains a modified redistribution of bridge
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The files /logger/filewriter.go and /logger/filewriter_test.go contain a modified redistribution of reopen (github.com/client9/reopen); the license follows:
The MIT License (MIT)
Copyright (c) 2015 Nick Galbreath
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -31,6 +31,8 @@ See our [Documentation](https://www.pilosa.com/docs/) for information about inst
1. [Install Pilosa](https://www.pilosa.com/docs/installation/). 1. [Install Pilosa](https://www.pilosa.com/docs/installation/).
Optionally, to include Lattice, the in-browser UI, follow the "Build from source" instructions, and run `make generate-statik` before `make install`. When you run a local Pilosa server on the default host, for example, you can access Lattice at [localhost:10101](http://localhost:10101).
2. [Start Pilosa](https://www.pilosa.com/docs/getting-started/#starting-pilosa) with the default configuration: 2. [Start Pilosa](https://www.pilosa.com/docs/getting-started/#starting-pilosa) with the default configuration:
```shell ```shell
@ -40,7 +42,7 @@ See our [Documentation](https://www.pilosa.com/docs/) for information about inst
and verify that it's running: and verify that it's running:
```shell ```shell
curl localhost:10101/nodes curl localhost:10101/status
``` ```
3. Follow along with the [Sample Project](https://www.pilosa.com/docs/getting-started/#sample-project) to get a better understanding of Pilosa's capabilities. 3. Follow along with the [Sample Project](https://www.pilosa.com/docs/getting-started/#sample-project) to get a better understanding of Pilosa's capabilities.
@ -63,13 +65,12 @@ There are supported libraries for the following languages:
- [Java](https://www.pilosa.com/docs/client-libraries/#java) - [Java](https://www.pilosa.com/docs/client-libraries/#java)
- [Python](https://www.pilosa.com/docs/client-libraries/#python) - [Python](https://www.pilosa.com/docs/client-libraries/#python)
## Licenses ## License
The core Pilosa code base and all default builds (referred to as Pilosa Community Edition) are licensed completely under the Apache License, Version 2.0. Pilosa is licensed under the Apache License, Version 2.0.
If you build Pilosa with the `enterprise` build tag (Pilosa Enterprise Edition), then that build will include features licensed under the GNU Affero General
Public License (AGPL). Enterprise code is located entirely in the [github.com/pilosa/pilosa/enterprise](https://github.com/pilosa/pilosa/tree/master/enterprise) A copy of the license is located in [github.com/pilosa/pilosa/LICENSE](https://github.com/pilosa/pilosa/blob/master/LICENSE).
directory. See [github.com/pilosa/pilosa/NOTICE](https://github.com/pilosa/pilosa/blob/master/NOTICE) and More details about licensing are found in [github.com/pilosa/pilosa/NOTICE](https://github.com/pilosa/pilosa/blob/master/NOTICE).
[github.com/pilosa/pilosa/LICENSE](https://github.com/pilosa/pilosa/blob/master/LICENSE) for more information about Pilosa licenses.
## Get Support ## Get Support

906
api.go

File diff suppressed because it is too large Load diff

214
api/client/grpc.go Normal file
View file

@ -0,0 +1,214 @@
// Copyright 2017 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package client
import (
"context"
"crypto/tls"
"log"
"sync"
pb "github.com/pilosa/pilosa/v2/proto"
"github.com/pkg/errors"
"google.golang.org/grpc"
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/credentials"
)
const maxMsgSize = 1024 * 1024 * 100 // 100 megs ought to be enough for anybody!
// GRPCClient is a client for working with the gRPC server.
type GRPCClient struct {
dialTargets []string
tlsConfig *tls.Config
mu sync.RWMutex
conn *grpc.ClientConn
targetIndex int
}
// NewGRPCClient returns a new instance of GRPCClient.
func NewGRPCClient(dialTargets []string, tlsConfig *tls.Config) (*GRPCClient, error) {
c := &GRPCClient{
dialTargets: dialTargets,
tlsConfig: tlsConfig,
}
// resetConn sets GRPCClient.conn when it doesn't
// exist yet.
if err := c.resetConn(); err != nil {
return nil, errors.Wrap(err, "setting connection")
}
return c, nil
}
// resetConn resets the gRPC client connection. This method
// can also be used to initially set the client connection
// because it only tries to first close the connection if
// the connection already exists.
func (c *GRPCClient) resetConn() error {
c.mu.Lock()
defer c.mu.Unlock()
// If an existing connection exists, close it first.
if c.conn != nil {
if err := c.conn.Close(); err != nil {
return errors.Wrap(err, "closing existing connection")
}
}
var opts []grpc.DialOption
if c.tlsConfig != nil {
creds := credentials.NewTLS(c.tlsConfig)
opts = append(opts, grpc.WithTransportCredentials(creds))
} else {
opts = append(opts, grpc.WithInsecure())
}
opts = append(opts, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxMsgSize)))
var err error
if c.conn, err = grpc.Dial(c.dialTargets[c.getTargetIndex()], opts...); err != nil {
return errors.Wrap(err, "creating new grpc client")
}
return nil
}
// getTargetIndex gets the current target index, then increments it for
// next time. Unprotected.
func (c *GRPCClient) getTargetIndex() int {
if len(c.dialTargets) == 0 {
return 0
}
ret := c.targetIndex
c.targetIndex = (c.targetIndex + 1) % len(c.dialTargets) // cycle through dialTargets
return ret
}
// Close closes any connections the client has opened.
func (c *GRPCClient) Close() error {
c.mu.RLock()
defer c.mu.RUnlock()
if c.conn != nil {
return c.conn.Close()
}
return nil
}
// Conn returns the gRPC client connection. If the connection
// has gone into state `TransientFailure`, this method tries
// to reset the connection and return that new connection.
func (c *GRPCClient) Conn() *grpc.ClientConn {
c.mu.RLock()
if c.conn == nil {
c.mu.RUnlock()
return nil
} else if c.conn.GetState() != connectivity.TransientFailure {
defer c.mu.RUnlock()
return c.conn
}
c.mu.RUnlock()
if err := c.resetConn(); err != nil {
// TODO: log this error with logger
log.Printf("error resetting connection: %s", err)
}
c.mu.RLock()
defer c.mu.RUnlock()
return c.conn
}
// Query returns a stream of RowResponse for the given index and PQL string.
func (c *GRPCClient) Query(ctx context.Context, index string, pql string) (pb.StreamClient, error) {
conn := c.Conn()
if conn == nil {
return nil, errors.New("client has not established a grpc connection")
}
grpcClient := pb.NewPilosaClient(conn)
stream, err := grpcClient.QueryPQL(ctx, &pb.QueryPQLRequest{
Index: index,
Pql: pql,
})
if err != nil {
return nil, errors.Wrap(err, "getting stream")
} else if stream == nil {
return nil, errors.New("could not create stream")
}
return stream, err
}
// QueryUnary returns a TableResponse for the given index and PQL string.
func (c *GRPCClient) QueryUnary(ctx context.Context, index string, pql string) (*pb.TableResponse, error) {
conn := c.Conn()
if conn == nil {
return nil, errors.New("client has not established a grpc connection")
}
grpcClient := pb.NewPilosaClient(conn)
return grpcClient.QueryPQLUnary(ctx, &pb.QueryPQLRequest{
Index: index,
Pql: pql,
})
}
// Inspect returns a stream of RowResponse for the given index, columns, and filters.
// It is intended to mimic something like "select [fields] from table where recordID IN (...)".
func (c *GRPCClient) Inspect(ctx context.Context, index string, columnIDs []uint64, columnKeys []string, query string, fieldFilters []string, limit, offset uint64) (pb.StreamClient, error) {
conn := c.Conn()
if conn == nil {
return nil, errors.New("client has not established a grpc connection")
}
if len(columnIDs) > 0 && len(columnKeys) > 0 {
return nil, errors.New("only provide column ids or keys, not both")
}
// Convert columns to proto type IdsOrKeys.
idsOrKeys := &pb.IdsOrKeys{}
if len(columnKeys) > 0 {
idsOrKeys.Type = &pb.IdsOrKeys_Keys{Keys: &pb.StringArray{Vals: columnKeys}}
} else {
idsOrKeys.Type = &pb.IdsOrKeys_Ids{Ids: &pb.Uint64Array{Vals: columnIDs}}
}
grpcClient := pb.NewPilosaClient(conn)
stream, err := grpcClient.Inspect(ctx, &pb.InspectRequest{
Index: index,
Columns: idsOrKeys,
FilterFields: fieldFilters,
Limit: limit,
Offset: offset,
Query: query,
})
if err != nil {
return nil, errors.Wrap(err, "getting stream")
} else if stream == nil {
return nil, errors.New("could not create stream")
}
return stream, err
}

View file

@ -19,6 +19,7 @@ import (
"fmt" "fmt"
"math" "math"
"reflect" "reflect"
"strconv"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -30,6 +31,140 @@ import (
"github.com/pilosa/pilosa/v2/test" "github.com/pilosa/pilosa/v2/test"
) )
// attrFun defines a mapping from columnID -> attr value
func attrFun(id uint64) string {
//return fmt.Sprintf("%x", md5.Sum([]byte(strconv.FormatInt(int64(id), 10))))
return strconv.FormatInt(int64(id), 10)
}
func TestAPI_ImportColumnAttrs(t *testing.T) {
/*
columns seconds
100 1.150
1000 1.568
10000 5.156
100000 38.179
*/
c := test.MustRunCluster(t, 2,
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerNodeID("node0"),
pilosa.OptServerClusterHasher(&offsetModHasher{}),
)},
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerNodeID("node1"),
pilosa.OptServerClusterHasher(&offsetModHasher{}),
)},
)
defer c.Close()
m0 := c.GetNode(0)
m1 := c.GetNode(1)
t.Run("ImportColumnAttrs", func(t *testing.T) {
ctx := context.Background()
indexName := "i"
fieldName := "f"
attrKey := "k"
index, err := m0.API.CreateIndex(ctx, indexName, pilosa.IndexOptions{})
if err != nil {
t.Fatalf("creating index: %v", err)
}
_, err = m0.API.CreateField(ctx, indexName, fieldName)
if err != nil {
t.Fatalf("creating field: %v", err)
}
// Generate some attrs for two shards
numAttrs := 100
columnIDs0 := make([]uint64, 0, numAttrs)
attrVals0 := make([]string, 0, numAttrs)
columnIDs1 := make([]uint64, 0, numAttrs)
attrVals1 := make([]string, 0, numAttrs)
for n := 0; n < 1000000; n += 1000000 / numAttrs {
columnIDs0 = append(columnIDs0, uint64(n))
val0 := attrFun(uint64(n))
attrVals0 = append(attrVals0, val0)
setPql0 := fmt.Sprintf("Set(%d, %s=0) ", n, fieldName)
if _, err := m0.API.Query(ctx, &pilosa.QueryRequest{Index: indexName, Query: setPql0}); err != nil {
t.Fatal(err)
}
columnIDs1 = append(columnIDs1, uint64(n+ShardWidth))
val1 := attrFun(uint64(n + ShardWidth))
attrVals1 = append(attrVals1, val1)
setPql1 := fmt.Sprintf("Set(%d, %s=0) ", n+ShardWidth, fieldName)
if _, err := m1.API.Query(ctx, &pilosa.QueryRequest{Index: indexName, Query: setPql1}); err != nil {
t.Fatal(err)
}
}
// send shard0 to node1
req := &pilosa.ImportColumnAttrsRequest{
AttrKey: attrKey,
ColumnIDs: columnIDs0,
AttrVals: attrVals0,
Shard: 0,
Index: indexName,
IndexCreatedAt: index.CreatedAt(),
}
if err := m1.API.ImportColumnAttrs(ctx, req); err != nil {
t.Fatal(err)
}
// send shard1 to node0
req = &pilosa.ImportColumnAttrsRequest{
AttrKey: attrKey,
ColumnIDs: columnIDs1,
AttrVals: attrVals1,
Shard: 1,
Index: indexName,
IndexCreatedAt: index.CreatedAt(),
}
if err := m0.API.ImportColumnAttrs(ctx, req); err != nil {
t.Fatal(err)
}
// Query node0.
pql := fmt.Sprintf("Options(Row(%s=0), columnAttrs=true)", fieldName)
res, err := m0.API.Query(ctx, &pilosa.QueryRequest{Index: indexName, Query: pql})
if err != nil {
t.Fatal(err)
}
m := len(res.ColumnAttrSets)
if m != 100 {
t.Fatalf("incorrect number of column attrs set; m = %v", m)
}
for _, v := range res.ColumnAttrSets {
attrVal := attrFun(v.ID)
if attrVal != v.Attrs[attrKey] {
t.Fatal(err)
}
}
// Query node1.
pql = fmt.Sprintf("Options(Row(%s=0), columnAttrs=true)", fieldName)
res, err = m1.API.Query(ctx, &pilosa.QueryRequest{Index: indexName, Query: pql})
if err != nil {
t.Fatal(err)
}
if len(res.ColumnAttrSets) != 100 {
t.Fatal("incorrect number of column attrs set")
}
for _, v := range res.ColumnAttrSets {
attrVal := attrFun(v.ID)
if attrVal != v.Attrs[attrKey] {
t.Fatal(err)
}
}
})
}
func TestAPI_Import(t *testing.T) { func TestAPI_Import(t *testing.T) {
c := test.MustRunCluster(t, 2, c := test.MustRunCluster(t, 2,
[]server.CommandOption{ []server.CommandOption{
@ -49,62 +184,79 @@ func TestAPI_Import(t *testing.T) {
) )
defer c.Close() defer c.Close()
m0 := c[0] m0 := c.GetNode(0)
m1 := c[1] m1 := c.GetNode(1)
t.Run("RowIDColumnKey", func(t *testing.T) { t.Run("RowIDColumnKey", func(t *testing.T) {
ctx := context.Background() ctx := context.Background()
index := "rick" indexName := "rick"
field := "f" fieldName := "f"
_, err := m0.API.CreateIndex(ctx, index, pilosa.IndexOptions{Keys: true, TrackExistence: true}) index, err := m0.API.CreateIndex(ctx, indexName, pilosa.IndexOptions{Keys: true, TrackExistence: true})
if err != nil { if err != nil {
t.Fatalf("creating index: %v", err) t.Fatalf("creating index: %v", err)
} }
_, err = m0.API.CreateField(ctx, index, field, pilosa.OptFieldTypeSet(pilosa.DefaultCacheType, 100)) if index.CreatedAt() == 0 {
t.Fatal("index createdAt is empty")
}
field, err := m0.API.CreateField(ctx, indexName, fieldName, pilosa.OptFieldTypeSet(pilosa.DefaultCacheType, 100))
if err != nil { if err != nil {
t.Fatalf("creating field: %v", err) t.Fatalf("creating field: %v", err)
} }
if field.CreatedAt() == 0 {
t.Fatal("field createdAt is empty")
}
rowID := uint64(1) rowID := uint64(1)
timestamp := int64(0) timestamp := int64(0)
// Generate some keyed records. // Generate some keyed records.
rowIDs := []uint64{} rowIDs := []uint64{}
colKeys := []string{}
timestamps := []int64{} timestamps := []int64{}
for i := 1; i <= 10; i++ { N := 10
for i := 1; i <= N; i++ {
rowIDs = append(rowIDs, rowID) rowIDs = append(rowIDs, rowID)
timestamps = append(timestamps, timestamp) timestamps = append(timestamps, timestamp)
colKeys = append(colKeys, fmt.Sprintf("col%d", i))
} }
// Keys are sharded so ordering is not guaranteed.
colKeys := []string{"col10", "col8", "col9", "col6", "col7", "col4", "col5", "col2", "col3", "col1"}
colKeys = colKeys[:N]
// Import data with keys to the coordinator (node0) and verify that it gets // Import data with keys to the coordinator (node0) and verify that it gets
// translated and forwarded to the owner of shard 0 (node1; because of offsetModHasher) // translated and forwarded to the owner of shard 0 (node1; because of offsetModHasher)
req := &pilosa.ImportRequest{ req := &pilosa.ImportRequest{
Index: index, Index: indexName,
Field: field, IndexCreatedAt: index.CreatedAt(),
Shard: 0, Field: fieldName,
RowIDs: rowIDs, FieldCreatedAt: field.CreatedAt(),
ColumnKeys: colKeys, Shard: 0, // import is all on shard 0, why are we making lots of other shards? b/c this is not a restriction.
Timestamps: timestamps, RowIDs: rowIDs,
} ColumnKeys: colKeys,
if err := m0.API.Import(ctx, req); err != nil { Timestamps: timestamps,
t.Fatal(err)
} }
pql := fmt.Sprintf("Row(%s=%d)", field, rowID) qcx := m0.API.Txf().NewQcx()
if err := m0.API.Import(ctx, qcx, req); err != nil {
t.Fatal(err)
}
panicOn(qcx.Finish())
pql := fmt.Sprintf("Row(%s=%d)", fieldName, rowID)
// Query node0. // Query node0.
if res, err := m0.API.Query(ctx, &pilosa.QueryRequest{Index: index, Query: pql}); err != nil { if res, err := m0.API.Query(ctx, &pilosa.QueryRequest{Index: indexName, Query: pql}); err != nil {
t.Fatal(err) t.Fatal(err)
} else if keys := res.Results[0].(*pilosa.Row).Keys; !reflect.DeepEqual(keys, colKeys) { } else if keys := res.Results[0].(*pilosa.Row).Keys; !reflect.DeepEqual(keys, colKeys) {
t.Fatalf("unexpected column keys: %#v", keys) t.Fatalf("expected colKeys='%#v'; observed column keys: %#v", colKeys, keys)
} }
// Query node1. // Query node1.
if err := test.RetryUntil(5*time.Second, func() error { if err := test.RetryUntil(5*time.Second, func() error {
if res, err := m1.API.Query(ctx, &pilosa.QueryRequest{Index: index, Query: pql}); err != nil { if res, err := m1.API.Query(ctx, &pilosa.QueryRequest{Index: indexName, Query: pql}); err != nil {
return err return err
} else if keys := res.Results[0].(*pilosa.Row).Keys; !reflect.DeepEqual(keys, colKeys) { } else if keys := res.Results[0].(*pilosa.Row).Keys; !reflect.DeepEqual(keys, colKeys) {
return fmt.Errorf("unexpected column keys: %#v", keys) return fmt.Errorf("unexpected column keys: %#v", keys)
@ -129,58 +281,6 @@ func TestAPI_Import(t *testing.T) {
} }
}) })
t.Run("RowKeyColumnID", func(t *testing.T) {
ctx := context.Background()
index := "rkci"
field := "f"
_, err := m0.API.CreateIndex(ctx, index, pilosa.IndexOptions{Keys: false})
if err != nil {
t.Fatalf("creating index: %v", err)
}
_, err = m0.API.CreateField(ctx, index, field, pilosa.OptFieldTypeSet(pilosa.DefaultCacheType, 100), pilosa.OptFieldKeys())
if err != nil {
t.Fatalf("creating field: %v", err)
}
rowKey := "rowkey"
// Generate some keyed records.
rowKeys := []string{rowKey, rowKey, rowKey}
colIDs := []uint64{1, 2, pilosa.ShardWidth + 1}
timestamps := []int64{0, 0, 0}
// Import data with keys to the coordinator (node0) and verify that it gets
// translated and forwarded to the owner of shard 0 (node1; because of offsetModHasher)
req := &pilosa.ImportRequest{
Index: index,
Field: field,
Shard: 0,
RowKeys: rowKeys,
ColumnIDs: colIDs,
Timestamps: timestamps,
}
if err := m0.API.Import(ctx, req); err != nil {
t.Fatal(err)
}
pql := fmt.Sprintf("Row(%s=%s)", field, rowKey)
// Query node0.
if res, err := m0.API.Query(ctx, &pilosa.QueryRequest{Index: index, Query: pql}); err != nil {
t.Fatal(err)
} else if columns := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(columns, colIDs) {
t.Fatalf("unexpected column ids: %+v", columns)
}
// Query node1.
if res, err := m1.API.Query(ctx, &pilosa.QueryRequest{Index: index, Query: pql}); err != nil {
t.Fatal(err)
} else if columns := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(columns, colIDs) {
t.Fatalf("unexpected column ids: %+v", columns)
}
})
} }
func TestAPI_ImportValue(t *testing.T) { func TestAPI_ImportValue(t *testing.T) {
@ -200,8 +300,8 @@ func TestAPI_ImportValue(t *testing.T) {
) )
defer c.Close() defer c.Close()
m0 := c[0] m0 := c.GetNode(0)
m1 := c[1] m1 := c.GetNode(1)
t.Run("ValColumnKey", func(t *testing.T) { t.Run("ValColumnKey", func(t *testing.T) {
ctx := context.Background() ctx := context.Background()
@ -219,12 +319,13 @@ func TestAPI_ImportValue(t *testing.T) {
// Generate some keyed records. // Generate some keyed records.
values := []int64{} values := []int64{}
colKeys := []string{}
for i := 1; i <= 10; i++ { for i := 1; i <= 10; i++ {
values = append(values, int64(i)) values = append(values, int64(i))
colKeys = append(colKeys, fmt.Sprintf("col%d", i))
} }
// Column keys are sharded so their order is not guaranteed.
colKeys := []string{"col10", "col8", "col9", "col6", "col7", "col4", "col5", "col2", "col3", "col1"}
// Import data with keys to the coordinator (node0) and verify that it gets // Import data with keys to the coordinator (node0) and verify that it gets
// translated and forwarded to the owner of shard 0 (node1; because of offsetModHasher) // translated and forwarded to the owner of shard 0 (node1; because of offsetModHasher)
req := &pilosa.ImportValueRequest{ req := &pilosa.ImportValueRequest{
@ -233,9 +334,12 @@ func TestAPI_ImportValue(t *testing.T) {
ColumnKeys: colKeys, ColumnKeys: colKeys,
Values: values, Values: values,
} }
if err := m0.API.ImportValue(ctx, req); err != nil {
qcx := m0.API.Txf().NewQcx()
if err := m0.API.ImportValue(ctx, qcx, req); err != nil {
t.Fatal(err) t.Fatal(err)
} }
panicOn(qcx.Finish())
pql := fmt.Sprintf("Row(%s>0)", field) pql := fmt.Sprintf("Row(%s>0)", field)
@ -258,6 +362,125 @@ func TestAPI_ImportValue(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
}) })
t.Run("ValDecimalField", func(t *testing.T) {
ctx := context.Background()
index := "valdec"
field := "fdec"
_, err := m1.API.CreateIndex(ctx, index, pilosa.IndexOptions{})
if err != nil {
t.Fatalf("creating index: %v", err)
}
_, err = m1.API.CreateField(ctx, index, field, pilosa.OptFieldTypeDecimal(1))
if err != nil {
t.Fatalf("creating field: %v", err)
}
// Generate some keyed records.
values := []float64{}
colIDs := []uint64{}
for i := 0; i < 10; i++ {
values = append(values, float64(i)+0.1)
colIDs = append(colIDs, uint64(i))
}
// Import data with keys to the coordinator (node0) and verify that it gets
// translated and forwarded to the owner of shard 0 (node1; because of offsetModHasher)
req := &pilosa.ImportValueRequest{
Index: index,
Field: field,
ColumnIDs: colIDs,
FloatValues: values,
}
qcx := m1.API.Txf().NewQcx()
if err := m1.API.ImportValue(ctx, qcx, req); err != nil {
t.Fatal(err)
}
panicOn(qcx.Finish())
query := fmt.Sprintf("Row(%s>6)", field)
// Query node0.
if res, err := m0.API.Query(ctx, &pilosa.QueryRequest{Index: index, Query: query}); err != nil {
t.Fatal(err)
} else if ids := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(ids, colIDs[6:]) {
t.Fatalf("unexpected column keys: observerd %+v; expected '%+v'", ids, colIDs[6:])
}
})
t.Run("ValDecimalFieldNegativeScale", func(t *testing.T) {
ctx := context.Background()
index := "valdecneg"
field := "fdecneg"
_, err := m0.API.CreateIndex(ctx, index, pilosa.IndexOptions{})
if err != nil {
t.Fatalf("creating index: %v", err)
}
_, err = m0.API.CreateField(ctx, index, field, pilosa.OptFieldTypeDecimal(-1))
if err == nil {
t.Fatal("expected error creating field")
}
})
t.Run("ValStringField", func(t *testing.T) {
ctx := context.Background()
index := "valstr"
field := "fstr"
fgnIndex := "fgnvalstr"
_, err := m0.API.CreateIndex(ctx, index, pilosa.IndexOptions{})
if err != nil {
t.Fatalf("creating index: %v", err)
}
_, err = m0.API.CreateIndex(ctx, fgnIndex, pilosa.IndexOptions{Keys: true})
if err != nil {
t.Fatalf("creating foreign index: %v", err)
}
_, err = m0.API.CreateField(ctx, index, field,
pilosa.OptFieldTypeInt(0, math.MaxInt64),
pilosa.OptFieldForeignIndex(fgnIndex),
)
if err != nil {
t.Fatalf("creating field: %v", err)
}
// Generate some keyed records.
values := []string{}
colIDs := []uint64{}
for i := 0; i < 10; i++ {
value := fmt.Sprintf("strval-%d", (i)*100+10)
values = append(values, value)
colIDs = append(colIDs, uint64(i))
}
// Import data with keys to the coordinator (node0) and verify that it gets
// translated and forwarded to the owner of shard 0 (node1; because of offsetModHasher)
req := &pilosa.ImportValueRequest{
Index: index,
Field: field,
ColumnIDs: colIDs,
StringValues: values,
}
qcx := m0.API.Txf().NewQcx()
if err := m0.API.ImportValue(ctx, qcx, req); err != nil {
t.Fatal(err)
}
panicOn(qcx.Finish())
pql := fmt.Sprintf(`Row(%s=="strval-110")`, field)
// Query node0.
if res, err := m0.API.Query(ctx, &pilosa.QueryRequest{Index: index, Query: pql}); err != nil {
t.Fatal(err)
} else if ids := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(ids, []uint64{1}) {
t.Fatalf("unexpected columns: observerd %+v; expected '%+v'", ids, []uint64{1})
}
})
} }
// offsetModHasher represents a simple, mod-based hashing offset by 1. // offsetModHasher represents a simple, mod-based hashing offset by 1.
@ -266,3 +489,134 @@ type offsetModHasher struct{}
func (*offsetModHasher) Hash(key uint64, n int) int { func (*offsetModHasher) Hash(key uint64, n int) int {
return int(key+1) % n return int(key+1) % n
} }
func TestAPI_ClearFlagForImportAndImportValues(t *testing.T) {
c := test.MustRunCluster(t, 1,
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerNodeID("node0"),
pilosa.OptServerClusterHasher(&offsetModHasher{}),
pilosa.OptServerOpenTranslateReader(http.GetOpenTranslateReaderFunc(nil)),
)},
)
defer c.Close()
// plan:
// 1. set a bit
// 2. clear with Import() using the ImportRequest.Clear flag
// 3. verifiy the clear is done.
// repeat for ImportValueRequest and ImportValues()
m0 := c.GetNode(0)
m0api := m0.API
ctx := context.Background()
index := "i"
fieldAcct0 := "acct0"
opts := pilosa.OptFieldTypeInt(-1000, 1000)
_, err := m0api.CreateIndex(ctx, index, pilosa.IndexOptions{})
if err != nil {
t.Fatalf("creating index: %v", err)
}
_, err = m0api.CreateField(ctx, index, fieldAcct0, opts)
if err != nil {
t.Fatalf("creating fieldAcct0: %v", err)
}
iraField := "ira" // set field.
iraRowID := uint64(3)
_, err = m0api.CreateField(ctx, index, iraField)
if err != nil {
t.Fatalf("creating fieldIRA: %v", err)
}
acctOwnerID := uint64(78) // ColumnID
shard := acctOwnerID / ShardWidth
acct0bal := int64(500)
ivr0 := &pilosa.ImportValueRequest{
Index: index,
Field: fieldAcct0,
Shard: shard,
ColumnIDs: []uint64{acctOwnerID},
Values: []int64{acct0bal},
}
ir0 := &pilosa.ImportRequest{
Index: index,
Field: iraField,
Shard: shard,
ColumnIDs: []uint64{acctOwnerID},
RowIDs: []uint64{iraRowID},
}
qcx := m0api.Txf().NewQcx()
if err := m0api.Import(ctx, qcx, ir0); err != nil {
t.Fatal(err)
}
if err := m0api.ImportValue(ctx, qcx, ivr0); err != nil {
t.Fatal(err)
}
panicOn(qcx.Finish())
bitIsSet := func() bool {
query := fmt.Sprintf("Row(%v=%v)", iraField, iraRowID)
res, err := m0api.Query(context.Background(), &pilosa.QueryRequest{Index: index, Query: query})
panicOn(err)
cols := res.Results[0].(*pilosa.Row).Columns()
for i := range cols {
if cols[i] == acctOwnerID {
return true
}
}
return false
}
if !bitIsSet() {
panic("IRA bit should have been set")
}
queryAcct := func(m0api *pilosa.API, acctOwnerID uint64, fieldAcct0, index string) (acctBal int64) {
query := fmt.Sprintf("FieldValue(field=%v, column=%v)", fieldAcct0, acctOwnerID)
res, err := m0api.Query(context.Background(), &pilosa.QueryRequest{Index: index, Query: query})
panicOn(err)
if len(res.Results) == 0 {
return 0
}
valCount := res.Results[0].(pilosa.ValCount)
return valCount.Val
}
bal := queryAcct(m0api, acctOwnerID, fieldAcct0, index)
if bal != acct0bal {
panic(fmt.Sprintf("expected %v, observed %v starting acct0 balance", acct0bal, bal))
}
// clear the bit
qcx = m0api.Txf().NewQcx()
ir0.Clear = true
if err := m0api.Import(ctx, qcx, ir0); err != nil {
t.Fatal(err)
}
panicOn(qcx.Finish())
if bitIsSet() {
panic("IRA bit should have been cleared")
}
// clear the BSI
qcx = m0api.Txf().NewQcx()
ivr0.Clear = true
if err := m0api.ImportValue(ctx, qcx, ivr0); err != nil {
t.Fatal(err)
}
panicOn(qcx.Finish())
bal = queryAcct(m0api, acctOwnerID, fieldAcct0, index)
if bal != 0 {
panic(fmt.Sprintf("expected %v, observed %v starting acct0 balance", acct0bal, 0))
}
}

View file

@ -19,25 +19,30 @@ func _() {
_ = x[apiFragmentBlockData-8] _ = x[apiFragmentBlockData-8]
_ = x[apiFragmentBlocks-9] _ = x[apiFragmentBlocks-9]
_ = x[apiFragmentData-10] _ = x[apiFragmentData-10]
_ = x[apiField-11] _ = x[apiTranslateData-11]
_ = x[apiFieldAttrDiff-12] _ = x[apiField-12]
_ = x[apiImport-13] _ = x[apiFieldAttrDiff-13]
_ = x[apiImportValue-14] _ = x[apiImport-14]
_ = x[apiIndex-15] _ = x[apiImportValue-15]
_ = x[apiIndexAttrDiff-16] _ = x[apiIndex-16]
_ = x[apiQuery-17] _ = x[apiIndexAttrDiff-17]
_ = x[apiRecalculateCaches-18] _ = x[apiQuery-18]
_ = x[apiRemoveNode-19] _ = x[apiRecalculateCaches-19]
_ = x[apiResizeAbort-20] _ = x[apiRemoveNode-20]
_ = x[apiSetCoordinator-21] _ = x[apiResizeAbort-21]
_ = x[apiShardNodes-22] _ = x[apiSetCoordinator-22]
_ = x[apiViews-23] _ = x[apiShardNodes-23]
_ = x[apiApplySchema-24] _ = x[apiViews-24]
_ = x[apiApplySchema-25]
_ = x[apiStartTransaction-26]
_ = x[apiFinishTransaction-27]
_ = x[apiTransactions-28]
_ = x[apiGetTransaction-29]
} }
const _apiMethod_name = "apiClusterMessageapiCreateFieldapiCreateIndexapiDeleteFieldapiDeleteAvailableShardapiDeleteIndexapiDeleteViewapiExportCSVapiFragmentBlockDataapiFragmentBlocksapiFragmentDataapiFieldapiFieldAttrDiffapiImportapiImportValueapiIndexapiIndexAttrDiffapiQueryapiRecalculateCachesapiRemoveNodeapiResizeAbortapiSetCoordinatorapiShardNodesapiViewsapiApplySchema" const _apiMethod_name = "apiClusterMessageapiCreateFieldapiCreateIndexapiDeleteFieldapiDeleteAvailableShardapiDeleteIndexapiDeleteViewapiExportCSVapiFragmentBlockDataapiFragmentBlocksapiFragmentDataapiTranslateDataapiFieldapiFieldAttrDiffapiImportapiImportValueapiIndexapiIndexAttrDiffapiQueryapiRecalculateCachesapiRemoveNodeapiResizeAbortapiSetCoordinatorapiShardNodesapiViewsapiApplySchemaapiStartTransactionapiFinishTransactionapiTransactionsapiGetTransaction"
var _apiMethod_index = [...]uint16{0, 17, 31, 45, 59, 82, 96, 109, 121, 141, 158, 173, 181, 197, 206, 220, 228, 244, 252, 272, 285, 299, 316, 329, 337, 351} var _apiMethod_index = [...]uint16{0, 17, 31, 45, 59, 82, 96, 109, 121, 141, 158, 173, 189, 197, 213, 222, 236, 244, 260, 268, 288, 301, 315, 332, 345, 353, 367, 386, 406, 421, 438}
func (i apiMethod) String() string { func (i apiMethod) String() string {
if i < 0 || i >= apiMethod(len(_apiMethod_index)-1) { if i < 0 || i >= apiMethod(len(_apiMethod_index)-1) {

25
audit.go Normal file
View file

@ -0,0 +1,25 @@
// Copyright 2020 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pilosa
import (
"github.com/pilosa/pilosa/v2/testhook"
)
var NewAuditor func() testhook.Auditor = NewNopAuditor
func NewNopAuditor() testhook.Auditor {
return testhook.NewNopAuditor()
}

52
audit_internal_test.go Normal file
View file

@ -0,0 +1,52 @@
// Copyright 2020 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pilosa
import (
"fmt"
"reflect"
"github.com/pilosa/pilosa/v2/testhook"
)
// These audit hooks are desireable during testing, but not in
// production.
type auditorViewHooks struct{}
type auditorFragmentHooks struct{}
// static type checks
var _ testhook.RegistryHookLive = &auditorViewHooks{}
var _ testhook.RegistryHookLive = &auditorFragmentHooks{}
func (*auditorViewHooks) Live(o interface{}, entry *testhook.RegistryEntry) error {
if entry != nil && entry.OpenCount != 0 {
return fmt.Errorf("view %s still open", o.(*view).name)
}
return nil
}
func (*auditorFragmentHooks) Live(o interface{}, entry *testhook.RegistryEntry) error {
if entry != nil && entry.OpenCount != 0 {
return fmt.Errorf("fragment %s still open", o.(*fragment).path)
}
return nil
}
func GetInternalTestHooks() testhook.RegistryHooks {
return map[reflect.Type]testhook.RegistryHook{
reflect.TypeOf((*view)(nil)): &auditorViewHooks{},
reflect.TypeOf((*fragment)(nil)): &auditorFragmentHooks{},
}
}

107
audit_test.go Normal file
View file

@ -0,0 +1,107 @@
// Copyright 2020 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pilosa_test
import (
"fmt"
"os"
"reflect"
"github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/testhook"
)
// AuditLeaksOn is a global switch to turn on resource
// leak checking at the end of a test run.
var AuditLeaksOn = true
// for tests, we use a single shared auditor used by all of the holders.
var globalTestAuditor = testhook.NewVerifyCloseAuditor(testHooks)
// These audit hooks are desireable during testing, but not in
// production.
type auditorIndexHooks struct{}
type auditorFieldHooks struct{}
type auditorHolderHooks struct{}
// static type checking
var _ testhook.RegistryHookLive = &auditorIndexHooks{}
var _ testhook.RegistryHookLive = &auditorFieldHooks{}
var _ testhook.RegistryHookPostDestroy = &auditorHolderHooks{}
var _ testhook.RegistryHookLive = &auditorHolderHooks{}
var testHooks = map[reflect.Type]testhook.RegistryHook{
reflect.TypeOf((*pilosa.Index)(nil)): &auditorIndexHooks{},
reflect.TypeOf((*pilosa.Field)(nil)): &auditorFieldHooks{},
reflect.TypeOf((*pilosa.Holder)(nil)): &auditorHolderHooks{},
}
func init() {
if !AuditLeaksOn {
return
}
for k, v := range pilosa.GetInternalTestHooks() {
testHooks[k] = v
}
testhook.RegisterPreTestHook(func() error {
pilosa.NewAuditor = NewTestAuditor
return nil
})
testhook.RegisterPostTestHook(func() error {
err, errs := globalTestAuditor.FinalCheck()
if err != nil {
for i, e := range errs {
fmt.Fprintf(os.Stderr, "[%d]: %v\n", i, e)
}
}
return err
})
}
func NewTestAuditor() testhook.Auditor {
return globalTestAuditor
}
func (*auditorIndexHooks) Live(o interface{}, entry *testhook.RegistryEntry) error {
if entry != nil && entry.OpenCount != 0 {
return fmt.Errorf("index %s still open", o.(*pilosa.Index).Name())
}
return nil
}
func (*auditorFieldHooks) Live(o interface{}, entry *testhook.RegistryEntry) error {
if entry != nil && entry.OpenCount != 0 {
return fmt.Errorf("field %s still open", o.(*pilosa.Field).Name())
}
return nil
}
func (*auditorHolderHooks) WasDestroyed(o interface{}, kv testhook.KV, ent *testhook.RegistryEntry, err error) error {
path := o.(*pilosa.Holder).Path()
if path == "" {
fmt.Fprintf(os.Stderr, "OOPS: trying to destroy a holder with no path! created: %s\n",
ent.Stack)
} else {
os.RemoveAll(o.(*pilosa.Holder).Path())
}
return err
}
func (*auditorHolderHooks) Live(o interface{}, entry *testhook.RegistryEntry) error {
if entry != nil && entry.OpenCount != 0 {
return fmt.Errorf("holder %s still open", o.(*pilosa.Holder).Path())
}
return nil
}

1924
badger.go Normal file

File diff suppressed because it is too large Load diff

1756
badger_test.go Normal file

File diff suppressed because it is too large Load diff

1040
bluegreentx.go Normal file

File diff suppressed because it is too large Load diff

98
bluegreentx_test.go Normal file
View file

@ -0,0 +1,98 @@
// Copyright 2020 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pilosa
import (
"bytes"
"context"
"io"
"io/ioutil"
"os"
"strings"
"testing"
cryrand "crypto/rand"
)
var _ = context.Background
var _ = os.Open
var _ = strings.Split
func TestMultiReaderB(t *testing.T) {
// MultiReaderB should read identical chunks of bytes from both its "a" and "b"
// member io.Readers, else it should panic. This should hold for
// varying sizes of inputs.
for n := 1 << 5; n < (1 << 18); n = n*2 - 13 {
src := io.LimitReader(cryrand.Reader, int64(n))
a := make([]byte, n)
nr := 0
for nr < n {
na, err := src.Read(a)
panicOn(err)
nr += na
}
if nr != n {
panic("short read")
}
b := make([]byte, n)
copy(b, a)
if !bytes.Equal(a, b) {
panic("test prep failed")
}
m := &MultiReaderB{
a: ioutil.NopCloser(bytes.NewBuffer(a)),
b: ioutil.NopCloser(bytes.NewBuffer(b)),
}
// should not trigger the internal panic of MultiReadB
ncp, err := io.Copy(ioutil.Discard, m)
panicOn(err)
if ncp != int64(n) {
panic("short copy")
}
for victim := 0; victim < n; victim += 7 {
copy(b, a)
if victim%2 == 0 {
// corrupt b
b[victim] = (b[victim] + 1) % 255
} else {
// corrupt a
a[victim] = (a[victim] + 1) % 255
}
m = &MultiReaderB{
a: ioutil.NopCloser(bytes.NewBuffer(a)),
b: ioutil.NopCloser(bytes.NewBuffer(b)),
}
helperShouldPanicOnCopy(m)
}
}
}
func helperShouldPanicOnCopy(m *MultiReaderB) {
// differences in bytes read should be noticed
defer func() {
r := recover()
if r == nil {
panic("expected panic on byte difference but didn't see it")
}
}()
_, _ = io.Copy(ioutil.Discard, m)
}

View file

@ -11,10 +11,13 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package boltdb package boltdb
import ( import (
"bytes"
"context" "context"
"io"
"os" "os"
"path/filepath" "path/filepath"
"sync" "sync"
@ -23,17 +26,32 @@ import (
"github.com/boltdb/bolt" "github.com/boltdb/bolt"
"github.com/pilosa/pilosa/v2" "github.com/pilosa/pilosa/v2"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/zeebo/blake3"
) )
var ( var (
// ErrTranslateStoreClosed is returned when reading from an TranslateEntryReader // ErrTranslateStoreClosed is returned when reading from an TranslateEntryReader
// and the underlying store is closed. // and the underlying store is closed.
ErrTranslateStoreClosed = errors.New("boltdb: translate store closing") ErrTranslateStoreClosed = errors.New("boltdb: translate store closing")
// ErrTranslateKeyNotFound is returned when translating key
// and the underlying store returns an empty set
ErrTranslateKeyNotFound = errors.New("boltdb: translating key returned empty set")
bucketKeys = []byte("keys")
bucketIDs = []byte("ids")
)
const (
// snapshotExt is the file extension used for an in-process snapshot.
snapshotExt = ".snapshotting"
errFmtTranslateBucketNotFound = "boltdb: translate bucket '%s' not found"
) )
// OpenTranslateStore opens and initializes a boltdb translation store. // OpenTranslateStore opens and initializes a boltdb translation store.
func OpenTranslateStore(path, index, field string) (pilosa.TranslateStore, error) { func OpenTranslateStore(path, index, field string, partitionID, partitionN int) (pilosa.TranslateStore, error) {
s := NewTranslateStore(index, field) s := NewTranslateStore(index, field, partitionID, partitionN)
s.Path = path s.Path = path
if err := s.Open(); err != nil { if err := s.Open(); err != nil {
return nil, err return nil, err
@ -45,12 +63,22 @@ func OpenTranslateStore(path, index, field string) (pilosa.TranslateStore, error
var _ pilosa.TranslateStore = &TranslateStore{} var _ pilosa.TranslateStore = &TranslateStore{}
// TranslateStore is an on-disk storage engine for translating string-to-uint64 values. // TranslateStore is an on-disk storage engine for translating string-to-uint64 values.
// An empty string will be converted into the sentinel byte slice:
// var emptyKey = []byte{
// 0x00, 0x00, 0x00,
// 0x4d, 0x54, 0x4d, 0x54, // MTMT
// 0x00,
// 0xc2, 0xa0, // NO-BREAK SPACE
// 0x00,
// }
type TranslateStore struct { type TranslateStore struct {
mu sync.RWMutex mu sync.RWMutex
db *bolt.DB db *bolt.DB
index string index string
field string field string
partitionID int
partitionN int
once sync.Once once sync.Once
closing chan struct{} closing chan struct{}
@ -63,10 +91,12 @@ type TranslateStore struct {
} }
// NewTranslateStore returns a new instance of TranslateStore. // NewTranslateStore returns a new instance of TranslateStore.
func NewTranslateStore(index, field string) *TranslateStore { func NewTranslateStore(index, field string, partitionID, partitionN int) *TranslateStore {
return &TranslateStore{ return &TranslateStore{
index: index, index: index,
field: field, field: field,
partitionID: partitionID,
partitionN: partitionN,
closing: make(chan struct{}), closing: make(chan struct{}),
writeNotify: make(chan struct{}), writeNotify: make(chan struct{}),
} }
@ -82,9 +112,9 @@ func (s *TranslateStore) Open() (err error) {
// Initialize buckets. // Initialize buckets.
if err := s.db.Update(func(tx *bolt.Tx) error { if err := s.db.Update(func(tx *bolt.Tx) error {
if _, err := tx.CreateBucketIfNotExists([]byte("keys")); err != nil { if _, err := tx.CreateBucketIfNotExists(bucketKeys); err != nil {
return err return err
} else if _, err := tx.CreateBucketIfNotExists([]byte("ids")); err != nil { } else if _, err := tx.CreateBucketIfNotExists(bucketIDs); err != nil {
return err return err
} }
return nil return nil
@ -108,6 +138,11 @@ func (s *TranslateStore) Close() (err error) {
return nil return nil
} }
// PartitionID returns the partition id the store was initialized with.
func (s *TranslateStore) PartitionID() int {
return s.partitionID
}
// ReadOnly returns true if the store is in read-only mode. // ReadOnly returns true if the store is in read-only mode.
func (s *TranslateStore) ReadOnly() bool { func (s *TranslateStore) ReadOnly() bool {
s.mu.RLock() s.mu.RLock()
@ -135,105 +170,82 @@ func (s *TranslateStore) Size() int64 {
return tx.Size() return tx.Size()
} }
// TranslateKeys converts a string key to an integer ID. // TranslateKey converts a string key to an integer ID.
// If key does not have an associated id then one is created. // If key does not have an associated id then one is created, unless writable is false,
func (s *TranslateStore) TranslateKey(key string) (id uint64, _ error) { // then the function will return the error pilosa.ErrTranslatingKeyNotFound.
// Find id by key under read lock. func (s *TranslateStore) TranslateKey(key string, writable bool) (uint64, error) {
if err := s.db.View(func(tx *bolt.Tx) error { ids, err := s.translateKeys([]string{key}, writable)
id = findIDByKey(tx.Bucket([]byte("keys")), key) if err != nil {
return nil
}); err != nil {
return 0, err
} else if id != 0 {
return id, nil
}
if s.ReadOnly() {
return 0, pilosa.ErrTranslateStoreReadOnly
}
// Find or create id under write lock.
var written bool
if err := s.db.Update(func(tx *bolt.Tx) (err error) {
bkt := tx.Bucket([]byte("keys"))
if id = findIDByKey(bkt, key); id != 0 {
return nil
} else if id, err = bkt.NextSequence(); err != nil {
return err
} else if err := bkt.Put([]byte(key), u64tob(id)); err != nil {
return err
} else if err := tx.Bucket([]byte("ids")).Put(u64tob(id), []byte(key)); err != nil {
return err
}
written = true
return nil
}); err != nil {
return 0, err return 0, err
} }
if len(ids) == 0 {
if written { return 0, ErrTranslateKeyNotFound
s.notifyWrite()
} }
return ids[0], nil
return id, nil
} }
// TranslateKeys converts a string key to an integer ID. // TranslateKeys converts a slice of string keys to a slice of integer IDs.
// If key does not have an associated id then one is created. // If a key does not have an associated id then one is created, unless writable is false,
func (s *TranslateStore) TranslateKeys(keys []string) (ids []uint64, _ error) { // then the function will return the error pilosa.ErrTranslatingKeyNotFound.
if len(keys) == 0 { func (s *TranslateStore) TranslateKeys(keys []string, writable bool) ([]uint64, error) {
return nil, nil return s.translateKeys(keys, writable)
} }
// Allocate slice for ID mapping. func (s *TranslateStore) translateKeys(keys []string, writable bool) ([]uint64, error) {
ids = make([]uint64, len(keys)) ids := make([]uint64, 0, len(keys))
// Find ids by key under read lock. if s.ReadOnly() || !writable {
var found int found := 0
if err := s.db.View(func(tx *bolt.Tx) error { if err := s.db.View(func(tx *bolt.Tx) error {
bkt := tx.Bucket([]byte("keys")) bkt := tx.Bucket(bucketKeys)
for i, key := range keys { if bkt == nil {
if id := findIDByKey(bkt, key); id != 0 { return errors.Errorf(errFmtTranslateBucketNotFound, bucketKeys)
ids[i] = id
found++
} }
for _, key := range keys {
if id, _ := findIDByKey(bkt, key); id != 0 {
ids = append(ids, id)
found++
}
}
return nil
}); err != nil {
return nil, err
} }
return nil if found == len(keys) {
}); err != nil { return ids, nil
return nil, err }
} else if found == len(keys) { if s.ReadOnly() {
return ids, nil return ids, pilosa.ErrTranslateStoreReadOnly
} }
if !writable {
if s.ReadOnly() { return nil, pilosa.ErrTranslatingKeyNotFound
return ids, pilosa.ErrTranslateStoreReadOnly }
return nil, nil
} }
// Find or create ids under write lock if any keys were not found. // Find or create ids under write lock if any keys were not found.
var written bool var written bool
if err := s.db.Update(func(tx *bolt.Tx) (err error) { if err := s.db.Update(func(tx *bolt.Tx) (err error) {
bkt := tx.Bucket([]byte("keys")) bkt := tx.Bucket(bucketKeys)
for i, key := range keys { for _, key := range keys {
if ids[i] != 0 { id, boltKey := findIDByKey(bkt, key)
if id != 0 {
ids = append(ids, id)
continue continue
} }
id = pilosa.GenerateNextPartitionedID(s.index, maxID(tx), s.partitionID, s.partitionN)
if ids[i] = findIDByKey(bkt, key); ids[i] != 0 { if err := bkt.Put(boltKey, u64tob(id)); err != nil {
continue
} else if ids[i], err = bkt.NextSequence(); err != nil {
return err return err
} else if err := bkt.Put([]byte(key), u64tob(ids[i])); err != nil { } else if err := tx.Bucket(bucketIDs).Put(u64tob(id), boltKey); err != nil {
return err
} else if err := tx.Bucket([]byte("ids")).Put(u64tob(ids[i]), []byte(key)); err != nil {
return err return err
} }
ids = append(ids, id)
written = true written = true
} }
return nil return nil
}); err != nil { }); err != nil {
return nil, err return nil, err
} }
if written { if written {
s.notifyWrite() s.notifyWrite()
} }
@ -248,7 +260,7 @@ func (s *TranslateStore) TranslateID(id uint64) (string, error) {
return "", err return "", err
} }
defer func() { _ = tx.Rollback() }() defer func() { _ = tx.Rollback() }()
return findKeyByID(tx.Bucket([]byte("ids")), id), nil return findKeyByID(tx.Bucket(bucketIDs), id), nil
} }
// TranslateIDs converts a list of integer IDs to a list of string keys. // TranslateIDs converts a list of integer IDs to a list of string keys.
@ -263,9 +275,11 @@ func (s *TranslateStore) TranslateIDs(ids []uint64) ([]string, error) {
} }
defer func() { _ = tx.Rollback() }() defer func() { _ = tx.Rollback() }()
bucket := tx.Bucket(bucketIDs)
keys := make([]string, len(ids)) keys := make([]string, len(ids))
for i, id := range ids { for i, id := range ids {
keys[i] = findKeyByID(tx.Bucket([]byte("ids")), id) keys[i] = findKeyByID(bucket, id)
} }
return keys, nil return keys, nil
} }
@ -273,9 +287,9 @@ func (s *TranslateStore) TranslateIDs(ids []uint64) ([]string, error) {
// ForceSet writes the id/key pair to the store even if read only. Used by replication. // ForceSet writes the id/key pair to the store even if read only. Used by replication.
func (s *TranslateStore) ForceSet(id uint64, key string) error { func (s *TranslateStore) ForceSet(id uint64, key string) error {
if err := s.db.Update(func(tx *bolt.Tx) (err error) { if err := s.db.Update(func(tx *bolt.Tx) (err error) {
if err := tx.Bucket([]byte("keys")).Put([]byte(key), u64tob(id)); err != nil { if err := tx.Bucket(bucketKeys).Put([]byte(key), u64tob(id)); err != nil {
return err return err
} else if err := tx.Bucket([]byte("ids")).Put(u64tob(id), []byte(key)); err != nil { } else if err := tx.Bucket(bucketIDs).Put(u64tob(id), []byte(key)); err != nil {
return err return err
} }
return nil return nil
@ -286,7 +300,7 @@ func (s *TranslateStore) ForceSet(id uint64, key string) error {
return nil return nil
} }
// Reader returns a reader that streams the underlying data file. // EntryReader returns a reader that streams the underlying data file.
func (s *TranslateStore) EntryReader(ctx context.Context, offset uint64) (pilosa.TranslateEntryReader, error) { func (s *TranslateStore) EntryReader(ctx context.Context, offset uint64) (pilosa.TranslateEntryReader, error) {
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
return &TranslateEntryReader{ctx: ctx, cancel: cancel, store: s, offset: offset}, nil return &TranslateEntryReader{ctx: ctx, cancel: cancel, store: s, offset: offset}, nil
@ -311,9 +325,7 @@ func (s *TranslateStore) notifyWrite() {
// MaxID returns the highest id in the store. // MaxID returns the highest id in the store.
func (s *TranslateStore) MaxID() (max uint64, err error) { func (s *TranslateStore) MaxID() (max uint64, err error) {
if err := s.db.View(func(tx *bolt.Tx) error { if err := s.db.View(func(tx *bolt.Tx) error {
if key, _ := tx.Bucket([]byte("ids")).Cursor().Last(); key != nil { max = maxID(tx)
max = btou64(key)
}
return nil return nil
}); err != nil { }); err != nil {
return 0, err return 0, err
@ -321,6 +333,61 @@ func (s *TranslateStore) MaxID() (max uint64, err error) {
return max, nil return max, nil
} }
// WriteTo writes the contents of the store to the writer.
func (s *TranslateStore) WriteTo(w io.Writer) (int64, error) {
tx, err := s.db.Begin(false)
if err != nil {
return 0, err
}
defer func() { _ = tx.Rollback() }()
return tx.WriteTo(w)
}
// ReadFrom reads the content and overwrites the existing store.
func (s *TranslateStore) ReadFrom(r io.Reader) (n int64, err error) {
// Close store.
if err := s.Close(); err != nil {
return 0, errors.Wrap(err, "closing store")
}
// Create a temporary file to snapshot to.
snapshotPath := s.Path + snapshotExt
file, err := os.Create(snapshotPath)
if err != nil {
return n, errors.Wrap(err, "creating snapshot file")
}
// Write payload to snapshot.
if n, err = io.Copy(file, r); err != nil {
file.Close()
return n, errors.Wrap(err, "snapshot write to")
}
// we close the file here so we don't still have it open when trying
// to open it in a moment.
file.Close()
// Move snapshot to data file location.
if err := os.Rename(snapshotPath, s.Path); err != nil {
return n, errors.Wrap(err, "renaming snapshot")
}
// Re-open the store.
if err := s.Open(); err != nil {
return n, errors.Wrap(err, "re-opening store")
}
return n, nil
}
// MaxID returns the highest id in the store.
func maxID(tx *bolt.Tx) uint64 {
if key, _ := tx.Bucket(bucketIDs).Cursor().Last(); key != nil {
return btou64(key)
}
return 0
}
type TranslateEntryReader struct { type TranslateEntryReader struct {
ctx context.Context ctx context.Context
store *TranslateStore store *TranslateStore
@ -353,7 +420,7 @@ func (r *TranslateEntryReader) ReadEntry(entry *pilosa.TranslateEntry) error {
var found bool var found bool
if err := r.store.db.View(func(tx *bolt.Tx) error { if err := r.store.db.View(func(tx *bolt.Tx) error {
// Find ID/key lookup at offset or later. // Find ID/key lookup at offset or later.
cur := tx.Bucket([]byte("ids")).Cursor() cur := tx.Bucket(bucketIDs).Cursor()
key, value := cur.Seek(u64tob(r.offset)) key, value := cur.Seek(u64tob(r.offset))
if key == nil { if key == nil {
return nil return nil
@ -387,13 +454,73 @@ func (r *TranslateEntryReader) ReadEntry(entry *pilosa.TranslateEntry) error {
} }
} }
func findIDByKey(bkt *bolt.Bucket, key string) uint64 { // emptyKey is a sentinel byte slice which stands for "" as a key.
if value := bkt.Get([]byte(key)); value != nil { var emptyKey = []byte{
return btou64(value) 0x00, 0x00, 0x00,
0x4d, 0x54, 0x4d, 0x54, // MTMT
0x00,
0xc2, 0xa0, // NO-BREAK SPACE
0x00,
}
func findIDByKey(bkt *bolt.Bucket, key string) (uint64, []byte) {
var boltKey []byte
if key == "" {
boltKey = emptyKey
} else {
boltKey = []byte(key)
} }
return 0
if value := bkt.Get(boltKey); value != nil {
return btou64(value), boltKey
}
return 0, boltKey
} }
func findKeyByID(bkt *bolt.Bucket, id uint64) string { func findKeyByID(bkt *bolt.Bucket, id uint64) string {
return string(bkt.Get(u64tob(id))) boltKey := bkt.Get(u64tob(id))
if bytes.Equal(boltKey, emptyKey) {
return ""
}
return string(boltKey)
}
func (s *TranslateStore) ComputeTranslatorSummary() (sum *pilosa.TranslatorSummary, err error) {
sum = &pilosa.TranslatorSummary{}
hasher := blake3.New()
err = s.db.View(func(tx *bolt.Tx) error {
bkt := tx.Bucket(bucketKeys)
if bkt == nil {
panic("bucketKeys not found")
}
cur := bkt.Cursor()
for k, v := cur.First(); k != nil; k, v = cur.Next() {
input := append(k, v...)
_, _ = hasher.Write(input)
sum.KeyCount++
}
bkt = tx.Bucket(bucketIDs)
if bkt == nil {
panic("bucketIDs not found")
}
cur = bkt.Cursor()
for k, v := cur.First(); k != nil; k, v = cur.Next() {
input := append(k, v...)
_, _ = hasher.Write(input)
sum.IDCount++
}
return nil
})
if err != nil {
return nil, err
}
var buf [16]byte
_, _ = hasher.Digest().Read(buf[0:])
sum.Checksum = string(buf[:])
return sum, nil
} }

View file

@ -14,9 +14,12 @@
package boltdb_test package boltdb_test
import ( import (
"bytes"
"context" "context"
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"reflect"
"testing" "testing"
"time" "time"
@ -28,24 +31,23 @@ func TestTranslateStore_TranslateKey(t *testing.T) {
s := MustOpenNewTranslateStore() s := MustOpenNewTranslateStore()
defer MustCloseTranslateStore(s) defer MustCloseTranslateStore(s)
// Ensure initial key translates to ID 1. // Ensure initial key translates to first ID for shard
if id, err := s.TranslateKey("foo"); err != nil { id1, err := s.TranslateKey("foo", true)
if err != nil {
t.Fatal(err) t.Fatal(err)
} else if got, want := id, uint64(1); got != want {
t.Fatalf("TranslateKey()=%d, want %d", got, want)
} }
// Ensure next key autoincrements. // Ensure next key autoincrements.
if id, err := s.TranslateKey("bar"); err != nil { if id, err := s.TranslateKey("bar", true); err != nil {
t.Fatal(err) t.Fatal(err)
} else if got, want := id, uint64(2); got != want { } else if got, want := id, id1+1; got != want {
t.Fatalf("TranslateKey()=%d, want %d", got, want) t.Fatalf("TranslateKey()=%d, want %d", got, want)
} }
// Ensure retranslating existing key returns original ID. // Ensure retranslating existing key returns original ID.
if id, err := s.TranslateKey("foo"); err != nil { if id, err := s.TranslateKey("foo", true); err != nil {
t.Fatal(err) t.Fatal(err)
} else if got, want := id, uint64(1); got != want { } else if got, want := id, id1; got != want {
t.Fatalf("TranslateKey()=%d, want %d", got, want) t.Fatalf("TranslateKey()=%d, want %d", got, want)
} }
} }
@ -54,59 +56,142 @@ func TestTranslateStore_TranslateKeys(t *testing.T) {
s := MustOpenNewTranslateStore() s := MustOpenNewTranslateStore()
defer MustCloseTranslateStore(s) defer MustCloseTranslateStore(s)
// Ensure initial keys translate to incrementing IDs. ids, err := s.TranslateKeys([]string{"abc", "abc"}, true)
if ids, err := s.TranslateKeys([]string{"foo", "bar"}); err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} else if got, want := ids[0], uint64(1); got != want { } else if got, want := ids[1], ids[0]; got != want {
t.Fatalf("TranslateKeys()[0]=%d, want %d", got, want) t.Fatalf("TranslateKeys()[1]=%d, want %d", got, want)
} else if got, want := ids[1], uint64(2); got != want { }
// Ensure initial keys translate to incrementing IDs.
ids1, err := s.TranslateKeys([]string{"foo", "bar"}, true)
if err != nil {
t.Fatal(err)
} else if got, want := ids1[1], ids1[0]+1; got != want {
t.Fatalf("TranslateKeys()[1]=%d, want %d", got, want) t.Fatalf("TranslateKeys()[1]=%d, want %d", got, want)
} }
// Ensure retranslation returns original IDs. // Ensure retranslation returns original IDs.
if ids, err := s.TranslateKeys([]string{"foo", "bar"}); err != nil { if ids, err := s.TranslateKeys([]string{"foo", "bar"}, true); err != nil {
t.Fatal(err) t.Fatal(err)
} else if got, want := ids[0], uint64(1); got != want { } else if got, want := ids[0], ids1[0]; got != want {
t.Fatalf("TranslateKeys()[0]=%d, want %d", got, want) t.Fatalf("TranslateKeys()[0]=%d, want %d", got, want)
} else if got, want := ids[1], uint64(2); got != want { } else if got, want := ids[1], ids1[1]; got != want {
t.Fatalf("TranslateKeys()[1]=%d, want %d", got, want) t.Fatalf("TranslateKeys()[1]=%d, want %d", got, want)
} }
// Ensure retranslating with existing and non-existing keys returns correctly. // Ensure retranslating with existing and non-existing keys returns correctly.
if ids, err := s.TranslateKeys([]string{"foo", "baz", "bar"}); err != nil { if ids, err := s.TranslateKeys([]string{"foo", "baz", "bar"}, true); err != nil {
t.Fatal(err) t.Fatal(err)
} else if got, want := ids[0], uint64(1); got != want { } else if got, want := ids[0], ids1[0]; got != want {
t.Fatalf("TranslateKeys()[0]=%d, want %d", got, want) t.Fatalf("TranslateKeys()[0]=%d, want %d", got, want)
} else if got, want := ids[1], uint64(3); got != want { } else if got, want := ids[1], ids1[0]+2; got != want {
t.Fatalf("TranslateKeys()[1]=%d, want %d", got, want) t.Fatalf("TranslateKeys()[1]=%d, want %d", got, want)
} else if got, want := ids[2], uint64(2); got != want { } else if got, want := ids[2], ids1[1]; got != want {
t.Fatalf("TranslateKeys()[2]=%d, want %d", got, want) t.Fatalf("TranslateKeys()[2]=%d, want %d", got, want)
} }
} }
func TestTranslateStore_ReadKey(t *testing.T) {
s := MustOpenNewTranslateStore()
defer MustCloseTranslateStore(s)
id, err := s.TranslateKey("foo", false)
if err != pilosa.ErrTranslatingKeyNotFound {
t.Fatal(err)
}
if id != 0 {
t.Fatalf("TranslateKey()=%d, want %d", id, 0)
}
s.SetReadOnly(true)
id, err = s.TranslateKey("foo", true)
if err == nil {
t.Fatalf("got error: %+v, want: 'translate store read only'", err)
}
if id != 0 {
t.Fatalf("TranslateKey()=%d, want %d", id, 0)
}
s.SetReadOnly(false)
// Ensure next key autoincrements.
if id, err = s.TranslateKey("foo", true); err != nil {
t.Fatal(err)
}
id1, err := s.TranslateKey("foo", false)
if err != nil {
t.Fatal(err)
}
if id1 != id {
t.Fatalf("TranslateKey()=%d, want %d", id1, id)
}
}
func TestTranslateStore_ReadKeys(t *testing.T) {
s := MustOpenNewTranslateStore()
defer MustCloseTranslateStore(s)
ids, err := s.TranslateKeys([]string{"foo", "bar", "baz", "baz", "bar", "foo"}, false)
if err != pilosa.ErrTranslatingKeyNotFound {
t.Fatal(err)
}
for _, id := range ids {
if id != 0 {
t.Fatalf("TranslateKeys()=%d, want %d", id, 0)
}
}
// Ensure next key autoincrements.
if ids, err = s.TranslateKeys([]string{"foo", "bar", "baz", "baz", "bar", "foo"}, true); err != nil {
t.Fatal(err)
}
ids1, err := s.TranslateKeys([]string{"foo", "bar", "baz", "baz", "bar", "foo"}, false)
if err != nil {
t.Fatal(err)
}
for i := range ids1 {
if ids1[i] != ids[i] {
t.Fatalf("TranslateKeys()=%d, want %d", ids1[i], ids[i])
}
}
}
func TestTranslateStore_TranslateID(t *testing.T) { func TestTranslateStore_TranslateID(t *testing.T) {
s := MustOpenNewTranslateStore() s := MustOpenNewTranslateStore()
defer MustCloseTranslateStore(s) defer MustCloseTranslateStore(s)
// Setup initial keys. // Setup initial keys.
if _, err := s.TranslateKey("foo"); err != nil { id1, err := s.TranslateKey("foo", true)
if err != nil {
t.Fatal(err) t.Fatal(err)
} else if _, err := s.TranslateKey("bar"); err != nil { }
id2, err := s.TranslateKey("bar", true)
if err != nil {
t.Fatal(err)
}
id3, err := s.TranslateKey("", true)
if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// Ensure IDs can be translated back to keys. // Ensure IDs can be translated back to keys.
if key, err := s.TranslateID(1); err != nil { if key, err := s.TranslateID(id1); err != nil {
t.Fatal(err) t.Fatal(err)
} else if got, want := key, "foo"; got != want { } else if got, want := key, "foo"; got != want {
t.Fatalf("TranslateID()=%s, want %s", got, want) t.Fatalf("TranslateID()=%s, want %s", got, want)
} }
if key, err := s.TranslateID(2); err != nil { if key, err := s.TranslateID(id2); err != nil {
t.Fatal(err) t.Fatal(err)
} else if got, want := key, "bar"; got != want { } else if got, want := key, "bar"; got != want {
t.Fatalf("TranslateID()=%s, want %s", got, want) t.Fatalf("TranslateID()=%s, want %s", got, want)
} }
if key, err := s.TranslateID(id3); err != nil {
t.Fatal(err)
} else if got, want := key, ""; got != want {
t.Fatalf("TranslateID()=%s, want %s", got, want)
}
} }
func TestTranslateStore_TranslateIDs(t *testing.T) { func TestTranslateStore_TranslateIDs(t *testing.T) {
@ -114,12 +199,13 @@ func TestTranslateStore_TranslateIDs(t *testing.T) {
defer MustCloseTranslateStore(s) defer MustCloseTranslateStore(s)
// Setup initial keys. // Setup initial keys.
if _, err := s.TranslateKeys([]string{"foo", "bar"}); err != nil { ids, err := s.TranslateKeys([]string{"foo", "bar"}, true)
if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// Ensure IDs can be translated back to keys. // Ensure IDs can be translated back to keys.
if keys, err := s.TranslateIDs([]uint64{1, 2, 3}); err != nil { if keys, err := s.TranslateIDs([]uint64{ids[0], ids[1], 1}); err != nil {
t.Fatal(err) t.Fatal(err)
} else if got, want := keys[0], "foo"; got != want { } else if got, want := keys[0], "foo"; got != want {
t.Fatalf("TranslateIDs()[0]=%s, want %s", got, want) t.Fatalf("TranslateIDs()[0]=%s, want %s", got, want)
@ -136,7 +222,8 @@ func TestTranslateStore_EntryReader(t *testing.T) {
defer MustCloseTranslateStore(s) defer MustCloseTranslateStore(s)
// Create multiple new keys. // Create multiple new keys.
if _, err := s.TranslateKeys([]string{"foo", "bar"}); err != nil { ids1, err := s.TranslateKeys([]string{"foo", "bar"}, true)
if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -151,7 +238,7 @@ func TestTranslateStore_EntryReader(t *testing.T) {
// Read first entry. // Read first entry.
if err := r.ReadEntry(&entry); err != nil { if err := r.ReadEntry(&entry); err != nil {
t.Fatal(err) t.Fatal(err)
} else if got, want := entry.ID, uint64(1); got != want { } else if got, want := entry.ID, ids1[0]; got != want {
t.Fatalf("ReadEntry() ID=%d, want %d", got, want) t.Fatalf("ReadEntry() ID=%d, want %d", got, want)
} else if got, want := entry.Key, "foo"; got != want { } else if got, want := entry.Key, "foo"; got != want {
t.Fatalf("ReadEntry() Key=%s, want %s", got, want) t.Fatalf("ReadEntry() Key=%s, want %s", got, want)
@ -160,21 +247,22 @@ func TestTranslateStore_EntryReader(t *testing.T) {
// Read next entry. // Read next entry.
if err := r.ReadEntry(&entry); err != nil { if err := r.ReadEntry(&entry); err != nil {
t.Fatal(err) t.Fatal(err)
} else if got, want := entry.ID, uint64(2); got != want { } else if got, want := entry.ID, ids1[1]; got != want {
t.Fatalf("ReadEntry() ID=%d, want %d", got, want) t.Fatalf("ReadEntry() ID=%d, want %d", got, want)
} else if got, want := entry.Key, "bar"; got != want { } else if got, want := entry.Key, "bar"; got != want {
t.Fatalf("ReadEntry() Key=%s, want %s", got, want) t.Fatalf("ReadEntry() Key=%s, want %s", got, want)
} }
// Insert next key while reader is open. // Insert next key while reader is open.
if _, err := s.TranslateKey("baz"); err != nil { id2, err := s.TranslateKey("baz", true)
if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// Read newly created entry. // Read newly created entry.
if err := r.ReadEntry(&entry); err != nil { if err := r.ReadEntry(&entry); err != nil {
t.Fatal(err) t.Fatal(err)
} else if got, want := entry.ID, uint64(3); got != want { } else if got, want := entry.ID, id2; got != want {
t.Fatalf("ReadEntry() ID=%d, want %d", got, want) t.Fatalf("ReadEntry() ID=%d, want %d", got, want)
} else if got, want := entry.Key, "baz"; got != want { } else if got, want := entry.Key, "baz"; got != want {
t.Fatalf("ReadEntry() Key=%s, want %s", got, want) t.Fatalf("ReadEntry() Key=%s, want %s", got, want)
@ -198,20 +286,25 @@ func TestTranslateStore_EntryReader(t *testing.T) {
} }
defer r.Close() defer r.Close()
// cache holds the translated key id so we can check it later
cache := make(chan uint64)
// Insert key in separate goroutine. // Insert key in separate goroutine.
// Sleep momentarily to reader hangs. // Sleep momentarily to reader hangs.
translateErr := make(chan error) translateErr := make(chan error)
go func() { go func() {
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
if _, err := s.TranslateKey("foo"); err != nil { id, err := s.TranslateKey("foo", true)
if err != nil {
translateErr <- err translateErr <- err
} }
cache <- id
}() }()
var entry pilosa.TranslateEntry var entry pilosa.TranslateEntry
if err := r.ReadEntry(&entry); err != nil { if err := r.ReadEntry(&entry); err != nil {
t.Fatal(err) t.Fatal(err)
} else if got, want := entry.ID, uint64(1); got != want { } else if got, want := entry.ID, <-cache; got != want {
t.Fatalf("ReadEntry() ID=%d, want %d", got, want) t.Fatalf("ReadEntry() ID=%d, want %d", got, want)
} else if got, want := entry.Key, "foo"; got != want { } else if got, want := entry.Key, "foo"; got != want {
t.Fatalf("ReadEntry() Key=%s, want %s", got, want) t.Fatalf("ReadEntry() Key=%s, want %s", got, want)
@ -302,11 +395,76 @@ func MustNewTranslateStore() *boltdb.TranslateStore {
panic(err) panic(err)
} }
s := boltdb.NewTranslateStore("I", "F") s := boltdb.NewTranslateStore("I", "F", 0, pilosa.DefaultPartitionN)
s.Path = f.Name() s.Path = f.Name()
return s return s
} }
func TestTranslateStore_ReadWrite(t *testing.T) {
t.Run("WriteTo_ReadFrom", func(t *testing.T) {
s := MustOpenNewTranslateStore()
defer MustCloseTranslateStore(s)
batch0 := []string{}
for i := 0; i < 100; i++ {
batch0 = append(batch0, fmt.Sprintf("key%d", i))
}
batch1 := []string{}
for i := 100; i < 200; i++ {
batch1 = append(batch1, fmt.Sprintf("key%d", i))
}
// Populate the store with the keys in batch0.
batch0IDs, err := s.TranslateKeys(batch0, true)
if err != nil {
t.Fatal(err)
}
// Put the contents of the store into a buffer.
buf := bytes.NewBuffer(nil)
expN := int64(32768)
// After this, the buffer should contain batch0.
if n, err := s.WriteTo(buf); err != nil {
t.Fatalf("writing to buffer: %s", err)
} else if n != expN {
t.Fatalf("expected buffer size: %d, but got: %d", expN, n)
}
// Populate the store with the keys in batch1.
batch1IDs, err := s.TranslateKeys(batch1, true)
if err != nil {
t.Fatal(err)
}
expIDs := []uint64{batch0IDs[50], batch1IDs[50]}
// Check the IDs for a key from each batch.
if ids, err := s.TranslateKeys([]string{"key50", "key150"}, false); err != nil {
t.Fatal(err)
} else if !reflect.DeepEqual(expIDs, ids) {
t.Fatalf("first expected ids: %v, but got: %v", expIDs, ids)
}
// Reset the contents of the store with the data in the buffer.
if n, err := s.ReadFrom(buf); err != nil {
t.Fatalf("reading from buffer: %s", err)
} else if n != expN {
t.Fatalf("expected buffer size: %d, but got: %d", expN, n)
}
// This time, we expect the second key to be different because
// we overwrote the store, and then just set that key.
if ids, err := s.TranslateKeys([]string{"key50", "key150"}, true); err != nil {
t.Fatal(err)
} else if ids[0] != expIDs[0] {
t.Fatalf("last expected ids[0]: %d, but got: %d", expIDs[0], ids[0])
} else if ids[1] == expIDs[1] {
t.Fatalf("last expected different ids[1]: %d, but got: %d", expIDs[1], ids[1])
}
})
}
// MustOpenNewTranslateStore returns a new, opened TranslateStore. // MustOpenNewTranslateStore returns a new, opened TranslateStore.
func MustOpenNewTranslateStore() *boltdb.TranslateStore { func MustOpenNewTranslateStore() *boltdb.TranslateStore {
s := MustNewTranslateStore() s := MustNewTranslateStore()
@ -324,3 +482,54 @@ func MustCloseTranslateStore(s *boltdb.TranslateStore) {
panic(err) panic(err)
} }
} }
func TestCryptoHashPerKey(t *testing.T) {
s := MustOpenNewTranslateStore()
defer MustCloseTranslateStore(s)
// hash one translation
expect := map[int]string{
1: string([]byte{0x76, 0x48, 0x8b, 0x70, 0xe8, 0x54, 0x35, 0xc6, 0x8e, 0xa6, 0x4, 0x6c, 0xfa, 0xd2, 0x1a, 0x12}),
2: string([]byte{0x81, 0x46, 0x84, 0x37, 0x26, 0x96, 0x41, 0xf3, 0x54, 0x4e, 0x98, 0xbc, 0x48, 0xab, 0x1b, 0xf0}),
3: string([]byte{0x7f, 0xe9, 0xf, 0x6d, 0x7b, 0x14, 0x1, 0x44, 0xb2, 0x4e, 0xd0, 0x86, 0x2f, 0x62, 0x8c, 0xa9}),
}
for n := 1; n < 4; n++ {
var batch0 []string
for i := 0; i < n; i++ {
batch0 = append(batch0, fmt.Sprintf("key%d", i))
}
// Populate the store with the keys in batch0.
batch0IDs, err := s.TranslateKeys(batch0, true)
_ = batch0IDs
if err != nil {
t.Fatal(err)
}
// done with setup
sum, err := s.ComputeTranslatorSummary()
if err != nil {
panic(err)
}
nkey := sum.KeyCount
nid := sum.IDCount
observedChecksum := sum.Checksum
if nkey != n {
panic("wrong key count")
}
if nkey != nid {
panic("key count should match id count")
}
// shardwidth 22 has different hashes, of course.
if pilosa.ShardWidth == 20 {
expectedChecksum := expect[n]
if observedChecksum != expectedChecksum {
panic(fmt.Sprintf("got wrong checksum obs '%#v' vs expected '%#v'", observedChecksum, expectedChecksum))
}
}
}
}

View file

@ -69,6 +69,7 @@ const (
messageTypeRecalculateCaches messageTypeRecalculateCaches
messageTypeNodeEvent messageTypeNodeEvent
messageTypeNodeStatus messageTypeNodeStatus
messageTypeTransaction
) )
// MarshalInternalMessage serializes the pilosa message and adds pilosa internal // MarshalInternalMessage serializes the pilosa message and adds pilosa internal
@ -116,6 +117,8 @@ func getMessage(typ byte) Message {
return &NodeEvent{} return &NodeEvent{}
case messageTypeNodeStatus: case messageTypeNodeStatus:
return &NodeStatus{} return &NodeStatus{}
case messageTypeTransaction:
return &TransactionMessage{}
default: default:
panic(fmt.Sprintf("unknown message type %d", typ)) panic(fmt.Sprintf("unknown message type %d", typ))
} }
@ -155,6 +158,8 @@ func getMessageType(m Message) byte {
return messageTypeNodeEvent return messageTypeNodeEvent
case *NodeStatus: case *NodeStatus:
return messageTypeNodeStatus return messageTypeNodeStatus
case *TransactionMessage:
return messageTypeTransaction
default: default:
panic(fmt.Sprintf("don't have type for message %#v", m)) panic(fmt.Sprintf("don't have type for message %#v", m))
} }

218
cache.go
View file

@ -16,6 +16,7 @@ package pilosa
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"io" "io"
"sort" "sort"
@ -23,7 +24,9 @@ import (
"time" "time"
"github.com/pilosa/pilosa/v2/lru" "github.com/pilosa/pilosa/v2/lru"
pb "github.com/pilosa/pilosa/v2/proto"
"github.com/pilosa/pilosa/v2/stats" "github.com/pilosa/pilosa/v2/stats"
"github.com/pkg/errors"
) )
const ( const (
@ -118,7 +121,8 @@ func (c *lruCache) Top() []bitmapPair {
Count: n, Count: n,
}) })
} }
sort.Sort(bitmapPairs(a)) pairs := bitmapPairs(a)
sort.Sort(&pairs)
return a return a
} }
@ -134,9 +138,11 @@ var _ cache = &lruCache{}
// rankCache represents a cache with sorted entries. // rankCache represents a cache with sorted entries.
type rankCache struct { type rankCache struct {
mu sync.Mutex mu sync.Mutex
entries map[uint64]uint64 entries map[uint64]uint64
rankings []bitmapPair // cached, ordered list rankings bitmapPairs // cached, ordered list
rankingsRead bool
dirty bool
updateN int updateN int
updateTime time.Time updateTime time.Time
@ -168,10 +174,16 @@ func NewRankCache(maxEntries uint32) *rankCache {
func (c *rankCache) Add(id uint64, n uint64) { func (c *rankCache) Add(id uint64, n uint64) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
// Flag the cache as dirty.
// This forces recalculation if top is called before the cache is recalculated.
c.dirty = true
// Ignore if the column count is below the threshold, // Ignore if the column count is below the threshold,
// unless the count is 0, which is effectively used // unless the count is 0, which is effectively used
// to clear the cache value. // to clear the cache value.
if n < c.thresholdValue && n > 0 { if n < c.thresholdValue && n > 0 {
delete(c.entries, id)
return return
} }
@ -184,7 +196,13 @@ func (c *rankCache) Add(id uint64, n uint64) {
func (c *rankCache) BulkAdd(id uint64, n uint64) { func (c *rankCache) BulkAdd(id uint64, n uint64) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
// Flag the cache as dirty.
// This forces recalculation if top is called before the cache is recalculated.
c.dirty = true
if n < c.thresholdValue { if n < c.thresholdValue {
delete(c.entries, id)
return return
} }
@ -209,12 +227,15 @@ func (c *rankCache) Len() int {
func (c *rankCache) IDs() []uint64 { func (c *rankCache) IDs() []uint64 {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
a := make([]uint64, 0, len(c.entries)) if len(c.entries) == 0 {
for id := range c.entries { return nil
a = append(a, id)
} }
sort.Sort(uint64Slice(a)) ids := make([]uint64, 0, len(c.entries))
return a for id := range c.entries {
ids = append(ids, id)
}
sort.Sort(uint64Slice(ids))
return ids
} }
// Invalidate recalculates the entries by rank. // Invalidate recalculates the entries by rank.
@ -228,7 +249,7 @@ func (c *rankCache) Invalidate() {
func (c *rankCache) Recalculate() { func (c *rankCache) Recalculate() {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
c.stats.Count("cache.recalculate", 1, 1.0) c.stats.Count(MetricRecalculateCache, 1, 1.0)
c.recalculate() c.recalculate()
} }
@ -236,27 +257,40 @@ func (c *rankCache) invalidate() {
// Don't invalidate more than once every X seconds. // Don't invalidate more than once every X seconds.
// TODO: consider making this configurable. // TODO: consider making this configurable.
if time.Since(c.updateTime).Seconds() < 10 { if time.Since(c.updateTime).Seconds() < 10 {
// Skipping recalculation means that the ranked cache's growth is unbounded.
// This is somewhat necessary for now since recalculation is not cheap.
// The cache will remain flagged as dirty and will be recalculated if Top is called.
// This may cause unexpected memory growth, so record it in metrics for debugging purposes.
c.stats.Count(MetricInvalidateCacheSkipped, 1, 1.0)
return return
} }
c.stats.Count("cache.invalidate", 1, 1.0) c.stats.Count(MetricInvalidateCache, 1, 1.0)
c.recalculate() c.recalculate()
} }
func (c *rankCache) recalculate() { func (c *rankCache) recalculate() {
if c.rankingsRead {
c.rankings = nil
c.rankingsRead = false
}
// Convert cache to a sorted list. // Convert cache to a sorted list.
rankings := make([]bitmapPair, 0, len(c.entries)) rankings := c.rankings[:0]
if cap(rankings) < len(c.entries) {
rankings = make([]bitmapPair, 0, len(c.entries))
}
for id, cnt := range c.entries { for id, cnt := range c.entries {
rankings = append(rankings, bitmapPair{ rankings = append(rankings, bitmapPair{
ID: id, ID: id,
Count: cnt, Count: cnt,
}) })
} }
sort.Sort(bitmapPairs(rankings)) c.rankings = rankings
sort.Sort(&c.rankings)
// Store the count of the item at the threshold index. // Store the count of the item at the threshold index.
c.rankings = rankings
length := len(c.rankings) length := len(c.rankings)
c.stats.Gauge("RankCache", float64(length), 1.0) c.stats.Gauge(MetricRankCacheLength, float64(length), 1.0)
var removeItems []bitmapPair // cached, ordered list var removeItems []bitmapPair // cached, ordered list
if length > int(c.maxEntries) { if length > int(c.maxEntries) {
@ -272,11 +306,14 @@ func (c *rankCache) recalculate() {
// If size is larger than the threshold then trim it. // If size is larger than the threshold then trim it.
if len(c.entries) > c.thresholdBuffer { if len(c.entries) > c.thresholdBuffer {
c.stats.Count("cache.threshold", 1, 1.0) c.stats.Count(MetricCacheThresholdReached, 1, 1.0)
for _, pair := range removeItems { for _, pair := range removeItems {
delete(c.entries, pair.ID) delete(c.entries, pair.ID)
} }
} }
// The cache is no longer dirty.
c.dirty = false
} }
// SetStats defines the stats client used in the cache. // SetStats defines the stats client used in the cache.
@ -285,7 +322,19 @@ func (c *rankCache) SetStats(s stats.StatsClient) {
} }
// Top returns an ordered list of pairs. // Top returns an ordered list of pairs.
func (c *rankCache) Top() []bitmapPair { return c.rankings } func (c *rankCache) Top() []bitmapPair {
c.mu.Lock()
defer c.mu.Unlock()
if c.dirty {
// The cache is dirty, so we need to recalculate it to get a consistent view.
c.stats.Count(MetricReadDirtyCache, 1, 1.0)
c.recalculate()
}
c.rankingsRead = true
return c.rankings
}
// WriteTo writes the cache to w. // WriteTo writes the cache to w.
func (c *rankCache) WriteTo(w io.Writer) (n int64, err error) { func (c *rankCache) WriteTo(w io.Writer) (n int64, err error) {
@ -309,17 +358,68 @@ type bitmapPair struct {
// bitmapPairs is a sortable list of BitmapPair objects. // bitmapPairs is a sortable list of BitmapPair objects.
type bitmapPairs []bitmapPair type bitmapPairs []bitmapPair
func (p bitmapPairs) Swap(i, j int) { p[i], p[j] = p[j], p[i] } func (p *bitmapPairs) Swap(i, j int) { (*p)[i], (*p)[j] = (*p)[j], (*p)[i] }
func (p bitmapPairs) Len() int { return len(p) } func (p *bitmapPairs) Len() int { return len(*p) }
func (p bitmapPairs) Less(i, j int) bool { return p[i].Count > p[j].Count } func (p *bitmapPairs) Less(i, j int) bool { return (*p)[i].Count > (*p)[j].Count }
// Pair holds an id/count pair. // Pair holds an id/count pair.
type Pair struct { type Pair struct {
ID uint64 `json:"id"` ID uint64 `json:"id"`
Key string `json:"key,omitempty"` Key string `json:"key"`
Count uint64 `json:"count"` Count uint64 `json:"count"`
} }
// PairField is a Pair with its associated field.
type PairField struct {
Pair Pair
Field string
}
func (p PairField) Clone() (r PairField) {
return PairField{
Pair: p.Pair,
Field: p.Field,
}
}
// ToTable implements the ToTabler interface.
func (p PairField) ToTable() (*pb.TableResponse, error) {
return pb.RowsToTable(p, 1)
}
// ToRows implements the ToRowser interface.
func (p PairField) ToRows(callback func(*pb.RowResponse) error) error {
if p.Pair.Key != "" {
return callback(&pb.RowResponse{
Headers: []*pb.ColumnInfo{
{Name: p.Field, Datatype: "string"},
{Name: "count", Datatype: "uint64"},
},
Columns: []*pb.ColumnResponse{
&pb.ColumnResponse{ColumnVal: &pb.ColumnResponse_StringVal{StringVal: p.Pair.Key}},
&pb.ColumnResponse{ColumnVal: &pb.ColumnResponse_Uint64Val{Uint64Val: p.Pair.Count}},
},
})
} else {
return callback(&pb.RowResponse{
Headers: []*pb.ColumnInfo{
{Name: p.Field, Datatype: "uint64"},
{Name: "count", Datatype: "uint64"},
},
Columns: []*pb.ColumnResponse{
&pb.ColumnResponse{ColumnVal: &pb.ColumnResponse_Uint64Val{Uint64Val: p.Pair.ID}},
&pb.ColumnResponse{ColumnVal: &pb.ColumnResponse_Uint64Val{Uint64Val: p.Pair.Count}},
},
})
}
}
// MarshalJSON marshals PairField into a JSON-encoded byte slice,
// excluding `Field`.
func (p PairField) MarshalJSON() ([]byte, error) {
return json.Marshal(p.Pair)
}
// Pairs is a sortable slice of Pair objects. // Pairs is a sortable slice of Pair objects.
type Pairs []Pair type Pairs []Pair
@ -395,6 +495,82 @@ func (p Pairs) String() string {
return buf.String() return buf.String()
} }
// PairsField is a Pairs object with its associated field.
type PairsField struct {
Pairs []Pair
Field string
}
func (p *PairsField) Clone() (r *PairsField) {
r = &PairsField{
Pairs: make([]Pair, len(p.Pairs)),
Field: p.Field,
}
copy(r.Pairs, p.Pairs)
return
}
// ToTable implements the ToTabler interface.
func (p *PairsField) ToTable() (*pb.TableResponse, error) {
return pb.RowsToTable(p, len(p.Pairs))
}
// ToRows implements the ToRowser interface.
func (p *PairsField) ToRows(callback func(*pb.RowResponse) error) error {
// Determine if the ID has string keys.
var stringKeys bool
if len(p.Pairs) > 0 {
if p.Pairs[0].Key != "" {
stringKeys = true
}
}
dtype := "uint64"
if stringKeys {
dtype = "string"
}
ci := []*pb.ColumnInfo{
{Name: p.Field, Datatype: dtype},
{Name: "count", Datatype: "uint64"},
}
for _, pair := range p.Pairs {
if stringKeys {
if err := callback(&pb.RowResponse{
Headers: ci,
Columns: []*pb.ColumnResponse{
&pb.ColumnResponse{ColumnVal: &pb.ColumnResponse_StringVal{StringVal: pair.Key}},
&pb.ColumnResponse{ColumnVal: &pb.ColumnResponse_Uint64Val{Uint64Val: uint64(pair.Count)}},
}}); err != nil {
return errors.Wrap(err, "calling callback")
}
} else {
if err := callback(&pb.RowResponse{
Headers: ci,
Columns: []*pb.ColumnResponse{
&pb.ColumnResponse{ColumnVal: &pb.ColumnResponse_Uint64Val{Uint64Val: uint64(pair.ID)}},
&pb.ColumnResponse{ColumnVal: &pb.ColumnResponse_Uint64Val{Uint64Val: uint64(pair.Count)}},
}}); err != nil {
return errors.Wrap(err, "calling callback")
}
}
ci = nil //only send on the first
}
return nil
}
// MarshalJSON marshals PairsField into a JSON-encoded byte slice,
// excluding `Field`.
func (p PairsField) MarshalJSON() ([]byte, error) {
return json.Marshal(p.Pairs)
}
// int64Slice represents a sortable slice of int64 numbers.
type int64Slice []int64
func (p int64Slice) Len() int { return len(p) }
func (p int64Slice) Less(i, j int) bool { return p[i] < p[j] }
func (p int64Slice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
// uint64Slice represents a sortable slice of uint64 numbers. // uint64Slice represents a sortable slice of uint64 numbers.
type uint64Slice []uint64 type uint64Slice []uint64

View file

@ -15,13 +15,14 @@
package pilosa_test package pilosa_test
import ( import (
"reflect"
"testing" "testing"
"github.com/pilosa/pilosa/v2" "github.com/pilosa/pilosa/v2"
) )
// Ensure a bitmap query can be executed. // Ensure cache stays constrained to its configured size.
func TestCache_Rank(t *testing.T) { func TestCache_Rank_Size(t *testing.T) {
cacheSize := uint32(3) cacheSize := uint32(3)
cache := pilosa.NewRankCache(cacheSize) cache := pilosa.NewRankCache(cacheSize)
for i := 1; i < int(2*cacheSize); i++ { for i := 1; i < int(2*cacheSize); i++ {
@ -31,5 +32,54 @@ func TestCache_Rank(t *testing.T) {
if cache.Len() != int(cacheSize) { if cache.Len() != int(cacheSize) {
t.Fatalf("unexpected cache Size: %d!=%d expected\n", cache.Len(), cacheSize) t.Fatalf("unexpected cache Size: %d!=%d expected\n", cache.Len(), cacheSize)
} }
}
// Ensure cache entries set below threshold are handled appropriately.
func TestCache_Rank_Threshold(t *testing.T) {
cacheSize := uint32(5)
cache := pilosa.NewRankCache(cacheSize)
for i := 1; i < int(2*cacheSize); i++ {
cache.Add(uint64(i), 3)
}
// Set the cache value for rows 4 and 5 to a number below the threshold
// value (which is 3), and ensure that they gets zeroed out.
cache.Add(4, 1)
cache.BulkAdd(5, 1)
cache.Recalculate()
if cache.Get(4) != 0 {
t.Fatalf("unexpected cache value after Add: %d!=%d expected\n", cache.Get(4), 0)
}
if cache.Get(5) != 0 {
t.Fatalf("unexpected cache value after BulkAdd: %d!=%d expected\n", cache.Get(5), 0)
}
}
// Test that consecutive writes show up in Top.
// On later writes, the cache skips recalculation to save CPU time.
// This used to mean that the later writes would not show up in Top.
// Now, the cache is flagged as dirty and recalculated during the call to Top.
func TestCache_Rank_Dirty(t *testing.T) {
cacheSize := uint32(5)
cache := pilosa.NewRankCache(cacheSize)
type pair struct{ ID, Count uint64 }
expect := []pair{
{5, 2},
{4, 1},
}
for _, v := range expect {
cache.Add(v.ID, v.Count)
}
var got []pair
for _, p := range cache.Top() {
got = append(got, pair(p))
}
if !reflect.DeepEqual(expect, got) {
t.Fatalf("wrote %v but got %v", expect, got)
}
} }

318
catcher.go Normal file
View file

@ -0,0 +1,318 @@
// Copyright 2020 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pilosa
import (
"fmt"
"io"
"github.com/pilosa/pilosa/v2/roaring"
)
// catcher is useful to report error locations with a
// stack dump before the complexity
// of the executor_test swallows up
// the location of a panic.
type catcherTx struct {
b Tx
}
func newCatcherTx(b Tx) *catcherTx {
return &catcherTx{b: b}
}
func init() {
// keep golangci-lint happy
_ = newCatcherTx
}
var _ Tx = (*catcherTx)(nil)
func (c *catcherTx) IncrementOpN(index, field, view string, shard uint64, changedN int) {
c.b.IncrementOpN(index, field, view, shard, changedN)
}
func (c *catcherTx) NewTxIterator(index, field, view string, shard uint64) *roaring.Iterator {
return c.b.NewTxIterator(index, field, view, shard)
}
func (c *catcherTx) ImportRoaringBits(index, field, view string, shard uint64, rit roaring.RoaringIterator, clear bool, log bool, rowSize uint64, data []byte) (changed int, rowSet map[uint64]int, err error) {
defer func() {
if r := recover(); r != nil {
AlwaysPrintf("see ImportRoaringBits() panic '%v' at '%v'", r, stack())
panic(r)
}
}()
return c.b.ImportRoaringBits(index, field, view, shard, rit, clear, log, rowSize, data)
}
func (c *catcherTx) Dump(short bool, shard uint64) {
c.b.Dump(short, shard)
}
func (c *catcherTx) Readonly() bool {
defer func() {
if r := recover(); r != nil {
AlwaysPrintf("see Readonly() panic '%v' at '%v'", r, stack())
panic(r)
}
}()
return c.b.Readonly()
}
func (tx *catcherTx) Pointer() string {
return fmt.Sprintf("%p", tx)
}
func (c *catcherTx) Rollback() {
defer func() {
if r := recover(); r != nil {
AlwaysPrintf("see Rollback() panic '%v' at '%v'", r, stack())
panic(r)
}
}()
c.b.Rollback()
}
func (c *catcherTx) Commit() error {
defer func() {
if r := recover(); r != nil {
AlwaysPrintf("see Commit() panic '%v' at '%v'", r, stack())
panic(r)
}
}()
return c.b.Commit()
}
func (c *catcherTx) RoaringBitmap(index, field, view string, shard uint64) (*roaring.Bitmap, error) {
defer func() {
if r := recover(); r != nil {
AlwaysPrintf("see RoaringBitmap() panic '%v' at '%v'", r, stack())
panic(r)
}
}()
return c.b.RoaringBitmap(index, field, view, shard)
}
func (c *catcherTx) Container(index, field, view string, shard uint64, key uint64) (ct *roaring.Container, err error) {
defer func() {
if r := recover(); r != nil {
AlwaysPrintf("see Container() panic '%v' at '%v'", r, stack())
panic(r)
}
}()
return c.b.Container(index, field, view, shard, key)
}
func (c *catcherTx) PutContainer(index, field, view string, shard uint64, key uint64, rc *roaring.Container) error {
defer func() {
if r := recover(); r != nil {
AlwaysPrintf("see PutContainer() panic '%v' at '%v'", r, stack())
panic(r)
}
}()
return c.b.PutContainer(index, field, view, shard, key, rc)
}
func (c *catcherTx) RemoveContainer(index, field, view string, shard uint64, key uint64) error {
defer func() {
if r := recover(); r != nil {
AlwaysPrintf("see RemoveContainer() panic '%v' at '%v'", r, stack())
panic(r)
}
}()
return c.b.RemoveContainer(index, field, view, shard, key)
}
func (c *catcherTx) UseRowCache() bool {
return c.b.UseRowCache()
}
func (c *catcherTx) IsDone() bool {
return c.b.IsDone()
}
func (c *catcherTx) Add(index, field, view string, shard uint64, batched bool, a ...uint64) (changeCount int, err error) {
defer func() {
if r := recover(); r != nil {
AlwaysPrintf("see Add() panic '%v' at '%v'", r, stack())
panic(r)
}
}()
return c.b.Add(index, field, view, shard, batched, a...)
}
func (c *catcherTx) Remove(index, field, view string, shard uint64, a ...uint64) (changeCount int, err error) {
defer func() {
if r := recover(); r != nil {
AlwaysPrintf("see Remove() panic '%v' at '%v'", r, stack())
panic(r)
}
}()
return c.b.Remove(index, field, view, shard, a...)
}
func (c *catcherTx) Contains(index, field, view string, shard uint64, key uint64) (exists bool, err error) {
defer func() {
if r := recover(); r != nil {
AlwaysPrintf("see Contains() panic '%v' at '%v'", r, stack())
panic(r)
}
}()
return c.b.Contains(index, field, view, shard, key)
}
func (c *catcherTx) ContainerIterator(index, field, view string, shard uint64, firstRoaringContainerKey uint64) (citer roaring.ContainerIterator, found bool, err error) {
defer func() {
if r := recover(); r != nil {
AlwaysPrintf("see ContainerIterator() panic '%v' at '%v'", r, stack())
panic(r)
}
}()
return c.b.ContainerIterator(index, field, view, shard, firstRoaringContainerKey)
}
func (c *catcherTx) ForEach(index, field, view string, shard uint64, fn func(i uint64) error) error {
defer func() {
if r := recover(); r != nil {
AlwaysPrintf("see ForEach() panic '%v' at '%v'", r, stack())
panic(r)
}
}()
return c.b.ForEach(index, field, view, shard, fn)
}
func (c *catcherTx) ForEachRange(index, field, view string, shard uint64, start, end uint64, fn func(uint64) error) error {
defer func() {
if r := recover(); r != nil {
AlwaysPrintf("see ForEachRange() panic '%v' at '%v'", r, stack())
panic(r)
}
}()
return c.b.ForEachRange(index, field, view, shard, start, end, fn)
}
func (c *catcherTx) Count(index, field, view string, shard uint64) (uint64, error) {
defer func() {
if r := recover(); r != nil {
AlwaysPrintf("see Count() panic '%v' at '%v'", r, stack())
panic(r)
}
}()
return c.b.Count(index, field, view, shard)
}
func (c *catcherTx) Max(index, field, view string, shard uint64) (uint64, error) {
defer func() {
if r := recover(); r != nil {
AlwaysPrintf("see Max() panic '%v' at '%v'", r, stack())
panic(r)
}
}()
return c.b.Max(index, field, view, shard)
}
func (c *catcherTx) Min(index, field, view string, shard uint64) (uint64, bool, error) {
defer func() {
if r := recover(); r != nil {
AlwaysPrintf("see Min() panic '%v' at '%v'", r, stack())
panic(r)
}
}()
return c.b.Min(index, field, view, shard)
}
func (c *catcherTx) UnionInPlace(index, field, view string, shard uint64, others ...*roaring.Bitmap) error {
defer func() {
if r := recover(); r != nil {
AlwaysPrintf("see UnionInPlace() panic '%v' at '%v'", r, stack())
panic(r)
}
}()
return c.b.UnionInPlace(index, field, view, shard, others...)
}
func (c *catcherTx) CountRange(index, field, view string, shard uint64, start, end uint64) (n uint64, err error) {
defer func() {
if r := recover(); r != nil {
AlwaysPrintf("see CountRange() panic '%v' at '%v'", r, stack())
panic(r)
}
}()
return c.b.CountRange(index, field, view, shard, start, end)
}
func (c *catcherTx) OffsetRange(index, field, view string, shard, offset, start, end uint64) (other *roaring.Bitmap, err error) {
defer func() {
if r := recover(); r != nil {
AlwaysPrintf("see OffsetRange() panic '%v' at '%v'", r, stack())
panic(r)
}
}()
return c.b.OffsetRange(index, field, view, shard, offset, start, end)
}
func (c *catcherTx) RoaringBitmapReader(index, field, view string, shard uint64, fragmentPathForRoaring string) (r io.ReadCloser, sz int64, err error) {
defer func() {
if r := recover(); r != nil {
AlwaysPrintf("see RoaringBitmapReader() panic '%v' at '%v'", r, stack())
panic(r)
}
}()
return c.b.RoaringBitmapReader(index, field, view, shard, fragmentPathForRoaring)
}
func (c *catcherTx) Type() string {
return c.b.Type()
}
func (c *catcherTx) SliceOfShards(index, field, view, optionalViewPath string) (sliceOfShards []uint64, err error) {
defer func() {
if r := recover(); r != nil {
AlwaysPrintf("see SliceOfShards() panic '%v' at '%v'", r, stack())
panic(r)
}
}()
return c.b.SliceOfShards(index, field, view, optionalViewPath)
}
func (c *catcherTx) Group() *TxGroup {
return c.b.Group()
}
func (c *catcherTx) Options() Txo {
return c.b.Options()
}
// Sn retreives the serial number of the Tx.
func (c *catcherTx) Sn() int64 {
return c.b.Sn()
}

View file

@ -17,6 +17,7 @@ package pilosa
import ( import (
"context" "context"
"io" "io"
"time"
) )
// Bit represents the intersection of a row and a column. It can be specified by // Bit represents the intersection of a row and a column. It can be specified by
@ -43,7 +44,11 @@ type FieldValue struct {
// something hasn't been architected correctly. // something hasn't been architected correctly.
// While I understand that putting the entire Client behind an interface might require this many methods, // While I understand that putting the entire Client behind an interface might require this many methods,
// I don't want to let it go unquestioned. // I don't want to let it go unquestioned.
// Another note from Travis: I think we eventually want to unify `InternalClient` with the `go-pilosa` client.
// Doing that may obviate the need to refactor this.
type InternalClient interface { type InternalClient interface {
InternalQueryClient
MaxShardByIndex(ctx context.Context) (map[string]uint64, error) MaxShardByIndex(ctx context.Context) (map[string]uint64, error)
Schema(ctx context.Context) ([]*IndexInfo, error) Schema(ctx context.Context) ([]*IndexInfo, error)
PostSchema(ctx context.Context, uri *URI, s *Schema, remote bool) error PostSchema(ctx context.Context, uri *URI, s *Schema, remote bool) error
@ -51,7 +56,6 @@ type InternalClient interface {
FragmentNodes(ctx context.Context, index string, shard uint64) ([]*Node, error) FragmentNodes(ctx context.Context, index string, shard uint64) ([]*Node, error)
Nodes(ctx context.Context) ([]*Node, error) Nodes(ctx context.Context) ([]*Node, error)
Query(ctx context.Context, index string, queryRequest *QueryRequest) (*QueryResponse, error) Query(ctx context.Context, index string, queryRequest *QueryRequest) (*QueryResponse, error)
QueryNode(ctx context.Context, uri *URI, index string, queryRequest *QueryRequest) (*QueryResponse, error)
Import(ctx context.Context, index, field string, shard uint64, bits []Bit, opts ...ImportOption) error Import(ctx context.Context, index, field string, shard uint64, bits []Bit, opts ...ImportOption) error
ImportK(ctx context.Context, index, field string, bits []Bit, opts ...ImportOption) error ImportK(ctx context.Context, index, field string, bits []Bit, opts ...ImportOption) error
EnsureIndex(ctx context.Context, name string, options IndexOptions) error EnsureIndex(ctx context.Context, name string, options IndexOptions) error
@ -59,6 +63,7 @@ type InternalClient interface {
EnsureFieldWithOptions(ctx context.Context, index, field string, opt FieldOptions) error EnsureFieldWithOptions(ctx context.Context, index, field string, opt FieldOptions) error
ImportValue(ctx context.Context, index, field string, shard uint64, vals []FieldValue, opts ...ImportOption) error ImportValue(ctx context.Context, index, field string, shard uint64, vals []FieldValue, opts ...ImportOption) error
ImportValueK(ctx context.Context, index, field string, vals []FieldValue, opts ...ImportOption) error ImportValueK(ctx context.Context, index, field string, vals []FieldValue, opts ...ImportOption) error
ImportValue2(ctx context.Context, req *ImportValueRequest, options *ImportOptions) error
ExportCSV(ctx context.Context, index, field string, shard uint64, w io.Writer) error ExportCSV(ctx context.Context, index, field string, shard uint64, w io.Writer) error
CreateField(ctx context.Context, index, field string) error CreateField(ctx context.Context, index, field string) error
CreateFieldWithOptions(ctx context.Context, index, field string, opt FieldOptions) error CreateFieldWithOptions(ctx context.Context, index, field string, opt FieldOptions) error
@ -68,7 +73,14 @@ type InternalClient interface {
RowAttrDiff(ctx context.Context, uri *URI, index, field string, blks []AttrBlock) (map[uint64]map[string]interface{}, error) RowAttrDiff(ctx context.Context, uri *URI, index, field string, blks []AttrBlock) (map[uint64]map[string]interface{}, error)
SendMessage(ctx context.Context, uri *URI, msg []byte) error SendMessage(ctx context.Context, uri *URI, msg []byte) error
RetrieveShardFromURI(ctx context.Context, index, field, view string, shard uint64, uri URI) (io.ReadCloser, error) RetrieveShardFromURI(ctx context.Context, index, field, view string, shard uint64, uri URI) (io.ReadCloser, error)
RetrieveTranslatePartitionFromURI(ctx context.Context, index string, partition int, uri URI) (io.ReadCloser, error)
ImportRoaring(ctx context.Context, uri *URI, index, field string, shard uint64, remote bool, req *ImportRoaringRequest) error ImportRoaring(ctx context.Context, uri *URI, index, field string, shard uint64, remote bool, req *ImportRoaringRequest) error
ImportColumnAttrs(ctx context.Context, uri *URI, index string, req *ImportColumnAttrsRequest) error
StartTransaction(ctx context.Context, id string, timeout time.Duration, exclusive bool) (*Transaction, error)
FinishTransaction(ctx context.Context, id string) (*Transaction, error)
Transactions(ctx context.Context) (map[string]*Transaction, error)
GetTransaction(ctx context.Context, id string) (*Transaction, error)
} }
//=============== //===============
@ -76,6 +88,10 @@ type InternalClient interface {
// InternalQueryClient is the internal interface for querying a node. // InternalQueryClient is the internal interface for querying a node.
type InternalQueryClient interface { type InternalQueryClient interface {
QueryNode(ctx context.Context, uri *URI, index string, queryRequest *QueryRequest) (*QueryResponse, error) QueryNode(ctx context.Context, uri *URI, index string, queryRequest *QueryRequest) (*QueryResponse, error)
// Trasnlate keys on the particular node. The parameter writable informs TranslateStore if we can generate a new ID if any of keys does not exist.
TranslateKeysNode(ctx context.Context, uri *URI, index, field string, keys []string, writable bool) ([]uint64, error)
TranslateIDsNode(ctx context.Context, uri *URI, index, field string, id []uint64) ([]string, error)
} }
type nopInternalQueryClient struct{} type nopInternalQueryClient struct{}
@ -84,6 +100,14 @@ func (n *nopInternalQueryClient) QueryNode(ctx context.Context, uri *URI, index
return nil, nil return nil, nil
} }
func (n nopInternalQueryClient) TranslateKeysNode(ctx context.Context, uri *URI, index, field string, keys []string, writable bool) ([]uint64, error) {
return nil, nil
}
func (n nopInternalQueryClient) TranslateIDsNode(ctx context.Context, uri *URI, index, field string, ids []uint64) ([]string, error) {
return nil, nil
}
func newNopInternalQueryClient() *nopInternalQueryClient { func newNopInternalQueryClient() *nopInternalQueryClient {
return &nopInternalQueryClient{} return &nopInternalQueryClient{}
} }
@ -123,15 +147,30 @@ func (n nopInternalClient) Query(ctx context.Context, index string, queryRequest
func (n nopInternalClient) QueryNode(ctx context.Context, uri *URI, index string, queryRequest *QueryRequest) (*QueryResponse, error) { func (n nopInternalClient) QueryNode(ctx context.Context, uri *URI, index string, queryRequest *QueryRequest) (*QueryResponse, error) {
return nil, nil return nil, nil
} }
func (n nopInternalClient) TranslateKeysNode(ctx context.Context, uri *URI, index, field string, keys []string, writable bool) ([]uint64, error) {
return nil, nil
}
func (n nopInternalClient) TranslateIDsNode(ctx context.Context, uri *URI, index, field string, ids []uint64) ([]string, error) {
return nil, nil
}
func (n nopInternalClient) Import(ctx context.Context, index, field string, shard uint64, bits []Bit, opts ...ImportOption) error { func (n nopInternalClient) Import(ctx context.Context, index, field string, shard uint64, bits []Bit, opts ...ImportOption) error {
return nil return nil
} }
func (n nopInternalClient) ImportK(ctx context.Context, index, field string, bits []Bit, opts ...ImportOption) error { func (n nopInternalClient) ImportK(ctx context.Context, index, field string, bits []Bit, opts ...ImportOption) error {
return nil return nil
} }
func (n nopInternalClient) ImportValue2(ctx context.Context, req *ImportValueRequest, options *ImportOptions) error {
return nil
}
func (n nopInternalClient) ImportRoaring(ctx context.Context, uri *URI, index, field string, shard uint64, remote bool, req *ImportRoaringRequest) error { func (n nopInternalClient) ImportRoaring(ctx context.Context, uri *URI, index, field string, shard uint64, remote bool, req *ImportRoaringRequest) error {
return nil return nil
} }
func (n nopInternalClient) ImportColumnAttrs(ctx context.Context, uri *URI, index string, req *ImportColumnAttrsRequest) error {
return nil
}
func (n nopInternalClient) EnsureIndex(ctx context.Context, name string, options IndexOptions) error { func (n nopInternalClient) EnsureIndex(ctx context.Context, name string, options IndexOptions) error {
return nil return nil
} }
@ -172,3 +211,19 @@ func (n nopInternalClient) SendMessage(ctx context.Context, uri *URI, msg []byte
func (n nopInternalClient) RetrieveShardFromURI(ctx context.Context, index, field, view string, shard uint64, uri URI) (io.ReadCloser, error) { func (n nopInternalClient) RetrieveShardFromURI(ctx context.Context, index, field, view string, shard uint64, uri URI) (io.ReadCloser, error) {
return nil, nil return nil, nil
} }
func (n nopInternalClient) RetrieveTranslatePartitionFromURI(ctx context.Context, index string, partition int, uri URI) (io.ReadCloser, error) {
return nil, nil
}
func (n nopInternalClient) StartTransaction(ctx context.Context, id string, timeout time.Duration, exclusive bool) (*Transaction, error) {
return nil, nil
}
func (n nopInternalClient) FinishTransaction(ctx context.Context, id string) (*Transaction, error) {
return nil, nil
}
func (n nopInternalClient) Transactions(ctx context.Context) (map[string]*Transaction, error) {
return nil, nil
}
func (n nopInternalClient) GetTransaction(ctx context.Context, id string) (*Transaction, error) {
return nil, nil
}

File diff suppressed because it is too large Load diff

View file

@ -17,7 +17,6 @@ package pilosa
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io/ioutil"
"math/rand" "math/rand"
"net" "net"
"net/http" "net/http"
@ -35,6 +34,7 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/pilosa/pilosa/v2/logger" "github.com/pilosa/pilosa/v2/logger"
"github.com/pilosa/pilosa/v2/roaring" "github.com/pilosa/pilosa/v2/roaring"
"github.com/pilosa/pilosa/v2/testhook"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -91,12 +91,17 @@ func TestFragCombos(t *testing.T) {
} }
// newIndexWithTempPath returns a new instance of Index. // newIndexWithTempPath returns a new instance of Index.
func newIndexWithTempPath(name string) *Index { func newIndexWithTempPath(tb testing.TB, name string) *Index {
path, err := ioutil.TempDir(*TempDir, "pilosa-index-") path, err := testhook.TempDirInDir(tb, *TempDir, "pilosa-index-")
if err != nil { if err != nil {
panic(err) panic(err)
} }
index, err := NewIndex(path, name) h := NewHolder(path, nil)
panicOn(h.Open())
index, err := h.CreateIndex(name, IndexOptions{})
testhook.Cleanup(tb, func() {
h.Close()
})
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -105,7 +110,6 @@ func newIndexWithTempPath(name string) *Index {
// Ensure that fragSources creates the correct fragment mapping. // Ensure that fragSources creates the correct fragment mapping.
func TestFragSources(t *testing.T) { func TestFragSources(t *testing.T) {
uri0, err := NewURIFromAddress("host0") uri0, err := NewURIFromAddress("host0")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -157,27 +161,53 @@ func TestFragSources(t *testing.T) {
c5.addNodeBasicSorted(node2) c5.addNodeBasicSorted(node2)
c5.addNodeBasicSorted(node3) c5.addNodeBasicSorted(node3)
idx := newIndexWithTempPath("i") idx := newIndexWithTempPath(t, "i")
defer idx.Close()
field, err := idx.CreateFieldIfNotExists("f", OptFieldTypeDefault()) field, err := idx.CreateFieldIfNotExists("f", OptFieldTypeDefault())
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
_, err = field.SetBit(1, 101, nil)
// Obtain transaction.
var shard uint64
tx := idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Shard: shard})
defer tx.Rollback()
_, err = field.SetBit(tx, 1, 101, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
_, err = field.SetBit(1, ShardWidth+1, nil) panicOn(tx.Commit())
shard = 1
tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Shard: shard})
defer tx.Rollback()
_, err = field.SetBit(tx, 1, ShardWidth*shard+1, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
_, err = field.SetBit(1, ShardWidth*2+1, nil) panicOn(tx.Commit())
shard = 2
tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Shard: shard})
defer tx.Rollback()
_, err = field.SetBit(tx, 1, ShardWidth*shard+1, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
_, err = field.SetBit(1, ShardWidth*3+1, nil) panicOn(tx.Commit())
shard = 3
tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Shard: shard})
defer tx.Rollback()
_, err = field.SetBit(tx, 1, ShardWidth*shard+1, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
panicOn(tx.Commit())
tests := []struct { tests := []struct {
from *cluster from *cluster
@ -351,7 +381,7 @@ func TestCluster_Partition(t *testing.T) {
c := newCluster() c := newCluster()
c.partitionN = partitionN c.partitionN = partitionN
partitionID := c.partition(index, shard) partitionID := c.shardPartition(index, shard)
if partitionID < 0 || partitionID >= partitionN { if partitionID < 0 || partitionID >= partitionN {
t.Errorf("partition out of range: shard=%d, p=%d, n=%d", shard, partitionID, partitionN) t.Errorf("partition out of range: shard=%d, p=%d, n=%d", shard, partitionID, partitionN)
} }
@ -391,7 +421,7 @@ func TestHasher(t *testing.T) {
// Ensure ContainsShards can find the actual shard list for node and index. // Ensure ContainsShards can find the actual shard list for node and index.
func TestCluster_ContainsShards(t *testing.T) { func TestCluster_ContainsShards(t *testing.T) {
c := NewTestCluster(5) c := NewTestCluster(t, 5)
c.ReplicaN = 3 c.ReplicaN = 3
shards := c.containsShards("test", roaring.NewBitmap(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10), c.nodes[2]) shards := c.containsShards("test", roaring.NewBitmap(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10), c.nodes[2])
@ -537,7 +567,7 @@ func TestCluster_Coordinator(t *testing.T) {
} }
func TestCluster_Topology(t *testing.T) { func TestCluster_Topology(t *testing.T) {
c1 := NewTestCluster(1) // automatically creates Node{ID: "node0"} c1 := NewTestCluster(t, 1) // automatically creates Node{ID: "node0"}
uri0 := NewTestURIFromHostPort("host0", 0) uri0 := NewTestURIFromHostPort("host0", 0)
uri1 := NewTestURIFromHostPort("host1", 0) uri1 := NewTestURIFromHostPort("host1", 0)
@ -585,7 +615,7 @@ func TestCluster_Topology(t *testing.T) {
func TestCluster_ResizeStates(t *testing.T) { func TestCluster_ResizeStates(t *testing.T) {
t.Run("Single node, no data", func(t *testing.T) { t.Run("Single node, no data", func(t *testing.T) {
tc := NewClusterCluster(1) tc := NewClusterCluster(t, 1)
// Open TestCluster. // Open TestCluster.
if err := tc.Open(); err != nil { if err := tc.Open(); err != nil {
@ -615,7 +645,7 @@ func TestCluster_ResizeStates(t *testing.T) {
}) })
t.Run("Single node, in topology", func(t *testing.T) { t.Run("Single node, in topology", func(t *testing.T) {
tc := NewClusterCluster(0) tc := NewClusterCluster(t, 0)
if err := tc.addNode(); err != nil { if err := tc.addNode(); err != nil {
t.Fatalf("adding node: %v", err) t.Fatalf("adding node: %v", err)
} }
@ -647,7 +677,7 @@ func TestCluster_ResizeStates(t *testing.T) {
}) })
t.Run("Single node, not in topology", func(t *testing.T) { t.Run("Single node, not in topology", func(t *testing.T) {
tc := NewClusterCluster(0) tc := NewClusterCluster(t, 0)
if err := tc.addNode(); err != nil { if err := tc.addNode(); err != nil {
t.Fatalf("adding node: %v", err) t.Fatalf("adding node: %v", err)
} }
@ -676,7 +706,7 @@ func TestCluster_ResizeStates(t *testing.T) {
}) })
t.Run("Multiple nodes, no data", func(t *testing.T) { t.Run("Multiple nodes, no data", func(t *testing.T) {
tc := NewClusterCluster(0) tc := NewClusterCluster(t, 0)
if err := tc.addNode(); err != nil { if err := tc.addNode(); err != nil {
t.Fatalf("adding node: %v", err) t.Fatalf("adding node: %v", err)
} }
@ -718,7 +748,7 @@ func TestCluster_ResizeStates(t *testing.T) {
}) })
t.Run("Multiple nodes, in/not in topology", func(t *testing.T) { t.Run("Multiple nodes, in/not in topology", func(t *testing.T) {
tc := NewClusterCluster(0) tc := NewClusterCluster(t, 0)
if err := tc.addNode(); err != nil { if err := tc.addNode(); err != nil {
t.Fatalf("adding node: %v", err) t.Fatalf("adding node: %v", err)
} }
@ -767,7 +797,7 @@ func TestCluster_ResizeStates(t *testing.T) {
}) })
t.Run("Multiple nodes, with data", func(t *testing.T) { t.Run("Multiple nodes, with data", func(t *testing.T) {
tc := NewClusterCluster(0) tc := NewClusterCluster(t, 0)
if err := tc.addNode(); err != nil { if err := tc.addNode(); err != nil {
t.Fatalf("adding node: %v", err) t.Fatalf("adding node: %v", err)
} }
@ -782,6 +812,7 @@ func TestCluster_ResizeStates(t *testing.T) {
if err := tc.CreateField("i", "f", OptFieldTypeDefault()); err != nil { if err := tc.CreateField("i", "f", OptFieldTypeDefault()); err != nil {
t.Fatalf("creating field: %v", err) t.Fatalf("creating field: %v", err)
} }
// Each tc.SetBit starts and commits its own Tx.
if err := tc.SetBit("i", "f", 1, 101, nil); err != nil { if err := tc.SetBit("i", "f", 1, 101, nil); err != nil {
t.Fatalf("setting bit: %v", err) t.Fatalf("setting bit: %v", err)
} }
@ -794,12 +825,21 @@ func TestCluster_ResizeStates(t *testing.T) {
node0Field := node0.holder.Field("i", "f") node0Field := node0.holder.Field("i", "f")
node0View := node0Field.view("standard") node0View := node0Field.view("standard")
node0Fragment := node0View.Fragment(1) node0Fragment := node0View.Fragment(1)
node0Checksum := node0Fragment.Checksum() node0Checksum, err := node0Fragment.Checksum()
if err != nil {
t.Fatal(err)
}
idx0 := node0.holder.Index("i")
if idx0 == nil {
t.Fatal(`idx0 was nil, could not retrieve Index("i")`)
}
// addNode needs to block until the resize process has completed. // addNode needs to block until the resize process has completed.
if err := tc.addNode(); err != nil { if err := tc.addNode(); err != nil {
t.Fatalf("adding node: %v", err) t.Fatalf("adding node: %v", err)
} }
node1 := tc.Clusters[1] node1 := tc.Clusters[1]
// Ensure that nodes come up in state NORMAL. // Ensure that nodes come up in state NORMAL.
@ -808,6 +848,7 @@ func TestCluster_ResizeStates(t *testing.T) {
} else if node1.State() != ClusterStateNormal { } else if node1.State() != ClusterStateNormal {
t.Errorf("expected node1 state: %v, but got: %v", ClusterStateNormal, node1.State()) t.Errorf("expected node1 state: %v, but got: %v", ClusterStateNormal, node1.State())
} }
// INVAR: after node1.State() is normal, the rebalancing should have been done.
expectedTop := &Topology{ expectedTop := &Topology{
nodeIDs: []string{node0.Node.ID, node1.Node.ID}, nodeIDs: []string{node0.Node.ID, node1.Node.ID},
@ -826,8 +867,15 @@ func TestCluster_ResizeStates(t *testing.T) {
node1View := node1Field.view("standard") node1View := node1Field.view("standard")
node1Fragment := node1View.Fragment(1) node1Fragment := node1View.Fragment(1)
idx1 := node1.holder.Index("i")
if idx1 == nil {
t.Fatal(`idx1 was nil, could not retrieve Index("i")`)
}
// Ensure checksums are the same. // Ensure checksums are the same.
if chksum := node1Fragment.Checksum(); !bytes.Equal(chksum, node0Checksum) { if chksum, err := node1Fragment.Checksum(); err != nil {
t.Fatal(err)
} else if !bytes.Equal(chksum, node0Checksum) {
t.Fatalf("expected standard view checksum to match: %x - %x", chksum, node0Checksum) t.Fatalf("expected standard view checksum to match: %x - %x", chksum, node0Checksum)
} }
@ -846,6 +894,7 @@ func TestAE(t *testing.T) {
c.abortAntiEntropy() c.abortAntiEntropy()
close(ch) close(ch)
}() }()
defer c.abortAntiEntropyQ() // avoid leaking a goroutine.
select { select {
case <-ch: case <-ch:
return return
@ -857,11 +906,13 @@ func TestAE(t *testing.T) {
t.Run("AbortBlocksInitialized", func(t *testing.T) { t.Run("AbortBlocksInitialized", func(t *testing.T) {
c := newCluster() c := newCluster()
c.initializeAntiEntropy() c.initializeAntiEntropy()
ch := make(chan struct{}) ch := make(chan struct{})
go func() { go func() {
c.abortAntiEntropy() c.abortAntiEntropy()
close(ch) close(ch)
}() }()
defer c.abortAntiEntropyQ() // avoid leak of goroutine.
select { select {
case <-ch: case <-ch:
t.Fatalf("aborting anti entropy on an initialized cluster didn't block") t.Fatalf("aborting anti entropy on an initialized cluster didn't block")
@ -899,7 +950,7 @@ func TestAE(t *testing.T) {
// Ensures that coordinator can be changed. // Ensures that coordinator can be changed.
func TestCluster_UpdateCoordinator(t *testing.T) { func TestCluster_UpdateCoordinator(t *testing.T) {
t.Run("UpdateCoordinator", func(t *testing.T) { t.Run("UpdateCoordinator", func(t *testing.T) {
c := NewTestCluster(2) c := NewTestCluster(t, 2)
oldNode := c.nodes[0] oldNode := c.nodes[0]
newNode := c.nodes[1] newNode := c.nodes[1]
@ -942,15 +993,22 @@ func TestCluster_confirmNodeDownUp(t *testing.T) {
t.Error(err) t.Error(err)
} }
uri.Port = uint16(iport) uri.Port = uint16(iport)
if confirmNodeDown(uri, logger.NewVerboseLogger(os.Stdout)) { c := newCluster()
c.logger = logger.NewVerboseLogger(os.Stdout)
if c.confirmNodeDown(uri) {
t.Errorf("expected node to be up") t.Errorf("expected node to be up")
} }
} }
func TestCluster_confirmNodeDownTimeout(t *testing.T) { func TestCluster_confirmNodeDownTimeout(t *testing.T) {
sleep := 50 * time.Millisecond
retries := 5
if testing.Short() {
t.Skip()
}
r := mux.NewRouter() r := mux.NewRouter()
r.HandleFunc("/version", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { r.HandleFunc("/version", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(confirmDownSleep * time.Second * confirmDownRetries) time.Sleep(sleep * time.Duration(retries))
fmt.Fprintln(w, "ignored") fmt.Fprintln(w, "ignored")
})) }))
server := httptest.NewServer(r) server := httptest.NewServer(r)
@ -969,21 +1027,29 @@ func TestCluster_confirmNodeDownTimeout(t *testing.T) {
t.Error(err) t.Error(err)
} }
uri.Port = uint16(iport) uri.Port = uint16(iport)
c := newCluster()
if !confirmNodeDown(uri, logger.NewVerboseLogger(os.Stdout)) { c.confirmDownSleep = sleep
c.confirmDownRetries = retries
c.logger = logger.NewVerboseLogger(os.Stdout)
if !c.confirmNodeDown(uri) {
t.Errorf("expected node to be down") t.Errorf("expected node to be down")
} }
} }
func TestCluster_confirmNodeDownDown(t *testing.T) { func TestCluster_confirmNodeDownDown(t *testing.T) {
if testing.Short() {
t.Skip()
}
uri := URI{} uri := URI{}
uri.Scheme = "http" uri.Scheme = "http"
uri.Host = "DoesntMatter" uri.Host = "DoesntMatter"
uri.Port = 6666 uri.Port = 6666
c := newCluster()
c.confirmDownSleep = 50 * time.Millisecond
c.confirmDownRetries = 5
c.logger = logger.NewVerboseLogger(os.Stdout)
if !confirmNodeDown(uri, logger.NewVerboseLogger(os.Stdout)) { if !c.confirmNodeDown(uri) {
t.Errorf("expected node to be down") t.Errorf("expected node to be down")
} }
} }

154
cmd/badloader/badloader.go Normal file
View file

@ -0,0 +1,154 @@
// Copyright 2020 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"archive/tar"
"compress/gzip"
"context"
"time"
//"fmt"
"fmt"
"github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/http"
"io"
"io/ioutil"
gohttp "net/http"
//"log"
"os"
//"path/filepath"
//"sort"
"strconv"
"strings"
)
func UploadTar(srcFile string, client *http.InternalClient) error {
t0 := time.Now()
f, err := os.Open(srcFile)
if err != nil {
return (err)
}
defer f.Close()
var tarReader *tar.Reader
if strings.HasSuffix(srcFile, "gz") {
gzf, err := gzip.NewReader(f)
if err != nil {
return err
}
tarReader = tar.NewReader(gzf)
} else {
tarReader = tar.NewReader(f)
}
viewData := make(map[string][]byte)
//given ordered by index/field/view
//trait_store/product_count__commercial_cd_or_share_certificate/views/bsig_product_count__commercial_cd_or_share_certificate/fragments/255
lastIndex := ""
lastField := ""
lastShard := uint64(0)
//vv("top of tar loop")
n := 0
for {
header, err := tarReader.Next()
if err == io.EOF {
if header != nil {
panic("header should not be nil on err io.EOF")
}
//submit any stuff we have left
if len(viewData) > 0 {
request := &pilosa.ImportRoaringRequest{
Views: viewData,
}
// Submit(lastIndex, lastField, lastShard, request)
//vv("about to submit lastIndex='%v' lastShard='%v'", lastIndex, lastShard)
uri := GetImportRoaringURI(lastIndex, lastShard)
err := client.ImportRoaring(context.Background(), uri, lastIndex, lastField, lastShard, false, request)
panicOn(err)
//vv("done with submit lastIndex='%v' lastShard='%v'", lastIndex, lastShard)
}
return nil
}
//vv("got header '%v'", header.Name)
n++
if n%500 == 0 {
vv("n = %v, progress, elapsed '%v'", n, time.Since(t0))
}
parts := strings.Split(header.Name, "/")
//vv("parts = '%#v'", parts)
index := parts[1]
field := parts[2]
view := parts[4]
shard, err := strconv.ParseUint(parts[6], 10, 64)
if err != nil {
return err
}
// TODO: shards can be loaded in parallel, so maybe farm out to a worker set of goro.
if index != lastIndex || field != lastField || shard != lastShard {
if len(viewData) > 0 {
request := &pilosa.ImportRoaringRequest{
Views: viewData,
}
//vv("about to submit lastIndex='%v' lastShard='%v'", lastIndex, lastShard)
uri := GetImportRoaringURI(lastIndex, lastShard)
panicOn(client.ImportRoaring(context.Background(), uri, lastIndex, lastField, lastShard, false, request))
viewData = make(map[string][]byte)
//vv("done with submit lastIndex='%v' lastShard='%v'; took='%v'", lastIndex, lastShard, time.Since(t0))
}
}
roaringData, err := ioutil.ReadAll(tarReader)
if err != nil {
return err
}
if _, already := viewData[view]; already {
panic(fmt.Sprintf("view '%v' already present!", view))
}
viewData[view] = roaringData
lastIndex = index
lastField = field
//lastShard = shard
//vv("bottom of loop")
}
}
// badloader reproduce a union in place issue for us. slurp is
// the new "good" loader, and should always be preferred now
// when not trying to repro that bug. pulled from 85fa67e8
func main() {
host := "127.0.0.1:10101"
h := &gohttp.Client{}
c, err := http.NewInternalClient(host, h)
panicOn(err)
tarSrcPath := "q2.tar.gz"
t0 := time.Now()
panicOn(UploadTar(tarSrcPath, c))
vv("total elapsed '%v'", time.Since(t0))
}
var globURI *pilosa.URI
func init() {
var err error
globURI, err = pilosa.NewURIFromHostPort("127.0.0.1", 10101)
panicOn(err)
}
// get correct node to go to.
func GetImportRoaringURI(index string, shard uint64) *pilosa.URI {
return globURI
}

177
cmd/badloader/vprint.go Normal file
View file

@ -0,0 +1,177 @@
// home: https://github.com/glycerine/vprint
// Copyright 2019 Jason E. Aten, Ph.D. All rights reserved.
// License: MIT
//
// MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package main
import (
"fmt"
"io"
"os"
"path"
"runtime"
"runtime/debug"
"sync"
"time"
)
const RFC3339MsecTz0 = "2006-01-02T15:04:05.000Z07:00"
const RFC3339UsecTz0 = "2006-01-02T15:04:05.000000Z07:00"
// for tons of debug output
var VerboseVerbose bool = false
// convience functions for . import
var pp = PP
var vv = VV
var panicOn = PanicOn
func init() {
// keeper linter happy
_ = pp
_ = vv
}
func PanicOn(err error) {
if err != nil {
panic(err)
}
}
func PP(format string, a ...interface{}) {
if VerboseVerbose {
TSPrintf(format, a...)
}
}
func VV(format string, a ...interface{}) {
TSPrintf(format, a...)
}
func AlwaysPrintf(format string, a ...interface{}) {
TSPrintf(format, a...)
}
var tsPrintfMut sync.Mutex
// time-stamped printf
func TSPrintf(format string, a ...interface{}) {
tsPrintfMut.Lock()
Printf("\n%s %s ", FileLine(3), ts())
Printf(format+"\n", a...)
tsPrintfMut.Unlock()
}
// get timestamp for logging purposes
func ts() string {
return time.Now().Format(RFC3339UsecTz0)
}
// so we can multi write easily, use our own printf
var OurStdout io.Writer = os.Stdout
// Printf formats according to a format specifier and writes to standard output.
// It returns the number of bytes written and any write error encountered.
func Printf(format string, a ...interface{}) (n int, err error) {
return fmt.Fprintf(OurStdout, format, a...)
}
func FileLine(depth int) string {
_, fileName, fileLine, ok := runtime.Caller(depth)
var s string
if ok {
s = fmt.Sprintf("%s:%d", path.Base(fileName), fileLine)
} else {
s = ""
}
return s
}
func stack() string {
return string(debug.Stack())
}
func FileExists(name string) bool {
fi, err := os.Stat(name)
if err != nil {
return false
}
if fi.IsDir() {
return false
}
return true
}
func DirExists(name string) bool {
fi, err := os.Stat(name)
if err != nil {
return false
}
if fi.IsDir() {
return true
}
return false
}
func FileSize(name string) (int64, error) {
fi, err := os.Stat(name)
if err != nil {
return -1, err
}
return fi.Size(), nil
}
// Caller returns the name of the calling function.
func Caller(upStack int) string {
// elide ourself and runtime.Callers
target := upStack + 2
pc := make([]uintptr, target+2)
n := runtime.Callers(0, pc)
f := runtime.Frame{Function: "unknown"}
if n > 0 {
frames := runtime.CallersFrames(pc[:n])
for i := 0; i <= target; i++ {
contender, more := frames.Next()
if i == target {
f = contender
}
if !more {
break
}
}
}
return f.Function
}
// happy linter:
var _ = DirExists
var _ = FileExists
var _ = Caller
var _ = stack
var _ = RFC3339MsecTz0
var _ = RFC3339UsecTz0
var _ = AlwaysPrintf
var _ = FileSize

View file

@ -45,5 +45,12 @@ Inspects a data file and provides stats.
return inspector.Run(context.Background()) return inspector.Run(context.Background())
}, },
} }
flags := inspectCmd.Flags()
flags.BoolVarP(&inspector.Quiet, "quiet", "q", false, "don't list details of containers")
flags.IntVarP(&inspector.Max, "max", "n", 0, "list at most max items (0 = unlimited)")
flags.StringVarP(&inspector.InspectOpts.Indexes, "index", "i", "", "filter indexes")
flags.StringVarP(&inspector.InspectOpts.Views, "view", "v", "", "filter views")
flags.StringVarP(&inspector.InspectOpts.Fields, "field", "f", "", "filter fields")
flags.StringVarP(&inspector.InspectOpts.Shards, "shard", "s", "", "filter shards")
return inspectCmd return inspectCmd
} }

43
cmd/convert/main.go Normal file
View file

@ -0,0 +1,43 @@
// Copyright 2017 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"log"
"os"
"github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/rbf"
)
func main() {
if len(os.Args) != 3 {
log.Fatal("USAGE convert srcPath destPath")
}
holder := pilosa.NewHolder(os.Args[1], nil)
err := holder.Open()
if err != nil {
log.Fatal(err)
}
c := &pilosa.RBFConverter{
Dbs: make(map[string]*rbf.DB),
Base: os.Args[2],
}
holder.ConvertToRBF(c)
}

267
cmd/demo-lmdb/lmdb.go Normal file
View file

@ -0,0 +1,267 @@
// Copyright 2020 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build !386
package main
import (
"bytes"
"fmt"
"log"
"os"
"github.com/glycerine/lmdb-go/lmdb"
)
// note: use runtime.LockOSThread on any write goroutine; must create and write the txn from
// the same goroutine.
// This example demonstrates a complete workflow for a simple application
// working with LMDB. First, an Env is configured and mapped to memory. Once
// mapped, database handles are opened and normal database operations may
// begin.
func main() {
// Create an environment and make sure it is eventually closed.
env, err := lmdb.NewEnv()
panicOn(err)
defer env.Close()
// Configure and open the environment. Most configuration must be done
// before opening the environment. The go documentation for each method
// should indicate if it must be called before calling env.Open()
err = env.SetMaxDBs(1)
panicOn(err)
err = env.SetMapSize(1 << 30)
panicOn(err)
path := "./db-lmdb"
panicOn(os.MkdirAll(path, 0755))
err = env.Open(path, 0, 0644) // lmdb.Create ?
panicOn(err)
// In any real application it is important to check for readers that were
// never closed by their owning process, and for which the owning process
// has exited. See the documentation on transactions for more information.
staleReaders, err := env.ReaderCheck()
panicOn(err)
if staleReaders > 0 {
log.Printf("cleared %d reader slots from dead processes", staleReaders)
}
// Open a database handle that will be used for the entire lifetime of this
// application. Because the database may not have existed before, and the
// database may need to be created, we need to get the database handle in
// an update transacation.
var dbi lmdb.DBI
_ = dbi
err = env.Update(func(txn *lmdb.Txn) (err error) {
dbi, err = txn.CreateDBI("example")
return err
})
panicOn(err)
// The database referenced by our DBI handle is now ready for the
// application to use. Here the application just opens a readonly
// transaction and reads the data stored in the "hello" key and prints its
// value to the application's standard output.
err = env.View(func(txn *lmdb.Txn) (err error) {
v, err := txn.Get(dbi, []byte("hello"))
if err != nil {
return err
}
fmt.Println(string(v))
return nil
})
_ = err
//panicOn(err) // mdb_get: MDB_NOTFOUND: No matching key/data pair found
err = env.Update(func(txn *lmdb.Txn) (err error) {
panicOn(txn.Put(dbi, []byte("099"), []byte("A"), 0))
panicOn(txn.Put(dbi, []byte("101"), []byte("B"), 0))
panicOn(txn.Put(dbi, []byte("199"), []byte("C"), 0))
panicOn(txn.Put(dbi, []byte("200"), []byte("D"), 0))
panicOn(txn.Put(dbi, []byte("300"), []byte("E"), 0))
panicOn(txn.Put(dbi, []byte("399"), []byte("F"), 0))
return nil
})
_ = err
// find max in [000,100) and get 099
// find max in [100,200) and get 199
// find max in [300,400) and get 399
// find max in [400,500) and get nothing back
err = env.View(func(txn *lmdb.Txn) (err error) {
v, err := txn.Get(dbi, []byte("hello"))
if err != nil {
return err
}
fmt.Printf("key 'hello' retreived value: '%v'\n", string(v))
return nil
})
_ = err
err = env.View(func(txn *lmdb.Txn) (err error) {
cur, err := txn.OpenCursor(dbi)
panicOn(err)
defer cur.Close()
var cur2 *lmdb.Cursor
var err2 error
var k, k2, v, v2 []byte
i := 0
for {
if i == 0 {
// lmdb.SetRange : The first key no less than the specified key.
k, v, err = cur.Get([]byte("200"), nil, lmdb.SetRange)
// cur2 should start at 'a'
cur2, err2 = txn.OpenCursor(dbi)
panicOn(err2)
defer cur2.Close()
k2, v2, err2 = cur2.Get(nil, nil, lmdb.Next)
_ = err2
} else {
k, v, err = cur.Get(nil, nil, lmdb.Next)
k2, v2, err2 = cur2.Get(nil, nil, lmdb.Next)
_ = err2
}
if lmdb.IsNotFound(err) {
return nil
}
if err != nil {
return err
}
fmt.Printf("i=%v, %s %s\n", i, k, v)
fmt.Printf("i=%v, k2:%s v2:%s\n", i, k2, v2)
i++
}
// return nil // unreachable
})
_ = err
// panicOn(txn.Put(dbi, []byte("099"), []byte("A"), 0))
// panicOn(txn.Put(dbi, []byte("101"), []byte("B"), 0))
// panicOn(txn.Put(dbi, []byte("199"), []byte("C"), 0))
// panicOn(txn.Put(dbi, []byte("200"), []byte("D"), 0))
// panicOn(txn.Put(dbi, []byte("300"), []byte("E"), 0))
// panicOn(txn.Put(dbi, []byte("399"), []byte("F"), 0))
//
// find max in [300,400) and get 399
// find max in [000,100) and get 099
// find max in [100,200) and get 199
// find max in [400,500) and get nothing back
// find max in [201,300) and get nothing back
err = env.View(func(txn *lmdb.Txn) (err error) {
cur, err := txn.OpenCursor(dbi)
panicOn(err)
defer cur.Close()
var k, v []byte
// find max in [300,400) and get 399
// lmdb.SetRange : The first key no less than the specified key.
k, v, err = cur.Get([]byte("400"), nil, lmdb.SetRange)
if lmdb.IsNotFound(err) {
fmt.Printf("400 not found, as expected\n") // happens on starting empty db
} else {
fmt.Printf("Get 400 => %v: %v\n", string(k), string(v))
}
k, v, err = cur.Get(nil, nil, lmdb.Prev)
if lmdb.IsNotFound(err) {
fmt.Printf("Get 400 then Get Prev => not found\n")
} else {
fmt.Printf("Get 400 then Get Prev => %v: %v\n", string(k), string(v)) // 399: F, so wraps backwards from beginning.
}
panicOn(err)
// now try for 199 in [100,200)
k, v, err = cur.Get([]byte("200"), nil, lmdb.SetRange)
if lmdb.IsNotFound(err) {
panic("200 not found, not expected")
} else {
fmt.Printf("Get 200 => %v: %v\n", string(k), string(v)) // Get 200 => 200: D
}
k, v, err = cur.Get(nil, nil, lmdb.Prev)
if lmdb.IsNotFound(err) {
fmt.Printf("Get 200 then Get Prev => not found\n")
} else {
fmt.Printf("Get 200 then Get Prev => %v: %v\n", string(k), string(v)) // Get 200 then Get Prev => 199: C
}
panicOn(err)
k, v, err = cur.Get([]byte("500"), nil, lmdb.SetRange)
if lmdb.IsNotFound(err) {
fmt.Printf("500 not found, as expected\n") // 500 not found, as expected
} else {
panic(fmt.Sprintf("Get 500 => %v: %v\n", string(k), string(v)))
}
k, v, err = cur.Get(nil, nil, lmdb.Prev)
if lmdb.IsNotFound(err) {
fmt.Printf("Get 500 then Get Prev => not found\n")
} else {
fmt.Printf("Get 500 then Get Prev => %v: %v\n", string(k), string(v)) // Get 500 then Get Prev => 399: F
}
panicOn(err)
k, v, err = cur.Get([]byte("100"), nil, lmdb.SetRange)
if lmdb.IsNotFound(err) {
panic("100 not found, not expected")
} else {
fmt.Printf("Get 100 => %v: %v\n", string(k), string(v)) // Get 100 => 101: B
}
k, v, err = cur.Get(nil, nil, lmdb.Prev)
if lmdb.IsNotFound(err) {
fmt.Printf("Get 100 then Get Prev => not found\n")
} else {
fmt.Printf("Get 100 then Get Prev => %v: %v\n", string(k), string(v)) // Get 100 then Get Prev => 099: A
}
panicOn(err)
// find max in [201,300) and get nothing back
k, v, err = cur.Get([]byte("300"), nil, lmdb.SetRange)
if lmdb.IsNotFound(err) {
panic("300 not found, not expected")
} else {
fmt.Printf("Get 300 => %v: %v\n", string(k), string(v)) // Get 300 => 300: E
}
k, v, err = cur.Get(nil, nil, lmdb.Prev)
if lmdb.IsNotFound(err) {
fmt.Printf("Get 300 then Get Prev => not found\n")
} else {
fmt.Printf("Get 300 then Get Prev => %v: %v\n", string(k), string(v)) // Get 300 then Get Prev => 200: D
}
cmp := bytes.Compare(k, []byte("201"))
if cmp >= 0 {
fmt.Printf("key k = '%v' was >= 201", string(k))
} else {
fmt.Printf("key k = '%v' was < 201", string(k)) // key k = '200' was < 201
}
panicOn(err)
return nil
})
panicOn(err)
vv("done")
}

179
cmd/demo-lmdb/vprint.go Normal file
View file

@ -0,0 +1,179 @@
// home: https://github.com/glycerine/vprint
// Copyright 2019 Jason E. Aten, Ph.D. All rights reserved.
// License: MIT
//
// MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// +build !386
package main
import (
"fmt"
"io"
"os"
"path"
"runtime"
"runtime/debug"
"sync"
"time"
)
const RFC3339MsecTz0 = "2006-01-02T15:04:05.000Z07:00"
const RFC3339UsecTz0 = "2006-01-02T15:04:05.000000Z07:00"
// for tons of debug output
var VerboseVerbose bool = false
// convience functions for . import
var pp = PP
var vv = VV
var panicOn = PanicOn
func init() {
// keeper linter happy
_ = pp
_ = vv
}
func PanicOn(err error) {
if err != nil {
panic(err)
}
}
func PP(format string, a ...interface{}) {
if VerboseVerbose {
TSPrintf(format, a...)
}
}
func VV(format string, a ...interface{}) {
TSPrintf(format, a...)
}
func AlwaysPrintf(format string, a ...interface{}) {
TSPrintf(format, a...)
}
var tsPrintfMut sync.Mutex
// time-stamped printf
func TSPrintf(format string, a ...interface{}) {
tsPrintfMut.Lock()
Printf("\n%s %s ", FileLine(3), ts())
Printf(format+"\n", a...)
tsPrintfMut.Unlock()
}
// get timestamp for logging purposes
func ts() string {
return time.Now().Format(RFC3339UsecTz0)
}
// so we can multi write easily, use our own printf
var OurStdout io.Writer = os.Stdout
// Printf formats according to a format specifier and writes to standard output.
// It returns the number of bytes written and any write error encountered.
func Printf(format string, a ...interface{}) (n int, err error) {
return fmt.Fprintf(OurStdout, format, a...)
}
func FileLine(depth int) string {
_, fileName, fileLine, ok := runtime.Caller(depth)
var s string
if ok {
s = fmt.Sprintf("%s:%d", path.Base(fileName), fileLine)
} else {
s = ""
}
return s
}
func stack() string {
return string(debug.Stack())
}
func FileExists(name string) bool {
fi, err := os.Stat(name)
if err != nil {
return false
}
if fi.IsDir() {
return false
}
return true
}
func DirExists(name string) bool {
fi, err := os.Stat(name)
if err != nil {
return false
}
if fi.IsDir() {
return true
}
return false
}
func FileSize(name string) (int64, error) {
fi, err := os.Stat(name)
if err != nil {
return -1, err
}
return fi.Size(), nil
}
// Caller returns the name of the calling function.
func Caller(upStack int) string {
// elide ourself and runtime.Callers
target := upStack + 2
pc := make([]uintptr, target+2)
n := runtime.Callers(0, pc)
f := runtime.Frame{Function: "unknown"}
if n > 0 {
frames := runtime.CallersFrames(pc[:n])
for i := 0; i <= target; i++ {
contender, more := frames.Next()
if i == target {
f = contender
}
if !more {
break
}
}
}
return f.Function
}
// happy linter:
var _ = DirExists
var _ = FileExists
var _ = Caller
var _ = stack
var _ = RFC3339MsecTz0
var _ = RFC3339UsecTz0
var _ = AlwaysPrintf
var _ = FileSize

View file

@ -50,7 +50,7 @@ The file does not contain any headers.
flags.StringVarP(&Exporter.Index, "index", "i", "", "Pilosa index to export") flags.StringVarP(&Exporter.Index, "index", "i", "", "Pilosa index to export")
flags.StringVarP(&Exporter.Field, "field", "f", "", "Field to export") flags.StringVarP(&Exporter.Field, "field", "f", "", "Field to export")
flags.StringVarP(&Exporter.Path, "output-file", "o", "", "File to write export to - default stdout") flags.StringVarP(&Exporter.Path, "output-file", "o", "", "File to write export to - default stdout")
ctl.SetTLSConfig(flags, &Exporter.TLS.CertificatePath, &Exporter.TLS.CertificateKeyPath, &Exporter.TLS.CACertPath, &Exporter.TLS.SkipVerify, &Exporter.TLS.EnableClientVerification) ctl.SetTLSConfig(flags, "", &Exporter.TLS.CertificatePath, &Exporter.TLS.CertificateKeyPath, &Exporter.TLS.CACertPath, &Exporter.TLS.SkipVerify, &Exporter.TLS.EnableClientVerification)
return exportCmd return exportCmd
} }

View file

@ -53,9 +53,9 @@ omitted. If it is present then its format should be YYYY-MM-DDTHH:MM.
flags.StringVarP(&Importer.Field, "field", "f", "", "Field to import into.") flags.StringVarP(&Importer.Field, "field", "f", "", "Field to import into.")
flags.BoolVar(&Importer.IndexOptions.Keys, "index-keys", false, "Specify keys=true when creating an index") flags.BoolVar(&Importer.IndexOptions.Keys, "index-keys", false, "Specify keys=true when creating an index")
flags.BoolVar(&Importer.FieldOptions.Keys, "field-keys", false, "Specify keys=true when creating a field") flags.BoolVar(&Importer.FieldOptions.Keys, "field-keys", false, "Specify keys=true when creating a field")
flags.StringVar(&Importer.FieldOptions.Type, "field-type", "", "Specify the field type when creating a field. One of: set, int, time, bool, mutex") flags.StringVar(&Importer.FieldOptions.Type, "field-type", "", "Specify the field type when creating a field. One of: set, int, decimal, time, bool, mutex")
flags.Int64Var(&Importer.FieldOptions.Min, "field-min", 0, "Specify the minimum for an int field on creation") flags.Int64Var(&Importer.FieldOptions.Min.Value, "field-min", 0, "Specify the minimum for an int field on creation") // TODO: noting that decimal field min/max are not supported here.
flags.Int64Var(&Importer.FieldOptions.Max, "field-max", 0, "Specify the maximum for an int field on creation") flags.Int64Var(&Importer.FieldOptions.Max.Value, "field-max", 0, "Specify the maximum for an int field on creation")
flags.StringVar(&Importer.FieldOptions.CacheType, "field-cache-type", pilosa.CacheTypeRanked, "Specify the cache type for a set field on creation. One of: none, lru, ranked") flags.StringVar(&Importer.FieldOptions.CacheType, "field-cache-type", pilosa.CacheTypeRanked, "Specify the cache type for a set field on creation. One of: none, lru, ranked")
flags.Uint32Var(&Importer.FieldOptions.CacheSize, "field-cache-size", 50000, "Specify the cache size for a set field on creation") flags.Uint32Var(&Importer.FieldOptions.CacheSize, "field-cache-size", 50000, "Specify the cache size for a set field on creation")
flags.Var(&Importer.FieldOptions.TimeQuantum, "field-time-quantum", "Specify the time quantum for a time field on creation. One of: D, DH, H, M, MD, MDH, Y, YM, YMD, YMDH") flags.Var(&Importer.FieldOptions.TimeQuantum, "field-time-quantum", "Specify the time quantum for a time field on creation. One of: D, DH, H, M, MD, MDH, Y, YM, YMD, YMDH")
@ -63,7 +63,7 @@ omitted. If it is present then its format should be YYYY-MM-DDTHH:MM.
flags.BoolVarP(&Importer.Sort, "sort", "", false, "Enables sorting before import.") flags.BoolVarP(&Importer.Sort, "sort", "", false, "Enables sorting before import.")
flags.BoolVarP(&Importer.CreateSchema, "create", "e", false, "Create the schema if it does not exist before import.") flags.BoolVarP(&Importer.CreateSchema, "create", "e", false, "Create the schema if it does not exist before import.")
flags.BoolVarP(&Importer.Clear, "clear", "", false, "Clear the data provided in the import.") flags.BoolVarP(&Importer.Clear, "clear", "", false, "Clear the data provided in the import.")
ctl.SetTLSConfig(flags, &Importer.TLS.CertificatePath, &Importer.TLS.CertificateKeyPath, &Importer.TLS.CACertPath, &Importer.TLS.SkipVerify, &Importer.TLS.EnableClientVerification) ctl.SetTLSConfig(flags, "", &Importer.TLS.CertificatePath, &Importer.TLS.CertificateKeyPath, &Importer.TLS.CACertPath, &Importer.TLS.SkipVerify, &Importer.TLS.EnableClientVerification)
return importCmd return importCmd
} }

View file

@ -21,6 +21,7 @@ import (
"github.com/pilosa/pilosa/v2" "github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/cmd" "github.com/pilosa/pilosa/v2/cmd"
"github.com/pilosa/pilosa/v2/pql"
) )
func TestImportHelp(t *testing.T) { func TestImportHelp(t *testing.T) {
@ -58,8 +59,8 @@ field = "f1"
v.Check(cmd.Importer.Field, "f1") v.Check(cmd.Importer.Field, "f1")
v.Check(cmd.Importer.FieldOptions, pilosa.FieldOptions{ v.Check(cmd.Importer.FieldOptions, pilosa.FieldOptions{
Keys: true, Keys: true,
Max: 100, Max: pql.NewDecimal(100, 0),
Min: -10, Min: pql.NewDecimal(-10, 0),
CacheType: pilosa.CacheTypeRanked, CacheType: pilosa.CacheTypeRanked,
CacheSize: 50000, CacheSize: 50000,
}) })

109
cmd/pilosa-chk/chk.go Normal file
View file

@ -0,0 +1,109 @@
// Copyright 2020 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"flag"
"fmt"
"log"
"os"
"github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/boltdb"
"github.com/pilosa/pilosa/v2/hash"
"github.com/zeebo/blake3"
)
// pilosa-chk : read boltdb files and print checksums and counts on the keys. With
// -v and -ops and -bits you can display every last bit if you want.
//
// pilosa-chk is deliberately NOT a part of pilosa so that it can run without
// forcing a customer to upgrade or downgrade their installed version.
func main() {
var dir string
var showOpsLog bool
var showBits bool
var showFrags bool
var dirChecksum bool
home := os.Getenv("HOME")
flag.StringVar(&dir, "dir", fmt.Sprintf("%v/.pilosa", home), "pilosa data dir to read")
flag.BoolVar(&showFrags, "v", false, "show the checksum hash for each fragment in each index. Warning: long output")
flag.BoolVar(&showOpsLog, "ops", false, "show the ops log for each fragment. Warning: very long output. Implies -v")
flag.BoolVar(&showBits, "bits", false, "show the hot bits for each fragment. Warning: very, very long output. Implies -v")
flag.BoolVar(&dirChecksum, "dirsum", false, "compute a directory hash")
flag.Parse()
if showBits {
showFrags = true
}
if showOpsLog {
showFrags = true
}
fmt.Printf("opening dir '%v'... this may take a few seconds...\n", dir)
if dirChecksum {
fmt.Printf("path '%v' has dirhash %v\n", dir, hash.HashOfDir(dir))
return
}
fmt.Printf(" the blake-3 hash includes the value of each mapping and the field or partitionID.\n")
holder := pilosa.NewHolder(dir, nil)
holder.OpenTranslateStore = boltdb.OpenTranslateStore
err := holder.Open()
if err != nil {
log.Fatal(err)
}
fmt.Printf("\ncalculating hashes of row and column key translation maps on data from dir '%v'...\n", dir)
var indexes []*pilosa.Index
final := pilosa.NewAllTranslatorSummary()
const verbose = true
for _, idx := range holder.Indexes() {
asum, err := idx.ComputeTranslatorSummary(verbose)
if err != nil {
log.Fatal(err)
}
final.Append(asum)
indexes = append(indexes, idx)
}
final.Sort()
hasher := blake3.New()
fmt.Printf("\nsummary of col/row translations%v:\n", dir)
for _, sum := range final.Sums {
//fmt.Printf("index: %v partitionID: %v blake3-%x keyCount: %v idCount: %v\n", sum.Index, sum.PartitionID, sum.Checksum, sum.KeyCount, sum.IDCount)
_, _ = hasher.Write([]byte(sum.Checksum))
}
var buf [16]byte
_, _ = hasher.Digest().Read(buf[0:])
fmt.Printf("all-checksum = blake3-%x\n", buf)
if showFrags {
for _, idx := range indexes {
fmt.Printf("==============================\n")
fmt.Printf("index: %v\n", idx.Name())
fmt.Printf("==============================\n")
idx.WriteFragmentChecksums(os.Stdout, showBits, showOpsLog)
}
}
}

View file

@ -0,0 +1,180 @@
// home https://github.com/glycerine/lmdb-go
// Copyright (c) 2020, the lmdb-go authors
// Copyright (c) 2015, Bryan Matsuo
// All rights reserved.
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
// Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
// Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
// Neither the name of the author nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// +build amd64
package main
import (
"fmt"
"os"
"runtime"
"github.com/glycerine/lmdb-go/lmdb"
"github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/hash"
"github.com/pilosa/pilosa/v2/roaring"
"github.com/pilosa/pilosa/v2/txkey"
)
// pilosa-keydump is a diagnostic tool that simply prints all the
// database keys in the database directory path specified
// as the first argument on the command line.
func main() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if len(os.Args) < 2 {
fmt.Fprintf(os.Stderr, "must supply path to database directory as only arg\n")
os.Exit(1)
}
path := os.Args[1]
if !DirExists(path) {
fmt.Fprintf(os.Stderr, "directory path '%v' does not exist.\n", path)
os.Exit(1)
}
maxr := 1
env, err := lmdb.NewEnvMaxReaders(maxr)
panicOn(err)
defer env.Close()
panicOn(env.SetMapSize(256 << 30))
err = env.SetMaxDBs(10)
panicOn(err)
//var myflags uint = NoReadahead | NoSubdir
var myflags uint = 0 //lmdb.NoSubdir
err = env.Open(path, myflags, 0664)
panicOn(err)
// In any real application it is important to check for readers that were
// never closed by their owning process, and for which the owning process
// has exited. See the documentation on transactions for more information.
staleReaders, err := env.ReaderCheck()
panicOn(err)
if staleReaders > 0 {
vv("cleared %d reader slots from dead processes", staleReaders)
}
dbnames := []string{}
var dbiRoot lmdb.DBI
var dbi lmdb.DBI
env.UseSphynxReader()
err = env.SphynxReader(func(txn *lmdb.Txn, readslot int) (err error) {
//txn.RawRead = true
dbiRoot, err = txn.OpenRoot(0)
panicOn(err)
cur, err := txn.OpenCursor(dbiRoot)
panicOn(err)
defer cur.Close()
for i := 0; true; i++ {
var k, v []byte
var err error
if i == 0 {
// must give it at least a zero byte here to start.
k, v, err = cur.Get([]byte{0}, nil, lmdb.SetRange)
panicOn(err)
} else {
k, v, err = cur.Get([]byte(nil), nil, lmdb.Next)
if lmdb.IsNotFound(err) {
break
} else {
panicOn(err)
}
}
dbnames = append(dbnames, string(k))
_ = v
}
cur.Close()
return
})
panicOn(err)
for _, dbn := range dbnames {
fmt.Printf(`
=========================
database '%v':
=========================
`, dbn)
err = env.SphynxReader(func(txn *lmdb.Txn, readslot int) (err error) {
//txn.RawRead = true
dbi, err = txn.OpenDBI(dbn, 0)
panicOn(err)
cur, err := txn.OpenCursor(dbi)
panicOn(err)
defer cur.Close()
for i := 0; true; i++ {
var k, v []byte
var err error
if i == 0 {
// must give it at least a zero byte here to start.
k, v, err = cur.Get([]byte{0}, nil, lmdb.SetRange)
panicOn(err)
} else {
k, v, err = cur.Get([]byte(nil), nil, lmdb.Next)
if lmdb.IsNotFound(err) {
break
} else {
panicOn(err)
}
}
ckey := txkey.KeyExtractContainerKey(k)
n := len(v)
hash := hash.Blake3sum16(v[0:(n - 1)])
ct := pilosa.ToContainer(v[n-1], v[0:(n-1)])
cts := roaring.NewSliceContainers()
cts.Put(ckey, ct)
rbm := &roaring.Bitmap{Containers: cts}
srbm := pilosa.BitmapAsString(rbm)
fmt.Printf("%04v %v -> %v (%v hot)\n", i, txkey.ToString(k), hash, ct.N())
fmt.Printf(" .......%v\n", srbm)
}
return
})
panicOn(err)
} // for dbnames
fmt.Printf("=================== done.\n")
}

View file

@ -0,0 +1,179 @@
// home: https://github.com/glycerine/vprint
// Copyright 2019 Jason E. Aten, Ph.D. All rights reserved.
// License: MIT
//
// MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// +build amd64
package main
import (
"fmt"
"io"
"os"
"path"
"runtime"
"runtime/debug"
"sync"
"time"
)
const RFC3339MsecTz0 = "2006-01-02T15:04:05.000Z07:00"
const RFC3339UsecTz0 = "2006-01-02T15:04:05.000000Z07:00"
// for tons of debug output
var VerboseVerbose bool = false
// convience functions for . import
var pp = PP
var vv = VV
var panicOn = PanicOn
func init() {
// keeper linter happy
_ = pp
_ = vv
}
func PanicOn(err error) {
if err != nil {
panic(err)
}
}
func PP(format string, a ...interface{}) {
if VerboseVerbose {
TSPrintf(format, a...)
}
}
func VV(format string, a ...interface{}) {
TSPrintf(format, a...)
}
func AlwaysPrintf(format string, a ...interface{}) {
TSPrintf(format, a...)
}
var tsPrintfMut sync.Mutex
// time-stamped printf
func TSPrintf(format string, a ...interface{}) {
tsPrintfMut.Lock()
Printf("\n%s %s ", FileLine(3), ts())
Printf(format+"\n", a...)
tsPrintfMut.Unlock()
}
// get timestamp for logging purposes
func ts() string {
return time.Now().Format(RFC3339UsecTz0)
}
// so we can multi write easily, use our own printf
var OurStdout io.Writer = os.Stdout
// Printf formats according to a format specifier and writes to standard output.
// It returns the number of bytes written and any write error encountered.
func Printf(format string, a ...interface{}) (n int, err error) {
return fmt.Fprintf(OurStdout, format, a...)
}
func FileLine(depth int) string {
_, fileName, fileLine, ok := runtime.Caller(depth)
var s string
if ok {
s = fmt.Sprintf("%s:%d", path.Base(fileName), fileLine)
} else {
s = ""
}
return s
}
func stack() string {
return string(debug.Stack())
}
func FileExists(name string) bool {
fi, err := os.Stat(name)
if err != nil {
return false
}
if fi.IsDir() {
return false
}
return true
}
func DirExists(name string) bool {
fi, err := os.Stat(name)
if err != nil {
return false
}
if fi.IsDir() {
return true
}
return false
}
func FileSize(name string) (int64, error) {
fi, err := os.Stat(name)
if err != nil {
return -1, err
}
return fi.Size(), nil
}
// Caller returns the name of the calling function.
func Caller(upStack int) string {
// elide ourself and runtime.Callers
target := upStack + 2
pc := make([]uintptr, target+2)
n := runtime.Callers(0, pc)
f := runtime.Frame{Function: "unknown"}
if n > 0 {
frames := runtime.CallersFrames(pc[:n])
for i := 0; i <= target; i++ {
contender, more := frames.Next()
if i == target {
f = contender
}
if !more {
break
}
}
}
return f.Function
}
// happy linter:
var _ = DirExists
var _ = FileExists
var _ = Caller
var _ = stack
var _ = RFC3339MsecTz0
var _ = RFC3339UsecTz0
var _ = AlwaysPrintf
var _ = FileSize

View file

@ -26,23 +26,18 @@ import (
) )
func NewRootCommand(stdin io.Reader, stdout, stderr io.Writer) *cobra.Command { func NewRootCommand(stdin io.Reader, stdout, stderr io.Writer) *cobra.Command {
productName := "Pilosa " + pilosa.Version
if pilosa.EnterpriseEnabled {
productName = "Pilosa Enterprise " + pilosa.Version
}
rc := &cobra.Command{ rc := &cobra.Command{
Use: "pilosa", Use: "pilosa",
// TODO: These short/long descriptions could use some updating.
Short: "Pilosa - A Distributed In-memory Binary Bitmap Index.", Short: "Pilosa - A Distributed In-memory Binary Bitmap Index.",
// TODO - is documentation actually there?
Long: `Pilosa is a fast index to turbocharge your database. Long: `Pilosa is a fast index to turbocharge your database.
This binary contains Pilosa itself, as well as common This binary contains Pilosa itself, as well as common
tools for administering pilosa, importing/exporting data, tools for administering Pilosa, importing/exporting data,
backing up, and more. Complete documentation is available backing up, and more. Complete documentation is available
at https://www.pilosa.com/docs/. at https://www.pilosa.com/docs/.
` + productName + ` ` + pilosa.VersionInfo() + "\n",
Build Time: ` + pilosa.BuildTime + "\n",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
v := viper.New() v := viper.New()
err := setAllConfig(v, cmd.Flags(), "PILOSA") err := setAllConfig(v, cmd.Flags(), "PILOSA")

View file

@ -55,6 +55,9 @@ func tExec(t *testing.T, cmd *cobra.Command, out io.Reader, w io.WriteCloser) (o
if err := w.Close(); err != nil { if err := w.Close(); err != nil {
return output, fmt.Errorf("closing cmd's stdout: %v", err) return output, fmt.Errorf("closing cmd's stdout: %v", err)
} }
// NOTE: if cmd.Execute doesn't return, then this select (and
// therefore the one-second timeout, won't be reached)
select { select {
case <-done: case <-done:
case <-time.After(time.Second * 1): case <-time.After(time.Second * 1):
@ -64,7 +67,7 @@ func tExec(t *testing.T, cmd *cobra.Command, out io.Reader, w io.WriteCloser) (o
} }
// ExecNewRootCommand executes the pilosa root command with the given arguments // ExecNewRootCommand executes the pilosa root command with the given arguments
// and returns it's output. It will fail if the command does not complete within // and returns its output. It will fail if the command does not complete within
// 1 second. // 1 second.
func ExecNewRootCommand(t *testing.T, args ...string) (string, error) { func ExecNewRootCommand(t *testing.T, args ...string) (string, error) {
out, w := io.Pipe() out, w := io.Pipe()

View file

@ -42,7 +42,7 @@ func TestServerConfig(t *testing.T) {
tests := []commandTest{ tests := []commandTest{
// TEST 0 // TEST 0
{ {
args: []string{"server", "--data-dir", actualDataDir, "--cluster.hosts", "localhost:42454,localhost:10110", "--bind", "localhost:42454", "--translation.map-size", "100000"}, args: []string{"server", "--data-dir", actualDataDir, "--cluster.hosts", "localhost:42454,localhost:10110", "--bind", "localhost:42454", "--bind-grpc", "localhost:30112", "--translation.map-size", "100000"},
env: map[string]string{ env: map[string]string{
"PILOSA_DATA_DIR": "/tmp/myEnvDatadir", "PILOSA_DATA_DIR": "/tmp/myEnvDatadir",
"PILOSA_CLUSTER_LONG_QUERY_TIME": "1m30s", "PILOSA_CLUSTER_LONG_QUERY_TIME": "1m30s",
@ -53,6 +53,7 @@ func TestServerConfig(t *testing.T) {
cfgFileContent: ` cfgFileContent: `
data-dir = "/tmp/myFileDatadir" data-dir = "/tmp/myFileDatadir"
bind = "localhost:0" bind = "localhost:0"
bind-grpc = "localhost:0"
max-writes-per-request = 3000 max-writes-per-request = 3000
[cluster] [cluster]
@ -96,6 +97,7 @@ func TestServerConfig(t *testing.T) {
}, },
cfgFileContent: ` cfgFileContent: `
bind = "localhost:0" bind = "localhost:0"
bind-grpc = "localhost:0"
data-dir = "` + actualDataDir + `" data-dir = "` + actualDataDir + `"
[cluster] [cluster]
disabled = true disabled = true
@ -122,6 +124,7 @@ func TestServerConfig(t *testing.T) {
env: map[string]string{}, env: map[string]string{},
cfgFileContent: ` cfgFileContent: `
bind = "localhost:19444" bind = "localhost:19444"
bind-grpc = "localhost:29444"
data-dir = "` + actualDataDir + `" data-dir = "` + actualDataDir + `"
[cluster] [cluster]
hosts = [ hosts = [

212
cmd/slurp/slurp.go Normal file
View file

@ -0,0 +1,212 @@
// Copyright 2020 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"flag"
"time"
//"fmt"
"fmt"
"io"
"io/ioutil"
gohttp "net/http"
"github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/http"
//"log"
"os"
//"path/filepath"
//"sort"
"strconv"
"strings"
)
// slurp: slurp is a load-tester for importing bulk data.
// It allows us to measure write performance.
type stateMachine struct {
viewData map[string][]byte
lastIndex string
lastField string
lastShard uint64
state string
client *http.InternalClient
start time.Time
direct bool
}
func (r *stateMachine) NewHeader(h *tar.Header, tr *tar.Reader) error {
parts := strings.Split(h.Name, "/")
switch parts[0] {
case "roaring":
index := parts[1]
field := parts[2]
view := parts[4]
shard, err := strconv.ParseUint(parts[6], 10, 64)
panicOn(err)
if index != r.lastIndex || field != r.lastField || shard != r.lastShard {
err := r.Upload()
if err != nil {
return err
}
}
roaringData, err := ioutil.ReadAll(tr)
if err != nil {
return err
}
if _, already := r.viewData[view]; already {
panic(fmt.Sprintf("view '%v' already present!", view))
}
r.viewData[view] = roaringData
r.lastIndex = index
r.lastField = field
r.lastShard = shard
case "bolt":
if r.state == "roaring" {
err := r.Upload()
if err != nil {
return err
}
vv("Finished import %v", time.Since(r.start))
}
//
uri := GetImportRoaringURI(r.lastIndex, r.lastShard)
switch v := parts[len(parts)-1]; v {
case "keys":
index := parts[1]
fieldName := parts[2]
if fieldName == "_keys" {
//skip index keys are not not real fields so will have no need for field keys
return nil
}
byteData, err := ioutil.ReadAll(tr)
panicOn(err)
br := bytes.NewReader(byteData)
err = r.client.ImportFieldKeys(context.Background(), uri, index, fieldName, false, br)
if err != nil {
return err
}
default:
pilosa.VV("%v", h.Name)
index := parts[1]
partition, err := strconv.ParseUint(v, 10, 64)
if err != nil {
return err
}
byteData, err := ioutil.ReadAll(tr)
panicOn(err)
br := bytes.NewReader(byteData)
err = r.client.ImportIndexKeys(context.Background(), uri, index, int(partition), false, br)
if err != nil {
return err
}
}
}
r.state = parts[0]
return nil
}
func (r *stateMachine) Upload() error {
if len(r.viewData) > 0 {
request := &pilosa.ImportRoaringRequest{
Views: r.viewData,
Direct: r.direct,
}
uri := GetImportRoaringURI(r.lastIndex, r.lastShard)
err := r.client.ImportRoaring(context.Background(), uri, r.lastIndex, r.lastField, r.lastShard, false, request)
if err != nil {
return err
}
r.viewData = make(map[string][]byte)
}
return nil
}
func UploadTar(srcFile string, direct bool, client *http.InternalClient) error {
f, err := os.Open(srcFile)
if err != nil {
return (err)
}
defer f.Close()
var tarReader *tar.Reader
if strings.HasSuffix(srcFile, "gz") {
gzf, err := gzip.NewReader(f)
if err != nil {
return err
}
tarReader = tar.NewReader(gzf)
} else {
tarReader = tar.NewReader(f)
}
runner := &stateMachine{
viewData: make(map[string][]byte),
start: time.Now(),
direct: direct,
}
runner.client = client
for {
header, err := tarReader.Next()
if err == io.EOF {
_ = runner.Upload()
break
}
if err != nil {
panicOn(err)
}
err = runner.NewHeader(header, tarReader)
panicOn(err)
}
return nil
}
func main() {
var host string
var direct bool
var tarSrcPath string
flag.StringVar(&host, "host", "127.0.0.1:10101", "host to import into")
flag.BoolVar(&direct, "direct", false, "direct write to database (unsafe)")
flag.StringVar(&tarSrcPath, "src", "q2.tar.gz", "data to import")
flag.Parse()
uri, err := pilosa.NewURIFromAddress(host)
panicOn(err)
globURI = uri
h := &gohttp.Client{}
c, err := http.NewInternalClient(host, h)
panicOn(err)
t0 := time.Now()
println("uploading", tarSrcPath)
panicOn(UploadTar(tarSrcPath, direct, c))
vv("total elapsed '%v'", time.Since(t0))
}
var globURI *pilosa.URI
// get correct node to go to.
func GetImportRoaringURI(index string, shard uint64) *pilosa.URI {
return globURI
}

177
cmd/slurp/vprint.go Normal file
View file

@ -0,0 +1,177 @@
// home: https://github.com/glycerine/vprint
// Copyright 2019 Jason E. Aten, Ph.D. All rights reserved.
// License: MIT
//
// MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package main
import (
"fmt"
"io"
"os"
"path"
"runtime"
"runtime/debug"
"sync"
"time"
)
const RFC3339MsecTz0 = "2006-01-02T15:04:05.000Z07:00"
const RFC3339UsecTz0 = "2006-01-02T15:04:05.000000Z07:00"
// for tons of debug output
var VerboseVerbose bool = false
// convience functions for . import
var pp = PP
var vv = VV
var panicOn = PanicOn
func init() {
// keeper linter happy
_ = pp
_ = vv
}
func PanicOn(err error) {
if err != nil {
panic(err)
}
}
func PP(format string, a ...interface{}) {
if VerboseVerbose {
TSPrintf(format, a...)
}
}
func VV(format string, a ...interface{}) {
TSPrintf(format, a...)
}
func AlwaysPrintf(format string, a ...interface{}) {
TSPrintf(format, a...)
}
var tsPrintfMut sync.Mutex
// time-stamped printf
func TSPrintf(format string, a ...interface{}) {
tsPrintfMut.Lock()
Printf("\n%s %s ", FileLine(3), ts())
Printf(format+"\n", a...)
tsPrintfMut.Unlock()
}
// get timestamp for logging purposes
func ts() string {
return time.Now().Format(RFC3339UsecTz0)
}
// so we can multi write easily, use our own printf
var OurStdout io.Writer = os.Stdout
// Printf formats according to a format specifier and writes to standard output.
// It returns the number of bytes written and any write error encountered.
func Printf(format string, a ...interface{}) (n int, err error) {
return fmt.Fprintf(OurStdout, format, a...)
}
func FileLine(depth int) string {
_, fileName, fileLine, ok := runtime.Caller(depth)
var s string
if ok {
s = fmt.Sprintf("%s:%d", path.Base(fileName), fileLine)
} else {
s = ""
}
return s
}
func stack() string {
return string(debug.Stack())
}
func FileExists(name string) bool {
fi, err := os.Stat(name)
if err != nil {
return false
}
if fi.IsDir() {
return false
}
return true
}
func DirExists(name string) bool {
fi, err := os.Stat(name)
if err != nil {
return false
}
if fi.IsDir() {
return true
}
return false
}
func FileSize(name string) (int64, error) {
fi, err := os.Stat(name)
if err != nil {
return -1, err
}
return fi.Size(), nil
}
// Caller returns the name of the calling function.
func Caller(upStack int) string {
// elide ourself and runtime.Callers
target := upStack + 2
pc := make([]uintptr, target+2)
n := runtime.Callers(0, pc)
f := runtime.Frame{Function: "unknown"}
if n > 0 {
frames := runtime.CallersFrames(pc[:n])
for i := 0; i <= target; i++ {
contender, more := frames.Next()
if i == target {
f = contender
}
if !more {
break
}
}
}
return f.Function
}
// happy linter:
var _ = DirExists
var _ = FileExists
var _ = Caller
var _ = stack
var _ = RFC3339MsecTz0
var _ = RFC3339UsecTz0
var _ = AlwaysPrintf
var _ = FileSize

View file

@ -31,12 +31,12 @@ type CommandWithTLSSupport interface {
} }
// SetTLSConfig creates common TLS flags // SetTLSConfig creates common TLS flags
func SetTLSConfig(flags *pflag.FlagSet, certificatePath *string, certificateKeyPath *string, caCertPath *string, skipVerify *bool, enableClientVerification *bool) { func SetTLSConfig(flags *pflag.FlagSet, prefix string, certificatePath *string, certificateKeyPath *string, caCertPath *string, skipVerify *bool, enableClientVerification *bool) {
flags.StringVarP(certificatePath, "tls.certificate", "", "", "TLS certificate path (usually has the .crt or .pem extension)") flags.StringVarP(certificatePath, prefix+"tls.certificate", "", "", "TLS certificate path (usually has the .crt or .pem extension)")
flags.StringVarP(certificateKeyPath, "tls.key", "", "", "TLS certificate key path (usually has the .key extension)") flags.StringVarP(certificateKeyPath, prefix+"tls.key", "", "", "TLS certificate key path (usually has the .key extension)")
flags.StringVarP(caCertPath, "tls.ca-certificate", "", "", "TLS CA certificate path (usually has the .pem extension)") flags.StringVarP(caCertPath, prefix+"tls.ca-certificate", "", "", "TLS CA certificate path (usually has the .pem extension)")
flags.BoolVarP(skipVerify, "tls.skip-verify", "", false, "Skip TLS certificate server verification (not secure)") flags.BoolVarP(skipVerify, prefix+"tls.skip-verify", "", false, "Skip TLS certificate server verification (not secure)")
flags.BoolVarP(enableClientVerification, "tls.enable-client-verification", "", false, "Enable TLS certificate client verification for incoming connections") flags.BoolVarP(enableClientVerification, prefix+"tls.enable-client-verification", "", false, "Enable TLS certificate client verification for incoming connections")
} }
// commandClient returns a pilosa.InternalHTTPClient for the command // commandClient returns a pilosa.InternalHTTPClient for the command

View file

@ -46,7 +46,7 @@ func TestExportCommand_Validation(t *testing.T) {
func TestExportCommand_Run(t *testing.T) { func TestExportCommand_Run(t *testing.T) {
cluster := test.MustRunCluster(t, 1) cluster := test.MustRunCluster(t, 1)
defer cluster.Close() defer cluster.Close()
cmd := cluster[0] cmd := cluster.GetNode(0)
buf := bytes.Buffer{} buf := bytes.Buffer{}
stdin, stdout, stderr := GetIO(buf) stdin, stdout, stderr := GetIO(buf)

View file

@ -20,6 +20,7 @@ import (
"fmt" "fmt"
"io" "io"
"log" "log"
"math"
"os" "os"
"sort" "sort"
"strconv" "strconv"
@ -27,6 +28,7 @@ import (
"github.com/pilosa/pilosa/v2" "github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/http" "github.com/pilosa/pilosa/v2/http"
"github.com/pilosa/pilosa/v2/pql"
"github.com/pilosa/pilosa/v2/server" "github.com/pilosa/pilosa/v2/server"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -102,11 +104,13 @@ func (cmd *ImportCommand) Run(ctx context.Context) error {
if cmd.FieldOptions.Type == "" { if cmd.FieldOptions.Type == "" {
// set the correct type for the field // set the correct type for the field
if cmd.FieldOptions.TimeQuantum != "" { if cmd.FieldOptions.TimeQuantum != "" {
cmd.FieldOptions.Type = "time" cmd.FieldOptions.Type = pilosa.FieldTypeTime
} else if cmd.FieldOptions.Min != 0 || cmd.FieldOptions.Max != 0 { } else if cmd.FieldOptions.Min != pql.NewDecimal(0, 0) || cmd.FieldOptions.Max != pql.NewDecimal(0, 0) {
cmd.FieldOptions.Type = "int" cmd.FieldOptions.Type = pilosa.FieldTypeInt
} else { } else {
cmd.FieldOptions.Type = "set" cmd.FieldOptions.Type = pilosa.FieldTypeSet
cmd.FieldOptions.CacheType = pilosa.CacheTypeRanked
cmd.FieldOptions.CacheSize = pilosa.DefaultCacheSize
} }
} }
err := cmd.ensureSchema(ctx) err := cmd.ensureSchema(ctx)
@ -163,8 +167,8 @@ func (cmd *ImportCommand) ensureSchema(ctx context.Context) error {
// importPath parses a path into bits and imports it to the server. // importPath parses a path into bits and imports it to the server.
func (cmd *ImportCommand) importPath(ctx context.Context, fieldType string, useColumnKeys, useRowKeys bool, path string) error { func (cmd *ImportCommand) importPath(ctx context.Context, fieldType string, useColumnKeys, useRowKeys bool, path string) error {
// If fieldType is `int`, treat the import data as values to be range-encoded. // If fieldType is `int`, treat the import data as values to be range-encoded.
if fieldType == pilosa.FieldTypeInt { if fieldType == pilosa.FieldTypeInt || fieldType == pilosa.FieldTypeDecimal {
return cmd.bufferValues(ctx, useColumnKeys, path) return cmd.bufferValues(ctx, useColumnKeys, fieldType == pilosa.FieldTypeDecimal, path)
} }
return cmd.bufferBits(ctx, useColumnKeys, useRowKeys, path) return cmd.bufferBits(ctx, useColumnKeys, useRowKeys, path)
} }
@ -285,9 +289,13 @@ func (cmd *ImportCommand) importBits(ctx context.Context, useColumnKeys, useRowK
return nil return nil
} }
// bufferValues buffers slices of FieldValues to be imported as a batch. // bufferValues buffers slices of record identifiers and values to be imported as a batch.
func (cmd *ImportCommand) bufferValues(ctx context.Context, useColumnKeys bool, path string) error { func (cmd *ImportCommand) bufferValues(ctx context.Context, useColumnKeys, parseAsFloat bool, path string) error {
a := make([]pilosa.FieldValue, 0, cmd.BufferSize) req := &pilosa.ImportValueRequest{
Index: cmd.Index,
Field: cmd.Field,
Shard: math.MaxUint64,
}
var r *csv.Reader var r *csv.Reader
@ -307,6 +315,7 @@ func (cmd *ImportCommand) bufferValues(ctx context.Context, useColumnKeys bool,
r.FieldsPerRecord = -1 r.FieldsPerRecord = -1
rnum := 0 rnum := 0
for { for {
rnum++ rnum++
@ -325,69 +334,44 @@ func (cmd *ImportCommand) bufferValues(ctx context.Context, useColumnKeys bool,
return fmt.Errorf("bad column count on row %d: col=%d", rnum, len(record)) return fmt.Errorf("bad column count on row %d: col=%d", rnum, len(record))
} }
var val pilosa.FieldValue
// Parse column id. // Parse column id.
if useColumnKeys { if useColumnKeys {
val.ColumnKey = record[0] req.ColumnKeys = append(req.ColumnKeys, record[0])
} else if columnID, err := strconv.ParseUint(record[0], 10, 64); err == nil {
req.ColumnIDs = append(req.ColumnIDs, columnID)
} else { } else {
if val.ColumnID, err = strconv.ParseUint(record[0], 10, 64); err != nil { return fmt.Errorf("invalid column id on row %d: %q", rnum, record[0])
return fmt.Errorf("invalid column id on row %d: %q", rnum, record[0])
}
} }
// Parse FieldValue. // Parse value.
value, err := strconv.ParseInt(record[1], 10, 64) if parseAsFloat {
if err != nil { value, err := strconv.ParseFloat(record[1], 64)
return fmt.Errorf("invalid value on row %d: %q", rnum, record[1]) if err != nil {
} return errors.Wrapf(err, "parseing value '%s' as float", record[1])
val.Value = value
a = append(a, val)
// If we've reached the buffer size then import FieldValues.
if len(a) == cmd.BufferSize {
if err := cmd.importValues(ctx, useColumnKeys, a); err != nil {
return err
} }
a = a[:0] req.FloatValues = append(req.FloatValues, value)
} else {
value, err := strconv.ParseInt(record[1], 10, 64)
if err != nil {
return errors.Wrapf(err, "invalid value on row %d: %q", rnum, record[1])
}
req.Values = append(req.Values, value)
}
// If we've reached the buffer size then import the batch.
if len(req.ColumnKeys) == cmd.BufferSize || len(req.ColumnIDs) == cmd.BufferSize {
if err := cmd.client.ImportValue2(ctx, req, &pilosa.ImportOptions{}); err != nil {
return errors.Wrap(err, "importing values")
}
req.ColumnIDs = req.ColumnIDs[:0]
req.ColumnKeys = req.ColumnKeys[:0]
req.Values = req.Values[:0]
req.FloatValues = req.FloatValues[:0]
} }
} }
// If there are still values in the buffer then flush them. // If there are still values in the buffer then flush them.
return cmd.importValues(ctx, useColumnKeys, a) return errors.Wrap(cmd.client.ImportValue2(ctx, req, &pilosa.ImportOptions{}), "importing values")
}
// importValues sends batches of FieldValues to the server.
func (cmd *ImportCommand) importValues(ctx context.Context, useColumnKeys bool, vals []pilosa.FieldValue) error {
logger := log.New(cmd.Stderr, "", log.LstdFlags)
// If keys are used, all values are sent to the primary translate store (i.e. coordinator).
if useColumnKeys {
logger.Printf("importing keyed values: n=%d", len(vals))
if err := cmd.client.ImportValueK(ctx, cmd.Index, cmd.Field, vals); err != nil {
return errors.Wrap(err, "importing keys")
}
return nil
}
// Group vals by shard.
logger.Printf("grouping %d vals", len(vals))
valsByShard := http.FieldValues(vals).GroupByShard()
// Parse path into FieldValues.
for shard, vals := range valsByShard {
if cmd.Sort {
sort.Sort(http.FieldValues(vals))
}
logger.Printf("importing shard: %d, n=%d", shard, len(vals))
if err := cmd.client.ImportValue(ctx, cmd.Index, cmd.Field, shard, vals, pilosa.OptImportOptionsClear(cmd.Clear)); err != nil {
return errors.Wrap(err, "importing values")
}
}
return nil
} }
func (cmd *ImportCommand) TLSHost() string { func (cmd *ImportCommand) TLSHost() string {

View file

@ -25,6 +25,7 @@ import (
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"time"
"github.com/pilosa/pilosa/v2" "github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/test" "github.com/pilosa/pilosa/v2/test"
@ -72,7 +73,7 @@ func TestImportCommand_Basic(t *testing.T) {
cluster := test.MustRunCluster(t, 1) cluster := test.MustRunCluster(t, 1)
defer cluster.Close() defer cluster.Close()
cmd := cluster[0] cmd := cluster.GetNode(0)
cm.Host = cmd.API.Node().URI.HostPort() cm.Host = cmd.API.Node().URI.HostPort()
cm.Index = "i" cm.Index = "i"
@ -101,7 +102,7 @@ func TestImportCommand_Basic(t *testing.T) {
cluster := test.MustRunCluster(t, 1) cluster := test.MustRunCluster(t, 1)
defer cluster.Close() defer cluster.Close()
cmd := cluster[0] cmd := cluster.GetNode(0)
cm.Host = cmd.API.Node().URI.HostPort() cm.Host = cmd.API.Node().URI.HostPort()
cm.Index = "i" cm.Index = "i"
@ -134,7 +135,7 @@ func TestImportCommand_RunValue(t *testing.T) {
cluster := test.MustRunCluster(t, 1) cluster := test.MustRunCluster(t, 1)
defer cluster.Close() defer cluster.Close()
cmd := cluster[0] cmd := cluster.GetNode(0)
cm.Host = cmd.API.Node().URI.HostPort() cm.Host = cmd.API.Node().URI.HostPort()
resp, err := http.DefaultClient.Do(MustNewHTTPRequest("POST", "http://"+cm.Host+"/index/i", strings.NewReader(""))) resp, err := http.DefaultClient.Do(MustNewHTTPRequest("POST", "http://"+cm.Host+"/index/i", strings.NewReader("")))
@ -176,7 +177,7 @@ func TestImportCommand_RunValue(t *testing.T) {
cluster := test.MustRunCluster(t, 1) cluster := test.MustRunCluster(t, 1)
defer cluster.Close() defer cluster.Close()
cmd := cluster[0] cmd := cluster.GetNode(0)
cm.Host = cmd.API.Node().URI.HostPort() cm.Host = cmd.API.Node().URI.HostPort()
resp, err := http.DefaultClient.Do(MustNewHTTPRequest("POST", "http://"+cm.Host+"/index/i", strings.NewReader(""))) resp, err := http.DefaultClient.Do(MustNewHTTPRequest("POST", "http://"+cm.Host+"/index/i", strings.NewReader("")))
@ -218,7 +219,7 @@ func TestImportCommand_RunKeys(t *testing.T) {
cluster := test.MustRunCluster(t, 1) cluster := test.MustRunCluster(t, 1)
defer cluster.Close() defer cluster.Close()
cmd := cluster[0] cmd := cluster.GetNode(0)
cm.Host = cmd.API.Node().URI.HostPort() cm.Host = cmd.API.Node().URI.HostPort()
resp, err := http.DefaultClient.Do(MustNewHTTPRequest("POST", "http://"+cm.Host+"/index/i", strings.NewReader(`{"options":{"keys": true}}`))) resp, err := http.DefaultClient.Do(MustNewHTTPRequest("POST", "http://"+cm.Host+"/index/i", strings.NewReader(`{"options":{"keys": true}}`)))
@ -270,8 +271,8 @@ func TestImportCommand_KeyReplication(t *testing.T) {
c := test.MustRunCluster(t, 2) c := test.MustRunCluster(t, 2)
defer c.Close() defer c.Close()
cmd0 := c[0] cmd0 := c.GetNode(0)
cmd1 := c[1] cmd1 := c.GetNode(1)
host0 := cmd0.API.Node().URI.HostPort() host0 := cmd0.API.Node().URI.HostPort()
host1 := cmd1.API.Node().URI.HostPort() host1 := cmd1.API.Node().URI.HostPort()
@ -299,20 +300,26 @@ func TestImportCommand_KeyReplication(t *testing.T) {
// Verify that the data is available on both nodes. // Verify that the data is available on both nodes.
for _, host := range []string{host0, host1} { for _, host := range []string{host0, host1} {
qry := "Count(Row(f=foo0))" if err := test.RetryUntil(2*time.Second, func() error {
resp, err := http.DefaultClient.Do(MustNewHTTPRequest("POST", "http://"+host+"/index/i/query", strings.NewReader(qry))) qry := "Count(Row(f=foo0))"
if err != nil { resp, err := http.DefaultClient.Do(MustNewHTTPRequest("POST", "http://"+host+"/index/i/query", strings.NewReader(qry)))
t.Fatalf("Querying data for validation: %s", err) if err != nil {
} return fmt.Errorf("Querying data for validation: %s", err)
}
// Read body and unmarshal response. // Read body and unmarshal response.
exp := `{"results":[100]}` + "\n" exp := `{"results":[100]}` + "\n"
if body, err := ioutil.ReadAll(resp.Body); err != nil { if body, err := ioutil.ReadAll(resp.Body); err != nil {
t.Fatalf("reading: %s", err) return fmt.Errorf("reading: %s", err)
} else if !reflect.DeepEqual(body, []byte(exp)) { } else if !reflect.DeepEqual(body, []byte(exp)) {
t.Fatalf("expected: %s, but got: %s", exp, body) return fmt.Errorf("expected: %s, but got: %s", exp, body)
}
return nil
}); err != nil {
t.Fatal(err)
} }
} }
} }
// Ensure that integer import with keys runs. // Ensure that integer import with keys runs.
@ -332,7 +339,7 @@ func TestImportCommand_RunValueKeys(t *testing.T) {
cluster := test.MustRunCluster(t, 1) cluster := test.MustRunCluster(t, 1)
defer cluster.Close() defer cluster.Close()
cmd := cluster[0] cmd := cluster.GetNode(0)
cm.Host = cmd.API.Node().URI.HostPort() cm.Host = cmd.API.Node().URI.HostPort()
resp, err := http.DefaultClient.Do(MustNewHTTPRequest("POST", "http://"+cm.Host+"/index/i", strings.NewReader(`{"options":{"keys": true}}`))) resp, err := http.DefaultClient.Do(MustNewHTTPRequest("POST", "http://"+cm.Host+"/index/i", strings.NewReader(`{"options":{"keys": true}}`)))
@ -358,7 +365,7 @@ func TestImportCommand_RunValueKeys(t *testing.T) {
func TestImportCommand_InvalidFile(t *testing.T) { func TestImportCommand_InvalidFile(t *testing.T) {
cluster := test.MustRunCluster(t, 1) cluster := test.MustRunCluster(t, 1)
defer cluster.Close() defer cluster.Close()
cmd := cluster[0] cmd := cluster.GetNode(0)
buf := bytes.Buffer{} buf := bytes.Buffer{}
stdin, stdout, stderr := GetIO(buf) stdin, stdout, stderr := GetIO(buf)
@ -446,7 +453,7 @@ func GetIO(buf bytes.Buffer) (io.Reader, io.Writer, io.Writer) {
func TestImportCommand_BugOverwriteValue(t *testing.T) { func TestImportCommand_BugOverwriteValue(t *testing.T) {
cluster := test.MustRunCluster(t, 1) cluster := test.MustRunCluster(t, 1)
defer cluster.Close() defer cluster.Close()
cmd := cluster[0] cmd := cluster.GetNode(0)
buf := bytes.Buffer{} buf := bytes.Buffer{}
stdin, stdout, stderr := GetIO(buf) stdin, stdout, stderr := GetIO(buf)
@ -522,7 +529,7 @@ func TestImportCommand_RunBool(t *testing.T) {
cluster := test.MustRunCluster(t, 1) cluster := test.MustRunCluster(t, 1)
defer cluster.Close() defer cluster.Close()
cmd := cluster[0] cmd := cluster.GetNode(0)
cm.Host = cmd.API.Node().URI.HostPort() cm.Host = cmd.API.Node().URI.HostPort()
resp, err := http.DefaultClient.Do(MustNewHTTPRequest("POST", "http://"+cm.Host+"/index/i", strings.NewReader(""))) resp, err := http.DefaultClient.Do(MustNewHTTPRequest("POST", "http://"+cm.Host+"/index/i", strings.NewReader("")))

View file

@ -16,15 +16,23 @@ package ctl
import ( import (
"context" "context"
"encoding/binary"
"fmt" "fmt"
"hash/fnv"
"io" "io"
"io/ioutil"
"os" "os"
"path/filepath"
"sort"
"strconv"
"strings"
"syscall" "syscall"
"text/tabwriter" "text/tabwriter"
"time" "time"
"unsafe"
"github.com/gogo/protobuf/proto"
"github.com/pilosa/pilosa/v2" "github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/internal"
"github.com/pilosa/pilosa/v2/roaring" "github.com/pilosa/pilosa/v2/roaring"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -33,6 +41,12 @@ import (
type InspectCommand struct { type InspectCommand struct {
// Path to data file // Path to data file
Path string Path string
// don't list details of objects
Quiet bool
// list only this many objects
Max int
// Filters:
InspectOpts pilosa.InspectRequest
// Standard input/output // Standard input/output
*pilosa.CmdIO *pilosa.CmdIO
@ -45,8 +59,119 @@ func NewInspectCommand(stdin io.Reader, stdout, stderr io.Writer) *InspectComman
} }
} }
type pointerContext struct {
from, to uintptr
}
func (p *pointerContext) pretty(c roaring.ContainerInfo) string {
var pointer string
if c.Mapped {
if c.Pointer >= p.from && c.Pointer < p.to {
pointer = fmt.Sprintf("@+0x%x", c.Pointer-p.from)
} else {
pointer = fmt.Sprintf("!0x%x!", c.Pointer)
}
} else {
pointer = fmt.Sprintf("0x%x", c.Pointer)
}
return fmt.Sprintf("%s \t%d \t%d \t%s ", c.Type, c.N, c.Alloc, pointer)
}
func (cmd *InspectCommand) PrintOps(info roaring.BitmapInfo) {
fmt.Fprintln(cmd.Stdout, " Ops:")
tw := tabwriter.NewWriter(cmd.Stdout, 0, 8, 0, '\t', 0)
fmt.Fprintf(tw, " \t%s\t%s\t%s\t\n", "TYPE", "OpN", "SIZE")
printed := 0
for _, op := range info.OpDetails {
fmt.Fprintf(tw, "\t%s\t%d\t%d\t\n", op.Type, op.OpN, op.Size)
printed++
if cmd.Max != 0 && printed >= cmd.Max {
break
}
}
tw.Flush()
}
func (cmd *InspectCommand) PrintContainers(info roaring.BitmapInfo, pC pointerContext) {
fmt.Fprintln(cmd.Stdout, " Containers:")
tw := tabwriter.NewWriter(cmd.Stdout, 0, 8, 0, '\t', 0)
fmt.Fprintf(tw, " \t\tRoaring\t\t\t\tOps\t\t\t\tFlags\t\n")
fmt.Fprintf(tw, "\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t\n", "KEY", "TYPE", "N", "ALLOC", "OFFSET", "TYPE", "N", "ALLOC", "OFFSET", "FLAGS")
c1s := info.Containers
c2s := info.OpContainers
l1 := len(c1s)
l2 := len(c2s)
i1 := 0
i2 := 0
var c1, c2 roaring.ContainerInfo
c1.Key = ^uint64(0)
c2.Key = ^uint64(0)
c1e := false
c2e := false
if i1 < l1 {
c1 = c1s[i1]
i1++
c1e = true
}
if i2 < l2 {
c2 = c2s[i2]
i2++
c2e = true
}
printed := 0
for c1e || c2e {
c1used := false
c2used := false
var key uint64
c1fmt := "-\t\t\t"
c2fmt := "-\t\t\t"
// If c2 exists, we'll always prefer its flags,
// if it doesn't, this gets overwritten.
flags := c2.Flags
if !c2e || (c1e && c1.Key < c2.Key) {
c1fmt = pC.pretty(c1)
key = c1.Key
c1used = true
flags = c1.Flags
} else if !c1e || (c2e && c2.Key < c1.Key) {
c2fmt = pC.pretty(c2)
key = c2.Key
c2used = true
} else {
// c1e and c2e both set, and neither key is < the other.
c1fmt = pC.pretty(c1)
c2fmt = pC.pretty(c2)
key = c1.Key
c1used = true
c2used = true
}
if c1used {
if i1 < l1 {
c1 = c1s[i1]
i1++
} else {
c1e = false
}
}
if c2used {
if i2 < l2 {
c2 = c2s[i2]
i2++
} else {
c2e = false
}
}
fmt.Fprintf(tw, "\t%d\t%s\t%s\t%s\t\n", key, c1fmt, c2fmt, flags)
printed++
if cmd.Max > 0 && printed >= cmd.Max {
break
}
}
tw.Flush()
}
// Run executes the inspect command. // Run executes the inspect command.
func (cmd *InspectCommand) Run(_ context.Context) error { func (cmd *InspectCommand) Run(ctx context.Context) error {
// Open file handle. // Open file handle.
f, err := os.Open(cmd.Path) f, err := os.Open(cmd.Path)
if err != nil { if err != nil {
@ -58,7 +183,172 @@ func (cmd *InspectCommand) Run(_ context.Context) error {
if err != nil { if err != nil {
return errors.Wrap(err, "statting file") return errors.Wrap(err, "statting file")
} }
if fi.IsDir() {
total := 0
infos, err := f.Readdir(0)
if err != nil {
return err
}
if len(infos) == 0 {
return errors.New("directory contains no files")
}
names := make([]string, len(infos))
nameToInfo := make(map[string]os.FileInfo, len(infos))
// find numeric-only names; we'll operate on
// either those, or the whole holder if we find
// a .topology file.
n := 0
for _, fi := range infos {
name := fi.Name()
if name == ".topology" {
return cmd.InspectHolder(ctx, cmd.Path)
}
if _, err := strconv.Atoi(name); err == nil {
names[n] = name
nameToInfo[name] = fi
n++
}
}
if n == 0 {
return fmt.Errorf("directory contains no fragments (looking for numeric names)")
}
names = names[:n]
fmt.Fprintf(cmd.Stdout, "%s contains %d fragments:\n", cmd.Path, n)
for _, name := range names {
f2, err := os.Open(filepath.Join(cmd.Path, name))
if err != nil {
return fmt.Errorf("opening %q: %v", name, err)
}
fmt.Fprintf(cmd.Stdout, "%s/%s:\n", cmd.Path, name)
err = cmd.InspectFile(f2, nameToInfo[name])
total++
f2.Close()
if err != nil {
return fmt.Errorf("inspecting %q: %v", name, err)
}
}
return nil
}
return cmd.InspectFile(f, fi)
}
// loadTopology is copied almost exactly from pilosa/cluster.go.
func loadTopology(path string) (topology internal.Topology, myID string, err error) {
buf, err := ioutil.ReadFile(filepath.Join(path, ".topology"))
if os.IsNotExist(err) {
return topology, myID, err
} else if err != nil {
return topology, myID, errors.Wrap(err, "reading file")
}
if err := proto.Unmarshal(buf, &topology); err != nil {
return topology, myID, errors.Wrap(err, "unmarshalling")
}
sort.Slice(topology.NodeIDs,
func(i, j int) bool {
return topology.NodeIDs[i] < topology.NodeIDs[j]
})
buf, err = ioutil.ReadFile(filepath.Join(path, ".id"))
if os.IsNotExist(err) {
return topology, myID, err
} else if err != nil {
return topology, myID, nil
}
myID = strings.TrimSpace(string(buf))
return topology, myID, nil
}
var partitions = make(map[string]map[uint64]int)
func findPartition(index string, shard uint64, partitionN int) (partition int) {
var shardMap map[uint64]int
var ok bool
if shardMap, ok = partitions[index]; !ok {
shardMap = make(map[uint64]int)
partitions[index] = shardMap
}
if partition, ok = shardMap[shard]; !ok {
var buf [8]byte
binary.BigEndian.PutUint64(buf[:], shard)
// Hash the bytes and mod by partition count.
h := fnv.New64a()
_, _ = h.Write([]byte(index))
_, _ = h.Write(buf[:])
partition = int(h.Sum64() % uint64(partitionN))
shardMap[shard] = partition
}
return partition
}
func findPartitionPath(path string, partitionN int) (int, error) {
parts := strings.Split(path, "/")
shard, err := strconv.ParseUint(parts[len(parts)-1], 10, 64)
if err != nil {
return 0, err
}
return findPartition(parts[0], shard, partitionN), nil
}
func (cmd *InspectCommand) InspectHolder(ctx context.Context, path string) error {
holder := pilosa.NewHolder(path, nil)
holder.Opts.Inspect = true
holder.Opts.ReadOnly = true
err := holder.Open()
if err != nil {
return fmt.Errorf("%s: holder open: %v", path, err)
}
holderInfo, err := holder.Inspect(ctx, &cmd.InspectOpts)
if err != nil {
return fmt.Errorf("%s: inspect: %v", path, err)
}
myPartition := 0
topology, myID, err := loadTopology(path)
if err == nil {
fmt.Fprintf(cmd.Stdout, "Cluster ID: %q\n", topology.ClusterID)
if len(topology.NodeIDs) > 1 {
fmt.Fprintf(cmd.Stdout, "Cluster of %d nodes, this node %q\n", len(topology.NodeIDs), myID)
} else {
fmt.Fprintf(cmd.Stdout, "Cluster has only one node: %q\n", myID)
}
found := false
for i := range topology.NodeIDs {
if topology.NodeIDs[i] == myID {
found = true
myPartition = i
break
}
}
if !found {
fmt.Fprintf(cmd.Stdout, "Warning: node ID %q not found in topology (%q)\n", myID, topology.NodeIDs)
}
} else {
fmt.Fprintf(cmd.Stdout, "warning: reading topology failed: %v\n", err)
}
for _, name := range holderInfo.FragmentNames {
partition, err := findPartitionPath(name, len(topology.NodeIDs))
if err != nil {
fmt.Fprintf(cmd.Stdout, "%s: [can't find partition: %v]\n", name, err)
} else {
if partition == myPartition {
fmt.Fprintf(cmd.Stdout, "%s:\n", name)
} else {
fmt.Fprintf(cmd.Stdout, "%s: [primary node %q]\n", name, topology.NodeIDs[partition])
}
}
details := holderInfo.FragmentInfo[name]
cmd.DisplayInfo(details.BitmapInfo)
if details.BlockChecksums != nil {
fmt.Fprintf(cmd.Stdout, " Checksums [%d total]:\n", len(details.BlockChecksums))
for _, block := range details.BlockChecksums {
fmt.Fprintf(cmd.Stdout, " %8d: %x\n", block.ID, block.Checksum)
}
}
}
return nil
}
func (cmd *InspectCommand) InspectFile(f *os.File, fi os.FileInfo) error {
// Memory map the file. // Memory map the file.
data, err := syscall.Mmap(int(f.Fd()), 0, int(fi.Size()), syscall.PROT_READ, syscall.MAP_SHARED) data, err := syscall.Mmap(int(f.Fd()), 0, int(fi.Size()), syscall.PROT_READ, syscall.MAP_SHARED)
if err != nil { if err != nil {
@ -72,39 +362,37 @@ func (cmd *InspectCommand) Run(_ context.Context) error {
}() }()
// Attach the mmap file to the bitmap. // Attach the mmap file to the bitmap.
t := time.Now() t := time.Now()
fmt.Fprintf(cmd.Stderr, "unmarshalling bitmap...") fmt.Fprintf(cmd.Stderr, "inspecting bitmap...")
bm := roaring.NewBitmap() var info roaring.BitmapInfo
if err := bm.UnmarshalBinary(data); err != nil { _, _, err = roaring.InspectBinary(data, true, &info)
return errors.Wrap(err, "unmarshalling") fmt.Fprintf(cmd.Stderr, " (%s)\n", time.Since(t))
cmd.DisplayInfo(info)
if err != nil {
return errors.Wrap(err, "inspecting")
} }
fmt.Fprintf(cmd.Stderr, " (%s)\n", time.Since(t)) return nil
}
// Retrieve stats. func (cmd *InspectCommand) DisplayInfo(info roaring.BitmapInfo) {
t = time.Now() pC := pointerContext{
fmt.Fprintf(cmd.Stderr, "calculating stats...") from: info.From,
info := bm.Info() to: info.To,
fmt.Fprintf(cmd.Stderr, " (%s)\n", time.Since(t)) }
// Print top-level info. // Print top-level info.
fmt.Fprintf(cmd.Stdout, "== Bitmap Info ==\n") fmt.Fprintf(cmd.Stdout, " Bitmap Info:\n")
fmt.Fprintf(cmd.Stdout, "Containers: %d\n", len(info.Containers)) fmt.Fprintf(cmd.Stdout, " Bits: %d\n", info.BitCount)
fmt.Fprintf(cmd.Stdout, "Operations: %d\n", info.OpN) fmt.Fprintf(cmd.Stdout, " Containers: %d (%d roaring)\n", info.ContainerCount, len(info.Containers))
fmt.Fprintf(cmd.Stdout, " Operations: %d (%d bits)\n", info.Ops, info.OpN)
fmt.Fprintln(cmd.Stdout, "") fmt.Fprintln(cmd.Stdout, "")
// Print info for each container. // Print info for each container.
fmt.Fprintln(cmd.Stdout, "== Containers ==") if !cmd.Quiet {
tw := tabwriter.NewWriter(cmd.Stdout, 0, 8, 0, '\t', 0) if info.ContainerCount > 0 {
fmt.Fprintf(tw, "%s\t%s\t% 8s \t% 8s\t%s\n", "KEY", "TYPE", "N", "ALLOC", "OFFSET") cmd.PrintContainers(info, pC)
for _, ci := range info.Containers { }
fmt.Fprintf(tw, "%d\t%s\t% 8d \t% 8d \t0x%08x\n", if info.Ops > 0 {
ci.Key, cmd.PrintOps(info)
ci.Type, }
ci.N,
ci.Alloc,
uintptr(ci.Pointer)-uintptr(unsafe.Pointer(&data[0])),
)
} }
tw.Flush()
return nil
} }

View file

@ -41,7 +41,7 @@ func TestInspectCommand_Run(t *testing.T) {
file.Close() file.Close()
cm.Path = file.Name() cm.Path = file.Name()
err = cm.Run(context.Background()) err = cm.Run(context.Background())
expectedError := "unmarshalling: " expectedError := "inspecting: "
if !strings.Contains(err.Error(), expectedError) { if !strings.Contains(err.Error(), expectedError) {
t.Fatalf("expected error '%s', got '%v'", expectedError, err) t.Fatalf("expected error '%s', got '%v'", expectedError, err)
} }
@ -52,7 +52,7 @@ func TestInspectCommand_Run(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("copying data: %v", err) t.Fatalf("copying data: %v", err)
} }
if !strings.Contains(buf.String(), "unmarshalling bitmap...") { if !strings.Contains(buf.String(), "inspecting bitmap...") {
t.Fatalf("Inspect doesn't work: %s", err) t.Fatalf("Inspect doesn't work: %s", err)
} }

View file

@ -26,7 +26,9 @@ func BuildServerFlags(cmd *cobra.Command, srv *server.Command) {
flags := cmd.Flags() flags := cmd.Flags()
flags.StringVarP(&srv.Config.DataDir, "data-dir", "d", srv.Config.DataDir, "Directory to store pilosa data files.") flags.StringVarP(&srv.Config.DataDir, "data-dir", "d", srv.Config.DataDir, "Directory to store pilosa data files.")
flags.StringVarP(&srv.Config.Bind, "bind", "b", srv.Config.Bind, "Default URI on which pilosa should listen.") flags.StringVarP(&srv.Config.Bind, "bind", "b", srv.Config.Bind, "Default URI on which pilosa should listen.")
flags.StringVar(&srv.Config.BindGRPC, "bind-grpc", srv.Config.BindGRPC, "URI on which pilosa should listen for gRPC requests.")
flags.StringVar(&srv.Config.Advertise, "advertise", srv.Config.Advertise, "Address to advertise externally.") flags.StringVar(&srv.Config.Advertise, "advertise", srv.Config.Advertise, "Address to advertise externally.")
flags.StringVar(&srv.Config.AdvertiseGRPC, "advertise-grpc", srv.Config.AdvertiseGRPC, "Address to advertise externally for gRPC.")
flags.IntVarP(&srv.Config.MaxWritesPerRequest, "max-writes-per-request", "", srv.Config.MaxWritesPerRequest, "Number of write commands per request.") flags.IntVarP(&srv.Config.MaxWritesPerRequest, "max-writes-per-request", "", srv.Config.MaxWritesPerRequest, "Number of write commands per request.")
flags.StringVar(&srv.Config.LogPath, "log-path", srv.Config.LogPath, "Log path") flags.StringVar(&srv.Config.LogPath, "log-path", srv.Config.LogPath, "Log path")
flags.BoolVar(&srv.Config.Verbose, "verbose", srv.Config.Verbose, "Enable verbose logging") flags.BoolVar(&srv.Config.Verbose, "verbose", srv.Config.Verbose, "Enable verbose logging")
@ -34,10 +36,10 @@ func BuildServerFlags(cmd *cobra.Command, srv *server.Command) {
flags.Uint64Var(&srv.Config.MaxFileCount, "max-file-count", srv.Config.MaxFileCount, "Soft limit on the maximum number of fragment files Pilosa keeps open simultaneously.") flags.Uint64Var(&srv.Config.MaxFileCount, "max-file-count", srv.Config.MaxFileCount, "Soft limit on the maximum number of fragment files Pilosa keeps open simultaneously.")
// TLS // TLS
SetTLSConfig(flags, &srv.Config.TLS.CertificatePath, &srv.Config.TLS.CertificateKeyPath, &srv.Config.TLS.CACertPath, &srv.Config.TLS.SkipVerify, &srv.Config.TLS.EnableClientVerification) SetTLSConfig(flags, "", &srv.Config.TLS.CertificatePath, &srv.Config.TLS.CertificateKeyPath, &srv.Config.TLS.CACertPath, &srv.Config.TLS.SkipVerify, &srv.Config.TLS.EnableClientVerification)
// Handler // Handler
flags.StringSliceVarP(&srv.Config.Handler.AllowedOrigins, "handler.allowed-origins", "", []string{}, "Comma separated list of allowed origin URIs (for CORS/WebUI).") flags.StringSliceVarP(&srv.Config.Handler.AllowedOrigins, "handler.allowed-origins", "", []string{}, "Comma separated list of allowed origin URIs (for CORS/Web UI).")
// Cluster // Cluster
flags.BoolVarP(&srv.Config.Cluster.Disabled, "cluster.disabled", "", srv.Config.Cluster.Disabled, "Disabled multi-node cluster communication (used for testing)") flags.BoolVarP(&srv.Config.Cluster.Disabled, "cluster.disabled", "", srv.Config.Cluster.Disabled, "Disabled multi-node cluster communication (used for testing)")
@ -70,17 +72,29 @@ func BuildServerFlags(cmd *cobra.Command, srv *server.Command) {
flags.DurationVarP((*time.Duration)(&srv.Config.AntiEntropy.Interval), "anti-entropy.interval", "", (time.Duration)(srv.Config.AntiEntropy.Interval), "Interval at which to run anti-entropy routine.") flags.DurationVarP((*time.Duration)(&srv.Config.AntiEntropy.Interval), "anti-entropy.interval", "", (time.Duration)(srv.Config.AntiEntropy.Interval), "Interval at which to run anti-entropy routine.")
// Metric // Metric
flags.StringVarP(&srv.Config.Metric.Service, "metric.service", "", srv.Config.Metric.Service, "Where to send stats: can be expvar (in-memory served at /debug/vars), statsd or none.") flags.StringVarP(&srv.Config.Metric.Service, "metric.service", "", srv.Config.Metric.Service, "Where to send stats: can be expvar (in-memory served at /debug/vars), prometheus, statsd or none.")
flags.StringVarP(&srv.Config.Metric.Host, "metric.host", "", srv.Config.Metric.Host, "URI to send metrics when metric.service is statsd.") flags.StringVarP(&srv.Config.Metric.Host, "metric.host", "", srv.Config.Metric.Host, "URI to send metrics when metric.service is statsd.")
flags.DurationVarP((*time.Duration)(&srv.Config.Metric.PollInterval), "metric.poll-interval", "", (time.Duration)(srv.Config.Metric.PollInterval), "Polling interval metrics.") flags.DurationVarP((*time.Duration)(&srv.Config.Metric.PollInterval), "metric.poll-interval", "", (time.Duration)(srv.Config.Metric.PollInterval), "Polling interval metrics.")
flags.BoolVarP((&srv.Config.Metric.Diagnostics), "metric.diagnostics", "", srv.Config.Metric.Diagnostics, "Enabled diagnostics reporting.") flags.BoolVarP((&srv.Config.Metric.Diagnostics), "metric.diagnostics", "", srv.Config.Metric.Diagnostics, "Enabled diagnostics reporting.")
// Tracing // Tracing
flags.StringVarP(&srv.Config.Tracing.AgentHostPort, "tracing.agent-host-port", "", srv.Config.Tracing.AgentHostPort, "Jaeger agent host:port.") flags.StringVarP(&srv.Config.Tracing.AgentHostPort, "tracing.agent-host-port", "", srv.Config.Tracing.AgentHostPort, "Jaeger agent host:port.")
flags.StringVarP(&srv.Config.Tracing.SamplerType, "tracing.sampler-type", "", srv.Config.Tracing.SamplerType, "Jaeger sampler type or 'off' to disable tracing completely.") flags.StringVarP(&srv.Config.Tracing.SamplerType, "tracing.sampler-type", "", srv.Config.Tracing.SamplerType, "Jaeger sampler type (remote, const, probabilistic, ratelimiting) or 'off' to disable tracing completely.")
flags.Float64VarP(&srv.Config.Tracing.SamplerParam, "tracing.sampler-param", "", srv.Config.Tracing.SamplerParam, "Jaeger sampler parameter.") flags.Float64VarP(&srv.Config.Tracing.SamplerParam, "tracing.sampler-param", "", srv.Config.Tracing.SamplerParam, "Jaeger sampler parameter.")
// Profiling // Profiling
flags.IntVar(&srv.Config.Profile.BlockRate, "profile.block-rate", srv.Config.Profile.BlockRate, "Sampling rate for goroutine blocking profiler. One sample per <rate> ns.") flags.IntVar(&srv.Config.Profile.BlockRate, "profile.block-rate", srv.Config.Profile.BlockRate, "Sampling rate for goroutine blocking profiler. One sample per <rate> ns.")
flags.IntVar(&srv.Config.Profile.MutexFraction, "profile.mutex-fraction", srv.Config.Profile.MutexFraction, "Sampling fraction for mutex contention profiling. Sample 1/<rate> of events.") flags.IntVar(&srv.Config.Profile.MutexFraction, "profile.mutex-fraction", srv.Config.Profile.MutexFraction, "Sampling fraction for mutex contention profiling. Sample 1/<rate> of events.")
// Transactional storage engine
flags.StringVarP(&srv.Config.Txsrc, "tx", "", "", "transaction/storage to use: one of roaring, rbf, badger, rbf_roaring, roaring_rbf, badger_roaring, roaring_badger, badger_rbf, or rbf_badger (default roaring)")
// Postgres endpoint
flags.StringVar(&srv.Config.Postgres.Bind, "postgres.bind", srv.Config.Postgres.Bind, "Address to which to bind a postgres endpoint (leave blank to disable)")
SetTLSConfig(flags, "postgres.", &srv.Config.Postgres.TLS.CertificatePath, &srv.Config.Postgres.TLS.CertificateKeyPath, &srv.Config.Postgres.TLS.CACertPath, &srv.Config.Postgres.TLS.SkipVerify, &srv.Config.Postgres.TLS.EnableClientVerification)
flags.DurationVar((*time.Duration)(&srv.Config.Postgres.StartupTimeout), "postgres.startup-timeout", time.Duration(srv.Config.Postgres.StartupTimeout), "Timeout for postgres connection startup. (set 0 to disable)")
flags.DurationVar((*time.Duration)(&srv.Config.Postgres.ReadTimeout), "postgres.read-timeout", time.Duration(srv.Config.Postgres.ReadTimeout), "Timeout for reads on a postgres connection. (set 0 to disable; does not include connection idling)")
flags.DurationVar((*time.Duration)(&srv.Config.Postgres.WriteTimeout), "postgres.write-timeout", time.Duration(srv.Config.Postgres.WriteTimeout), "Timeout for writes on a postgres connection. (set 0 to disable)")
flags.Uint32Var(&srv.Config.Postgres.MaxStartupSize, "postgres.max-startup-size", srv.Config.Postgres.MaxStartupSize, "Maximum acceptable size of a postgres startup packet, in bytes. (set 0 to disable)")
flags.Uint16Var(&srv.Config.Postgres.ConnectionLimit, "postgres.connection-limit", srv.Config.Postgres.ConnectionLimit, "Maximum number of simultaneous postgres connections to allow. (set 0 to disable)")
} }

868
dbshard.go Normal file
View file

@ -0,0 +1,868 @@
// Copyright 2020 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pilosa
import (
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"github.com/pkg/errors"
)
var _ = sort.Sort
// types to support a database file per shard
type DBHolder struct {
Index map[string]*DBIndex
}
func NewDBHolder() *DBHolder {
return &DBHolder{
Index: make(map[string]*DBIndex),
}
}
type DBIndex struct {
Shard map[uint64]*DBShard
}
type DBWrapper interface {
NewTx(write bool, initialIndexName string, o Txo) (tx Tx, err error)
DeleteDBPath(dbs *DBShard) error
Close() error
DeleteFragment(index, field, view string, shard uint64, frag interface{}) error
DeleteField(index, field, fieldPath string) error
OpenListString() string
OpenSnList() (sns []int64)
Path() string
HasData() (has bool, err error)
SetHolder(h *Holder)
}
type DBRegistry interface {
OpenDBWrapper(path string, doAllocZero bool) (DBWrapper, error)
}
type DBShard struct {
HolderPath string
Index string
Shard uint64
Open bool
// With RWMutex, the
// writer who calls Lock() automatically gets priority over
// any reader who arrives later, even if the lock is held
// by a reader to start with.
mut sync.RWMutex
types []txtype
hasRoaring bool // if either of the types is roaringTxn
W []DBWrapper
ParentDBIndex *DBIndex
idx *Index
per *DBPerShard
useOpenList int
closed bool
}
func (dbs *DBShard) DeleteFragment(index, field, view string, shard uint64, frag interface{}) (err error) {
for _, w := range dbs.W {
err = w.DeleteFragment(index, field, view, shard, frag)
if err != nil {
return err
}
}
return
}
func (dbs *DBShard) DeleteFieldFromStore(index, field, fieldPath string) (err error) {
for _, w := range dbs.W {
err = w.DeleteField(index, field, fieldPath)
if err != nil {
return err
}
}
return
}
func (dbs *DBShard) Close() (err error) {
for _, w := range dbs.W {
err = w.Close()
if err != nil {
return err
}
}
dbs.closed = true
return
}
func (dbs *DBShard) HolderString() string {
return dbs.HolderPath
}
// Cleanup must be called at every commit/rollback of a Tx, in
// order to release the read-write mutex that guarantees a single
// writer at a time. Each tx must take care to call cleanup()
// exactly once. examples:
// tx.o.dbs.Cleanup(tx)
// tx.Options().dbs.Cleanup(tx)
//
func (dbs *DBShard) Cleanup(tx Tx) {
if dbs == nil {
return // some tests are using Tx only, no dbs available.
}
if useRWLock {
if !dbs.hasRoaring {
if tx.Readonly() {
dbs.mut.RUnlock()
} else {
dbs.mut.Unlock()
}
}
}
}
// experimental feature, off for now.
const useRWLock = false
func (dbs *DBShard) NewTx(write bool, initialIndexName string, o Txo) (tx Tx, err error) {
if useRWLock {
// enforce only one writer at a time. The dbs.mut is held until
// the Tx finishes.
if !dbs.hasRoaring {
if write {
dbs.mut.Lock()
} else {
dbs.mut.RLock()
}
}
}
if o.dbs != dbs {
panic(fmt.Sprintf("TxFactory.NewTx() should have set o.dbs(%p) to equal dbs(%p)", o.dbs, dbs))
}
if o.Shard != dbs.Shard {
panic(fmt.Sprintf("shard disagreement! o.Shard='%v' but dbs.Shard='%v'", int(o.Shard), int(dbs.Shard)))
}
var txns []Tx
for _, w := range dbs.W {
tx, err = w.NewTx(write, initialIndexName, o)
if err != nil {
return nil, err
}
txns = append(txns, tx)
}
if len(txns) == 1 {
return
}
// blue green
return dbs.per.txf.newBlueGreenTx(txns[0], txns[1], o.Index, o), nil
}
func (dbs *DBShard) DeleteDBPath() (err error) {
for _, w := range dbs.W {
err = w.DeleteDBPath(dbs)
if err != nil {
return err
}
}
return
}
type flatkey struct {
index string
shard uint64
}
type DBPerShard struct {
Mu sync.Mutex
HolderDir string
dbh *DBHolder
// just flat, not buried within the Node heirarchy.
// Easily see how many we have.
Flatmap map[flatkey]*DBShard
types []txtype
hasRoaring bool
txf *TxFactory
holder *Holder
// which of our types is not-roaring, since
// roaring doesn't keep a list of open Tx sn.
// or default to the 2nd.
useOpenList int
}
// HasData returns true if the database has at least one key.
// For roaring it returns true if we a fragment stored.
// The `which` argument is the index into the per.W slice. 0 for blue, 1 for green.
// If you pass 1, be sure you have a blue-green configuration.
func (per *DBPerShard) HasData(which int) (hasData bool, err error) {
// has to aggregate across all available DBShard for each index and shard.
for _, v := range per.Flatmap {
hasData, err = v.W[which].HasData()
if err != nil {
return
}
if hasData {
return
}
}
return
}
func (per *DBPerShard) ListOpenString() (r string) {
for _, v := range per.Flatmap {
r += v.HolderPath + " -> " + v.W[per.useOpenList].OpenListString() + "\n"
}
return
}
func (per *DBPerShard) LoadExistingDBs() (err error) {
idxs := per.holder.Indexes()
for _, idx := range idxs {
sos, err := per.txf.GetShardsForIndex(idx, "", true)
if err != nil {
return err
}
for _, shard := range sos {
_, err := per.GetDBShard(idx.name, shard, idx)
if err != nil {
return errors.Wrap(err, "DBPerShard.LoadExistingDBs GetDBShard()")
}
}
}
return
}
func (txf *TxFactory) NewDBPerShard(types []txtype, holderDir string, holder *Holder) (d *DBPerShard) {
useOpenList := 0
hasRoaring := false
if types[0] == roaringTxn {
hasRoaring = true
}
if len(types) == 2 {
// blue-green, avoid the empty roaring Tx open list.
// Prefer B's open list if neither is roaring.
if types[0] == roaringTxn || types[1] != roaringTxn {
useOpenList = 1
}
if types[1] == roaringTxn {
hasRoaring = true
}
}
d = &DBPerShard{
types: types,
HolderDir: holderDir,
holder: holder,
dbh: NewDBHolder(),
Flatmap: make(map[flatkey]*DBShard),
txf: txf,
useOpenList: useOpenList,
hasRoaring: hasRoaring,
}
return
}
func (per *DBPerShard) DeleteIndex(index string) (err error) {
per.Mu.Lock()
defer per.Mu.Unlock()
dbi, ok := per.dbh.Index[index]
if !ok {
// since we lazily make indexes upon use by a Tx now, we won't
// have an index for server/ TestQuerySQLUnary/test-20 to delete.
// Don't freak out. Just return nil.
return nil
}
for _, dbs := range dbi.Shard {
err = dbs.Close()
if err != nil {
return errors.Wrap(err, "DBPerShard.DeleteIndex dbs.Close()")
}
for _, ty := range per.types {
path := dbs.pathForType(ty)
err = os.RemoveAll(path)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("DBPerShard.DeleteIndex os.RemoveAll('%v')", path))
}
}
}
return
}
func (per *DBPerShard) DeleteFieldFromStore(index, field, fieldPath string) (err error) {
per.Mu.Lock()
defer func() {
if fieldPath != "" {
panicOn(os.RemoveAll(fieldPath))
}
per.Mu.Unlock()
}()
dbi, ok := per.dbh.Index[index]
if !ok {
// TestIndex_Existence_Delete in index_internal_test.go
// will call us without having ever created a Tx or DB,
// so we can't complain here.
return nil
}
for _, dbs := range dbi.Shard {
for _, w := range dbs.W {
err := w.DeleteField(index, field, fieldPath)
panicOn(err)
}
}
return
}
func (per *DBPerShard) DeleteFragment(index, field, view string, shard uint64, frag *fragment) error {
idx := per.txf.holder.Index(index)
dbs, err := per.GetDBShard(index, shard, idx)
if err != nil {
return err
}
return dbs.DeleteFragment(index, field, view, shard, frag)
}
func (dbs *DBShard) DumpAll() {
short := false
fmt.Printf("\n============= begin DumpAll dbs=%p index='%v', shard=%v ========\n", dbs, dbs.Index, int(dbs.Shard))
for i, ty := range dbs.types {
_ = i
tx, err := dbs.W[i].NewTx(!writable, "", Txo{Index: dbs.idx})
panicOn(err)
defer tx.Rollback()
fmt.Printf("\n============= dumping dbs.W[%v] %v ========\n", i, ty)
tx.Dump(short, dbs.Shard)
switch ty {
case roaringTxn:
case rbfTxn:
case lmdbTxn:
case badgerTxn:
default:
panic(fmt.Sprintf("unknown txtyp: '%v'", ty))
}
}
fmt.Printf("\n============= end of DumpAll index='%v', shard=%v ========\n", dbs.Index, int(dbs.Shard))
}
func (per *DBPerShard) DumpAll() {
per.Mu.Lock()
defer per.Mu.Unlock()
found1 := false
for _, dbi := range per.dbh.Index {
for _, dbs := range dbi.Shard {
if dbs.Open {
found1 = true
dbs.DumpAll()
}
}
}
if !found1 {
AlwaysPrintf("DBPerShard.DumpAll() sees no databases. dir='%v'", per.HolderDir)
}
}
// if you know the shard, you can use this
// pathForType and prefixForType must be kept in sync!
func (dbs *DBShard) pathForType(ty txtype) string {
// top level paths will end in "@@"
// what here for roaring? well, roaringRegistrar.OpenDBWrapper()
// is a no-op anyhow. so doesn't need to be correct atm.
return dbs.HolderPath + sep + dbs.Index + ".index.txstores@@@" + sep + "store" + ty.FileSuffix() + "@" + sep + fmt.Sprintf("shard.%04v%v", dbs.Shard, ty.FileSuffix())
}
// if you don't know the shard, you have to use this.
// prefixForType and pathForType must be kept in sync!
func (per *DBPerShard) prefixForType(idx *Index, ty txtype) string {
// top level paths will end in "@@"
return per.HolderDir + sep + idx.name + ".index.txstores@@@" + sep + "store" + ty.FileSuffix() + "@" + sep
}
var ErrNoData = fmt.Errorf("no data")
func (per *DBPerShard) GetDBShard(index string, shard uint64, idx *Index) (dbs *DBShard, err error) {
per.Mu.Lock()
defer per.Mu.Unlock()
dbi, ok := per.dbh.Index[index]
if !ok {
dbi = &DBIndex{
Shard: make(map[uint64]*DBShard),
}
per.dbh.Index[index] = dbi
}
dbs, ok = dbi.Shard[shard]
if dbs != nil && dbs.closed {
if len(per.types) == 1 && per.types[0] == roaringTxn {
// roaring txn are nil/fake anyway. Don't freak out.
} else {
panic(fmt.Sprintf("cannot retain closed dbs across holder ReOpen dbs='%p'", dbs))
}
}
if !ok {
dbs = &DBShard{
types: per.types,
ParentDBIndex: dbi,
Index: index,
Shard: shard,
HolderPath: per.HolderDir,
idx: idx,
per: per,
useOpenList: per.useOpenList,
hasRoaring: per.hasRoaring,
}
dbi.Shard[shard] = dbs
}
if !dbs.Open {
var registry DBRegistry
for _, ty := range dbs.types {
switch ty {
case roaringTxn:
registry = globalRoaringReg
case rbfTxn:
registry = globalRbfDBReg
case lmdbTxn:
registry = globalLMDBReg
case badgerTxn:
registry = globalBadgerReg
default:
panic(fmt.Sprintf("unknown txtyp: '%v'", ty))
}
path := dbs.pathForType(ty)
w, err := registry.OpenDBWrapper(path, DetectMemAccessPastTx)
panicOn(err)
h := idx.Holder()
w.SetHolder(h)
dbs.Open = true
if w != nil && len(dbs.W) == 0 {
per.Flatmap[flatkey{index: index, shard: shard}] = dbs
}
dbs.W = append(dbs.W, w)
}
}
return
}
func (per *DBPerShard) Del(dbs *DBShard) (err error) {
per.Mu.Lock()
defer per.Mu.Unlock()
err = dbs.Close()
if err != nil {
return
}
panicOn(dbs.DeleteDBPath())
delete(per.Flatmap, flatkey{index: dbs.Index, shard: dbs.Shard})
// delete from the heirarchy
delete(dbs.ParentDBIndex.Shard, dbs.Shard)
return nil
}
func (per *DBPerShard) Close() (err error) {
per.Mu.Lock()
defer per.Mu.Unlock()
for _, dbi := range per.dbh.Index {
for _, dbs := range dbi.Shard {
err = dbs.Close()
panicOn(err)
}
}
return
}
// DBPerShardGetShardsForIndex returns the shards for idx.
// If requireData, we open the database and see that it has a key, rather
// than assume that the database file presence is enough.
func (f *TxFactory) GetShardsForIndex(idx *Index, roaringViewPath string, requireData bool) (sliceOfShards []uint64, err error) {
var shards [][]uint64
for _, ty := range f.types {
var slc []uint64
slc, err = f.dbPerShard.TypedDBPerShardGetShardsForIndex(ty, idx, roaringViewPath, requireData)
if err != nil {
return
}
shards = append(shards, slc)
}
n := len(f.types)
if n != 1 && n != 2 {
panic(fmt.Sprintf("internal error. only green or blue/green supported. we see types len %v", n))
}
// Note: we don't actually know when the blue call and when the green call comes
// through here. So if we are deleting a shard, we will see a difference earlier
// in one than the other. TestAPI_ClearFlagForImportAndImportValues for example.
// Hence we cannot do a blue-green check here for matching shards.
// If we are populating blue from green, it does matter that we return green.
return shards[n-1], nil
}
// if roaringViewPath is "" then for ty == roaringTxn we go to disk to discover
// all the view paths under idx for type ty.
// requireData means open the database file and verify that at least one key is set.
func (per *DBPerShard) TypedDBPerShardGetShardsForIndex(ty txtype, idx *Index, roaringViewPath string, requireData bool) (sliceOfShards []uint64, err error) {
if ty == roaringTxn {
rx := &RoaringTx{
Index: idx,
}
if roaringViewPath == "" {
fields := idx.Fields()
for _, field := range fields {
for _, view := range field.views() {
sos, err := rx.SliceOfShards("", "", "", view.path)
if err != nil {
return nil,
errors.Wrap(err, fmt.Sprintf(
"TypedDBPerShardGetLocalShardsForIndex roaringTxn view.path='%v'", view.path))
}
sliceOfShards = append(sliceOfShards, sos...)
}
}
return dedupShardSlice(sliceOfShards), nil
}
return rx.SliceOfShards("", "", "", roaringViewPath)
}
requiredSuffix := ty.FileSuffix()
path := per.prefixForType(idx, ty)
ignoreEmpty := false
includeRoot := true
dbf, err := listDirUnderDir(path, includeRoot, requiredSuffix, ignoreEmpty)
panicOn(err)
for _, nm := range dbf {
base := filepath.Base(nm)
splt := strings.Split(base, requiredSuffix)
if len(splt) != 2 {
panic(fmt.Sprintf("should have 2 parts: nm='%v', base(nm)='%v'; requiredSuffix='%v'", nm, base, requiredSuffix))
}
prefix := splt[0]
const shardPrefix = "shard."
const lenOfShardPrefix = len(shardPrefix)
if !strings.HasPrefix(prefix, shardPrefix) {
continue
}
// Parse filename into integer.
shard, err := strconv.ParseUint(prefix[lenOfShardPrefix:], 10, 64)
if err != nil {
panicOn(err)
continue
}
// exclude those without data?
hasData := false
if requireData {
hasData, err = per.TypedIndexShardHasData(ty, idx, shard)
if err != nil {
return nil, err
}
if hasData {
sliceOfShards = append(sliceOfShards, shard)
}
} else {
// file presence is enough
sliceOfShards = append(sliceOfShards, shard)
}
}
return
}
func (per *DBPerShard) TypedIndexShardHasData(ty txtype, idx *Index, shard uint64) (hasData bool, err error) {
whichty := 0
if len(per.types) == 2 {
if ty == per.types[1] {
whichty = 1
}
}
if ty != per.types[whichty] {
return
}
// make the dbs if it doesn't get exist
dbs, err := per.GetDBShard(idx.name, shard, idx)
if err != nil {
return false, errors.Wrap(err, fmt.Sprintf("DBPerShard.TypedIndexShardHasData() "+
"per.GetDBShard(index='%v', shard='%v', ty='%v')", idx.name, shard, ty.String()))
}
return dbs.W[whichty].HasData()
}
func listDirUnderDir(root string, includeRoot bool, requiredSuffix string, ignoreEmpty bool) (files []string, err error) {
if !dirExists(root) {
return
}
n := len(root) + 1
if includeRoot {
n = 0
}
err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if len(path) < n {
// ignore
} else {
if info == nil {
// re-opening an RBF database hit this, racing with a directory rename.
// Don't freak out.
return nil
}
if !info.IsDir() {
// ignore files
} else {
if ignoreEmpty && info.Size() == 0 {
return nil
}
if requiredSuffix == "" || strings.HasSuffix(path, requiredSuffix) {
files = append(files, path[n:])
}
}
}
return nil
})
return
}
// populateBlueFromGreen prepares for a blue_green run at startup time.
//
// It is called at the end of Holder.Open(). This allows the application
// of blue-green checking to pilosa instances that
// were previously run only with a single (solo) backend.
//
// PRE: This operation requires, at its start, either:
//
// (1) an empty blue database -- this allows transitioning from
// a solo database to blue_green checking where the solo
// becomes the green; or
//
// (2) that the blue data, if present, be logically
// identical to the green data -- this allows one to restart
// a pilosa that was already running in blue_green mode
// and remain in blue_green mode.
//
// In either case, the goal to to finish populateBlueFromGreen()
// and have the exact same logical set of data in both backends.
//
// Why must the data be identical after Holder.Open() finishes?
// Otherwise subsequent blue-green checks have no hope of
// being accurate.
//
// The blue is the destination -- this is always types[0].
// The green source is always types[1]. The mnemonic is blue_geen.
// The blue is first, so it is in types[0]. The green
// is second, in types[1]. For example, with PILOSA_TXSRC=lmdb_roaring
// we have lmdb as blue, and roaring as green. The contents of
// lmdb must be empty or exactly match roaring. If lmdb
// starts empty, it will be populated from roaring by
// populateBlueFromGreen().
//
func (dbs *DBShard) populateBlueFromGreen() (err error) {
n := len(dbs.W)
if n != 2 {
panic(fmt.Sprintf("populateBlueFromGreen did not find 2 open DBs: have %v", n))
}
dest := dbs.W[0] // blue
src := dbs.W[1] // green
// copy all the key/container pairs.
// Since a shard is fairly small, we think one Tx will suffice.
readtx, err := src.NewTx(!writable, dbs.Index, Txo{Write: !writable, Index: dbs.idx, Shard: dbs.Shard})
panicOn(err)
defer readtx.Rollback()
writetx, err := dest.NewTx(writable, dbs.Index, Txo{Write: writable, Index: dbs.idx, Shard: dbs.Shard})
panicOn(err)
defer writetx.Rollback()
for _, fld := range dbs.idx.Fields() {
field := fld.Name()
for _, vw := range fld.views() {
view := vw.name
citer, _, err := readtx.ContainerIterator(dbs.Index, field, view, dbs.Shard, 0)
if err != nil {
// might be an empty fragment. If so, let's not freak out.
if strings.Contains(err.Error(), "fragment not found") {
continue
} else {
return errors.Wrap(err, "DBShard.populateBlueFromGreen readtx.ContainerIterator")
}
}
for citer.Next() {
ckey, rc := citer.Value()
err := writetx.PutContainer(dbs.Index, field, view, dbs.Shard, ckey, rc)
if err != nil {
citer.Close()
return errors.Wrap(err, "DBShard.populateBlueFromGreen writetx.PutContainer")
}
}
citer.Close()
}
}
err = writetx.Commit()
if err != nil {
return errors.Wrap(err, "writetx.Commit()")
}
return nil
}
// verifyBlueEqualsGreen checks that blue and green are identical.
func (dbs *DBShard) verifyBlueEqualsGreen() (err error) {
n := len(dbs.W)
if n != 2 {
panic(fmt.Sprintf("verifyBlueEqualsGreen did not find 2 open DBs: have %v", n))
}
blue := dbs.W[0]
green := dbs.W[1]
greentx, err := green.NewTx(!writable, dbs.Index, Txo{Write: !writable, Index: dbs.idx, Shard: dbs.Shard})
panicOn(err)
defer greentx.Rollback()
bluetx, err := blue.NewTx(!writable, dbs.Index, Txo{Write: !writable, Index: dbs.idx, Shard: dbs.Shard})
panicOn(err)
defer bluetx.Rollback()
for _, fld := range dbs.idx.Fields() {
field := fld.Name()
for _, vw := range fld.views() {
view := vw.name
gCiter, _, err := greentx.ContainerIterator(dbs.Index, field, view, dbs.Shard, 0)
if err != nil {
if strings.Contains(err.Error(), "fragment not found") {
continue
} else {
return errors.Wrap(err, "DBShard.verifyBlueEqualsGreen greentx.ContainerIterator")
}
}
bCiter, _, err := bluetx.ContainerIterator(dbs.Index, field, view, dbs.Shard, 0)
if err != nil {
gCiter.Close()
if bCiter != nil {
bCiter.Close()
}
return errors.Wrap(err, "DBShard.verifyBlueEqualsGreen bluetx.ContainerIterator")
}
for gCiter.Next() {
greenCkey, greenc := gCiter.Value()
if !bCiter.Next() {
bCiter.Close()
gCiter.Close()
return errors.Wrap(err, fmt.Sprintf("DBShard.verifyBlueEqualsGreen "+
"sees missing blue container at index: '%v' field: '%v' view: '%v' "+
"shard: '%v' the greenCkey: '%v'",
dbs.Index, field, view, dbs.Shard, greenCkey))
}
blueCkey, bluec := bCiter.Value()
if blueCkey != greenCkey {
bCiter.Close()
gCiter.Close()
return fmt.Errorf("DBShard.verifyBlueEqualsGreen sees sequence-of-ckey "+
"difference: blueCkey %v not equal to greenCkey %v at index: '%v' field: '%v' view: '%v' "+
"shard: '%v'",
blueCkey, greenCkey, dbs.Index, field, view, dbs.Shard)
}
nGreen := greenc.N()
nBlue := bluec.N()
if nBlue != nGreen {
bCiter.Close()
gCiter.Close()
return errors.Wrap(err, fmt.Sprintf("DBShard.verifyBlueEqualsGreen "+
"sees variation in blue at index: '%v' field: '%v' view: '%v' "+
"shard: '%v' ckey: '%v' nHotGreen= %v nHotBlue= %v",
dbs.Index, field, view, dbs.Shard, greenCkey, nGreen, nBlue))
}
err = bluec.BitwiseCompare(greenc)
if err != nil {
bCiter.Close()
gCiter.Close()
return errors.Wrap(err, fmt.Sprintf("DBShard.verifyBlueEqualsGreen "+
"sees variation in blue at index: '%v' field: '%v' view: '%v' "+
"shard: '%v' ckey: '%v' nHotGreen= %v nHotBlue= %v ; BitwiseCompare response: '%v'",
dbs.Index, field, view, dbs.Shard, greenCkey, nGreen, nBlue, err))
}
}
if bCiter.Next() {
blueCkey, _ := bCiter.Value()
bCiter.Close()
gCiter.Close()
return errors.Wrap(err, fmt.Sprintf("DBShard.verifyBlueEqualsGreen "+
"sees extra blue container (not present in green) at index: '%v' field: '%v' view: '%v' "+
"shard: '%v' the ckey: '%v'",
dbs.Index, field, view, dbs.Shard, blueCkey))
}
bCiter.Close()
gCiter.Close()
}
}
return nil
}
func dedupShardSlice(sos []uint64) (r []uint64) {
m := make(map[uint64]struct{})
for _, s := range sos {
m[s] = struct{}{}
}
for k := range m {
r = append(r, k)
}
return
}

256
dbshard_internal_test.go Normal file
View file

@ -0,0 +1,256 @@
// Copyright 2020 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pilosa
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"github.com/pilosa/pilosa/v2/rbf"
)
// Shard per db evaluation
func TestShardPerDB_SetBit(t *testing.T) {
f, idx, tx := mustOpenFragment(t, "i", "f", viewStandard, 0, "")
_ = idx
defer f.Clean(t)
// Set bits on the fragment.
if _, err := f.setBit(tx, 120, 1); err != nil {
t.Fatal(err)
} else if _, err := f.setBit(tx, 120, 6); err != nil {
t.Fatal(err)
} else if _, err := f.setBit(tx, 121, 0); err != nil {
t.Fatal(err)
}
// should have two containers set in the fragment.
// Verify counts on rows.
if n := f.mustRow(tx, 120).Count(); n != 2 {
t.Fatalf("unexpected count: %d", n)
} else if n := f.mustRow(tx, 121).Count(); n != 1 {
t.Fatalf("unexpected count: %d", n)
}
// commit the change, and verify it is still there
panicOn(tx.Commit())
// Close and reopen the fragment & verify the data.
err := f.Reopen() // roaring data not being flushed? red on roaring
if err != nil {
t.Fatal(err)
}
tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard})
defer tx.Rollback()
if n := f.mustRow(tx, 120).Count(); n != 2 {
t.Fatalf("unexpected count (reopen): %d", n)
} else if n := f.mustRow(tx, 121).Count(); n != 1 {
t.Fatalf("unexpected count (reopen): %d", n)
}
}
// test that we find all *local* shards
func Test_DBPerShard_GetShardsForIndex_LocalOnly(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "Test_DBPerShard_GetShardsForIndex_LocalOnly")
panicOn(err)
orig := os.Getenv("PILOSA_TXSRC")
defer os.Setenv("PILOSA_TXSRC", orig) // must restore or will mess up other tests!
for _, src := range []string{"lmdb", "roaring", "badger", "rbf"} {
os.Setenv("PILOSA_TXSRC", src)
// must make Holder AFTER setting src.
holder := NewHolder(tmpdir, nil)
makeSampleRoaringDir(tmpdir, src, 1, holder)
idx, err := NewIndex(holder, tmpdir, "rick")
panicOn(err)
estd := "rick/_exists/views/standard"
std := "rick/f/views/standard"
sos, err := holder.txf.GetShardsForIndex(idx, tmpdir+sep+std, false)
panicOn(err)
for _, shard := range []uint64{93, 223, 221, 215, 219, 217} {
if !inSlice(sos, shard) {
panic(fmt.Sprintf("missing shard=%v from sos='%#v'", shard, sos))
}
}
if src == "roaring" {
// check estd too
sos, err = holder.txf.GetShardsForIndex(idx, tmpdir+sep+estd, false)
panicOn(err)
for _, shard := range []uint64{93, 223, 221, 215, 219, 217} {
if !inSlice(sos, shard) {
panic(fmt.Sprintf("missing shard=%v from sos='%#v'", shard, sos))
}
}
}
holder.Close()
}
}
func inSlice(sos []uint64, shard uint64) bool {
for i := range sos {
if shard == sos[i] {
return true
}
}
return false
}
// data for Test_DBPerShard_GetShardsForIndex
//
var sampleRoaringDirList = map[string]string{"roaring": `
rick/f/views/standard/fragments/215.cache
rick/f/views/standard/fragments/221.cache
rick/f/views/standard/fragments/223.cache
rick/f/views/standard/fragments/93.cache
rick/f/views/standard/fragments/217.cache
rick/f/views/standard/fragments/219.cache
rick/f/views/standard/fragments/217
rick/f/views/standard/fragments/219
rick/f/views/standard/fragments/215
rick/f/views/standard/fragments/221
rick/f/views/standard/fragments/223
rick/f/views/standard/fragments/93
rick/_exists/views/standard/fragments/221
rick/_exists/views/standard/fragments/215
rick/_exists/views/standard/fragments/217
rick/_exists/views/standard/fragments/93
rick/_exists/views/standard/fragments/219
rick/_exists/views/standard/fragments/223
`,
"lmdb": `
rick.index.txstores@@@/store-lmdb@@/shard.0093-lmdb@
rick.index.txstores@@@/store-lmdb@@/shard.0215-lmdb@
rick.index.txstores@@@/store-lmdb@@/shard.0217-lmdb@
rick.index.txstores@@@/store-lmdb@@/shard.0219-lmdb@
rick.index.txstores@@@/store-lmdb@@/shard.0221-lmdb@
rick.index.txstores@@@/store-lmdb@@/shard.0223-lmdb@
`,
"badger": `
rick.index.txstores@@@/store-badgerdb@@/shard.0093-badgerdb@
rick.index.txstores@@@/store-badgerdb@@/shard.0215-badgerdb@
rick.index.txstores@@@/store-badgerdb@@/shard.0217-badgerdb@
rick.index.txstores@@@/store-badgerdb@@/shard.0219-badgerdb@
rick.index.txstores@@@/store-badgerdb@@/shard.0221-badgerdb@
rick.index.txstores@@@/store-badgerdb@@/shard.0223-badgerdb@
`,
"rbf": `
rick.index.txstores@@@/store-rbfdb@@/shard.0093-rbfdb@
rick.index.txstores@@@/store-rbfdb@@/shard.0215-rbfdb@
rick.index.txstores@@@/store-rbfdb@@/shard.0217-rbfdb@
rick.index.txstores@@@/store-rbfdb@@/shard.0219-rbfdb@
rick.index.txstores@@@/store-rbfdb@@/shard.0221-rbfdb@
rick.index.txstores@@@/store-rbfdb@@/shard.0223-rbfdb@
`,
}
func makeSampleRoaringDir(root, txsrc string, minBytes int, h *Holder) {
index := "rick"
shards := []uint64{0, 93, 215, 217, 219, 221, 223}
fns := strings.Split(sampleRoaringDirList[txsrc], "\n")
for i, fn := range fns {
if fn == "" {
continue
}
var shard uint64
if txsrc != "roaring" {
// only have shards for the non-roaring
shard = shards[i]
}
switch txsrc {
case "lmdb":
makeLMDBtestDB(root+sep+fn, h, shard)
// also have to make the DBShard in our in-memory tree,
// or else the search won't find it because
// DBPerShard won't know anything about it.
helperCreateDBShard(h, index, shard)
continue
case "badger":
makeBadgertestDB(root+sep+fn, h, shard)
helperCreateDBShard(h, index, shard)
continue
case "rbf":
makeRBFtestDB(root+sep+fn, h, shard)
helperCreateDBShard(h, index, shard)
continue
}
path := root + sep + filepath.Dir(fn)
panicOn(os.MkdirAll(path, 0755))
fd, err := os.Create(root + sep + fn)
panicOn(err)
if minBytes > 0 {
_, err := fd.Write(make([]byte, minBytes))
panicOn(err)
}
fd.Close()
}
}
func helperCreateDBShard(h *Holder, index string, shard uint64) {
idx, err := h.CreateIndexIfNotExists(index, IndexOptions{})
panicOn(err)
dbs, err := h.txf.dbPerShard.GetDBShard(index, shard, idx)
panicOn(err)
_ = dbs
}
func makeLMDBtestDB(path string, h *Holder, shard uint64) {
i := uint64(1)
w, _ := mustOpenEmptyLMDBWrapper(path)
LMDBMustSetBitvalue(w, "index", "field", "view", shard, i)
w.Close()
}
func makeBadgertestDB(path string, h *Holder, shard uint64) {
i := uint64(1)
w, _ := mustOpenEmptyBadgerWrapper(path)
badgerDBMustSetBitvalue(w, "index", "field", "view", shard, i)
w.Close()
}
func makeRBFtestDB(path string, h *Holder, shard uint64) {
i := uint64(1)
db := rbf.NewDB(path)
err := db.Open()
panicOn(err)
defer db.Close()
tx, err := db.Begin(true)
panicOn(err)
err = tx.CreateBitmap("x")
panicOn(err)
_, err = tx.Add("x", i)
panicOn(err)
err = tx.Commit()
panicOn(err)
}

173
dbshard_test.go Normal file
View file

@ -0,0 +1,173 @@
// Copyright 2020 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pilosa_test
import (
"context"
"fmt"
"os"
"reflect"
"testing"
"github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/boltdb"
"github.com/pilosa/pilosa/v2/http"
"github.com/pilosa/pilosa/v2/server"
"github.com/pilosa/pilosa/v2/test"
)
func skipForNonLMDB(t *testing.T) {
src := os.Getenv("PILOSA_TXSRC")
if src != "lmdb" {
t.Skip("skip if not lmdb")
}
}
var _ = skipForNonLMDB // happy linter
var _ = skipForNonBadger // happy linter
func skipForNonBadger(t *testing.T) {
src := os.Getenv("PILOSA_TXSRC")
if src != "badger" {
t.Skip("skip if not badger")
}
}
// Can't write it all to one shard like we do (did).
func Test_DBPerShard_multiple_shards_used(t *testing.T) {
skipForNonLMDB(t)
c := test.MustRunCluster(t, 1)
defer c.Close()
hldr := c.GetHolder(0)
index := "i"
hldr.SetBit(index, "general", 10, 0)
hldr.SetBit(index, "general", 10, ShardWidth+1)
hldr.SetBit(index, "general", 10, ShardWidth+2)
hldr.SetBit(index, "general", 11, 2)
hldr.SetBit(index, "general", 11, ShardWidth+2)
types := pilosa.MustTxsrcToTxtype("lmdb")
idx := hldr.Index(index)
shardsU := []uint64{0, 1, 2}
pathShard := []string{}
// check that 3 different shard databases/files were made
for i := 0; i < 2; i++ {
path, err := hldr.Txf().GetDBShardPath(index, shardsU[i], idx, types[0], !writable)
panicOn(err)
pathShard = append(pathShard, path)
if !DirExists(pathShard[i]) {
panic(fmt.Sprintf("no shard made for pathShard[%v]='%v'", i, pathShard[i]))
}
sz, err := pilosa.DiskUse(pathShard[i], "")
panicOn(err)
if sz < 100 {
panic(fmt.Sprintf("shard %v was too small", i))
}
}
if res, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{Index: index, Query: `Union(Row(general=10), Row(general=11))`}); err != nil {
t.Fatal(err)
} else if columns := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(columns, []uint64{0, 2, ShardWidth + 1, ShardWidth + 2}) {
t.Fatalf("unexpected columns: %+v", columns)
}
}
func TestAPI_SimplerOneNode_ImportColumnKey(t *testing.T) {
c := test.MustRunCluster(t, 1,
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerNodeID("node0"),
pilosa.OptServerClusterHasher(&offsetModHasher{}),
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
pilosa.OptServerOpenTranslateReader(http.GetOpenTranslateReaderFunc(nil)),
)},
)
defer c.Close()
m0 := c.GetNode(0)
t.Run("RowIDColumnKey", func(t *testing.T) {
ctx := context.Background()
indexName := "rick"
fieldName := "f"
index, err := m0.API.CreateIndex(ctx, indexName, pilosa.IndexOptions{Keys: true, TrackExistence: true})
if err != nil {
t.Fatalf("creating index: %v", err)
}
if index.CreatedAt() == 0 {
t.Fatal("index createdAt is empty")
}
field, err := m0.API.CreateField(ctx, indexName, fieldName, pilosa.OptFieldTypeSet(pilosa.DefaultCacheType, 100))
if err != nil {
t.Fatalf("creating field: %v", err)
}
if field.CreatedAt() == 0 {
t.Fatal("field createdAt is empty")
}
rowID := uint64(1)
timestamp := int64(0)
// Generate some keyed records.
rowIDs := []uint64{}
timestamps := []int64{}
for i := 1; i <= 10; i++ {
rowIDs = append(rowIDs, rowID)
timestamps = append(timestamps, timestamp)
}
// Keys are sharded so ordering is not guaranteed.
colKeys := []string{"col10", "col8", "col9", "col6", "col7", "col4", "col5", "col2", "col3", "col1"}
// Import data with keys to the coordinator (node0) and verify that it gets
// translated and forwarded to the owner of shard 0 (node1; because of offsetModHasher)
req := &pilosa.ImportRequest{
Index: indexName,
IndexCreatedAt: index.CreatedAt(),
Field: fieldName,
FieldCreatedAt: field.CreatedAt(),
Shard: 0, // import is all on shard 0, why are we making bocu other shards? b/c this is ignored.
RowIDs: rowIDs,
ColumnKeys: colKeys,
Timestamps: timestamps,
}
qcx := m0.API.Txf().NewQcx()
if err := m0.API.Import(ctx, qcx, req); err != nil {
t.Fatal(err)
}
panicOn(qcx.Finish())
//select {}
pql := fmt.Sprintf("Row(%s=%d)", fieldName, rowID)
// Query node0.
if res, err := m0.API.Query(ctx, &pilosa.QueryRequest{Index: indexName, Query: pql}); err != nil {
t.Fatal(err)
} else if keys := res.Results[0].(*pilosa.Row).Keys; !reflect.DeepEqual(keys, colKeys) {
t.Fatalf("unexpected column keys: %#v", keys)
}
})
}

View file

@ -229,11 +229,11 @@ func (d *diagnosticsCollector) EnrichWithSchemaProperties() {
timeQuantumEnabled := false timeQuantumEnabled := false
for _, index := range d.server.holder.Indexes() { for _, index := range d.server.holder.Indexes() {
numShards += index.AvailableShards().Count() numShards += index.AvailableShards(includeRemote).Count()
numIndexes++ numIndexes++
for _, field := range index.Fields() { for _, field := range index.Fields() {
numFields++ numFields++
if field.Type() == FieldTypeInt { if field.Type() == FieldTypeInt || field.Type() == FieldTypeDecimal {
bsiFieldCount++ bsiFieldCount++
} }
if field.TimeQuantum() != "" { if field.TimeQuantum() != "" {

View file

@ -29,6 +29,7 @@ import (
func TestDiagnosticsClient(t *testing.T) { func TestDiagnosticsClient(t *testing.T) {
// Mock server. // Mock server.
server := httptest.NewServer(nil) server := httptest.NewServer(nil)
defer server.Close()
// Create a new client. // Create a new client.
d := newDiagnosticsCollector(server.URL) d := newDiagnosticsCollector(server.URL)
@ -121,6 +122,7 @@ func TestDiagnosticsVersion_Check(t *testing.T) {
t.Fatalf("couldn't encode version response: %v", err) t.Fatalf("couldn't encode version response: %v", err)
} }
})) }))
defer server.Close()
// Create a new client. // Create a new client.
d := newDiagnosticsCollector("localhost:10101") d := newDiagnosticsCollector("localhost:10101")
@ -158,6 +160,7 @@ func compareJSON(a, b []byte) (bool, error) {
func BenchmarkDiagnostics(b *testing.B) { func BenchmarkDiagnostics(b *testing.B) {
// Mock server. // Mock server.
server := httptest.NewServer(nil) server := httptest.NewServer(nil)
defer server.Close()
// Create a new client. // Create a new client.
d := newDiagnosticsCollector(server.URL) d := newDiagnosticsCollector(server.URL)

View file

@ -15,7 +15,7 @@ Is equivalent to `GET /schema` and returns the same response.
### List index schema ### List index schema
`GET /index/<index-name>` `GET /index/{index-name}`
Returns the schema of the specified index in JSON. Returns the schema of the specified index in JSON.
@ -24,27 +24,31 @@ curl -XGET localhost:10101/index/user
``` ```
``` response ``` response
{ {
"fields": [ "name": "user",
{ "createdAt": 1591178953061239000,
"name": "event", "options": {
"options": { "keys": false,
"keys": false, "trackExistence": true
"timeQuantum": "YMD", },
"type": "time" "fields": [
} {
} "name": "event",
], "createdAt": 1591178962332452000,
"name": "user", "options": {
"options": { "type": "set",
"keys": false, "cacheType": "ranked",
"trackExistence": true "cacheSize": 50000,
"keys": false
}
} }
],
"shardWidth": 1048576
} }
``` ```
### Create index ### Create index
`POST /index/<index-name>` `POST /index/{index-name}`
Creates an index with the given name. Creates an index with the given name.
@ -57,7 +61,7 @@ The request payload is in JSON, and may contain the `options` field. The `option
curl -XPOST localhost:10101/index/user -d '{"options":{"keys":true}}' curl -XPOST localhost:10101/index/user -d '{"options":{"keys":true}}'
``` ```
``` response ``` response
{"success":true} {"success":true,"name":"user","createdAt":1591179042178854000}
``` ```
### Remove index ### Remove index
@ -75,7 +79,7 @@ curl -XDELETE localhost:10101/index/user
### Query index ### Query index
`POST /index/<index-name>/query` `POST /index/{index-name}/query`
Sends a [query](../query-language/) to the Pilosa server with the given index. The request body is UTF-8 encoded text and response body is in JSON by default. Sends a [query](../query-language/) to the Pilosa server with the given index. The request body is UTF-8 encoded text and response body is in JSON by default.
@ -133,7 +137,7 @@ By default, all bits and attributes (*for `Row` queries only*) are returned. In
### Import Data ### Import Data
`POST /index/<index-name>/field/<field-name>/import` `POST /index/{index-name}/field/{field-name}/import`
Supports high-rate data ingest to a particular shard of a particular field. The Supports high-rate data ingest to a particular shard of a particular field. The
official client libraries use this endpoint for their import functionality - it official client libraries use this endpoint for their import functionality - it
@ -151,23 +155,37 @@ represents a particular bit to be set. Timestamps are optional, but if they
exist must also contain the same number of items as rows and columns. The exist must also contain the same number of items as rows and columns. The
column IDs must all be in the shard specified in the request. column IDs must all be in the shard specified in the request.
Some endpoints and data structures include a `CreatedAt` fields.
This is typically stored as a timestamp, but it's purpose is not to inform of the creation date of a particular index or field,
but to serve as a unique identifier for use in cache invalidation.
The problem is that users of Pilosa (such as ingesters e.g. the [IDK](https://github.com/molecula/idk))
can usually assume that translation keys for records and field values never change - they are only appended to, and can therefore be trivially cached.
This is true except in cases where an index or field gets deleted and then recreated,
or if Pilosa is restored from a backup.
So the ingesters must send their current `CreatedAt` value which will have changed if either of those two conditions has occured (or if Pilosa was just restarted),
and the ingester will know that it needs to drop its cache.
``` ```
message ImportRequest { message ImportRequest {
string Index = 1; string Index = 1;
string Field = 2; string Field = 2;
uint64 Shard = 3; uint64 Shard = 3;
repeated uint64 RowIDs = 4; repeated uint64 RowIDs = 4;
repeated uint64 ColumnIDs = 5; repeated uint64 ColumnIDs = 5;
repeated string RowKeys = 7; repeated int64 Timestamps = 6;
repeated string ColumnKeys = 8; repeated string RowKeys = 7;
repeated int64 Timestamps = 6; repeated string ColumnKeys = 8;
int64 IndexCreatedAt = 9;
int64 FieldCreatedAt = 10;
} }
``` ```
### Create field ### Create field
`POST /index/<index-name>/field/<field-name>` `POST /index/{index-name}/field/{field-name}`
Creates a field in the given index with the given name. Creates a field in the given index with the given name.
@ -200,7 +218,7 @@ curl localhost:10101/index/user/field/quantity \
-d '{"options": {"type": "int", "min": -1000, "max":2000}}' -d '{"options": {"type": "int", "min": -1000, "max":2000}}'
``` ```
``` response ``` response
{"success":true} {"success":true,"name":"quantity","createdAt":1591180110914425000}
``` ```
Integer fields are stored as n-bit range-encoded values. Pilosa supports 63-bit, signed integers with values between `min` and `max`. Integer fields are stored as n-bit range-encoded values. Pilosa supports 63-bit, signed integers with values between `min` and `max`.
@ -209,21 +227,21 @@ Integer fields are stored as n-bit range-encoded values. Pilosa supports 63-bit,
curl localhost:10101/index/user/field/language -X POST curl localhost:10101/index/user/field/language -X POST
``` ```
``` response ``` response
{"success":true} {"success":true,"name":"language","createdAt":1591180128294321000}
``` ```
``` request ``` request
curl localhost:10101/index/repository/field/stats \ curl localhost:10101/index/repository/field/stats \
-X POST \ -X POST \
-d '{"fields": [{"name": "pullrequests", "type": "int", "min": 0, "max": 1000000}]}' -d '{"options":{"type": "int", "min": 0, "max": 1000000}}'
``` ```
``` response ``` response
{"success":true} {"success":true,"name":"stats","createdAt":1591180737881627000}
``` ```
### Remove field ### Remove field
`DELETE /index/<index-name>/field/<field-name>` `DELETE /index/{index-name}/field/{field-name}`
Removes the given field. Removes the given field.
@ -245,34 +263,52 @@ curl -XGET localhost:10101/schema
``` ```
``` response ``` response
{ {
"indexes": [ "indexes": [
{
"name": "user",
"createdAt": 1591178953061239000,
"options": {
"keys": false,
"trackExistence": true
},
"fields": [
{ {
"fields": [ "name": "event",
{ "createdAt": 1591178962332452000,
"name": "event", "options": {
"options": { "type": "set",
"keys": false, "cacheType": "ranked",
"timeQuantum": "YMD", "cacheSize": 50000,
"type": "time" "keys": false
} }
}, },
{ {
"name": "language", "name": "language",
"options": { "createdAt": 1591180128294321000,
"cacheSize": 50000, "options": {
"cacheType": "ranked", "type": "set",
"keys": false, "cacheType": "ranked",
"type": "set" "cacheSize": 50000,
} "keys": false
} }
], },
"name": "user", {
"options": { "name": "quantity",
"keys": false, "createdAt": 1591180110914425000,
"trackExistence": true "options": {
} "type": "int",
"base": 0,
"bitDepth": 0,
"min": -1000,
"max": 2000,
"keys": false,
"foreignIndex": ""
}
} }
] ],
"shardWidth": 1048576
}
]
} }
``` ```
@ -304,7 +340,7 @@ Returns the version of the Pilosa server.
curl -XGET localhost:10101/version curl -XGET localhost:10101/version
``` ```
``` response ``` response
{"version":"v0.6.0"} {"version":"2.0.0-alpha.20-6-gb9d8d6b4"}
``` ```
### Get status ### Get status
@ -318,22 +354,49 @@ curl -XGET localhost:10101/status
``` ```
```response ```response
{ {
"localID": "d3369125-29d8-4305-a351-b4474d14a542", "state": "NORMAL",
"nodes": [ "nodes": [
{ {
"id": "d3369125-29d8-4305-a351-b4474d14a542", "id": "1b018ce0-5de5-4da9-9285-6c4c0d8106f9",
"isCoordinator": true, "uri": {
"uri": { "scheme": "http",
"host": "localhost", "host": "localhost",
"port": 10101, "port": 10101
"scheme": "http" },
} "grpc-uri": {
} "scheme": "http",
], "host": "localhost",
"state": "NORMAL" "port": 20101
},
"isCoordinator": true,
"state": "READY"
}
],
"localID": "1b018ce0-5de5-4da9-9285-6c4c0d8106f9"
} }
``` ```
### Get active queries
`GET /queries`
Returns the set of active queries. Supports pretty printing in `text/plain` format or JSON output in `application/json` format.
Also includes the amount of time that the query has been running (in nanoseconds when using JSON).
```request
curl -XGET localhost:10101/queries
```
```response
182.412µs All()
```
```request
curl -XGET -H "Accept: application/json" localhost:10101/queries
```
```response
[{"query":"All()","age":135123}]
```
### Recalculate Caches ### Recalculate Caches
`POST /recalculate-caches` `POST /recalculate-caches`

View file

@ -302,7 +302,7 @@ The config file is in the [toml format](https://github.com/toml-lang/toml) and h
```toml ```toml
[metric] [metric]
service = “statsd” service = "statsd"
``` ```
#### Metric Host #### Metric Host
@ -319,7 +319,7 @@ The config file is in the [toml format](https://github.com/toml-lang/toml) and h
#### Metric Poll Interval #### Metric Poll Interval
* Description: Rate at which runtime metrics (such as open file handles and memory usage) are collected. * Description: Rate at which runtime metrics (such as open file handles and memory usage) are collected.
* Flag: `metric.poll-interval=”0m15s”` * Flag: `metric.poll-interval="0m15s"`
* Env: `PILOSA_METRIC_POLL_INTERVAL=0m15s` * Env: `PILOSA_METRIC_POLL_INTERVAL=0m15s`
* Config: * Config:
@ -403,7 +403,7 @@ The config file is in the [toml format](https://github.com/toml-lang/toml) and h
#### Tracing Sampler Type #### Tracing Sampler Type
* Description: Jaeger sampler type (const, probabilistic, ratelimiting, or remote). Set to 'off' to disable tracing completely. * Description: Jaeger sampler type (const, probabilistic, ratelimiting, or remote). Set to 'off' to disable tracing completely. Default is 'off'.
* Flag: `tracing.sampler-type` * Flag: `tracing.sampler-type`
* Env: `PILOSA_TRACING_SAMPLER_TYPE` * Env: `PILOSA_TRACING_SAMPLER_TYPE`
* Config: * Config:

View file

@ -121,6 +121,18 @@ Upon creation, fields are configured to be of a certain type. Pilosa supports th
#### Set #### Set
Set is the default field type in Pilosa. Set fields represent a standard, binary matrix of rows and columns where each row key represents a possible field value. The following example creates a `set` field called "info" with a ranked cache containing up to 100,000 records. Set is the default field type in Pilosa. Set fields represent a standard, binary matrix of rows and columns where each row key represents a possible field value. The following example creates a `set` field called "info" with a ranked cache containing up to 100,000 records.
Row and/or column key can be a string literal (e.g. "value"). This mapping is also stored in a separate BoltDB data structure. Becauase BoltDB does not allow to have empty strings as keys, in pilosa we translate an empty string key into sentinel byte slice:
```go
[]byte{
0x00, 0x00, 0x00,
0x4d, 0x54, 0x4d, 0x54, // MTMT
0x00,
0xc2, 0xa0, // NO-BREAK SPACE
0x00,
}
```
(where the first three bytes are _zero_ bytes, next four bytes stands for `MTMT` literal and the rest four bytes represent NBSP prefixed and suffixed with _zero_ byte).
In reverse translation, if we get from BoltDB the sentinel key, pilosa will rewrite it into an empty string (`""`).
``` request ``` request
curl localhost:10101/index/repository/field/info \ curl localhost:10101/index/repository/field/info \

View file

@ -81,7 +81,7 @@ lfm := pdk.LinearFloatMapper{
} }
``` ```
`Min` and `Max` define the linear function, and `Res` determines the maximum allowed value for the output row ID - we chose these values to produce a “round to nearest integer” behavior. Other predefined mappers have their own specific parameters, usually two or three. `Min` and `Max` define the linear function, and `Res` determines the maximum allowed value for the output row ID - we chose these values to produce a "round to nearest integer" behavior. Other predefined mappers have their own specific parameters, usually two or three.
This mapper function is the core operation, but we need a few other pieces to define the overall process, which is encapsulated in the ColumnMapper object. This object defines which field(s) of the input data source to use (`Fields`), how to parse them (`Parsers`), what mapping to use (`Mapper`), and the name of the field to use (`Field`). <!-- TODO update so this makes sense --> This mapper function is the core operation, but we need a few other pieces to define the overall process, which is encapsulated in the ColumnMapper object. This object defines which field(s) of the input data source to use (`Fields`), how to parse them (`Parsers`), what mapping to use (`Mapper`), and the name of the field to use (`Field`). <!-- TODO update so this makes sense -->
```go ```go

View file

@ -100,7 +100,7 @@ curl localhost:10101/index/repository -X POST
``` response ``` response
{"success":true} {"success":true}
``` ```
The index name must be 64 characters or fewer, start with a letter, and consist only of lowercase alphanumeric characters or `_-`. The same goes for field names. The index name must be 230 characters or fewer, start with a letter, and consist only of lowercase alphanumeric characters or `_-`. The same goes for field names.
Let's create the `stargazer` field which has user IDs of stargazers as its rows: Let's create the `stargazer` field which has user IDs of stargazers as its rows:
``` request ``` request
@ -325,7 +325,7 @@ Next, let's create the `repository` index:
repository := schema.Index("repository") repository := schema.Index("repository")
``` ```
The index name must be 64 characters or fewer, start with a letter, and consist only of lowercase alphanumeric characters or `_-`. The same goes for field names. The index name must be 230 characters or fewer, start with a letter, and consist only of lowercase alphanumeric characters or `_-`. The same goes for field names.
Let's create the `stargazer` field which has user IDs of stargazers as its rows: Let's create the `stargazer` field which has user IDs of stargazers as its rows:
``` ```
@ -615,7 +615,7 @@ Next, let's create the `repository` index:
``` ```
Index repository = schema.index("repository"); Index repository = schema.index("repository");
``` ```
The index name must be 64 characters or fewer, start with a letter, and consist only of lowercase alphanumeric characters or `_-`. The same goes for field names. The index name must be 230 characters or fewer, start with a letter, and consist only of lowercase alphanumeric characters or `_-`. The same goes for field names.
Let's create the `stargazer` field which has user IDs of stargazers as its rows: Let's create the `stargazer` field which has user IDs of stargazers as its rows:
``` ```
@ -818,7 +818,7 @@ Next, let's create the `repository` index:
``` ```
repository = schema.index("repository") repository = schema.index("repository")
``` ```
The index name must be 64 characters or fewer, start with a letter, and consist only of lowercase alphanumeric characters or `_-`. The same goes for field names. The index name must be 230 characters or fewer, start with a letter, and consist only of lowercase alphanumeric characters or `_-`. The same goes for field names.
Let's create the `stargazer` field which has user IDs of stargazers as its rows: Let's create the `stargazer` field which has user IDs of stargazers as its rows:
``` ```

View file

@ -58,7 +58,7 @@ nav = []
<strong id="range-bsi">[Row (BSI)](../query-language/#row-bsi):</strong> A [PQL](#pql) query that returns bits based on comparison to integers stored in [BSI](#bsi) [fields](#field). <strong id="range-bsi">[Row (BSI)](../query-language/#row-bsi):</strong> A [PQL](#pql) query that returns bits based on comparison to integers stored in [BSI](#bsi) [fields](#field).
<strong id="rows">[Rows](../query-language/#rows):<strong> A [PQL](#pql) query that returns a list of row IDs in the given field which have at least one bit set. The field argument is mandatory, the others are optional. `Rows` is the primary argument used with the [GroupBy](#groupby) query. <strong id="rows">[Rows](../query-language/#rows):</strong> A [PQL](#pql) query that returns a list of row IDs in the given field which have at least one bit set. The field argument is mandatory, the others are optional. `Rows` is the primary argument used with the [GroupBy](#groupby) query.
<strong id="slice">[Slice](../data-model/#shard):</strong> Prior to Pilosa 1.0, shards were known as slices. <strong id="slice">[Slice](../data-model/#shard):</strong> Prior to Pilosa 1.0, shards were known as slices.

View file

@ -43,7 +43,7 @@ curl localhost:10101/index/repository/query \
#### Arguments and Types #### Arguments and Types
* `field` The field specifies on which Pilosa [field](../glossary/#field) the query will operate. Valid field names are lower case strings; they start with a lowercase letter, and contain only alphanumeric characters and `_-`. They must be 64 characters or less in length. * `field` The field specifies on which Pilosa [field](../glossary/#field) the query will operate. Valid field names are lower case strings; they start with a lowercase letter, and contain only alphanumeric characters and `_-`. They must be 230 characters or less in length.
* `TIMESTAMP` This is a timestamp in the following format `YYYY-MM-DDTHH:MM` (e.g. 2006-01-02T15:04). * `TIMESTAMP` This is a timestamp in the following format `YYYY-MM-DDTHH:MM` (e.g. 2006-01-02T15:04).
* `UINT` An unsigned integer (e.g. 42839). * `UINT` An unsigned integer (e.g. 42839).
* `BOOL` A boolean value, `true` or `false`. * `BOOL` A boolean value, `true` or `false`.
@ -52,6 +52,7 @@ curl localhost:10101/index/repository/query \
* `CALL` Any query. * `CALL` Any query.
* `ROW_CALL` Any query which returns a row, such as `Row`, `Union`, `Difference`, `Xor`, `Intersect`, `Not`. * `ROW_CALL` Any query which returns a row, such as `Row`, `Union`, `Difference`, `Xor`, `Intersect`, `Not`.
* `ROWS_CALL` A query that returns a `Rows` result (i.e. a list of row IDs). Currently only the `Rows` query. * `ROWS_CALL` A query that returns a `Rows` result (i.e. a list of row IDs). Currently only the `Rows` query.
* `ROWSET_CALL` A query that returns a set of rows. Currently only the `Rows` and `TopN` queries.
* `[]ATTR_VALUE` Denotes an array of `ATTR_VALUE`s. (e.g. `["a", "b", "c"]`). * `[]ATTR_VALUE` Denotes an array of `ATTR_VALUE`s. (e.g. `["a", "b", "c"]`).
### Write Operations ### Write Operations
@ -428,9 +429,9 @@ Union([ROW_CALL ...])
**Description:** **Description:**
Union performs a logical OR on the results of all `ROW_CALL` queries passed to it. Union performs a set union on the column indexes in the results of all `ROW_CALL` queries passed to it. In comparison to a relational query, this is similar to combining clauses in the "OR" sense.
**Result Type:** object with attrs and bits **Result Type:** object with attrs and columns
attrs will always be empty attrs will always be empty
@ -456,7 +457,7 @@ Intersect(<ROW_CALL>, [ROW_CALL ...])
**Description:** **Description:**
Intersect performs a logical AND on the results of all `ROW_CALL` queries passed to it. Intersect performs a set intersection on the column indexes in the results of all `ROW_CALL` queries passed to it. In comparison to a relational query, this is similar to combining clauses in the "AND" sense.
**Result Type:** object with attrs and columns **Result Type:** object with attrs and columns
@ -570,6 +571,38 @@ Not(Row(stargazer=1))
* columns are repositories that were not starred by user 1 * columns are repositories that were not starred by user 1
#### Limit
**Spec:**
```
Limit(<ROW_CALL>, [limit=<UINT>], [offset=<UINT>])
```
**Description:**
Limit executes a `ROW_CALL` and returns a subset of the results.
If a limit of `n` is specified, then this query will return the first `n` results of the row call.
If an offset of `m` is specified, then this query will skip the first `m` results of the row call.
If both a limit and offset are specified, the offset is applied before the limit.
This can be used to implement pagination.
**Result Type:** object with attrs and columns
attrs will always be empty
**Examples:**
Find the second column that has a bit set in the given row.
```request
Limit(Row(stargazer=1), limit=1, offset=1)
```
```response
{"results":[{"attrs":{},"columns":[30]}]}
```
* columns are repositories that were starred by user 1
#### Count #### Count
**Spec:** **Spec:**
@ -595,34 +628,6 @@ Count(Row(stargazer=1))
* Result is the number of repositories that user 1 has starred. * Result is the number of repositories that user 1 has starred.
#### Shift
**Spec:**
```
Shift(<ROW_CALL>, [n=UINT])
```
**Description:**
Returns the row specified by `ROW_CALL` shifted by `n` bits.
**Result Type:** object with attrs and columns
attrs will always be empty
**Examples:**
Query all columns with a bit set in row 1 of the field `stargazer`
and shift the result by 2:
```request
Shift(Row(stargazer=1), n=2)
```
```response
{"attrs":{},"columns":[12, 22]}
```
* columns are the repositories which user 1 has starred shifted by 2 bits.
#### TopN #### TopN
**Spec:** **Spec:**
@ -643,9 +648,15 @@ have the attribute specified by `attrName` with one of the values specified in
**Caveats:** **Caveats:**
* Performing a TopN() query on a field with cache type ranked will return the top rows sorted by count in descending order. In general, the order of the resulting row keys is not guaranteed to reflect the true order of bit counts across an index. The exact solution to the problem of computing the TopN counts is prohibitively expensive, so TopN is instead implemented as a heuristic. This provides a significant performance improvement, at the cost of uncertainty in the result order.
* Fields with cache type lru will maintain an LRU (Least Recently Used replacement policy) cache, thus a TopN query on this type of field will return rows sorted in order of most recently set bit.
* The field's cache size determines the number of sorted rows to maintain in the cache for purposes of TopN queries. There is a tradeoff between performance and accuracy; increasing the cache size will improve accuracy of results at the cost of performance. The implementation is based on a per-shard cache. The accuracy of the results depends on how well the counts for the overall index are reflected in the individual shards (so TopN queries on a single-shard index are exact). If the distribution of bits across shards is uniform, shard counts are representative. This is often a reasonable assumption, especially for the top results for large data sets, in which counts might follow Zipfian, exponential, or other long-tail distributions. However, this assumption may not hold for some applications.
Additional implementation details:
* The field's cache size determines the number of sorted rows to maintain in the cache for purposes of TopN queries. There is a tradeoff between performance and accuracy; increasing the cache size will improve accuracy of results at the cost of performance. Note that this per-shard tradeoff is independent of the per-index performance/accuracy tradeoff mentioned above.
* Fields with cache type `ranked` will return the top rows sorted by count in descending order.
* Fields with cache type `lru` will maintain an LRU (Least Recently Used replacement policy) cache, thus a TopN query on this type of field will return rows sorted in order of most recently set bit.
* Once full, the cache will truncate the set of rows according to the field option CacheSize. Rows that straddle the limit and have the same count will be truncated in no particular order. * Once full, the cache will truncate the set of rows according to the field option CacheSize. Rows that straddle the limit and have the same count will be truncated in no particular order.
* The TopN query's attribute filter is applied to the existing sorted cache of rows. Rows that fall outside of the sorted cache range, even if they would normally pass the filter, are ignored. * The TopN query's attribute filter is applied to the existing sorted cache of rows. Rows that fall outside of the sorted cache range, even if they would normally pass the filter, are ignored.
@ -813,12 +824,39 @@ Options(Row(f1=10), shards=[0, 2])
{"attrs":{},"columns":[100, 2097152]} {"attrs":{},"columns":[100, 2097152]}
``` ```
#### Row Constant
**Spec:**
```
ConstRow(columns=<[]COLUMN>)
```
**Description:**
`ConstRow` provides a constant bitmap value that can be used in place of a `Row` call.
The columns can be specified as integer IDs or strings.
**Result Type:** row value columns.
e.g. `{"attrs":{},"columns":[10, 20]}`
**Examples:**
Filter specified columns to only those with a bit set in row 1 of the field `stargazer` (repositories that are starred by user 1):
```request
Intersect(ConstRow(columns=[10, 20, 30]), Row(stargazer=1))
```
```response
{"attrs":{},"columns":[10, 20]}
```
#### Rows #### Rows
**Spec:** **Spec:**
``` ```
Rows(<FIELD>, previous=<UINT|STRING>, limit=<UINT>, column=<UINT|STRING>, from=<TIMESTAMP>, to=<TIMESTAMP>) Rows(<FIELD>, previous=<UINT|STRING>, limit=<UINT>, column=<UINT|STRING>, from=<TIMESTAMP>, to=<TIMESTAMP>, like=<STRING>)
``` ```
**Description:** **Description:**
@ -839,6 +877,10 @@ If the field is of type `time`, the `from` and `to` arguments can be provided
to restrict the result to a specific time span. If `from` and `to` are to restrict the result to a specific time span. If `from` and `to` are
not provided, the full range of existing data will be queried. not provided, the full range of existing data will be queried.
If `like` is given, only keys matching a pattern will be selected.
A `like` pattern may use `_` as a placeholder to match a single UTF-8 codepoint, and `%` to match 0 or more codepoints.
All other characters will be matched exactly.
**Result Type:** Object with `"rows" or "keys" and an array of integers or strings respectively.` **Result Type:** Object with `"rows" or "keys" and an array of integers or strings respectively.`
**Examples:** **Examples:**
@ -856,7 +898,50 @@ With keys:
Rows(job) Rows(job)
``` ```
```response ```response
{"rows":null,"keys":["engineer","management","student""]} {"rows":null,"keys":["engineer","management","student"]}
```
With `like`:
```request
Rows(job, like="%t")
```
```response
{"rows":null,"keys":["management","student"]}
```
#### Extract
**Spec:**
```
Extract(<ROW_CALL>, [<ROWS_CALL>...])
```
**Description:**
Extract intersects a set of columns with a set of rows in order to extract a subset of the index.
The result is a table consisting of the matched columns and the rows which they intersect.
This is similar to a select query in a SQL database.
**Result Type:** Object with an array of the selected fields and an array of the selected columns.
The column array contains objects containing a column identifier and an array of field values.
Field values are typed as such:
- Bool Field - boolean or null
- Mutex Field (unkeyed) - 64-bit unsigned integer or null
- Mutex Field (keyed) - string or null
- Integer Field - 64-bit signed integer or null
- Decimal Field - Pilosa decimal value or null
- Set Field (unkeyed) - array of 64-bit unsigned integers
- Set Field (keyed) - array of strings
- Time Field - same as the equivalent Set
**Examples:**
List all stargazers who have starred repository 1, and the full set of repositories they have starred:
```request
Extract(Row(stargazer=1), Rows(stargazer))
```
```response
{"fields":[{"name":"stargazer","type":"set"}],"columns":[{"column":3,"rows":[[1, 2, 3]]}]}
``` ```
#### Group By #### Group By
@ -864,7 +949,7 @@ Rows(job)
**Spec:** **Spec:**
``` ```
GroupBy(<ROWS_CALL>, [<ROWS_CALL>...], limit=<UINT>, filter=<ROW_CALL>) GroupBy(<ROWS_CALL>, [<ROWS_CALL>...], limit=<UINT>, filter=<ROW_CALL>, aggregate=<CALL>)
``` ```
**Description:** **Description:**
@ -874,14 +959,18 @@ taking one row each from the specified `Rows` calls. It returns only those
combinations for which the count is greater than 0. combinations for which the count is greater than 0.
The optional `filter` argument takes any type of `Row` query (e.g. Row, Union, The optional `filter` argument takes any type of `Row` query (e.g. Row, Union,
Intersect, etc.) which will be intersected with each result prior to returning Intersect, etc.) which will be intersected with each result prior to returning
the count. This is analogous to a WHERE clause applied to a relational GROUP BY the count. This is analagous to a WHERE clause applied to a relational GROUP BY
query. query.
The optional `limit` argument limits the number of results returned. The results The optional `limit` argument limits the number of results returned. The results
are ordered, so as long as the data isn't changing, the same query will return are ordered, so as long as the data isn't changing, the same query will return
the same result set. the same result set.
The optional `aggregate` argument takes a `Sum()` query which will be used to
calculate the sum & count of each group. This is similar to using a `SUM()` in
the SELECT clause of a relation GROUP BY query.
Paging through results is supported by passing the `previous` argument to each Paging through results is supported by passing the `previous` argument to each
of the `Rows` calls in the GroupBy. Take the last result from your previous of the `Rows` calls in the GroupBy. Take the last result from your previous
`GroupBy` query, and pass each row ID in that result as the `previous` argument `GroupBy` query, and pass each row ID in that result as the `previous` argument
@ -941,3 +1030,31 @@ GroupBy(Rows(age), Rows(job), limit=7, filter=Row(country=USA))
{"group":[{"field":"age","rowID":22},{"field":"job","rowKey":"student"}],"count":3}, {"group":[{"field":"age","rowID":22},{"field":"job","rowKey":"student"}],"count":3},
{"group":[{"field":"age","rowID":29},{"field":"job","rowKey":"management"}],"count":7}] {"group":[{"field":"age","rowID":29},{"field":"job","rowKey":"management"}],"count":7}]
``` ```
#### UnionRows
**Spec:**
```
UnionRows([ROWSET_CALL ...])
```
**Description:**
UnionRows performs a logical OR on the rows matched by the results of all `ROWSET_CALL` queries passed to it.
**Result Type:** object with attrs and columns
attrs will always be empty
**Examples:**
Query columns with a bit set in any row (repositories that are starred by any user):
```request
UnionRows(Rows(stargazer))
```
```response
{"attrs":{},"columns":[10, 20, 30]}
```
* columns are repositories that were starred by any user

File diff suppressed because it is too large Load diff

View file

@ -1,661 +0,0 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

View file

@ -1,23 +0,0 @@
// Copyright (c) 2018 Pilosa Corp. All rights reserved.
//
// This file is part of Pilosa Enterprise Edition.
//
// Pilosa Enterprise Edition is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Pilosa Enterprise Edition is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Pilosa Enterprise Edition. If not, see <http://www.gnu.org/licenses/>.
// Package enterprise is now deprecated, and the functionality under
// enterprise/b has been copied into roaring/. It existed to inject enterprise
// implementations of various Pilosa features when Pilosa was built with
// "ENTERPRISE=1 make install". These features were dual-licensed separately
// from Pilosa community edition under the AGPL and Pilosa's commercial license.
package enterprise

File diff suppressed because it is too large Load diff

View file

@ -15,20 +15,28 @@
package pilosa package pilosa
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "strconv"
"strings" "strings"
"testing" "testing"
"github.com/pilosa/pilosa/v2/pql" "github.com/pilosa/pilosa/v2/pql"
"github.com/pilosa/pilosa/v2/testhook"
) )
func TestExecutor_TranslateGroupByCall(t *testing.T) { func TestExecutor_TranslateGroupByCall(t *testing.T) {
path, _ := testhook.TempDirInDir(t, *TempDir, "pilosa-executor-")
holder := NewHolder(path, nil)
defer holder.Close()
cluster := NewTestCluster(t, 1)
e := &executor{ e := &executor{
Holder: NewHolder(), Holder: holder,
Cluster: cluster,
} }
e.Holder.Path, _ = ioutil.TempDir(*TempDir, "")
err := e.Holder.Open() err := e.Holder.Open()
if err != nil { if err != nil {
t.Fatalf("opening holder: %v", err) t.Fatalf("opening holder: %v", err)
@ -46,12 +54,14 @@ func TestExecutor_TranslateGroupByCall(t *testing.T) {
t.Fatalf("creating fields %v, %v, %v", erra, errb, errc) t.Fatalf("creating fields %v, %v, %v", erra, errb, errc)
} }
query, err := pql.ParseString(`GroupBy(Rows(ak), Rows(b), Rows(ck), previous=["la", 0, "ha"])`) query, err := pql.ParseString(`GroupBy(Rows(ak), Rows(b), Rows(ck), previous=["la", 0, "ha"], having=Condition(count > 10))`)
if err != nil { if err != nil {
t.Fatalf("parsing query: %v", err) t.Fatalf("parsing query: %v", err)
} }
c := query.Calls[0] c := query.Calls[0]
err = e.translateGroupByCall("i", idx, c) // this is writable call just for testing purpose - to test previous argument
// generally GroupBy calls are not writable and keys should already exist
err = e.translateCall(context.Background(), "i", c, make(map[string]map[string]uint64), true)
if err != nil { if err != nil {
t.Fatalf("translating call: %v", err) t.Fatalf("translating call: %v", err)
} }
@ -64,6 +74,19 @@ func TestExecutor_TranslateGroupByCall(t *testing.T) {
} }
} }
if having, hok := c.Args["having"].(*pql.Call); !hok {
t.Fatal("expected having to be a call")
} else if cond, cok := having.Args["count"].(*pql.Condition); !cok {
t.Fatal("expected condition to be a count")
} else if cond.Op != pql.GT {
t.Fatal("expected condition op to be >")
} else {
val, ok := cond.Uint64Value()
if !ok || val != uint64(10) {
t.Fatal("expected condition val to be uint64(10)")
}
}
errTests := []struct { errTests := []struct {
pql string pql string
err string err string
@ -102,7 +125,7 @@ func TestExecutor_TranslateGroupByCall(t *testing.T) {
t.Fatalf("parsing query: %v", err) t.Fatalf("parsing query: %v", err)
} }
c := query.Calls[0] c := query.Calls[0]
err = e.translateGroupByCall("i", idx, c) err = e.translateCall(context.Background(), "i", c, make(map[string]map[string]uint64), false)
if err == nil { if err == nil {
t.Fatalf("expected error, but translated call is '%s", c) t.Fatalf("expected error, but translated call is '%s", c)
} }
@ -113,6 +136,69 @@ func TestExecutor_TranslateGroupByCall(t *testing.T) {
} }
} }
func TestExecutor_TranslateRowsOnBool(t *testing.T) {
path, _ := testhook.TempDirInDir(t, *TempDir, "pilosa-executor-")
holder := NewHolder(path, nil)
defer holder.Close()
e := &executor{
Holder: holder,
Cluster: NewTestCluster(t, 1),
}
if err := e.Holder.Open(); err != nil {
t.Fatalf("opening holder: %v", err)
}
idx, err := e.Holder.CreateIndex("i", IndexOptions{})
if err != nil {
t.Fatalf("creating index: %v", err)
}
shard := uint64(0)
tx := idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Shard: shard})
defer tx.Rollback()
fb, errb := idx.CreateField("b", OptFieldTypeBool())
_, errbk := idx.CreateField("bk", OptFieldTypeBool(), OptFieldKeys())
if errb != nil || errbk != nil {
t.Fatalf("creating fields %v, %v", errb, errbk)
}
_, err1 := fb.SetBit(tx, 1, 1, nil)
_, err2 := fb.SetBit(tx, 2, 2, nil)
_, err3 := fb.SetBit(tx, 3, 3, nil)
if err1 != nil || err2 != nil || err3 != nil {
t.Fatalf("setting bit %v, %v, %v", err1, err2, err3)
}
if err := tx.Commit(); err != nil {
t.Fatal(err)
}
tests := []struct {
pql string
}{
{pql: "Rows(b)"},
{pql: "GroupBy(Rows(b))"},
{pql: "Set(4, b=true)"},
}
for _, test := range tests {
t.Run(test.pql, func(t *testing.T) {
query, err := pql.ParseString(test.pql)
if err != nil {
t.Fatalf("parsing query: %v", err)
}
c := query.Calls[0]
err = e.translateCall(context.Background(), "i", c, make(map[string]map[string]uint64), true)
if err != nil {
t.Fatalf("translating call: %v", err)
}
})
}
}
func isInt(a interface{}) bool { func isInt(a interface{}) bool {
switch a.(type) { switch a.(type) {
case int, int64, uint, uint64: case int, int64, uint, uint64:
@ -222,3 +308,268 @@ func TestFieldRowMarshalJSON(t *testing.T) {
t.Fatalf("unexpected json: %s", b) t.Fatalf("unexpected json: %s", b)
} }
} }
func TestExecutor_GroupCountCondition(t *testing.T) {
t.Run("satisfiesCondition", func(t *testing.T) {
type condCheck struct {
cond string
exp bool
}
tests := []struct {
groupCount GroupCount
checks []condCheck
}{
{
groupCount: GroupCount{Count: 100},
checks: []condCheck{
{cond: "count == 99", exp: false},
{cond: "count != 99", exp: true},
{cond: "count < 99", exp: false},
{cond: "count <= 99", exp: false},
{cond: "count > 99", exp: true},
{cond: "count >= 99", exp: true},
{cond: "count == 100", exp: true},
{cond: "count != 100", exp: false},
{cond: "count < 100", exp: false},
{cond: "count <= 100", exp: true},
{cond: "count > 100", exp: false},
{cond: "count >= 100", exp: true},
{cond: "count == 101", exp: false},
{cond: "count != 101", exp: true},
{cond: "count < 101", exp: true},
{cond: "count <= 101", exp: true},
{cond: "count > 101", exp: false},
{cond: "count >= 101", exp: false},
{cond: "98 < count < 100", exp: false},
{cond: "98 < count <= 100", exp: true},
{cond: "98 < count < 101", exp: true},
{cond: "100 <= count < 102", exp: true},
{cond: "100 < count < 102", exp: false},
{cond: "98 <= count <= 102", exp: true},
},
},
{
groupCount: GroupCount{Sum: 100},
checks: []condCheck{
{cond: "sum == 99", exp: false},
{cond: "sum != 99", exp: true},
{cond: "sum < 99", exp: false},
{cond: "sum <= 99", exp: false},
{cond: "sum > 99", exp: true},
{cond: "sum >= 99", exp: true},
{cond: "sum == 100", exp: true},
{cond: "sum != 100", exp: false},
{cond: "sum < 100", exp: false},
{cond: "sum <= 100", exp: true},
{cond: "sum > 100", exp: false},
{cond: "sum >= 100", exp: true},
{cond: "sum == 101", exp: false},
{cond: "sum != 101", exp: true},
{cond: "sum < 101", exp: true},
{cond: "sum <= 101", exp: true},
{cond: "sum > 101", exp: false},
{cond: "sum >= 101", exp: false},
{cond: "98 < sum < 100", exp: false},
{cond: "98 < sum <= 100", exp: true},
{cond: "98 < sum < 101", exp: true},
{cond: "100 <= sum < 102", exp: true},
{cond: "100 < sum < 102", exp: false},
{cond: "98 <= sum <= 102", exp: true},
},
},
{
groupCount: GroupCount{Sum: -100},
checks: []condCheck{
{cond: "sum == -99", exp: false},
{cond: "sum != -99", exp: true},
{cond: "sum < -99", exp: true},
{cond: "sum <= -99", exp: true},
{cond: "sum > -99", exp: false},
{cond: "sum >= -99", exp: false},
{cond: "sum == -100", exp: true},
{cond: "sum != -100", exp: false},
{cond: "sum < -100", exp: false},
{cond: "sum <= -100", exp: true},
{cond: "sum > -100", exp: false},
{cond: "sum >= -100", exp: true},
{cond: "sum == -101", exp: false},
{cond: "sum != -101", exp: true},
{cond: "sum < -101", exp: false},
{cond: "sum <= -101", exp: false},
{cond: "sum > -101", exp: true},
{cond: "sum >= -101", exp: true},
{cond: "-100 < sum < -98", exp: false},
{cond: "-100 <= sum < -98", exp: true},
{cond: "-101 < sum < -98", exp: true},
{cond: "-102 < sum <= -100", exp: true},
{cond: "-102 < sum < -100", exp: false},
{cond: "-102 <= sum <= -98", exp: true},
},
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("test (#%d):", i), func(t *testing.T) {
for j, check := range test.checks {
t.Run(fmt.Sprintf("check (#%d):", j), func(t *testing.T) {
query, err := pql.ParseString(fmt.Sprintf("GroupBy(Rows(a), having=Condition(%s))", check.cond))
if err != nil {
t.Fatalf("parsing query: %v", err)
}
c := query.Calls[0]
having := c.Args["having"].(*pql.Call)
var got bool
for subj, cond := range having.Args {
switch subj {
case "count", "sum":
condition, ok := cond.(*pql.Condition)
if !ok {
t.Fatalf("not a valid condition")
}
got = test.groupCount.satisfiesCondition(subj, condition)
}
}
if got != check.exp {
t.Fatalf("expected: %v, but got: %v", check.exp, got)
}
})
}
})
}
})
}
func TestValCountComparisons(t *testing.T) {
tests := []struct {
name string
vc ValCount
other ValCount
expLarger ValCount
expSmaller ValCount
}{
{
name: "zero",
},
{
name: "ints",
vc: ValCount{Val: 10, Count: 1},
other: ValCount{Val: 3, Count: 2},
expLarger: ValCount{Val: 10, Count: 1},
expSmaller: ValCount{Val: 3, Count: 2},
},
{
name: "floats",
vc: ValCount{FloatVal: 10.2, Count: 1},
other: ValCount{FloatVal: 3.4, Count: 2},
expLarger: ValCount{FloatVal: 10.2, Count: 1},
expSmaller: ValCount{FloatVal: 3.4, Count: 2},
},
{
name: "intsEquality",
vc: ValCount{Val: 10, Count: 1},
other: ValCount{Val: 10, Count: 2},
expLarger: ValCount{Val: 10, Count: 3},
expSmaller: ValCount{Val: 10, Count: 3},
},
{
name: "floatsEquality",
vc: ValCount{FloatVal: 10.7, Count: 1},
other: ValCount{FloatVal: 10.7, Count: 2},
expLarger: ValCount{FloatVal: 10.7, Count: 3},
expSmaller: ValCount{FloatVal: 10.7, Count: 3},
},
}
for i, test := range tests {
t.Run(test.name+strconv.Itoa(i), func(t *testing.T) {
gotLarger := test.vc.larger(test.other)
if gotLarger != test.expLarger {
t.Fatalf("larger failed, expected:\n%+v\ngot:\n%+v", test.expLarger, gotLarger)
}
gotSmaller := test.vc.smaller(test.other)
if gotSmaller != test.expSmaller {
t.Fatalf("smaller failed, expected:\n%+v\ngot:\n%+v", test.expSmaller, gotSmaller)
}
})
}
}
func TestToNegInt64(t *testing.T) {
tests := []struct {
u64 uint64
i64 int64
overflow bool
}{
{
u64: uint64(1 << 63),
i64: int64(-1 << 63),
},
{
u64: uint64(1<<63) - 1,
i64: int64(-1<<63) + 1,
},
{
u64: uint64(1<<63) + 1,
overflow: true,
},
}
for _, tc := range tests {
val, err := toNegInt64(tc.u64)
if err != nil && !tc.overflow {
t.Fatalf("error: %+v, expected: %+v", err, tc)
}
if val != tc.i64 {
t.Fatalf("Expected: %+v, Got: %+v", tc.i64, val)
}
}
}
func TestToInt64(t *testing.T) {
tests := []struct {
u64 uint64
i64 int64
overflow bool
}{
{
u64: uint64(1<<63) - 1,
i64: 1<<63 - 1,
},
{
u64: uint64(0),
i64: 0,
},
{
u64: uint64(1 << 63),
overflow: true,
},
{
u64: 1<<64 - 1,
overflow: true,
},
}
for _, tc := range tests {
val, err := toInt64(tc.u64)
if err != nil && !tc.overflow {
t.Fatalf("error: %+v, expected: %+v", err, tc)
}
if val != tc.i64 {
t.Fatalf("Expected: %+v, Got: %+v", tc.i64, val)
}
}
}

File diff suppressed because it is too large Load diff

1052
field.go

File diff suppressed because it is too large Load diff

View file

@ -16,15 +16,18 @@ package pilosa
import ( import (
"fmt" "fmt"
"io/ioutil"
"math" "math"
"os" "os"
"path/filepath"
"reflect" "reflect"
"strconv"
"strings"
"testing" "testing"
"time" "time"
"github.com/pilosa/pilosa/v2/pql" "github.com/pilosa/pilosa/v2/pql"
"github.com/pilosa/pilosa/v2/roaring" "github.com/pilosa/pilosa/v2/roaring"
"github.com/pilosa/pilosa/v2/testhook"
) )
// Ensure a bsiGroup can adjust to its baseValue. // Ensure a bsiGroup can adjust to its baseValue.
@ -66,12 +69,12 @@ func TestBSIGroup_BaseValue(t *testing.T) {
{b0, pql.LT, 5, 105, false}, {b0, pql.LT, 5, 105, false},
{b0, pql.LT, -8, 92, false}, {b0, pql.LT, -8, 92, false},
{b0, pql.LT, -108, -8, false}, {b0, pql.LT, -108, -8, false},
{b0, pql.LT, 1005, 1023, false}, {b0, pql.LT, 1005, 1024, false},
{b0, pql.LT, 0, 100, false}, {b0, pql.LT, 0, 100, false},
{b1, pql.LT, 5, 5, false}, {b1, pql.LT, 5, 5, false},
{b1, pql.LT, -8, -8, false}, {b1, pql.LT, -8, -8, false},
{b1, pql.LT, 1005, 255, false}, {b1, pql.LT, 1005, 256, false},
{b1, pql.LT, 0, 0, false}, {b1, pql.LT, 0, 0, false},
{b2, pql.LT, 5, -95, false}, {b2, pql.LT, 5, -95, false},
@ -89,6 +92,7 @@ func TestBSIGroup_BaseValue(t *testing.T) {
{b1, pql.GT, -8, -8, false}, {b1, pql.GT, -8, -8, false},
{b1, pql.GT, 1005, 0, true}, {b1, pql.GT, 1005, 0, true},
{b1, pql.GT, 0, 0, false}, {b1, pql.GT, 0, 0, false},
{b1, pql.GT, -300, -256, false},
{b2, pql.GT, 5, -95, false}, {b2, pql.GT, 5, -95, false},
{b2, pql.GT, -8, -108, false}, {b2, pql.GT, -8, -108, false},
@ -141,6 +145,7 @@ func TestBSIGroup_BaseValue(t *testing.T) {
{b1, 5, 20, 5, 20, false}, {b1, 5, 20, 5, 20, false},
{b1, 20, 1005, 20, 255, false}, {b1, 20, 1005, 20, 255, false},
{b1, 1005, 2000, 0, 0, true}, {b1, 1005, 2000, 0, 0, true},
{b1, 0, -1, 0, 0, true},
{b2, 5, 95, -95, -5, false}, {b2, 5, 95, -95, -5, false},
{b2, 95, 120, -5, 20, false}, {b2, 95, 120, -5, 20, false},
@ -158,7 +163,7 @@ func TestBSIGroup_BaseValue(t *testing.T) {
// Ensure field can open and retrieve a view. // Ensure field can open and retrieve a view.
func TestField_DeleteView(t *testing.T) { func TestField_DeleteView(t *testing.T) {
f := MustOpenField(OptFieldTypeDefault()) f := OpenField(t, OptFieldTypeDefault())
defer f.Close() defer f.Close()
viewName := viewStandard + "_v" viewName := viewStandard + "_v"
@ -192,64 +197,73 @@ func TestField_DeleteView(t *testing.T) {
// TestField represents a test wrapper for Field. // TestField represents a test wrapper for Field.
type TestField struct { type TestField struct {
*Field *Field
parent *Index
tb testing.TB
} }
// NewTestField returns a new instance of TestField d/0. // NewTestField returns a new instance of TestField d/0.
func NewTestField(opts FieldOption) *TestField { func NewTestField(t *testing.T, opts FieldOption) *TestField {
path, err := ioutil.TempDir(*TempDir, "pilosa-field-") path, err := testhook.TempDirInDir(t, *TempDir, "pilosa-field-")
if err != nil {
t.Fatal(err)
}
h := NewHolder(path, nil)
panicOn(h.Open())
idx, err := h.CreateIndex("i", IndexOptions{})
if err != nil { if err != nil {
panic(err) panic(err)
} }
field, err := NewField(path, "i", "f", opts) field, err := idx.CreateField("f", opts)
if err != nil { if err != nil {
panic(err) t.Fatal(err)
} }
return &TestField{Field: field} tf := &TestField{Field: field, parent: idx, tb: t}
testhook.Cleanup(t, func() {
h.Close()
})
return tf
} }
// MustOpenField returns a new, opened field at a temporary path. Panic on error. // OpenField returns a new, opened field at a temporary path.
func MustOpenField(opts FieldOption) *TestField { func OpenField(t *testing.T, opts FieldOption) *TestField {
f := NewTestField(opts) f := NewTestField(t, opts)
if err := f.Open(); err != nil {
panic(err)
}
return f return f
} }
// Close closes the field and removes the underlying data. // Close closes the field and removes the underlying data.
func (f *TestField) Close() error { func (f *TestField) Close() error {
if f.idx != nil {
panicOn(f.idx.holder.txf.CloseIndex(f.idx))
}
defer os.RemoveAll(f.Path()) defer os.RemoveAll(f.Path())
return f.Field.Close() return f.Field.Close()
} }
// Reopen closes the index and reopens it. // Reopen closes the index and reopens it.
func (f *TestField) Reopen() error { func (f *TestField) Reopen() error {
var err error name := f.Field.Name()
if err := f.Field.Close(); err != nil { if err := f.parent.Close(); err != nil {
f.parent = nil
return err return err
} }
if err := f.parent.Open(); err != nil {
path, index, name := f.Path(), f.Index(), f.Name() f.parent = nil
f.Field, err = NewField(path, index, name, OptFieldTypeDefault())
if err != nil {
return err
}
if err := f.Open(); err != nil {
return err return err
} }
f.Field = f.parent.Field(name)
return nil return nil
} }
func (f *TestField) MustSetBit(row, col uint64, ts ...time.Time) { func (f *TestField) MustSetBit(tx Tx, row, col uint64, ts ...time.Time) {
if len(ts) == 0 { if len(ts) == 0 {
_, err := f.Field.SetBit(row, col, nil) _, err := f.Field.SetBit(tx, row, col, nil)
if err != nil { if err != nil {
panic(err) panic(err)
} }
} }
for _, t := range ts { for _, t := range ts {
_, err := f.Field.SetBit(row, col, &t) _, err := f.Field.SetBit(tx, row, col, &t)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -258,7 +272,7 @@ func (f *TestField) MustSetBit(row, col uint64, ts ...time.Time) {
// Ensure field can open and retrieve a view. // Ensure field can open and retrieve a view.
func TestField_CreateViewIfNotExists(t *testing.T) { func TestField_CreateViewIfNotExists(t *testing.T) {
f := MustOpenField(OptFieldTypeDefault()) f := OpenField(t, OptFieldTypeDefault())
defer f.Close() defer f.Close()
// Create view. // Create view.
@ -283,7 +297,7 @@ func TestField_CreateViewIfNotExists(t *testing.T) {
} }
func TestField_SetTimeQuantum(t *testing.T) { func TestField_SetTimeQuantum(t *testing.T) {
f := MustOpenField(OptFieldTypeTime(TimeQuantum(""))) f := OpenField(t, OptFieldTypeTime(TimeQuantum("")))
defer f.Close() defer f.Close()
// Set & retrieve time quantum. // Set & retrieve time quantum.
@ -302,44 +316,54 @@ func TestField_SetTimeQuantum(t *testing.T) {
} }
func TestField_RowTime(t *testing.T) { func TestField_RowTime(t *testing.T) {
f := MustOpenField(OptFieldTypeTime(TimeQuantum(""))) f := OpenField(t, OptFieldTypeTime(TimeQuantum("")))
defer f.Close() defer f.Close()
// Obtain transaction.
tx := f.idx.holder.txf.NewTx(Txo{Write: writable, Index: f.idx, Field: f.Field, Shard: 0})
defer tx.Rollback()
if err := f.setTimeQuantum(TimeQuantum("YMDH")); err != nil { if err := f.setTimeQuantum(TimeQuantum("YMDH")); err != nil {
t.Fatal(err) t.Fatal(err)
} }
f.MustSetBit(1, 1, time.Date(2010, time.January, 5, 12, 0, 0, 0, time.UTC)) f.MustSetBit(tx, 1, 1, time.Date(2010, time.January, 5, 12, 0, 0, 0, time.UTC))
f.MustSetBit(1, 2, time.Date(2011, time.January, 5, 12, 0, 0, 0, time.UTC)) f.MustSetBit(tx, 1, 2, time.Date(2011, time.January, 5, 12, 0, 0, 0, time.UTC))
f.MustSetBit(1, 3, time.Date(2010, time.February, 5, 12, 0, 0, 0, time.UTC)) f.MustSetBit(tx, 1, 3, time.Date(2010, time.February, 5, 12, 0, 0, 0, time.UTC))
f.MustSetBit(1, 4, time.Date(2010, time.January, 6, 12, 0, 0, 0, time.UTC)) f.MustSetBit(tx, 1, 4, time.Date(2010, time.January, 6, 12, 0, 0, 0, time.UTC))
f.MustSetBit(1, 5, time.Date(2010, time.January, 5, 13, 0, 0, 0, time.UTC)) f.MustSetBit(tx, 1, 5, time.Date(2010, time.January, 5, 13, 0, 0, 0, time.UTC))
if r, err := f.RowTime(1, time.Date(2010, time.November, 5, 12, 0, 0, 0, time.UTC), "Y"); err != nil { panicOn(tx.Commit())
// obtain 2nd transaction to read it back.
tx = f.idx.holder.txf.NewTx(Txo{Write: !writable, Index: f.idx, Field: f.Field, Shard: 0})
defer tx.Rollback()
if r, err := f.RowTime(tx, 1, time.Date(2010, time.November, 5, 12, 0, 0, 0, time.UTC), "Y"); err != nil {
t.Fatal(err) t.Fatal(err)
} else if !reflect.DeepEqual(r.Columns(), []uint64{1, 3, 4, 5}) { } else if !reflect.DeepEqual(r.Columns(), []uint64{1, 3, 4, 5}) {
t.Fatalf("wrong columns: %#v", r.Columns()) t.Fatalf("wrong columns: %#v", r.Columns())
} }
if r, err := f.RowTime(1, time.Date(2010, time.February, 7, 13, 0, 0, 0, time.UTC), "YM"); err != nil { if r, err := f.RowTime(tx, 1, time.Date(2010, time.February, 7, 13, 0, 0, 0, time.UTC), "YM"); err != nil {
t.Fatal(err) t.Fatal(err)
} else if !reflect.DeepEqual(r.Columns(), []uint64{3}) { } else if !reflect.DeepEqual(r.Columns(), []uint64{3}) {
t.Fatalf("wrong columns: %#v", r.Columns()) t.Fatalf("wrong columns: %#v", r.Columns())
} }
if r, err := f.RowTime(1, time.Date(2010, time.February, 7, 13, 0, 0, 0, time.UTC), "M"); err != nil { if r, err := f.RowTime(tx, 1, time.Date(2010, time.February, 7, 13, 0, 0, 0, time.UTC), "M"); err != nil {
t.Fatal(err) t.Fatal(err)
} else if !reflect.DeepEqual(r.Columns(), []uint64{3}) { } else if !reflect.DeepEqual(r.Columns(), []uint64{3}) {
t.Fatalf("wrong columns: %#v", r.Columns()) t.Fatalf("wrong columns: %#v", r.Columns())
} }
if r, err := f.RowTime(1, time.Date(2010, time.January, 5, 12, 0, 0, 0, time.UTC), "MD"); err != nil { if r, err := f.RowTime(tx, 1, time.Date(2010, time.January, 5, 12, 0, 0, 0, time.UTC), "MD"); err != nil {
t.Fatal(err) t.Fatal(err)
} else if !reflect.DeepEqual(r.Columns(), []uint64{1, 5}) { } else if !reflect.DeepEqual(r.Columns(), []uint64{1, 5}) {
t.Fatalf("wrong columns: %#v", r.Columns()) t.Fatalf("wrong columns: %#v", r.Columns())
} }
if r, err := f.RowTime(1, time.Date(2010, time.January, 5, 13, 0, 0, 0, time.UTC), "MDH"); err != nil { if r, err := f.RowTime(tx, 1, time.Date(2010, time.January, 5, 13, 0, 0, 0, time.UTC), "MDH"); err != nil {
t.Fatal(err) t.Fatal(err)
} else if !reflect.DeepEqual(r.Columns(), []uint64{5}) { } else if !reflect.DeepEqual(r.Columns(), []uint64{5}) {
t.Fatalf("wrong columns: %#v", r.Columns()) t.Fatalf("wrong columns: %#v", r.Columns())
@ -348,7 +372,9 @@ func TestField_RowTime(t *testing.T) {
} }
func TestField_PersistAvailableShards(t *testing.T) { func TestField_PersistAvailableShards(t *testing.T) {
f := MustOpenField(OptFieldTypeDefault()) availableShardFileFlushDuration.Set(200 * time.Millisecond) //shorten the default time to force a file write
f := OpenField(t, OptFieldTypeDefault())
defer f.Close()
// bm represents remote available shards. // bm represents remote available shards.
bm := roaring.NewBitmap(1, 2, 3) bm := roaring.NewBitmap(1, 2, 3)
@ -356,6 +382,7 @@ func TestField_PersistAvailableShards(t *testing.T) {
if err := f.AddRemoteAvailableShards(bm); err != nil { if err := f.AddRemoteAvailableShards(bm); err != nil {
t.Fatal(err) t.Fatal(err)
} }
time.Sleep(2 * availableShardFileFlushDuration.Get())
// Reload field and verify that shard data is persisted. // Reload field and verify that shard data is persisted.
if err := f.Reopen(); err != nil { if err := f.Reopen(); err != nil {
@ -366,10 +393,74 @@ func TestField_PersistAvailableShards(t *testing.T) {
} }
func TestField_CorruptAvailableShards(t *testing.T) {
availableShardFileFlushDuration.Set(200 * time.Millisecond) //shorten the default time to force a file write
f := OpenField(t, OptFieldTypeDefault())
defer f.Close()
// bm represents remote available shards.
bm := roaring.NewBitmap(1, 2, 3)
if err := f.AddRemoteAvailableShards(bm); err != nil {
t.Fatal(err)
}
time.Sleep(2 * availableShardFileFlushDuration.Get())
path := filepath.Join(f.path, ".available.shards")
avail, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
t.Fatal(err)
}
n, err := avail.Write([]byte{23})
if err != nil || n != 1 {
t.Fatal(err)
}
avail.Close()
// Reload field and verify that shard data is persisted.
if err := f.Reopen(); err != nil {
t.Fatal(err)
} else if !reflect.DeepEqual(f.remoteAvailableShards.Slice(), []uint64(nil)) {
t.Fatalf("unexpected available shards (reopen). expected: %#v, but got: %#v", []uint64{}, f.remoteAvailableShards.Slice())
}
}
func TestField_TruncatedAvailableShards(t *testing.T) {
availableShardFileFlushDuration.Set(200 * time.Millisecond) //shorten the default time to force a file write
f := OpenField(t, OptFieldTypeDefault())
defer f.Close()
// bm represents remote available shards.
bm := roaring.NewBitmap(1, 2, 3)
if err := f.AddRemoteAvailableShards(bm); err != nil {
t.Fatal(err)
}
time.Sleep(2 * availableShardFileFlushDuration.Get())
path := filepath.Join(f.path, ".available.shards")
avail, err := os.OpenFile(path, os.O_TRUNC|os.O_WRONLY, 0644)
if err != nil {
t.Fatal(err)
}
avail.Close()
// Reload field and verify that shard data is persisted.
if err := f.Reopen(); err != nil {
t.Fatal(err)
} else if !reflect.DeepEqual(f.remoteAvailableShards.Slice(), []uint64(nil)) {
t.Fatalf("unexpected available shards (reopen). expected: %#v, but got: %#v", []uint64{}, f.remoteAvailableShards.Slice())
}
}
// Ensure that persisting available shards having a smaller footprint (for example, // Ensure that persisting available shards having a smaller footprint (for example,
// when going from a bitmap to a smaller, RLE representation) succeeds. // when going from a bitmap to a smaller, RLE representation) succeeds.
func TestField_PersistAvailableShardsFootprint(t *testing.T) { func TestField_PersistAvailableShardsFootprint(t *testing.T) {
f := MustOpenField(OptFieldTypeDefault()) availableShardFileFlushDuration.Set(200 * time.Millisecond) //shorten the default time to force a file write
f := OpenField(t, OptFieldTypeDefault())
defer f.Close()
// bm represents remote available shards. // bm represents remote available shards.
bm := roaring.NewBitmap() bm := roaring.NewBitmap()
@ -383,12 +474,14 @@ func TestField_PersistAvailableShardsFootprint(t *testing.T) {
if err := f.AddRemoteAvailableShards(bm); err != nil { if err := f.AddRemoteAvailableShards(bm); err != nil {
t.Fatal(err) t.Fatal(err)
} }
time.Sleep(2 * availableShardFileFlushDuration.Get())
// Reload field and verify that shard data is persisted. // Reload field and verify that shard data is persisted.
if err := f.Reopen(); err != nil { if err := f.Reopen(); err != nil {
t.Fatal(err) t.Fatal(err)
} else if !reflect.DeepEqual(f.remoteAvailableShards.Slice(), bm.Slice()) { } else if !reflect.DeepEqual(f.remoteAvailableShards.Slice(), bm.Slice()) {
t.Fatalf("unexpected available shards (reopen). expected: %v, but got: %v", bm.Slice(), f.remoteAvailableShards.Slice()) t.Fatalf("unexpected available shards (reopen). expected: %v, \n but got: %v", bm.Slice(), f.remoteAvailableShards.Slice())
} }
bm1 := roaring.NewBitmap() bm1 := roaring.NewBitmap()
@ -438,3 +531,389 @@ func TestBSIGroup_BaseDefaultValue(t *testing.T) {
} }
} }
} }
func TestField_ApplyOptions(t *testing.T) {
for i, tt := range []struct {
opts FieldOptions
expOpts FieldOptions
}{
{
FieldOptions{
Type: FieldTypeSet,
CacheType: CacheTypeNone,
CacheSize: 0,
},
FieldOptions{
Type: FieldTypeSet,
CacheType: CacheTypeNone,
CacheSize: 0,
},
},
} {
fld := &Field{}
fld.options = *applyDefaultOptions(&FieldOptions{})
if err := fld.applyOptions(tt.opts); err != nil {
t.Fatal(err)
}
if fld.options.CacheType != tt.expOpts.CacheType {
t.Fatalf("test %d, unexpected FieldOptions.CacheType value. expected: %s, but got: %s", i, tt.expOpts.CacheType, fld.options.CacheType)
} else if fld.options.CacheSize != tt.expOpts.CacheSize {
t.Fatalf("test %d, unexpected FieldOptions.CacheSize value. expected: %d, but got: %d", i, tt.expOpts.CacheSize, fld.options.CacheSize)
}
}
}
// Ensure that importValue handles requiredDepth correctly.
// This test sets the same column value to 1, then 8, then 1.
// A previous bug was incorrectly determining bitDepth based
// on the values in the import, and not taking existing values
// into consideration. This would cause an import of 1/8/1
// to result in a value of 9 instead of 1.
func TestBSIGroup_importValue(t *testing.T) {
f := OpenField(t, OptFieldTypeInt(-100, 200))
defer f.Close()
qcx := f.idx.holder.txf.NewQcx()
defer qcx.Abort()
options := &ImportOptions{}
for i, tt := range []struct {
columnIDs []uint64
values []int64
checkVal int64
expCols []uint64
}{
{
[]uint64{100},
[]int64{1},
1,
[]uint64{100},
},
{
[]uint64{100},
[]int64{8},
8,
[]uint64{100},
},
{
[]uint64{100},
[]int64{1},
1,
[]uint64{100},
},
} {
if err := f.importValue(qcx, tt.columnIDs, tt.values, options); err != nil {
t.Fatalf("test %d, importing values: %s", i, err.Error())
}
panicOn(qcx.Finish())
if row, err := f.Range(qcx, f.name, pql.EQ, tt.checkVal); err != nil {
t.Fatalf("test %d, getting range: %s", i, err.Error())
} else if !reflect.DeepEqual(row.Columns(), tt.expCols) {
t.Fatalf("test %d, expected columns: %v, but got: %v", i, tt.expCols, row.Columns())
}
panicOn(qcx.Finish())
} // loop
}
func TestIntField_MinMaxForShard(t *testing.T) {
f := OpenField(t, OptFieldTypeInt(-100, 200))
defer f.Close()
qcx := f.idx.holder.txf.NewQcx()
defer qcx.Abort()
options := &ImportOptions{}
for i, test := range []struct {
name string
columnIDs []uint64
values []int64
expMax ValCount
expMin ValCount
}{
{
name: "zero",
columnIDs: []uint64{},
values: []int64{},
},
{
name: "single",
columnIDs: []uint64{1},
values: []int64{10},
expMax: ValCount{Val: 10, Count: 1},
expMin: ValCount{Val: 10, Count: 1},
},
{
name: "twovals",
columnIDs: []uint64{1, 2},
values: []int64{10, 20},
expMax: ValCount{Val: 20, Count: 1},
expMin: ValCount{Val: 10, Count: 1},
},
{
name: "multiplecounts",
columnIDs: []uint64{1, 2, 3, 4, 5},
values: []int64{10, 20, 10, 10, 20},
expMax: ValCount{Val: 20, Count: 2},
expMin: ValCount{Val: 10, Count: 3},
},
{
name: "middlevals",
columnIDs: []uint64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
values: []int64{10, 20, 10, 10, 20, 11, 12, 11, 13, 11},
expMax: ValCount{Val: 20, Count: 2},
expMin: ValCount{Val: 10, Count: 3},
},
{
name: "middlevals",
columnIDs: []uint64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100000000, 100000001},
values: []int64{10, 20, 10, 10, 20, 11, 12, 11, 13, 11, 44, 1},
expMax: ValCount{Val: 20, Count: 2},
expMin: ValCount{Val: 10, Count: 3},
},
} {
t.Run(test.name+strconv.Itoa(i), func(t *testing.T) {
if err := f.importValue(qcx, test.columnIDs, test.values, options); err != nil {
t.Fatalf("test %d, importing values: %s", i, err.Error())
}
panicOn(qcx.Finish())
shard := uint64(0)
tx := f.idx.holder.txf.NewTx(Txo{Write: !writable, Index: f.idx, Field: f.Field, Shard: shard})
// Rollback below manually, because we are in a loop.
maxvc, err := f.MaxForShard(tx, shard, nil)
if err != nil {
t.Fatalf("getting max for shard: %v", err)
}
if maxvc != test.expMax {
t.Fatalf("max expected:\n%+v\ngot:\n%+v", test.expMax, maxvc)
}
minvc, err := f.MinForShard(tx, shard, nil)
if err != nil {
t.Fatalf("getting min for shard: %v", err)
}
if minvc != test.expMin {
t.Fatalf("min expected:\n%+v\ngot:\n%+v", test.expMin, minvc)
}
tx.Rollback()
})
}
}
// Ensure we get errors when they are expected.
func TestDecimalField_MinMaxBoundaries(t *testing.T) {
th := newTestHolder(t)
for i, test := range []struct {
scale int64
min pql.Decimal
max pql.Decimal
expErr bool
}{
{
scale: 3,
min: pql.NewDecimal(math.MinInt64, 0),
max: pql.NewDecimal(math.MaxInt64, 0),
expErr: true,
},
{
scale: 3,
min: pql.NewDecimal(math.MinInt64, 3),
max: pql.NewDecimal(math.MaxInt64, 3),
expErr: false,
},
{
scale: 3,
min: pql.NewDecimal(44, 0),
max: pql.NewDecimal(88, 0),
expErr: false,
},
{
scale: 3,
min: pql.NewDecimal(-44, 0),
max: pql.NewDecimal(88, 0),
expErr: false,
},
{
scale: 19,
min: pql.NewDecimal(1, 0),
max: pql.NewDecimal(2, 0),
expErr: true,
},
{
scale: 19,
min: pql.NewDecimal(math.MinInt64, 18),
max: pql.NewDecimal(math.MaxInt64, 18),
expErr: true,
},
{
scale: 0,
min: pql.NewDecimal(1, 20),
max: pql.NewDecimal(2, 20),
expErr: true,
},
{
scale: 0,
min: pql.NewDecimal(1, -1),
max: pql.NewDecimal(2, -1),
expErr: false,
},
{
scale: 0,
min: pql.NewDecimal(1, -19),
max: pql.NewDecimal(2, -19),
expErr: true,
},
} {
t.Run("minmax"+strconv.Itoa(i), func(t *testing.T) {
_, err := NewField(th, "no-path", "i", "f", OptFieldTypeDecimal(test.scale, test.min, test.max))
if err != nil && test.expErr {
if !strings.Contains(err.Error(), "is not supported") {
t.Fatal(err)
}
} else if err != nil && !test.expErr {
t.Fatalf("did not expect error, but got: %s", err)
} else if err == nil && test.expErr {
t.Fatal("expected error, but got none")
}
})
}
}
func TestDecimalField_MinMaxForShard(t *testing.T) {
f := OpenField(t, OptFieldTypeDecimal(3))
defer f.Close()
qcx := f.idx.holder.txf.NewQcx()
defer qcx.Abort()
options := &ImportOptions{}
for i, test := range []struct {
name string
columnIDs []uint64
values []float64
expMax ValCount
expMin ValCount
}{
{
name: "zero",
columnIDs: []uint64{},
values: []float64{},
},
{
name: "single",
columnIDs: []uint64{1},
values: []float64{10.1},
expMax: ValCount{DecimalVal: &pql.Decimal{Value: 10100, Scale: 3}, Count: 1},
expMin: ValCount{DecimalVal: &pql.Decimal{Value: 10100, Scale: 3}, Count: 1},
},
{
name: "twovals",
columnIDs: []uint64{1, 2},
values: []float64{10.1, 20.2},
expMax: ValCount{DecimalVal: &pql.Decimal{Value: 20200, Scale: 3}, Count: 1},
expMin: ValCount{DecimalVal: &pql.Decimal{Value: 10100, Scale: 3}, Count: 1},
},
{
name: "multiplecounts",
columnIDs: []uint64{1, 2, 3, 4, 5},
values: []float64{10.1, 20.2, 10.1, 10.1, 20.2},
expMax: ValCount{DecimalVal: &pql.Decimal{Value: 20200, Scale: 3}, Count: 2},
expMin: ValCount{DecimalVal: &pql.Decimal{Value: 10100, Scale: 3}, Count: 3},
},
{
name: "middlevals",
columnIDs: []uint64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
values: []float64{10.1, 20.2, 10.1, 10.1, 20.2, 11, 12, 11, 13, 11},
expMax: ValCount{DecimalVal: &pql.Decimal{Value: 20200, Scale: 3}, Count: 2},
expMin: ValCount{DecimalVal: &pql.Decimal{Value: 10100, Scale: 3}, Count: 3},
},
{
name: "another shard",
columnIDs: []uint64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100000000, 100000001},
values: []float64{10.1, 20.2, 10.1, 10.1, 20.2, 11, 12, 11, 13, 11, 44.39, 0.23},
expMax: ValCount{DecimalVal: &pql.Decimal{Value: 20200, Scale: 3}, Count: 2},
expMin: ValCount{DecimalVal: &pql.Decimal{Value: 10100, Scale: 3}, Count: 3},
},
} {
t.Run(test.name+strconv.Itoa(i), func(t *testing.T) {
if err := f.importFloatValue(qcx, test.columnIDs, test.values, options); err != nil {
t.Fatalf("test %d, importing values: %s", i, err.Error())
}
shard := uint64(0)
tx := f.idx.holder.txf.NewTx(Txo{Write: !writable, Index: f.idx, Field: f.Field, Shard: shard})
defer tx.Rollback()
maxvc, err := f.MaxForShard(tx, shard, nil)
if err != nil {
t.Fatalf("getting max for shard: %v", err)
}
if !reflect.DeepEqual(maxvc, test.expMax) {
t.Fatalf("max expected:\n%+v\ngot:\n%+v", test.expMax, maxvc)
}
minvc, err := f.MinForShard(tx, shard, nil)
if err != nil {
t.Fatalf("getting min for shard: %v", err)
}
if !reflect.DeepEqual(minvc, test.expMin) {
t.Fatalf("min expected:\n%+v\ngot:\n%+v", test.expMin, minvc)
}
})
}
}
func TestBSIGroup_TxReopenDB(t *testing.T) {
f := OpenField(t, OptFieldTypeInt(-100, 200))
defer f.Close()
qcx := f.idx.holder.txf.NewQcx()
defer qcx.Abort()
options := &ImportOptions{}
for i, tt := range []struct {
columnIDs []uint64
values []int64
checkVal int64
expCols []uint64
}{
{
[]uint64{100},
[]int64{1},
1,
[]uint64{100},
},
{
[]uint64{100},
[]int64{8},
8,
[]uint64{100},
},
{
[]uint64{100},
[]int64{1},
1,
[]uint64{100},
},
} {
if err := f.importValue(qcx, tt.columnIDs, tt.values, options); err != nil {
t.Fatalf("test %d, importing values: %s", i, err.Error())
}
panicOn(qcx.Finish())
if row, err := f.Range(qcx, f.name, pql.EQ, tt.checkVal); err != nil {
t.Fatalf("test %d, getting range: %s", i, err.Error())
} else if !reflect.DeepEqual(row.Columns(), tt.expCols) {
t.Fatalf("test %d, expected columns: %v, but got: %v", i, tt.expCols, row.Columns())
}
panicOn(qcx.Finish())
} // loop
// the test: can we re-open a BSI fragment under badger/rbf.
_ = f.Reopen()
}

View file

@ -15,7 +15,6 @@
package pilosa_test package pilosa_test
import ( import (
"io/ioutil"
"math" "math"
"testing" "testing"
@ -23,12 +22,13 @@ import (
"github.com/pilosa/pilosa/v2" "github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/roaring" "github.com/pilosa/pilosa/v2/roaring"
"github.com/pilosa/pilosa/v2/test" "github.com/pilosa/pilosa/v2/test"
"github.com/pilosa/pilosa/v2/testhook"
) )
// Ensure a field can set & read a bsiGroup value. // Ensure a field can set & read a bsiGroup value.
func TestField_SetValue(t *testing.T) { func TestField_SetValue(t *testing.T) {
t.Run("OK", func(t *testing.T) { t.Run("OK", func(t *testing.T) {
idx := test.MustOpenIndex() idx := test.MustOpenIndex(t)
defer idx.Close() defer idx.Close()
f, err := idx.CreateField("f", pilosa.OptFieldTypeInt(math.MinInt64, math.MaxInt64)) f, err := idx.CreateField("f", pilosa.OptFieldTypeInt(math.MinInt64, math.MaxInt64))
@ -36,15 +36,18 @@ func TestField_SetValue(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
// It is okay to pass in a nil tx. f.SetValue will lazily instantiate Tx.
var tx pilosa.Tx
// Set value on field. // Set value on field.
if changed, err := f.SetValue(100, 21); err != nil { if changed, err := f.SetValue(tx, 100, 21); err != nil {
t.Fatal(err) t.Fatal(err)
} else if !changed { } else if !changed {
t.Fatal("expected change") t.Fatal("expected change")
} }
// Read value. // Read value.
if value, exists, err := f.Value(100); err != nil { if value, exists, err := f.Value(tx, 100); err != nil {
t.Fatal(err) t.Fatal(err)
} else if value != 21 { } else if value != 21 {
t.Fatalf("unexpected value: %d", value) t.Fatalf("unexpected value: %d", value)
@ -53,7 +56,7 @@ func TestField_SetValue(t *testing.T) {
} }
// Setting value should return no change. // Setting value should return no change.
if changed, err := f.SetValue(100, 21); err != nil { if changed, err := f.SetValue(tx, 100, 21); err != nil {
t.Fatal(err) t.Fatal(err)
} else if changed { } else if changed {
t.Fatal("expected no change") t.Fatal("expected no change")
@ -61,7 +64,7 @@ func TestField_SetValue(t *testing.T) {
}) })
t.Run("Overwrite", func(t *testing.T) { t.Run("Overwrite", func(t *testing.T) {
idx := test.MustOpenIndex() idx := test.MustOpenIndex(t)
defer idx.Close() defer idx.Close()
f, err := idx.CreateField("f", pilosa.OptFieldTypeInt(math.MinInt64, math.MaxInt64)) f, err := idx.CreateField("f", pilosa.OptFieldTypeInt(math.MinInt64, math.MaxInt64))
@ -69,22 +72,25 @@ func TestField_SetValue(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
// It is okay to pass in a nil tx. f.SetValue will lazily instantiate Tx.
var tx pilosa.Tx
// Set value. // Set value.
if changed, err := f.SetValue(100, 21); err != nil { if changed, err := f.SetValue(tx, 100, 21); err != nil {
t.Fatal(err) t.Fatal(err)
} else if !changed { } else if !changed {
t.Fatal("expected change") t.Fatal("expected change")
} }
// Set different value. // Set different value.
if changed, err := f.SetValue(100, 23); err != nil { if changed, err := f.SetValue(tx, 100, 23); err != nil {
t.Fatal(err) t.Fatal(err)
} else if !changed { } else if !changed {
t.Fatal("expected change") t.Fatal("expected change")
} }
// Read value. // Read value.
if value, exists, err := f.Value(100); err != nil { if value, exists, err := f.Value(tx, 100); err != nil {
t.Fatal(err) t.Fatal(err)
} else if value != 23 { } else if value != 23 {
t.Fatalf("unexpected value: %d", value) t.Fatalf("unexpected value: %d", value)
@ -94,7 +100,7 @@ func TestField_SetValue(t *testing.T) {
}) })
t.Run("ErrBSIGroupNotFound", func(t *testing.T) { t.Run("ErrBSIGroupNotFound", func(t *testing.T) {
idx := test.MustOpenIndex() idx := test.MustOpenIndex(t)
defer idx.Close() defer idx.Close()
f, err := idx.CreateField("f", pilosa.OptFieldTypeDefault()) f, err := idx.CreateField("f", pilosa.OptFieldTypeDefault())
@ -102,14 +108,17 @@ func TestField_SetValue(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
// It is okay to pass in a nil tx. f.SetValue will lazily instantiate Tx.
var tx pilosa.Tx
// Set value. // Set value.
if _, err := f.SetValue(100, 21); err != pilosa.ErrBSIGroupNotFound { if _, err := f.SetValue(tx, 100, 21); err != pilosa.ErrBSIGroupNotFound {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
}) })
t.Run("ErrBSIGroupValueTooLow", func(t *testing.T) { t.Run("ErrBSIGroupValueTooLow", func(t *testing.T) {
idx := test.MustOpenIndex() idx := test.MustOpenIndex(t)
defer idx.Close() defer idx.Close()
f, err := idx.CreateField("f", pilosa.OptFieldTypeInt(20, 30)) f, err := idx.CreateField("f", pilosa.OptFieldTypeInt(20, 30))
@ -117,14 +126,17 @@ func TestField_SetValue(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
// It is okay to pass in a nil tx. f.SetValue will lazily instantiate Tx.
var tx pilosa.Tx
// Set value. // Set value.
if _, err := f.SetValue(100, 15); err != pilosa.ErrBSIGroupValueTooLow { if _, err := f.SetValue(tx, 100, 15); err != pilosa.ErrBSIGroupValueTooLow {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
}) })
t.Run("ErrBSIGroupValueTooHigh", func(t *testing.T) { t.Run("ErrBSIGroupValueTooHigh", func(t *testing.T) {
idx := test.MustOpenIndex() idx := test.MustOpenIndex(t)
defer idx.Close() defer idx.Close()
f, err := idx.CreateField("f", pilosa.OptFieldTypeInt(20, 30)) f, err := idx.CreateField("f", pilosa.OptFieldTypeInt(20, 30))
@ -132,19 +144,22 @@ func TestField_SetValue(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
// It is okay to pass in a nil tx. f.SetValue will lazily instantiate Tx.
var tx pilosa.Tx
// Set value. // Set value.
if _, err := f.SetValue(100, 31); err != pilosa.ErrBSIGroupValueTooHigh { if _, err := f.SetValue(tx, 100, 31); err != pilosa.ErrBSIGroupValueTooHigh {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
}) })
} }
func TestField_NameRestriction(t *testing.T) { func TestField_NameRestriction(t *testing.T) {
path, err := ioutil.TempDir("", "pilosa-field-") path, err := testhook.TempDir(t, "pilosa-field-")
if err != nil { if err != nil {
panic(err) panic(err)
} }
field, err := pilosa.NewField(path, "i", ".meta", pilosa.OptFieldTypeDefault()) field, err := pilosa.NewField(pilosa.NewHolder(path, nil), path, "i", ".meta", pilosa.OptFieldTypeDefault())
if field != nil { if field != nil {
t.Fatalf("unexpected field name %s", err) t.Fatalf("unexpected field name %s", err)
} }
@ -158,6 +173,7 @@ func TestField_NameValidation(t *testing.T) {
"under_score", "under_score",
"abc123", "abc123",
"trailing_", "trailing_",
"charact2301234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890",
} }
invalidFieldNames := []string{ invalidFieldNames := []string{
"", "",
@ -168,30 +184,32 @@ func TestField_NameValidation(t *testing.T) {
"abc def", "abc def",
"camelCase", "camelCase",
"UPPERCASE", "UPPERCASE",
"a12345678901234567890123456789012345678901234567890123456789012345", "charact23112345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901",
} }
path, err := ioutil.TempDir("", "pilosa-field-") path, err := testhook.TempDir(t, "pilosa-field-")
if err != nil { if err != nil {
panic(err) panic(err)
} }
for _, name := range validFieldNames { for _, name := range validFieldNames {
_, err := pilosa.NewField(path, "i", name, pilosa.OptFieldTypeDefault()) _, err := pilosa.NewField(pilosa.NewHolder(path, nil), path, "i", name, pilosa.OptFieldTypeDefault())
if err != nil { if err != nil {
t.Fatalf("unexpected field name: %s %s", name, err) t.Fatalf("unexpected field name: %s %s", name, err)
} }
} }
for _, name := range invalidFieldNames { for _, name := range invalidFieldNames {
_, err := pilosa.NewField(path, "i", name, pilosa.OptFieldTypeDefault()) _, err := pilosa.NewField(pilosa.NewHolder(path, nil), path, "i", name, pilosa.OptFieldTypeDefault())
if err == nil { if err == nil {
t.Fatalf("expected error on field name: %s", name) t.Fatalf("expected error on field name: %s", name)
} }
} }
} }
const includeRemote = false // for calls to Index.AvailableShards(localOnly bool)
// Ensure can update and delete available shards. // Ensure can update and delete available shards.
func TestField_AvailableShards(t *testing.T) { func TestField_AvailableShards(t *testing.T) {
idx := test.MustOpenIndex() idx := test.MustOpenIndex(t)
defer idx.Close() defer idx.Close()
f, err := idx.CreateField("f", pilosa.OptFieldTypeDefault()) f, err := idx.CreateField("f", pilosa.OptFieldTypeDefault())
@ -199,12 +217,15 @@ func TestField_AvailableShards(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
// It is okay to pass in a nil tx. f.SetBit will lazily instantiate Tx.
var tx pilosa.Tx
// Set values on shards 0 & 2, and verify. // Set values on shards 0 & 2, and verify.
if _, err := f.SetBit(0, 100, nil); err != nil { if _, err := f.SetBit(tx, 0, 100, nil); err != nil {
t.Fatal(err) t.Fatal(err)
} else if _, err := f.SetBit(0, ShardWidth*2, nil); err != nil { } else if _, err := f.SetBit(tx, 0, ShardWidth*2, nil); err != nil {
t.Fatal(err) t.Fatal(err)
} else if diff := cmp.Diff(f.AvailableShards().Slice(), []uint64{0, 2}); diff != "" { } else if diff := cmp.Diff(f.AvailableShards(includeRemote).Slice(), []uint64{0, 2}); diff != "" {
t.Fatal(diff) t.Fatal(diff)
} }
@ -212,7 +233,7 @@ func TestField_AvailableShards(t *testing.T) {
if err := f.AddRemoteAvailableShards(roaring.NewBitmap(1, 2, 4)); err != nil { if err := f.AddRemoteAvailableShards(roaring.NewBitmap(1, 2, 4)); err != nil {
t.Fatalf("adding remote shards: %v", err) t.Fatalf("adding remote shards: %v", err)
} }
if diff := cmp.Diff(f.AvailableShards().Slice(), []uint64{0, 1, 2, 4}); diff != "" { if diff := cmp.Diff(f.AvailableShards(includeRemote).Slice(), []uint64{0, 1, 2, 4}); diff != "" {
t.Fatal(diff) t.Fatal(diff)
} }
@ -223,7 +244,50 @@ func TestField_AvailableShards(t *testing.T) {
t.Fatalf("removing shard %d: %v", i, err) t.Fatalf("removing shard %d: %v", i, err)
} }
} }
if diff := cmp.Diff(f.AvailableShards().Slice(), []uint64{0, 2}); diff != "" { if diff := cmp.Diff(f.AvailableShards(includeRemote).Slice(), []uint64{0, 2}); diff != "" {
t.Fatal(diff) t.Fatal(diff)
} }
} }
func TestField_ClearValue(t *testing.T) {
t.Run("OK", func(t *testing.T) {
idx := test.MustOpenIndex(t)
defer idx.Close()
f, err := idx.CreateField("f", pilosa.OptFieldTypeInt(math.MinInt64, math.MaxInt64))
if err != nil {
t.Fatal(err)
}
// It is okay to pass in a nil tx. f.SetValue will lazily instantiate Tx.
var tx pilosa.Tx
// Set value on field.
if changed, err := f.SetValue(tx, 100, 21); err != nil {
t.Fatal(err)
} else if !changed {
t.Fatal("expected change")
}
// Read value.
if value, exists, err := f.Value(tx, 100); err != nil {
t.Fatal(err)
} else if value != 21 {
t.Fatalf("unexpected value: %d", value)
} else if !exists {
t.Fatal("expected value to exist")
}
if changed, err := f.ClearValue(tx, 100); err != nil {
t.Fatal(err)
} else if !changed {
t.Fatal(err)
}
// Read value.
if _, exists, err := f.Value(tx, 100); err != nil {
t.Fatal(err)
} else if exists {
t.Fatal("expected value to not exist")
}
})
}

42
filesystem.go Normal file
View file

@ -0,0 +1,42 @@
// Copyright 2017 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pilosa
import (
"fmt"
"net/http"
)
// Ensure nopFileSystem implements interface.
var _ FileSystem = &nopFileSystem{}
// FileSystem represents an interface for file system for serving the Lattice UI.
type FileSystem interface {
New() (http.FileSystem, error)
}
func init() {
NopFileSystem = &nopFileSystem{}
}
// NopFileSystem represents a FileSystem that returns an error if called.
var NopFileSystem FileSystem
type nopFileSystem struct{}
// New is a no-op implementation of FileSystem New method.
func (n *nopFileSystem) New() (http.FileSystem, error) {
return nil, fmt.Errorf("file system not implemented")
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

47
gendebug_test.go Normal file
View file

@ -0,0 +1,47 @@
// Copyright 2019 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// +build generationdebug
package pilosa
import (
"errors"
"fmt"
"runtime"
"github.com/pilosa/pilosa/v2/testhook"
)
func examineResults() error {
runtime.GC()
stats, results := reportGenerations()
if len(stats) > 0 {
fmt.Printf("generation stats: %s\n", stats)
}
if len(results) == 0 {
return nil
}
if len(results) > 0 {
fmt.Printf("generations:\n")
for _, res := range results {
fmt.Printf(" %s\n", res)
}
}
return errors.New("outstanding generations detected")
}
func init() {
testhook.RegisterPostTestHook(examineResults)
}

463
generation.go Normal file
View file

@ -0,0 +1,463 @@
// Copyright 2019 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pilosa
import (
"fmt"
"io"
"io/ioutil"
"os"
"runtime"
// "runtime/debug"
"sync"
"syscall"
"time"
"github.com/pilosa/pilosa/v2/logger"
"github.com/pilosa/pilosa/v2/roaring"
"github.com/pilosa/pilosa/v2/syswrap"
"github.com/pkg/errors"
)
// generation represents one "generation" of opening a data file.
// This is what determines when it's safe to unmap a data file, if it
// got mapped, and handles closing/reopening files if we need to
// manage file handle availability. It's an interface because this
// lets us write simpler code for specific cases, rather than handling
// the whole matrix of mapped/unmapped, staying open/being reopened,
// etcetera.
//
// You create a generation by calling newGeneration with a file
// path. If it succeeds in opening that path, it calls a provided
// setup function with the data from the generation, and a flag
// indicating whether the data is mmapped. If the setup function
// fails, newGeneration cleans things up and closes. Otherwise,
// it returns a generation.
//
// The generation itself uses runtime.SetFinalizer to clean up when
// the last reference to it goes away. You should store a pointer
// to the generation in any object which is reliant on the generation.
//
// When you anticipate a generation should be done (for instance,
// opening a new generation), the old one gets marked done, which
// stashes a timestamp in it. Later operations can check whether
// the timestamp is a while back, and if so, complain that something
// might be wrong.
//
// In some cases, we don't have enough open file limit to keep every
// file actually open. To address this, use the `Transaction` function,
// which ensures that the file is open, stores a reference to it in
// a provided `*io.Writer`, and then restores the previous value of
// the io.Writer when it's done. For instance, for a bitmap, this might
// be used with `&b.OpWriter`.
//
// newGeneration takes an optional previous generation; it calls
// that generation's Done function after running the provided setup,
// and bumps the generation count.
type generation interface {
// Transaction runs the given transaction with the generation's
// file open. If the **os.File parameter is
// non-nil, the generation's file will be open, and stored
// into that pointer, during the execution of func, after
// which the previous contents are restored. Otherwise
// the file may or may not be open during the operation.
Transaction(*io.Writer, func() error) error
// Done() should be called exactly once, to indicate that a
// generation is expected not to be in use for long -- for instance,
// when a new generation replaces it.
Done()
// Generation count.
Generation() int64
// ID indicates the source -- path and generation number -- that
// this generation represents.
ID() string
// Dead indicates whether this generation is Done.
Dead() bool
// Bytes reports the storage associated with this generation, if any.
// DO NOT USE THIS. Except if you're debugging mmap segfaults.
Bytes() []byte
}
type mmapGeneration struct {
mu sync.Mutex // mutex guards modifiers of generation, not of data
transMu sync.Mutex // guards transactions, specifically
path string
id string
file *os.File
data []byte
generation int64 // generation counter
dead bool // we think this generation is dead
deadSince time.Time // when this generation was marked dead
retries int // for cases where we're retrying
logger logger.Logger
}
func (m *mmapGeneration) Dead() bool {
m.mu.Lock()
defer m.mu.Unlock()
return m.dead
}
func (m *mmapGeneration) ID() string {
return m.id
}
func (m *mmapGeneration) Generation() int64 {
return m.generation
}
// Transaction runs an exclusive call, ensuring that the file is open if
// the *io.Writer parameter is present.
func (m *mmapGeneration) Transaction(fileP *io.Writer, fn func() error) (transactionErr error) {
m.transMu.Lock()
defer m.transMu.Unlock()
// HEY LOOK CAREFULLY AT THIS BIT:
// We can't just defer this unlock. We specifically want to be
// sure to unlock the regular mutex *before* this function is over,
// and if we error out trying to open the file, we want to do it
// even sooner. If we deferred this, the transaction would block
// *everything*, including things like sanity checks against the
// generation being Dead(), but also including the deferred
// re-close-the-file.
m.mu.Lock()
// if we've been asked for a file pointer, we need to ensure that
// our file is open, and that the file pointer to it is stored in
// the requested location, then revert that when we're done.
// if we aren't asked for a file pointer, nothing needs the file
// open.
if m.dead {
elapsed := time.Since(m.deadSince)
m.logger.Printf("WARNING: transaction against %s, which has been dead for %v\n", m.id, elapsed)
}
if fileP != nil {
if m.file == nil {
// we ignore the shouldClose response here; if this
// fragment was previously not being kept open, we're
// going to stick with that.
_, err := m.openFile()
if err != nil {
m.mu.Unlock()
return err
}
defer func() {
// report a close error if we have no other error to report
m.mu.Lock()
defer m.mu.Unlock()
err := m.closeFile()
if transactionErr == nil {
transactionErr = err
}
}()
}
var fileStash io.Writer
fileStash, *fileP = *fileP, m.file
defer func() {
*fileP = fileStash
}()
}
// We are done locking the generation itself for now.
m.mu.Unlock()
// wouldPanic := debug.SetPanicOnFault(true)
// defer func() {
// debug.SetPanicOnFault(wouldPanic)
// if r := recover(); r != nil {
// if err, ok := r.(error); ok {
// // special case: if we caught a page fault, we diagnose that directly. sadly,
// // we can't see the actual values that were used to generate this, probably.
// if err.Error() == "runtime error: invalid memory address or nil pointer dereference" {
// if transactionErr == nil {
// transactionErr = errors.New("invalid memory access during transaction")
// } else {
// transactionErr = fmt.Errorf("invalid memory access during transaction, previous error %v", transactionErr)
// }
// return
// }
// }
// if transactionErr == nil {
// transactionErr = fmt.Errorf("panic during transaction: %v", r)
// } else {
// transactionErr = fmt.Errorf("panic during erroring transaction: panic %v, previous error %v", r, transactionErr)
// }
// }
// }()
return fn()
}
func (m *mmapGeneration) Bytes() []byte {
return m.data
}
// Done marks the generation done, and closes its file, but may not unmap it.
// It's still conceptually possible to end up doing a Transaction against a
// done generation, but it's a red flag.
func (m *mmapGeneration) Done() {
if m == nil {
return
}
m.mu.Lock()
defer m.mu.Unlock()
if m.dead {
oops := fmt.Sprintf("generation %s, marked done again at %v, previously marked dead at %v",
m.id, time.Now(), m.deadSince)
panic(oops)
}
m.dead = true
m.deadSince = time.Now()
err := m.closeFile()
if err != nil {
m.logger.Printf("error closing generation %s: %v", m.id, err)
}
// If we're not debugging, the finalizer won't have been enabled
// previously. Finalizers have non-zero cost, so having them not be
// created until they're needed seems rewarding?
if !generationDebug {
runtime.SetFinalizer(m, generationFinalizer)
}
endGeneration(m.id)
// note, Done() doesn't close the file; only the finalizer actually
// does the shutdown.
}
// Try to close the file if it's currently open.
func (m *mmapGeneration) closeFile() error {
var lastErr error
// report the most serious error encountered, but still close
// file even if something else failed.
if m.file != nil {
if err := m.file.Sync(); err != nil {
lastErr = fmt.Errorf("sync: %s", err)
}
if err := syscall.Flock(int(m.file.Fd()), syscall.LOCK_UN); err != nil {
lastErr = fmt.Errorf("unlock: %s", err)
}
if err := syswrap.CloseFile(m.file); err != nil {
lastErr = fmt.Errorf("close file: %s", err)
}
m.file = nil
}
return lastErr
}
// openFile ensures the file is open and locked, or fails. If it does
// open the file, it will also report the "you need to close this file
// when you're done" flag from syswrap.
func (m *mmapGeneration) openFile() (shouldClose bool, err error) {
if m.file != nil {
return false, nil
}
m.file, shouldClose, err = syswrap.OpenFile(m.path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
return false, err
}
// do we actually want this in every openFile? I don't know.
if err := syscall.Flock(int(m.file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
_ = syswrap.CloseFile(m.file)
m.file = nil
return false, fmt.Errorf("flock: %s", err)
}
return shouldClose, nil
}
func generationFinalizer(m *mmapGeneration) {
m.mu.Lock()
if !m.dead {
m.logger.Printf("finalizing generation %s which isn't dead yet\n",
m.id)
}
m.mu.Unlock()
err := m.closeFile()
if err != nil {
m.logger.Printf("finalizing generation, closing file: %v\n", err)
}
if m.data != nil {
err := syswrap.Munmap(m.data)
if err != nil {
m.logger.Printf("finalizing generation, munmap: %v\n", err)
}
m.data = nil
}
finalizeGeneration(m.id)
}
// Cancel closes a generation out entirely. It cancels any finalizer,
// unmaps any data, ends generation tracking, and closes any files.
// It does each of these separately whether or not the others need to be done,
// or succeed. It's used to handle failures from newGeneration; it makes sure
// the generation isn't holding any resources and doesn't need to be cleaned
// up otherwise.
//
// Mostly a helper function because there's several cases where newGeneration
// might fail.
func (m *mmapGeneration) Cancel() {
if m.data != nil {
_ = syswrap.Munmap(m.data)
m.data = nil
}
err := m.closeFile()
if err != nil {
m.logger.Printf("error cancelling generation %s: %v", m.id, err)
}
runtime.SetFinalizer(m, nil)
m.dead = true
m.deadSince = time.Now()
cancelGeneration(m.id)
}
// newGeneration creates a new generation using the given file path. It
// then calls the provided setup function with the allocated storage, a
// file handle, the new generation, and a flag indicatting whether the storage
// is memory-mapped. If the setup function returns a non-nil error, the
// generation is cleaned up, and newGeneration fails. The setup function
// also returns a boolean indicating whether it used the mapping; if it
// didn't, newGeneration discards the mapping and returns a nil generation.
//
// If generationDebug is enabled, we track the generation even if no mapping
// is actually in use, so we can verify that the tracking is working.
//
// On failure, newGeneration returns nil values for generation and func,
// and an error. On success, the func returned is the close func to use
// when the generation is no longer needed by the caller.
func newGeneration(existing generation, path string, readData bool, setup func([]byte, *os.File, generation, bool) (bool, error), logger logger.Logger) (generation, error) {
m := mmapGeneration{path: path, logger: logger}
if existing != nil {
m.generation = existing.Generation() + 1
m.retries = existing.(*mmapGeneration).retries
// we might keep a previous generation around just for its generation count.
if !existing.Dead() {
defer existing.Done()
}
}
shouldClose, err := m.openFile()
if err != nil {
return nil, err
}
m.id = fmt.Sprintf("%s:%d", m.path, m.generation)
// possibly assign new generation ID if this one's been used, which can
// happen with reopens, especially during testing.
m.id = registerGeneration(m.id)
// if debugging, we always want the finalizer on so we notice if a
// generation is finalized without being closed. for non-debugging
// use, we only need it when the generation is closed.
if generationDebug {
runtime.SetFinalizer(&m, generationFinalizer)
}
// Mmap the underlying file so it can be zero copied.
var mapped bool
var data []byte
fi, err := m.file.Stat()
if err == nil && fi.Size() > 0 {
data, err = syswrap.Mmap(int(m.file.Fd()), 0, int(fi.Size()), syscall.PROT_READ, syscall.MAP_SHARED)
if err == syswrap.ErrMaxMapCountReached {
// I have no idea where/how to display this message.
m.logger.Printf("maximum number of maps reached, reading file '%s' instead", m.path)
} else if err != nil {
m.Cancel()
return nil, errors.Wrap(err, "mmap failed")
} else {
mapped = true
}
}
if data == nil && readData {
data, err = ioutil.ReadAll(m.file)
if err != nil {
m.Cancel()
return nil, errors.Wrap(err, "failure file readall")
}
}
// if we got here, data's the expected data, so let's try to use it
mappedAny, err := setup(data, m.file, &m, mapped)
// if the setup failed, we unmap data if we previously mapped it,
// and exit. Note that having no data, or having only trivial
// data (like a zero-container Roaring file) isn't "failed".
if err != nil {
m.Cancel()
// Unless, that is, we think the file probably ought to
// be truncated: For instance, if a bitmap has a corrupted
// ops log, we could truncate that part of it and retry.
if err, ok := err.(roaring.FileShouldBeTruncatedError); ok && m.retries < 1 {
m.logger.Printf("file %s read partially, but should-be-truncated at %d bytes\n", m.path, err.SuggestedLength())
// close this generation, then try again. once.
m.retries++
err := os.Truncate(m.path, err.SuggestedLength())
if err != nil {
m.logger.Printf("truncating file failed [but retrying anyway]: %v\n", err)
}
return newGeneration(&m, path, readData, setup, logger)
}
return nil, err
}
if mapped {
// when generationDebug is on, we want to track this even
// if it's not being used.
if generationDebug || mappedAny {
// Advise the kernel that the mmap is accessed randomly.
// We don't care much about errors with this.
_ = madvise(data, syscall.MADV_RANDOM)
// store the data, so we can unmap it when this generation
// gets finalized.
m.data = data
} else {
// unmap the data and don't stash the pointer in this
// generation. It's not being used. This generation
// doesn't need to exist, yay.
unmapErr := syswrap.Munmap(data)
if unmapErr != nil {
m.logger.Printf("error unmapping (probably harmless): %v", unmapErr)
}
}
}
// shouldClose comes from underlying syswrap.OpenFile, which checks
// a count of open files to hint at us when we need to start closing
// files to preserve open file descriptor limit.
if shouldClose {
err := m.closeFile()
if err != nil {
m.logger.Printf("closing file to preserve open files failed: %v\n", err)
}
}
// It's possible that the generation has no actual data to track,
// because nothing's mapped, in which case there won't be any bitmap
// sources following this, just the fragment source. (Bitmaps won't
// be attached to the source unless they're actually mapped to it,
// or generationDebug is true). That's okay. We pay a tiny cost
// for the finalizer, but we also get higher confidence that it really
// does get cleaned up.
return &m, nil
}
// NopGeneration is used in fragment.openStorage() to short-circuit
// generation stuff that only applies to RoaringTx; doesn't apply to RBFTx/BadgerTx/etc.
type NopGeneration struct {
}
func (g *NopGeneration) Transaction(w *io.Writer, f func() error) error {
return f()
}
func (g *NopGeneration) Done() {}
func (g *NopGeneration) Generation() int64 {
return 0
}
func (g *NopGeneration) ID() string {
return "NOP"
}
func (g *NopGeneration) Dead() bool {
return true
}
func (g *NopGeneration) Bytes() (ret []byte) {
return
}

164
generation_debug.go Normal file
View file

@ -0,0 +1,164 @@
// Copyright 2019 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build generationdebug
package pilosa
import (
"fmt"
"math/rand"
"runtime"
"runtime/debug"
"sort"
"sync"
"time"
)
const generationDebug = true
type lifespan struct {
from, to, finalized time.Time
stack []byte
}
var knownGenerations map[string]lifespan
var knownGenerationLock sync.Mutex
var timeZero time.Time
var generationDebugVerbose bool
// History reports the finalized/dead/created status of a span which we think
// is in some way in error. It's shared between a couple of places.
func (span *lifespan) History() string {
dead := "not dead"
finalized := "not finalized"
if span.finalized != timeZero {
finalized = fmt.Sprintf("finalized at %v", span.finalized)
}
if span.to != timeZero {
dead = fmt.Sprintf("dead at %v", span.to)
}
return fmt.Sprintf("%s, %s, created at %v at %s", dead, finalized, span.from, span.stack)
}
func (span *lifespan) reportHistory(reason string, id string) string {
return fmt.Sprintf("%s %s: %s", id, reason, span.History())
}
func registerGeneration(id string) string {
knownGenerationLock.Lock()
defer knownGenerationLock.Unlock()
if knownGenerations == nil {
knownGenerations = make(map[string]lifespan)
}
newSpan := lifespan{from: time.Now(), stack: debug.Stack()}
origId := id
// if you have more than 65k of the same file open, maybe you have bigger
// problems than this.
for span, exists := knownGenerations[id]; exists; span, exists = knownGenerations[id] {
suffix := fmt.Sprintf("::%04x", rand.Int63n(65536))
if generationDebugVerbose {
history := span.History()
fmt.Printf("new generation: adding suffix %s, previous %s\n",
suffix, history)
}
id = origId + suffix
}
if generationDebugVerbose {
fmt.Printf("new generation %s\n", id)
}
knownGenerations[id] = newSpan
return id
}
func endGeneration(id string) {
knownGenerationLock.Lock()
defer knownGenerationLock.Unlock()
span, exists := knownGenerations[id]
if !exists {
oops := fmt.Sprintf("ending generation %s: unknown", id)
panic(oops)
}
if span.finalized != timeZero || span.to != timeZero {
panic(span.reportHistory("ending generation", id))
}
span.to = time.Now()
knownGenerations[id] = span
}
// cancelGeneration marks the generation as finalized. In principle it's
// only used in cases where we just started a generation but something
// went wrong. it's not fancier than this because of the weird cases
// where the same generation shows up again, such as when closing and
// reopening an index so we don't know about previous instances of the
// same files.
func cancelGeneration(id string) {
knownGenerationLock.Lock()
defer knownGenerationLock.Unlock()
span, exists := knownGenerations[id]
if exists {
span.finalized = time.Now()
span.to = span.finalized
knownGenerations[id] = span
}
}
func finalizeGeneration(id string) {
knownGenerationLock.Lock()
defer knownGenerationLock.Unlock()
span, exists := knownGenerations[id]
if !exists {
oops := fmt.Sprintf("finalizing generation %s: unknown", id)
panic(oops)
}
if span.finalized != timeZero {
panic(span.reportHistory("finalizing", id))
}
span.finalized = time.Now()
knownGenerations[id] = span
}
func reportGenerations() (stats string, surviving []string) {
runtime.GC()
knownGenerationLock.Lock()
defer knownGenerationLock.Unlock()
times := make([]int64, 0, len(knownGenerations))
for id, span := range knownGenerations {
if span.to == timeZero || span.finalized == timeZero {
surviving = append(surviving, span.reportHistory("surviving", id))
} else {
times = append(times, int64(span.finalized.Sub(span.to)))
}
}
stats = "no recorded finalized spans"
if len(times) > 0 {
sort.Slice(times, func(i, j int) bool { return times[i] < times[j] })
var total int64
for _, d := range times {
total += d
}
var mean, median, p90, p99, worst int64
mean = total / int64(len(times))
median = times[len(times)/2]
p90 = times[(len(times)*9)/10]
p99 = times[(len(times)*99)/100]
worst = times[len(times)-1]
stats = fmt.Sprintf("%d finalized spans. lag: mean %v, median %v, p90 %v, p99 %v, worst %v",
len(times), time.Duration(mean), time.Duration(median), time.Duration(p90), time.Duration(p99), time.Duration(worst))
}
return stats, surviving
}

37
generation_nodebug.go Normal file
View file

@ -0,0 +1,37 @@
// Copyright 2019 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build !generationdebug
package pilosa
const generationDebug = false
func registerGeneration(id string) string {
return id
}
func endGeneration(id string) {
}
func cancelGeneration(id string) {
}
func finalizeGeneration(id string) {
}
//lint:ignore U1000 this is conditional on a build flag, see generation_test.go.
func reportGenerations() []string { //nolint:unused,deadcode
return nil
}

70
generation_test.go Normal file
View file

@ -0,0 +1,70 @@
// Copyright 2020 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// +build generationparanoia
package pilosa
import (
"runtime"
"testing"
"unsafe"
)
func TestGenerationPanic(t *testing.T) {
f := mustOpenFragment("i", "f", viewStandard, 0, "none")
defer f.Clean(t)
for i := 0; i < f.MaxOpN; i++ {
_, _ = f.setBit(0, uint64(i*32))
}
// force snapshot so we get a mmapped row...
_ = f.Snapshot()
_ = f.row(0)
var prevData []byte
if f.gen.(*mmapGeneration).data == nil {
t.Fatalf("generation code didn't create a mapping, apparently?")
}
prevData = f.gen.(*mmapGeneration).data
f.mu.Lock()
_ = defaultSnapshotQueue.Immediate(f)
f.mu.Unlock()
runtime.GC()
for i := 0; i < (f.MaxOpN / 2); i++ {
_, _ = f.setBit(0, uint64(i*32)+23)
}
f.mu.Lock()
defaultSnapshotQueue.Await(f)
f.mu.Unlock()
runtime.GC()
newData := f.gen.(*mmapGeneration).data
if unsafe.Pointer(&prevData[0]) == unsafe.Pointer(&newData[0]) {
t.Fatalf("test can't run usefully, didn't get new data pointer")
}
var wp *io.Writer
if f.storage != nil {
wp = &f.storage.OpWriter
}
err := f.gen.Transaction(wp, func() error {
prevData[0] = 0x3c
return nil
})
if err == nil {
t.Fatalf("expected a panic to get caught, but nothing happened")
}
if err.Error() != "invalid memory access during transaction" {
t.Fatalf("expected \"invalid memory access during transaction\", got %q", err.Error())
}
}

51
generator/slice.go Normal file
View file

@ -0,0 +1,51 @@
// Copyright 2020 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package generator
import (
"math/rand"
"sort"
)
// Uint64Slice generates between [0, n) random uint64 numbers between min and max.
func Uint64Slice(n int, min, max uint64, sorted bool, rand *rand.Rand) []uint64 {
a := make([]uint64, rand.Intn(n))
for i := range a {
a[i] = min + uint64(rand.Int63n(int64(max-min)))
}
if sorted {
sort.Sort(uint64Slice(a))
}
return a
}
// Uint64SetSlice returns the values in a uint64 set.
func Uint64SetSlice(m map[uint64]struct{}) []uint64 {
a := make([]uint64, 0, len(m))
for v := range m {
a = append(a, v)
}
sort.Sort(uint64Slice(a))
return a
}
// uint64Slice represents a sortable slice of uint64 numbers.
type uint64Slice []uint64
func (u uint64Slice) Swap(i, j int) { u[i], u[j] = u[j], u[i] }
func (u uint64Slice) Len() int { return len(u) }
func (u uint64Slice) Less(i, j int) bool { return u[i] < u[j] }

164
gid.go Normal file
View file

@ -0,0 +1,164 @@
// Copyright (c) 2014 The Go Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package pilosa
import (
"bytes"
"errors"
"fmt"
"runtime"
"strconv"
"sync"
)
// Sourced https://github.com/bradfitz/http2/blob/dc0c5c000ec33e263612939744d51a3b68b9cece/gotrack.go
var goroutineSpace = []byte("goroutine ")
var littleBuf = sync.Pool{
New: func() interface{} {
buf := make([]byte, 64)
return &buf
},
}
var _ = curGID // happy linter
func curGID() uint64 {
if true {
return 0 // avoid doing too much work during production profiling.
}
bp := littleBuf.Get().(*[]byte)
defer littleBuf.Put(bp)
b := *bp
b = b[:runtime.Stack(b, false)]
// Parse the 4707 out of "goroutine 4707 ["
b = bytes.TrimPrefix(b, goroutineSpace)
i := bytes.IndexByte(b, ' ')
if i < 0 {
panic(fmt.Sprintf("No space found in %q", b))
}
b = b[:i]
n, err := parseUintBytes(b, 10, 64)
if err != nil {
panic(fmt.Sprintf("Failed to parse goroutine ID out of %q: %v", b, err))
}
return n
}
// parseUintBytes is like strconv.ParseUint, but using a []byte.
func parseUintBytes(s []byte, base int, bitSize int) (n uint64, err error) {
var cutoff, maxVal uint64
if bitSize == 0 {
bitSize = int(strconv.IntSize)
}
s0 := s
switch {
case len(s) < 1:
err = strconv.ErrSyntax
return n, &strconv.NumError{Func: "ParseUint", Num: string(s0), Err: err}
case 2 <= base && base <= 36:
// valid base; nothing to do
case base == 0:
// Look for octal, hex prefix.
switch {
case s[0] == '0' && len(s) > 1 && (s[1] == 'x' || s[1] == 'X'):
base = 16
s = s[2:]
if len(s) < 1 {
err = strconv.ErrSyntax
return n, &strconv.NumError{Func: "ParseUint", Num: string(s0), Err: err}
}
case s[0] == '0':
base = 8
default:
base = 10
}
default:
err = errors.New("invalid base " + strconv.Itoa(base))
return n, &strconv.NumError{Func: "ParseUint", Num: string(s0), Err: err}
}
n = 0
cutoff = cutoff64(base)
maxVal = 1<<uint(bitSize) - 1
for i := 0; i < len(s); i++ {
var v byte
d := s[i]
switch {
case '0' <= d && d <= '9':
v = d - '0'
case 'a' <= d && d <= 'z':
v = d - 'a' + 10
case 'A' <= d && d <= 'Z':
v = d - 'A' + 10
default:
n = 0
err = strconv.ErrSyntax
return n, &strconv.NumError{Func: "ParseUint", Num: string(s0), Err: err}
}
if int(v) >= base {
n = 0
err = strconv.ErrSyntax
return n, &strconv.NumError{Func: "ParseUint", Num: string(s0), Err: err}
}
if n >= cutoff {
// n*base overflows
n = 1<<64 - 1
err = strconv.ErrRange
return n, &strconv.NumError{Func: "ParseUint", Num: string(s0), Err: err}
}
n *= uint64(base)
n1 := n + uint64(v)
if n1 < n || n1 > maxVal {
// n+v overflows
n = 1<<64 - 1
err = strconv.ErrRange
return n, &strconv.NumError{Func: "ParseUint", Num: string(s0), Err: err}
}
n = n1
}
return n, nil
}
// Return the first number n such that n*base >= 1<<64.
func cutoff64(base int) uint64 {
if base < 2 {
return 0
}
return (1<<64-1)/uint64(base) + 1
}

42
go.mod
View file

@ -3,45 +3,51 @@ module github.com/pilosa/pilosa/v2
replace github.com/hashicorp/memberlist => github.com/pilosa/memberlist v0.1.4-0.20190415211605-f6512523c021 replace github.com/hashicorp/memberlist => github.com/pilosa/memberlist v0.1.4-0.20190415211605-f6512523c021
require ( require (
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/CAFxX/gcnotifier v0.0.0-20190112062741-224a280d589d github.com/CAFxX/gcnotifier v0.0.0-20190112062741-224a280d589d
github.com/DataDog/datadog-go v0.0.0-20180822151419-281ae9f2d895 github.com/DataDog/datadog-go v0.0.0-20180822151419-281ae9f2d895
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
github.com/benbjohnson/immutable v0.2.0
github.com/boltdb/bolt v1.3.1 github.com/boltdb/bolt v1.3.1
github.com/cespare/xxhash v1.1.0 github.com/cespare/xxhash v1.1.0
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd // indirect github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd // indirect
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/dgraph-io/badger/v2 v2.0.1-rc1.0.20200709123515-8e896a7af361
github.com/glycerine/lmdb-go v1.9.32
github.com/go-ole/go-ole v1.2.4 // indirect github.com/go-ole/go-ole v1.2.4 // indirect
github.com/gogo/protobuf v1.2.0 github.com/gogo/protobuf v1.2.1
github.com/golang/protobuf v1.3.1 github.com/golang/protobuf v1.3.3
github.com/google/go-cmp v0.2.0 github.com/google/go-cmp v0.4.0
github.com/gorilla/handlers v1.3.0 github.com/gorilla/handlers v1.3.0
github.com/gorilla/mux v1.7.0 github.com/gorilla/mux v1.7.0
github.com/hashicorp/memberlist v0.1.3 github.com/hashicorp/memberlist v0.1.3
github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/lib/pq v1.8.0
github.com/opentracing/opentracing-go v1.1.0 github.com/opentracing/opentracing-go v1.1.0
github.com/pelletier/go-toml v1.2.0 github.com/pelletier/go-toml v1.2.0
github.com/pkg/errors v0.8.1 github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v0.9.3 github.com/prometheus/client_golang v1.0.0
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 github.com/prometheus/client_model v0.1.0
github.com/prometheus/prom2json v1.3.0
github.com/rakyll/statik v0.1.7
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237 // indirect github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237 // indirect
github.com/satori/go.uuid v1.2.0 github.com/satori/go.uuid v1.2.0
github.com/shirou/gopsutil v2.18.12+incompatible github.com/shirou/gopsutil v2.18.12+incompatible
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect
github.com/spf13/cobra v0.0.3 github.com/spf13/cobra v1.0.0
github.com/spf13/pflag v1.0.3 github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.3.1 github.com/spf13/viper v1.4.0
github.com/uber-go/atomic v1.4.0 // indirect github.com/uber-go/atomic v1.4.0 // indirect
github.com/uber/jaeger-client-go v2.16.0+incompatible github.com/uber/jaeger-client-go v2.16.0+incompatible
github.com/uber/jaeger-lib v2.2.0+incompatible // indirect github.com/uber/jaeger-lib v2.2.0+incompatible // indirect
go.uber.org/atomic v1.4.0 // indirect github.com/zeebo/blake3 v0.0.4
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 // indirect golang.org/x/mod v0.3.0
golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6 // indirect golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect
golang.org/x/sync v0.0.0-20190423024810-112230192c58 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872 // indirect golang.org/x/text v0.3.3 // indirect
golang.org/x/text v0.3.2 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/grpc v1.28.0
modernc.org/mathutil v1.0.0 modernc.org/mathutil v1.0.0
modernc.org/strutil v1.0.0 modernc.org/strutil v1.0.0
vitess.io/vitess v3.0.0-rc.3.0.20190602171040-12bfde34629c+incompatible
) )
go 1.11 go 1.13

214
go.sum
View file

@ -1,56 +1,113 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/CAFxX/gcnotifier v0.0.0-20190112062741-224a280d589d h1:n0G4ckjMEj7bWuGYUX0i8YlBeBBJuZ+HEHvHfyBDZtI= github.com/CAFxX/gcnotifier v0.0.0-20190112062741-224a280d589d h1:n0G4ckjMEj7bWuGYUX0i8YlBeBBJuZ+HEHvHfyBDZtI=
github.com/CAFxX/gcnotifier v0.0.0-20190112062741-224a280d589d/go.mod h1:Rn2zM2MnHze07LwkneP48TWt6UiZhzQTwCvw6djVGfE= github.com/CAFxX/gcnotifier v0.0.0-20190112062741-224a280d589d/go.mod h1:Rn2zM2MnHze07LwkneP48TWt6UiZhzQTwCvw6djVGfE=
github.com/DataDog/datadog-go v0.0.0-20180822151419-281ae9f2d895 h1:dmc/C8bpE5VkQn65PNbbyACDC8xw8Hpp/NEurdPmQDQ= github.com/DataDog/datadog-go v0.0.0-20180822151419-281ae9f2d895 h1:dmc/C8bpE5VkQn65PNbbyACDC8xw8Hpp/NEurdPmQDQ=
github.com/DataDog/datadog-go v0.0.0-20180822151419-281ae9f2d895/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/datadog-go v0.0.0-20180822151419-281ae9f2d895/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/benbjohnson/immutable v0.2.0 h1:t0rW3lNFwfQ85IDO1mhMbumxdVSti4nnVaal4r45Oio=
github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/badger/v2 v2.0.1-rc1.0.20200709123515-8e896a7af361 h1:JBNM90aGLCiF9iJYvpvayMpYeW498v5ZDZqE2chqZ2A=
github.com/dgraph-io/badger/v2 v2.0.1-rc1.0.20200709123515-8e896a7af361/go.mod h1:26P/7fbL4kUZVEVKLAKXkBXKOydDmM2p1e+NhhnBCAE=
github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de h1:t0UHb5vdojIDUqktM6+xJAfScFBsVpXZmqC9dsgJmeA=
github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31 h1:gclg6gY70GLy3PbkQ1AERPfmLMMagS60DKF78eWwLn8=
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
github.com/glycerine/idem v0.0.0-20190127113923-7a8083893311 h1:AAXH0ZvYIHHqU06ASy0H2tYAkAGrQlZvEy2QZrrtt4E=
github.com/glycerine/idem v0.0.0-20190127113923-7a8083893311/go.mod h1:B72P/ZM99sNiCmaQJflpmMAF5LsDzStpLdWzn0+Vr2Y=
github.com/glycerine/lmdb-go v1.9.32 h1:thLnzCykFcmn2rACYnwpR4ovYauLNKaAuk+xj7YMbS0=
github.com/glycerine/lmdb-go v1.9.32/go.mod h1:DrPeeTGooMg6B7cjNSP14perptTJzzdBy5YoosthrRs=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI= github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/handlers v1.3.0 h1:tsg9qP3mjt1h4Roxp+M1paRjrVBfPSOpBuVclh6YluI= github.com/gorilla/handlers v1.3.0 h1:tsg9qP3mjt1h4Roxp+M1paRjrVBfPSOpBuVclh6YluI=
github.com/gorilla/handlers v1.3.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/handlers v1.3.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U= github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U=
github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
@ -69,17 +126,34 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA= github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
@ -93,23 +167,40 @@ github.com/pilosa/memberlist v0.1.4-0.20190415211605-f6512523c021/go.mod h1:ajVT
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.1.0 h1:ElTg5tNp4DqfV7UQjDqv2+RJlNzsDtvNAWccbItceIE=
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY=
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/prom2json v1.3.0 h1:BlqrtbT9lLH3ZsOVhXPsHzFrApCTKRifB7gjJuypu6Y=
github.com/prometheus/prom2json v1.3.0/go.mod h1:rMN7m0ApCowcoDlypBHlkNbp5eJQf/+1isKykIP5ZnM=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ=
github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc=
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237 h1:HQagqIiBmr8YXawX/le3+O26N+vPPC1PtjaF3mwnook= github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237 h1:HQagqIiBmr8YXawX/le3+O26N+vPPC1PtjaF3mwnook=
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
@ -118,75 +209,160 @@ github.com/shirou/gopsutil v2.18.12+incompatible h1:1eaJvGomDnH74/5cF4CTmTbLHAri
github.com/shirou/gopsutil v2.18.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/gopsutil v2.18.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U=
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.1 h1:5+8j8FTpnFV4nEImW/ofkzEt8VoOiLXxdYIDsB73T38= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/uber-go/atomic v1.4.0 h1:yOuPqEq4ovnhEjpHmfFwsqBXDYbQeT6Nb0bwD6XnD5o= github.com/uber-go/atomic v1.4.0 h1:yOuPqEq4ovnhEjpHmfFwsqBXDYbQeT6Nb0bwD6XnD5o=
github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
github.com/uber/jaeger-client-go v2.16.0+incompatible h1:Q2Pp6v3QYiocMxomCaJuwQGFt7E53bPYqEgug/AoBtY= github.com/uber/jaeger-client-go v2.16.0+incompatible h1:Q2Pp6v3QYiocMxomCaJuwQGFt7E53bPYqEgug/AoBtY=
github.com/uber/jaeger-client-go v2.16.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-client-go v2.16.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-lib v2.2.0+incompatible h1:MxZXOiR2JuoANZ3J6DE/U0kSFv/eJ/GfSYVCjK7dyaw= github.com/uber/jaeger-lib v2.2.0+incompatible h1:MxZXOiR2JuoANZ3J6DE/U0kSFv/eJ/GfSYVCjK7dyaw=
github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/zeebo/assert v0.0.0-20181109011804-10f827ce2ed6/go.mod h1:yssERNPivllc1yU3BvpjYI5BUW+zglcz6QWqeVRL5t0=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.0.4 h1:vtZ4X8B2lKXZFg2Xyg6Wo36mvmnJvc2VQYTtA4RDCkI=
github.com/zeebo/blake3 v0.0.4/go.mod h1:YOZo8A49yNqM0X/Y+JmDUZshJWLt1laHsNSn5ny2i34=
github.com/zeebo/pcg v0.0.0-20181207190024-3cdc6b625a05 h1:4pW5fMvVkrgkMXdvIsVRRTs69DWYA8uNNQsu1stfVKU=
github.com/zeebo/pcg v0.0.0-20181207190024-3cdc6b625a05/go.mod h1:Gr+78ptB0MwXxm//LBaEvBiaXY7hXJ6KGe2V32X2F6E=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519 h1:x6rhz8Y9CjbgQkccRGmELH6K+LJj7tOoh3XWeC1yaQM= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519 h1:x6rhz8Y9CjbgQkccRGmELH6K+LJj7tOoh3XWeC1yaQM=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6 h1:FP8hkuE6yUEaJnK7O2eTuejKWwW+Rhfj80dQ2JcKxCU= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a h1:1n5lsVfiQW3yfsRGu98756EH1YthsFqr/5mxHduZW2A=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872 h1:cGjJzUd8RgBw428LXP65YXni0aiGNA4Bl+ls8SmLOm8= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.28.0 h1:bO/TA4OxCOummhSf10siHuG7vJOiwh7SpRpFZDkOgl4=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
modernc.org/mathutil v1.0.0 h1:93vKjrJopTPrtTNpZ8XIovER7iCIH1QU7wNbOQXC60I= modernc.org/mathutil v1.0.0 h1:93vKjrJopTPrtTNpZ8XIovER7iCIH1QU7wNbOQXC60I=
modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
modernc.org/strutil v1.0.0 h1:XVFtQwFVwc02Wk+0L/Z/zDDXO81r5Lhe6iMKmGX3KhE= modernc.org/strutil v1.0.0 h1:XVFtQwFVwc02Wk+0L/Z/zDDXO81r5Lhe6iMKmGX3KhE=
modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
vitess.io/vitess v3.0.0-rc.3.0.20190602171040-12bfde34629c+incompatible h1:GWnLrAdetgJM0Co5bwwczO49iFZBSInpyGAT77BP9Y0=
vitess.io/vitess v3.0.0-rc.3.0.20190602171040-12bfde34629c+incompatible/go.mod h1:h4qvkyNYTOC0xI+vcidSWoka0gQAZc9ZPHbkHo48gP0=

View file

@ -15,6 +15,7 @@
package gopsutil package gopsutil
import ( import (
"math"
"runtime" "runtime"
"strings" "strings"
@ -114,6 +115,13 @@ func (s *systemInfo) collectPlatformInfo() error {
} }
s.cpuModel = infos[0].ModelName s.cpuModel = infos[0].ModelName
s.cpuMHz = computeMHz(s.cpuModel) s.cpuMHz = computeMHz(s.cpuModel)
if s.cpuMHz < 0 {
s.cpuMHz = int(math.Round(infos[0].Mhz))
}
if s.cpuMHz < 0 {
// This is supposed to be unsigned.
s.cpuMHz = 0
}
// gopsutil reports core and clock speed info inconsistently // gopsutil reports core and clock speed info inconsistently
// by OS // by OS

View file

@ -104,6 +104,7 @@ func (g *memberSet) Open() (err error) {
// Close attempts to gracefully leave the cluster, and finally calls shutdown // Close attempts to gracefully leave the cluster, and finally calls shutdown
// after (at most) a timeout period. // after (at most) a timeout period.
func (g *memberSet) Close() error { func (g *memberSet) Close() error {
g.eventReceiver.Close()
leaveErr := g.memberlist.Leave(5 * time.Second) leaveErr := g.memberlist.Leave(5 * time.Second)
shutdownErr := g.memberlist.Shutdown() shutdownErr := g.memberlist.Shutdown()
if leaveErr != nil || shutdownErr != nil { if leaveErr != nil || shutdownErr != nil {
@ -324,16 +325,20 @@ func (g *memberSet) LocalState(join bool) []byte {
Schema: &pilosa.Schema{Indexes: g.papi.Schema(context.Background())}, Schema: &pilosa.Schema{Indexes: g.papi.Schema(context.Background())},
} }
for _, idx := range m.Schema.Indexes { for _, idx := range m.Schema.Indexes {
is := &pilosa.IndexStatus{Name: idx.Name} is := &pilosa.IndexStatus{Name: idx.Name, CreatedAt: idx.CreatedAt}
for _, f := range idx.Fields { for _, f := range idx.Fields {
availableShards := roaring.NewBitmap() availableShards := roaring.NewBitmap()
if field, _ := g.papi.Field(context.Background(), idx.Name, f.Name); field != nil { if field, _ := g.papi.Field(context.Background(), idx.Name, f.Name); field != nil {
availableShards = field.AvailableShards() availableShards = field.AvailableShards(false)
} }
is.Fields = append(is.Fields, &pilosa.FieldStatus{
fs := &pilosa.FieldStatus{
Name: f.Name, Name: f.Name,
CreatedAt: f.CreatedAt,
AvailableShards: availableShards, AvailableShards: availableShards,
}) }
is.Fields = append(is.Fields, fs)
} }
m.Indexes = append(m.Indexes, is) m.Indexes = append(m.Indexes, is)
} }
@ -362,8 +367,9 @@ func (g *memberSet) MergeRemoteState(buf []byte, join bool) {
// Care must be taken that events are processed in a timely manner from // Care must be taken that events are processed in a timely manner from
// the channel, since this delegate will block until an event can be sent. // the channel, since this delegate will block until an event can be sent.
type eventReceiver struct { type eventReceiver struct {
ch chan memberlist.NodeEvent ch chan memberlist.NodeEvent
papi *pilosa.API closed chan struct{}
papi *pilosa.API
logger logger.Logger logger logger.Logger
} }
@ -372,6 +378,7 @@ type eventReceiver struct {
func newEventReceiver(logger logger.Logger, papi *pilosa.API) *eventReceiver { func newEventReceiver(logger logger.Logger, papi *pilosa.API) *eventReceiver {
ger := &eventReceiver{ ger := &eventReceiver{
ch: make(chan memberlist.NodeEvent, 1), ch: make(chan memberlist.NodeEvent, 1),
closed: make(chan struct{}),
logger: logger, logger: logger,
papi: papi, papi: papi,
} }
@ -385,7 +392,10 @@ func (g *eventReceiver) NotifyJoin(n *memberlist.Node) {
n2.Meta = make([]byte, len(n.Meta)) n2.Meta = make([]byte, len(n.Meta))
copy(n2.Meta, n.Meta) copy(n2.Meta, n.Meta)
g.ch <- memberlist.NodeEvent{Event: memberlist.NodeJoin, Node: &n2} select {
case g.ch <- memberlist.NodeEvent{Event: memberlist.NodeJoin, Node: &n2}:
case <-g.closed:
}
} }
func (g *eventReceiver) NotifyLeave(n *memberlist.Node) { func (g *eventReceiver) NotifyLeave(n *memberlist.Node) {
@ -394,7 +404,10 @@ func (g *eventReceiver) NotifyLeave(n *memberlist.Node) {
n2.Meta = make([]byte, len(n.Meta)) n2.Meta = make([]byte, len(n.Meta))
copy(n2.Meta, n.Meta) copy(n2.Meta, n.Meta)
g.ch <- memberlist.NodeEvent{Event: memberlist.NodeLeave, Node: &n2} select {
case g.ch <- memberlist.NodeEvent{Event: memberlist.NodeLeave, Node: &n2}:
case <-g.closed:
}
} }
func (g *eventReceiver) NotifyUpdate(n *memberlist.Node) { func (g *eventReceiver) NotifyUpdate(n *memberlist.Node) {
@ -403,13 +416,25 @@ func (g *eventReceiver) NotifyUpdate(n *memberlist.Node) {
n2.Meta = make([]byte, len(n.Meta)) n2.Meta = make([]byte, len(n.Meta))
copy(n2.Meta, n.Meta) copy(n2.Meta, n.Meta)
g.ch <- memberlist.NodeEvent{Event: memberlist.NodeUpdate, Node: &n2} select {
case g.ch <- memberlist.NodeEvent{Event: memberlist.NodeUpdate, Node: &n2}:
case <-g.closed:
}
}
func (g *eventReceiver) Close() {
close(g.closed)
} }
func (g *eventReceiver) listen() { func (g *eventReceiver) listen() {
var nodeEventType pilosa.NodeEventType var nodeEventType pilosa.NodeEventType
for { for {
e := <-g.ch var e memberlist.NodeEvent
select {
case <-g.closed:
return
case e = <-g.ch:
}
switch e.Event { switch e.Event {
case memberlist.NodeJoin: case memberlist.NodeJoin:
nodeEventType = pilosa.NodeJoin nodeEventType = pilosa.NodeJoin

View file

@ -16,6 +16,9 @@ package pilosa
import ( import (
"encoding/json" "encoding/json"
"github.com/pilosa/pilosa/v2/tracing"
"github.com/pkg/errors"
) )
// QueryRequest represent a request to process a query. // QueryRequest represent a request to process a query.
@ -42,12 +45,21 @@ type QueryRequest struct {
// If true, indicates that query is part of a larger distributed query. // If true, indicates that query is part of a larger distributed query.
// If false, this request is on the originating node. // If false, this request is on the originating node.
Remote bool Remote bool
// Should we profile this query?
Profile bool
// Additional data associated with the query, in cases where there's
// row-style inputs for precomputed values.
EmbeddedData []*Row
} }
// QueryResponse represent a response from a processed query. // QueryResponse represent a response from a processed query.
type QueryResponse struct { type QueryResponse struct {
// Result for each top-level query call. // Result for each top-level query call.
// Can be a Bitmap, Pairs, or uint64. // The result type differs depending on the query; types
// include: Row, RowIdentifiers, GroupCounts, SignedRow,
// ValCount, Pair, Pairs, bool, uint64.
Results []interface{} Results []interface{}
// Set of column attribute objects matching IDs returned in Result. // Set of column attribute objects matching IDs returned in Result.
@ -55,6 +67,9 @@ type QueryResponse struct {
// Error during parsing or execution. // Error during parsing or execution.
Err error Err error
// Profiling data, if any
Profile *tracing.Profile
} }
// MarshalJSON marshals QueryResponse into a JSON-encoded byte slice // MarshalJSON marshals QueryResponse into a JSON-encoded byte slice
@ -68,9 +83,11 @@ func (resp *QueryResponse) MarshalJSON() ([]byte, error) {
return json.Marshal(struct { return json.Marshal(struct {
Results []interface{} `json:"results"` Results []interface{} `json:"results"`
ColumnAttrSets []*ColumnAttrSet `json:"columnAttrs,omitempty"` ColumnAttrSets []*ColumnAttrSet `json:"columnAttrs,omitempty"`
Profile *tracing.Profile `json:"profile,omitempty"`
}{ }{
Results: resp.Results, Results: resp.Results,
ColumnAttrSets: resp.ColumnAttrSets, ColumnAttrSets: resp.ColumnAttrSets,
Profile: resp.Profile,
}) })
} }
@ -96,33 +113,146 @@ var NopHandler Handler = nopHandler{}
// ImportValueRequest describes the import request structure // ImportValueRequest describes the import request structure
// for a value (BSI) import. // for a value (BSI) import.
// Note: no RowIDs here. have to convert BSI Values into RowIDs internally.
type ImportValueRequest struct { type ImportValueRequest struct {
Index string Index string
Field string IndexCreatedAt int64
Shard uint64 Field string
ColumnIDs []uint64 FieldCreatedAt int64
ColumnKeys []string // if Shard is MaxUint64 (an impossible shard value), this
Values []int64 // indicates that the column IDs may come from multiple shards.
Shard uint64
ColumnIDs []uint64 // e.g. weather stationID
ColumnKeys []string
Values []int64 // e.g. temperature, humidity, barometric pressure
FloatValues []float64
StringValues []string
Clear bool
}
// AtomicRecord applies all its Ivr and Ivr atomically, in a Tx.
// The top level Shard has to agree with Ivr[i].Shard and the Iv[i].Shard
// for all i included (in Ivr and Ir). The same goes for the top level Index: all records
// have to be writes to the same Index. These requirements are checked.
//
type AtomicRecord struct {
Index string
Shard uint64
Ivr []*ImportValueRequest // BSI values
Ir []*ImportRequest // other field types, e.g. single bit
}
func (ivr *ImportValueRequest) Len() int { return len(ivr.ColumnIDs) }
func (ivr *ImportValueRequest) Less(i, j int) bool { return ivr.ColumnIDs[i] < ivr.ColumnIDs[j] }
func (ivr *ImportValueRequest) Swap(i, j int) {
ivr.ColumnIDs[i], ivr.ColumnIDs[j] = ivr.ColumnIDs[j], ivr.ColumnIDs[i]
if len(ivr.Values) > 0 {
ivr.Values[i], ivr.Values[j] = ivr.Values[j], ivr.Values[i]
} else if len(ivr.FloatValues) > 0 {
ivr.FloatValues[i], ivr.FloatValues[j] = ivr.FloatValues[j], ivr.FloatValues[i]
} else if len(ivr.StringValues) > 0 {
ivr.StringValues[i], ivr.StringValues[j] = ivr.StringValues[j], ivr.StringValues[i]
}
}
// Validate ensures that the payload of the request is valid.
func (ivr *ImportValueRequest) Validate() error {
return ivr.ValidateWithTimestamp(ivr.IndexCreatedAt, ivr.FieldCreatedAt)
}
// ValidateWithTimestamp ensures that the payload of the request is valid.
func (ivr *ImportValueRequest) ValidateWithTimestamp(indexCreatedAt, fieldCreatedAt int64) error {
if ivr.Index == "" || ivr.Field == "" {
return errors.Errorf("index and field required, but got '%s' and '%s'", ivr.Index, ivr.Field)
}
if len(ivr.ColumnIDs) != 0 && len(ivr.ColumnKeys) != 0 {
return errors.Errorf("must pass either column ids or keys, but not both")
}
var valueSetCount int
if len(ivr.Values) != 0 {
valueSetCount++
}
if len(ivr.FloatValues) != 0 {
valueSetCount++
}
if len(ivr.StringValues) != 0 {
valueSetCount++
}
if valueSetCount > 1 {
return errors.Errorf("must pass ints, floats, or strings but not multiple")
}
if ivr.IndexCreatedAt != 0 && ivr.FieldCreatedAt != 0 {
if ivr.IndexCreatedAt != indexCreatedAt || ivr.FieldCreatedAt != fieldCreatedAt {
return ErrPreconditionFailed
}
}
return nil
}
// ImportColumnAttrsRequest describes the import request structure
// for a ColumnAttr import.
type ImportColumnAttrsRequest struct {
AttrKey string
ColumnIDs []uint64
AttrVals []string
Shard int64
Index string
IndexCreatedAt int64
} }
// ImportRequest describes the import request structure // ImportRequest describes the import request structure
// for an import. // for an import. BSIs use the ImportValueRequest instead.
type ImportRequest struct { type ImportRequest struct {
Index string Index string
Field string IndexCreatedAt int64
Shard uint64 Field string
RowIDs []uint64 FieldCreatedAt int64
ColumnIDs []uint64 Shard uint64
RowKeys []string RowIDs []uint64
ColumnKeys []string ColumnIDs []uint64
Timestamps []int64 RowKeys []string
ColumnKeys []string
Timestamps []int64
Clear bool
} }
// ValidateWithTimestamp ensures that the payload of the request is valid.
func (ir *ImportRequest) ValidateWithTimestamp(indexCreatedAt, fieldCreatedAt int64) error {
if ir.IndexCreatedAt != 0 && ir.FieldCreatedAt != 0 {
if ir.IndexCreatedAt != indexCreatedAt || ir.FieldCreatedAt != fieldCreatedAt {
return ErrPreconditionFailed
}
}
return nil
}
const (
RequestActionSet = "set"
RequestActionClear = "clear"
RequestActionOverwrite = "overwrite"
)
// ImportRoaringRequest describes the import request structure // ImportRoaringRequest describes the import request structure
// for an import containing roaring-encoded data. // for an import containing roaring-encoded data.
type ImportRoaringRequest struct { type ImportRoaringRequest struct {
Clear bool IndexCreatedAt int64
Views map[string][]byte FieldCreatedAt int64
Clear bool
Action string // [set, clear, overwrite]
Block int
Views map[string][]byte
Direct bool
}
// ValidateWithTimestamp ensures that the payload of the request is valid.
func (irr *ImportRoaringRequest) ValidateWithTimestamp(indexCreatedAt, fieldCreatedAt int64) error {
if irr.IndexCreatedAt != 0 && irr.FieldCreatedAt != 0 {
if irr.IndexCreatedAt != indexCreatedAt || irr.FieldCreatedAt != fieldCreatedAt {
return ErrPreconditionFailed
}
}
return nil
} }
// ImportResponse is the structured response of an import. // ImportResponse is the structured response of an import.
@ -153,6 +283,9 @@ type TranslateKeysRequest struct {
Index string Index string
Field string Field string
Keys []string Keys []string
// it's a awkward name, just to keep backward compatibility with go-pilosa and idk.
NotWritable bool
} }
// TranslateKeysResponse is the structured response of a key // TranslateKeysResponse is the structured response of a key
@ -160,3 +293,45 @@ type TranslateKeysRequest struct {
type TranslateKeysResponse struct { type TranslateKeysResponse struct {
IDs []uint64 IDs []uint64
} }
// TranslateIDsRequest describes the structure of a request
// for a batch of id translations.
type TranslateIDsRequest struct {
Index string
Field string
IDs []uint64
}
// TranslateIDsResponse is the structured response of a id
// translation request.
type TranslateIDsResponse struct {
Keys []string
}
// InspectRequestParams represents the parts of an InspectRequest that
// aren't generic holder filtering attributes.
type InspectRequestParams struct {
Containers bool // include container details
Checksum bool // perform checksums
}
// InspectRequest represents a request for a possibly-partial
// holder inspection, using a provided holder filter and inspect-specific
// parameters.
type InspectRequest struct {
HolderFilterParams
InspectRequestParams
}
// InspectResponse contains the structured results for an InspectRequest.
// It may some day be expanded to include metadata about views or indexes.
type InspectResponse struct {
Fragments []struct {
Index string
Field string
View string
Shard int64
Path string
Info *FragmentInfo
}
}

107
hash/blake3.go Normal file
View file

@ -0,0 +1,107 @@
// Copyright 2020 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hash
import (
cryptorand "crypto/rand"
"encoding/binary"
"fmt"
"sync"
"github.com/zeebo/blake3"
"golang.org/x/mod/sumdb/dirhash"
)
// Blake3Hasher is a thread/goroutine safe way to
// obtain a blake3 cryptographic hash of input []byte.
// Reference https://github.com/BLAKE3-team/BLAKE3
// suggests it is 6x faster than BLAKE2B.
// The Go github.com/zeebo/blake3 version is
// AVX2 and SSE4.1 accelerated.
type Blake3Hasher struct {
hasher *blake3.Hasher
hasherMu sync.Mutex
}
// NewBlake3Hasher returns a new Blake3Hasher.
func NewBlake3Hasher() *Blake3Hasher {
return &Blake3Hasher{
hasher: blake3.New(),
}
}
// CryptoHash writes the blake3 cryptographic hash of
// input into buffer and returns it.
// Like the standard libary's hash.Hash interface's Sum() method,
// the buffer is re-used and overwritten
// to avoid allocation. The caller determines the byte length of
// the outputCryptohash by the size of the supplied buffer
// slice, and this will be exactly equal to the supplies bytes.
// In this way, shorter or longer hashes can be provided as
// needed.
func (w *Blake3Hasher) CryptoHash(input []byte, buffer []byte) (outputCryptohash []byte) {
w.hasherMu.Lock()
w.hasher.Reset()
// "Write implements part of the hash.Hash interface. It never returns an error."
// -- https://godoc.org/github.com/zeebo/blake3#Hasher.Write
_, _ = w.hasher.Write(input)
// Digest.Read reads data from the hasher into buffer.
// "It always fills the entire buffer and never errors."
// -- https://godoc.org/github.com/zeebo/blake3#Digest
_, _ = w.hasher.Digest().Read(buffer)
// no chance of panic, so avoid any defer cost.
w.hasherMu.Unlock()
return buffer
}
// Blake3sum16 might be slower because we allocate a new hasher every time, but
// it is more conenient for writing debug code. It returns
// a 16 byte hash as a hexidecimal string.
func Blake3sum16(input []byte) string {
hasher := blake3.New()
_, _ = hasher.Write(input)
var buf [16]byte
_, _ = hasher.Digest().Read(buf[0:])
return fmt.Sprintf("%x", buf)
}
// CryptoRandInt64 uses crypto/rand to get an random int64
func CryptoRandInt64() int64 {
c := 8
b := make([]byte, c)
_, err := cryptorand.Read(b)
if err != nil {
panic(err)
}
r := int64(binary.LittleEndian.Uint64(b))
return r
}
// HashOfDir returns the hash of the local file system directory dir
func HashOfDir(path string) string {
prefix := ""
h, err := dirhash.HashDir(path, prefix, dirhash.Hash1)
if err != nil {
panic(err)
}
return h
}

90
hash/blake3_test.go Normal file
View file

@ -0,0 +1,90 @@
// Copyright 2020 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hash
import (
"encoding/hex"
"fmt"
"io/ioutil"
"os"
"path"
"testing"
"github.com/pilosa/pilosa/v2/testhook"
)
func TestBlake3Hasher(t *testing.T) {
hasher := NewBlake3Hasher()
hash := make([]byte, 16)
input := []byte("hello world")
hash = hasher.CryptoHash(input, hash)
expected := "d74981efa70a0c880b8d8c1985d075db"
observed := hex.EncodeToString(hash)
if observed != expected {
panic(fmt.Sprintf("expected hash:'%v' but observed hash '%v'", expected, observed))
}
obs2 := Blake3sum16(input)
if obs2 != expected {
panic(fmt.Sprintf("expected hash:'%v' but observed hash from blake2sum16: '%v'", expected, obs2))
}
}
func TestCryptoRandInt64(t *testing.T) {
rnd := CryptoRandInt64()
if rnd == 0 {
panic("cryptoRandInt64() gave 0, very high odds it has broken")
}
}
func TestHashOfDir(t *testing.T) {
dir, err := testhook.TempDir(t, "TestHashOfDir-dir")
if err != nil {
t.Fatal(err)
}
b := path.Join(dir, "A", "B")
if err := os.MkdirAll(b, 0755); err != nil {
t.Fatal(err)
}
c := path.Join(dir, "A", "C")
if err := os.MkdirAll(c, 0755); err != nil {
t.Fatal(err)
}
bmessage := []byte("hello B\n")
if err := ioutil.WriteFile(path.Join(b, "b_content"), bmessage, 0644); err != nil {
t.Fatal(err)
}
cmessage := []byte("hello C\n")
if err := ioutil.WriteFile(path.Join(c, "c_content"), cmessage, 0644); err != nil {
t.Fatal(err)
}
hsh := HashOfDir(dir)
c2message := []byte("hello C2\n")
if err := ioutil.WriteFile(path.Join(c, "c_content"), c2message, 0644); err != nil {
t.Fatal(err)
}
hsh2 := HashOfDir(dir)
if hsh2 == hsh {
t.Fatal("HashOfDir did not detect 1 byte change")
}
}

1537
holder.go

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
// Copyright 2017 Pilosa Corp. // Copyright 2020 Pilosa Corp.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -15,309 +15,235 @@
package pilosa package pilosa
import ( import (
"io/ioutil" "context"
"fmt"
"os" "os"
"path/filepath"
"reflect"
"strings"
"testing" "testing"
"github.com/pilosa/pilosa/v2/roaring" "github.com/pilosa/pilosa/v2/testhook"
) )
type tHolder struct { var _ = fmt.Printf
*Holder
type testHolderOperator struct {
indexSeen, indexProcessed int
fieldSeen, fieldProcessed int
viewSeen, viewProcessed int
fragmentSeen, fragmentProcessed int
waitHere chan struct{}
} }
// Close closes the holder and removes all underlying data. func (t *testHolderOperator) CheckIndex(string) (bool, bool) {
func (h *tHolder) Close() error { t.indexSeen++
defer os.RemoveAll(h.Path) return true, true
return h.Holder.Close()
} }
// Reopen instantiates and opens a new holder. func (t *testHolderOperator) CheckField(string, string) (bool, bool) {
// Note that the holder must be Closed first. t.fieldSeen++
func (h *tHolder) Reopen() error { return true, true
path, logger := h.Path, h.Holder.Logger
h.Holder = NewHolder()
h.Holder.Path = path
h.Holder.Logger = logger
return h.Holder.Open()
} }
func newHolder() *tHolder { func (t *testHolderOperator) CheckView(string, string, string) (bool, bool) {
path, err := ioutil.TempDir(*TempDir, "pilosa-") t.viewSeen++
return true, true
}
func (t *testHolderOperator) CheckFragment(string, string, string, uint64) bool {
t.fragmentSeen++
return true
}
func (t *testHolderOperator) ProcessIndex(*Index) error {
t.indexProcessed++
return nil
}
func (t *testHolderOperator) ProcessField(*Field) error {
t.fieldProcessed++
return nil
}
func (t *testHolderOperator) ProcessView(*view) error {
t.viewProcessed++
return nil
}
func (t *testHolderOperator) ProcessFragment(*fragment) error {
if t.waitHere != nil {
<-t.waitHere
}
t.fragmentProcessed++
return nil
}
func makeHolder(tb testing.TB) (*Holder, string, error) {
path, err := testhook.TempDir(tb, "pilosa-")
if err != nil { if err != nil {
panic(err) return nil, "", err
}
h := NewHolder(path, nil)
return h, path, h.Open()
}
func testSetBit(t *testing.T, h *Holder, index, field string, rowID, columnID uint64) {
idx, err := h.CreateIndexIfNotExists(index, IndexOptions{})
if err != nil {
t.Fatalf("creating index: %v", err)
} }
h := &tHolder{Holder: NewHolder()} f, err := idx.CreateFieldIfNotExists(field, OptFieldTypeDefault())
h.Path = path
return h
}
// MustCreateFieldIfNotExists returns a given field. Panic on error.
func (h *tHolder) MustCreateFieldIfNotExists(index, field string) *Field {
f, err := h.MustCreateIndexIfNotExists(index, IndexOptions{}).CreateFieldIfNotExists(field, OptFieldTypeDefault())
if err != nil { if err != nil {
panic(err) t.Fatalf("setting bit: %v", err)
} }
return f _, err = f.SetBit(nil, rowID, columnID, nil)
}
// MustCreateIndexIfNotExists returns a given index. Panic on error.
func (h *tHolder) MustCreateIndexIfNotExists(index string, opt IndexOptions) *Index {
idx, err := h.Holder.CreateIndexIfNotExists(index, opt)
if err != nil { if err != nil {
panic(err) t.Fatalf("setting bit: %v", err)
}
return idx
}
// SetBit clears a bit on the given field.
func (h *tHolder) SetBit(index, field string, rowID, columnID uint64) {
f := h.MustCreateFieldIfNotExists(index, field)
_, err := f.SetBit(rowID, columnID, nil)
if err != nil {
panic(err)
} }
} }
// Row returns a Row for a given field. func testMustHaveBit(t *testing.T, h *Holder, index, field string, rowID, columnID uint64) {
func (h *tHolder) Row(index, field string, rowID uint64) *Row {
f := h.MustCreateFieldIfNotExists(index, field) //shard := columnID / ShardWidth
row, err := f.Row(rowID)
if err != nil { // hmm... if its a new holder, meta data isn't there, so ask for it.
panic(err) idx, err := h.CreateIndexIfNotExists(index, IndexOptions{})
panicOn(err)
f := idx.Field(field)
if f == nil {
t.Fatalf("no such field '%v'", field)
} }
return row
row, err := f.Row(nil, rowID)
if err != nil {
t.Fatalf("error getting field.Row(rowID=%v): %v", rowID, err)
}
cols := row.Columns()
if len(cols) == 0 {
t.Fatalf("error getting field.Row().Columns(): empty columns, colID %v bit was not hot", columnID)
}
for _, c := range cols {
if c == columnID {
return // ok, found it.
}
}
t.Fatalf("error getting field.Row().Columns(): colID %v bit was not hot", columnID)
} }
func TestHolder_Optn(t *testing.T) { func testMustNotHaveBit(t *testing.T, h *Holder, index, field string, rowID, columnID uint64) {
t.Run("ErrViewPermission", func(t *testing.T) { if testHasBit(t, h, index, field, rowID, columnID) {
if os.Geteuid() == 0 { t.Fatalf("error, expected no bit but this bit was hot: index='%v', field='%v', rowID='%v', columnID='%v'", index, field, rowID, columnID)
t.Skip("Skipping permissions test since user is root.")
}
h := newHolder()
defer h.Close()
if idx, err := h.CreateIndex("foo", IndexOptions{}); err != nil {
t.Fatal(err)
} else if field, err := idx.CreateField("bar", OptFieldTypeDefault()); err != nil {
t.Fatal(err)
} else if _, err := field.createViewIfNotExists(viewStandard); err != nil {
t.Fatal(err)
} else if err := h.Holder.Close(); err != nil {
t.Fatal(err)
} else if err := os.Chmod(filepath.Join(h.Path, "foo", "bar", "views", "standard"), 0000); err != nil {
t.Fatal(err)
}
defer func() {
// we don't care about a failure here
_ = os.Chmod(filepath.Join(h.Path, "foo", "bar", "views", "standard"), 0755)
}()
if err := h.Reopen(); err == nil || !strings.Contains(err.Error(), "permission denied") {
t.Fatalf("unexpected error: %s", err)
}
})
t.Run("ErrViewFragmentsMkdir", func(t *testing.T) {
if os.Geteuid() == 0 {
t.Skip("Skipping permissions test since user is root.")
}
h := newHolder()
defer h.Close()
if idx, err := h.CreateIndex("foo", IndexOptions{}); err != nil {
t.Fatal(err)
} else if field, err := idx.CreateField("bar", OptFieldTypeDefault()); err != nil {
t.Fatal(err)
} else if _, err := field.createViewIfNotExists(viewStandard); err != nil {
t.Fatal(err)
} else if err := h.Holder.Close(); err != nil {
t.Fatal(err)
} else if err := os.Chmod(filepath.Join(h.Path, "foo", "bar", "views", "standard", "fragments"), 0000); err != nil {
t.Fatal(err)
}
defer func() {
// we don't care about a failure here
_ = os.Chmod(filepath.Join(h.Path, "foo", "bar", "views", "standard", "fragments"), 0755)
}()
if err := h.Reopen(); err == nil || !strings.Contains(err.Error(), "permission denied") {
t.Fatalf("unexpected error: %s", err)
}
})
t.Run("ErrFragmentCachePermission", func(t *testing.T) {
if os.Geteuid() == 0 {
t.Skip("Skipping permissions test since user is root.")
}
h := newHolder()
defer h.Close()
if idx, err := h.CreateIndex("foo", IndexOptions{}); err != nil {
t.Fatal(err)
} else if field, err := idx.CreateField("bar", OptFieldTypeDefault()); err != nil {
t.Fatal(err)
} else if view, err := field.createViewIfNotExists(viewStandard); err != nil {
t.Fatal(err)
} else if _, err := field.SetBit(0, 0, nil); err != nil {
t.Fatal(err)
} else if err := view.Fragment(0).FlushCache(); err != nil {
t.Fatal(err)
} else if err := h.Holder.Close(); err != nil {
t.Fatal(err)
} else if err := os.Chmod(filepath.Join(h.Path, "foo", "bar", "views", "standard", "fragments", "0.cache"), 0000); err != nil {
t.Fatal(err)
}
defer func() {
_ = os.Chmod(filepath.Join(h.Path, "foo", "bar", "views", "standard", "fragments", "0.cache"), 0644)
}()
if err := h.Reopen(); err == nil || !strings.Contains(err.Error(), "permission denied") {
t.Fatalf("unexpected error: %s", err)
}
})
}
// Ensure holder can clean up orphaned fragments.
func TestHolderCleaner_CleanHolder(t *testing.T) {
cluster := NewTestCluster(2)
// Create a local holder.
hldr0 := newHolder()
defer hldr0.Close()
// Mock 2-node, fully replicated cluster.
cluster.ReplicaN = 2
cluster.nodes[0].URI = NewTestURIFromHostPort("localhost", 0)
// Create fields on nodes.
for _, hldr := range []*tHolder{hldr0} {
hldr.MustCreateFieldIfNotExists("i", "f")
hldr.MustCreateFieldIfNotExists("i", "f0")
hldr.MustCreateFieldIfNotExists("y", "z")
}
// Set data on the local holder.
hldr0.SetBit("i", "f", 0, 10)
hldr0.SetBit("i", "f", 0, 4000)
hldr0.SetBit("i", "f", 2, 20)
hldr0.SetBit("i", "f", 3, 10)
hldr0.SetBit("i", "f", 120, 10)
hldr0.SetBit("i", "f", 200, 4)
hldr0.SetBit("i", "f0", 9, ShardWidth+5)
hldr0.SetBit("y", "z", 10, (2*ShardWidth)+4)
hldr0.SetBit("y", "z", 10, (2*ShardWidth)+5)
hldr0.SetBit("y", "z", 10, (2*ShardWidth)+7)
// Set highest shard.
err := hldr0.Field("i", "f").AddRemoteAvailableShards(roaring.NewBitmap(0, 1))
if err != nil {
t.Fatalf("adding remote shards: %v", err)
}
err = hldr0.Field("y", "z").AddRemoteAvailableShards(roaring.NewBitmap(0, 1, 2))
if err != nil {
t.Fatalf("adding remote shards: %v", err)
}
// Keep replication the same and ensure we get the expected results.
cluster.ReplicaN = 2
// Set up cleaner for replication 2.
cleaner2 := holderCleaner{
Node: cluster.nodes[0],
Holder: hldr0.Holder,
Cluster: cluster,
}
if err := cleaner2.CleanHolder(); err != nil {
t.Fatal(err)
}
// Verify data is the same on both nodes.
for i, hldr := range []*tHolder{hldr0} {
if a := hldr.Row("i", "f", 0).Columns(); !reflect.DeepEqual(a, []uint64{10, 4000}) {
t.Fatalf("unexpected columns(%d/0): %+v", i, a)
} else if a := hldr.Row("i", "f", 2).Columns(); !reflect.DeepEqual(a, []uint64{20}) {
t.Fatalf("unexpected columns(%d/2): %+v", i, a)
} else if a := hldr.Row("i", "f", 3).Columns(); !reflect.DeepEqual(a, []uint64{10}) {
t.Fatalf("unexpected columns(%d/3): %+v", i, a)
} else if a := hldr.Row("i", "f", 120).Columns(); !reflect.DeepEqual(a, []uint64{10}) {
t.Fatalf("unexpected columns(%d/120): %+v", i, a)
} else if a := hldr.Row("i", "f", 200).Columns(); !reflect.DeepEqual(a, []uint64{4}) {
t.Fatalf("unexpected columns(%d/200): %+v", i, a)
}
if a := hldr.Row("i", "f0", 9).Columns(); !reflect.DeepEqual(a, []uint64{ShardWidth + 5}) {
t.Fatalf("unexpected columns(%d/d/f0): %+v", i, a)
}
if a := hldr.Row("y", "z", 10).Columns(); !reflect.DeepEqual(a, []uint64{(2 * ShardWidth) + 4, (2 * ShardWidth) + 5, (2 * ShardWidth) + 7}) {
t.Fatalf("unexpected columns(%d/y/z): %+v", i, a)
}
}
// Change replication factor to ensure we have fragments to remove.
cluster.ReplicaN = 1
// Set up cleaner for replication 1.
cleaner1 := holderCleaner{
Node: cluster.nodes[0],
Holder: hldr0.Holder,
Cluster: cluster,
}
if err := cleaner1.CleanHolder(); err != nil {
t.Fatal(err)
}
// Verify data is the same on both nodes.
for i, hldr := range []*tHolder{hldr0} {
if a := hldr.Row("i", "f", 0).Columns(); !reflect.DeepEqual(a, []uint64{10, 4000}) {
t.Fatalf("unexpected columns(%d/0): %+v", i, a)
} else if a := hldr.Row("i", "f", 2).Columns(); !reflect.DeepEqual(a, []uint64{20}) {
t.Fatalf("unexpected columns(%d/2): %+v", i, a)
} else if a := hldr.Row("i", "f", 3).Columns(); !reflect.DeepEqual(a, []uint64{10}) {
t.Fatalf("unexpected columns(%d/3): %+v", i, a)
} else if a := hldr.Row("i", "f", 120).Columns(); !reflect.DeepEqual(a, []uint64{10}) {
t.Fatalf("unexpected columns(%d/120): %+v", i, a)
} else if a := hldr.Row("i", "f", 200).Columns(); !reflect.DeepEqual(a, []uint64{4}) {
t.Fatalf("unexpected columns(%d/200): %+v", i, a)
}
f := hldr.fragment("i", "f0", viewStandard, 1)
if f != nil {
t.Fatalf("expected fragment to be deleted: (%d/i/f0): %+v", i, f)
}
if a := hldr.Row("y", "z", 10).Columns(); !reflect.DeepEqual(a, []uint64{(2 * ShardWidth) + 4, (2 * ShardWidth) + 5, (2 * ShardWidth) + 7}) {
t.Fatalf("unexpected columns(%d/y/z): %+v", i, a)
}
} }
} }
// Ensure holder can reopen. func testHasBit(t *testing.T, h *Holder, index, field string, rowID, columnID uint64) bool {
func TestHolderCleaner_Reopen(t *testing.T) {
h := NewHolder() idx := h.Index(index)
h.Path = "path" if idx == nil {
err := h.Open() return false // not even an index by this name. Obviously no hot bits either.
if err != nil {
t.Fatalf("couldn't open holder: %v", err)
} }
err = h.Close()
if err != nil { f := idx.Field(field)
t.Fatalf("couldn't close holder: %v", err) if f == nil {
return false
} }
err = h.Open()
row, err := f.Row(nil, rowID)
if err != nil { if err != nil {
t.Fatalf("couldn't open holder: %v", err) return false
} }
err = h.Close()
cols := row.Columns()
if len(cols) == 0 {
return false
}
for _, c := range cols {
if c == columnID {
return true // ok, found it.
}
}
return false
}
func TestHolderOperatorProcess(t *testing.T) {
h, path, err := makeHolder(t)
if err != nil { if err != nil {
t.Fatalf("couldn't close holder: %v", err) t.Fatalf("creating holder: %v", err)
}
defer os.RemoveAll(path)
defer h.Close()
// Write bits to separate indexes.
testSetBit(t, h, "i0", "f", 100, 200)
testSetBit(t, h, "i1", "f", 100, 200)
testSetBit(t, h, "i1", "f", 100, 12345678)
testOp := testHolderOperator{}
ctx := context.Background()
err = h.Process(ctx, &testOp)
if err != nil {
t.Fatalf("processing holder: %v", err)
}
expected := testHolderOperator{
indexSeen: 2, indexProcessed: 2,
fieldSeen: 2, fieldProcessed: 2,
viewSeen: 2, viewProcessed: 2,
fragmentSeen: 3, fragmentProcessed: 3,
}
if testOp != expected {
t.Fatalf("holder processor did not process as expected. expected %#v, got %#v", expected, testOp)
}
}
func TestHolderOperatorCancel(t *testing.T) {
h, path, err := makeHolder(t)
if err != nil {
t.Fatalf("creating holder: %v", err)
}
defer os.RemoveAll(path)
defer h.Close()
// Write bits to separate indexes.
testSetBit(t, h, "i0", "f", 100, 200)
testSetBit(t, h, "i1", "f", 100, 200)
testSetBit(t, h, "i1", "f", 100, 12345678)
// Here, we want to ensure that the operation gets cancelled
// successfully. In practice we expect it to process one fragment, then
// end up blocked on the waitHere, then get cancelled... But the
// waitHere blockage isn't really something holder.Process can do
// anything about, so we close the channel, so two fragments are
// processed. But in theory you could end up with only one fragment
// processed if this goroutine managed to cancel before the processor
// gets to the next fragment. Point is, it shouldn't hit all three,
// because the checks against the cancellation should fire before it
// gets there.
testOp := testHolderOperator{waitHere: make(chan struct{})}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() {
err = h.Process(ctx, &testOp)
close(done)
}()
testOp.waitHere <- struct{}{}
cancel()
close(testOp.waitHere)
<-done
if err != context.Canceled {
t.Fatalf("processing holder: expected context.Canceled, got %v", err)
}
testOp.waitHere = nil
expected := testHolderOperator{
indexSeen: 2, indexProcessed: 2,
fieldSeen: 2, fieldProcessed: 2,
viewSeen: 2, viewProcessed: 2,
fragmentSeen: 3, fragmentProcessed: 3,
}
if testOp == expected {
t.Fatalf("holder processor did not cancel. expected something other than %#v", expected)
} }
} }

View file

@ -17,6 +17,7 @@ package pilosa_test
import ( import (
"bytes" "bytes"
"context" "context"
"math"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
@ -25,12 +26,14 @@ import (
"time" "time"
"github.com/pilosa/pilosa/v2" "github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/pql"
"github.com/pilosa/pilosa/v2/test" "github.com/pilosa/pilosa/v2/test"
"github.com/pkg/errors"
) )
func TestHolder_Open(t *testing.T) { func TestHolder_Open(t *testing.T) {
t.Run("ErrIndexName", func(t *testing.T) { t.Run("ErrIndexName", func(t *testing.T) {
h := test.MustOpenHolder() h := test.MustOpenHolder(t)
bufLogger := test.NewBufferLogger() bufLogger := test.NewBufferLogger()
h.Holder.Logger = bufLogger h.Holder.Logger = bufLogger
@ -57,7 +60,7 @@ func TestHolder_Open(t *testing.T) {
if os.Geteuid() == 0 { if os.Geteuid() == 0 {
t.Skip("Skipping permissions test since user is root.") t.Skip("Skipping permissions test since user is root.")
} }
h := test.MustOpenHolder() h := test.MustOpenHolder(t)
defer h.Close() defer h.Close()
if _, err := h.CreateIndex("test", pilosa.IndexOptions{}); err != nil { if _, err := h.CreateIndex("test", pilosa.IndexOptions{}); err != nil {
@ -76,7 +79,7 @@ func TestHolder_Open(t *testing.T) {
} }
}) })
t.Run("ErrIndexAttrStoreCorrupt", func(t *testing.T) { t.Run("ErrIndexAttrStoreCorrupt", func(t *testing.T) {
h := test.MustOpenHolder() h := test.MustOpenHolder(t)
defer h.Close() defer h.Close()
if _, err := h.CreateIndex("test", pilosa.IndexOptions{}); err != nil { if _, err := h.CreateIndex("test", pilosa.IndexOptions{}); err != nil {
@ -96,7 +99,7 @@ func TestHolder_Open(t *testing.T) {
if os.Geteuid() == 0 { if os.Geteuid() == 0 {
t.Skip("Skipping permissions test since user is root.") t.Skip("Skipping permissions test since user is root.")
} }
h := test.MustOpenHolder() h := test.MustOpenHolder(t)
defer h.Close() defer h.Close()
if idx, err := h.CreateIndex("foo", pilosa.IndexOptions{}); err != nil { if idx, err := h.CreateIndex("foo", pilosa.IndexOptions{}); err != nil {
@ -105,27 +108,32 @@ func TestHolder_Open(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} else if err := h.Holder.Close(); err != nil { } else if err := h.Holder.Close(); err != nil {
t.Fatal(err) t.Fatal(err)
} else if err := os.Chmod(filepath.Join(h.Path, "foo", "bar"), 0000); err != nil { } else if err := os.Chmod(filepath.Join(h.Path(), "foo", "bar"), 0000); err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer func() { defer func() {
_ = os.Chmod(filepath.Join(h.Path, "foo", "bar"), 0755) _ = os.Chmod(filepath.Join(h.Path(), "foo", "bar"), 0755)
}() }()
if err := h.Reopen(); err == nil || !strings.Contains(err.Error(), "permission denied") { if err := h.Reopen(); err == nil || !strings.Contains(err.Error(), "permission denied") {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
}) })
t.Run("ErrFieldOptionsCorrupt", func(t *testing.T) { t.Run("ErrFieldOptionsCorrupt", func(t *testing.T) {
h := test.MustOpenHolder() h := test.MustOpenHolder(t)
defer h.Close() defer h.Close()
if idx, err := h.CreateIndex("foo", pilosa.IndexOptions{}); err != nil { var idx *pilosa.Index
var err error
if idx, err = h.CreateIndex("foo", pilosa.IndexOptions{}); err != nil {
t.Fatal(err) t.Fatal(err)
} else if _, err := idx.CreateField("bar", pilosa.OptFieldTypeDefault()); err != nil { }
if _, err := idx.CreateField("bar", pilosa.OptFieldTypeDefault()); err != nil {
t.Fatal(err) t.Fatal(err)
} else if err := h.Holder.Close(); err != nil { } else if err := h.Holder.Close(); err != nil {
t.Fatal(err) t.Fatal(err)
} else if err := os.Truncate(filepath.Join(h.Path, "foo", "bar", ".meta"), 2); err != nil { } else if err := os.Truncate(filepath.Join(h.Path(), "foo", "bar", ".meta"), 2); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -134,16 +142,20 @@ func TestHolder_Open(t *testing.T) {
} }
}) })
t.Run("ErrFieldAttrStoreCorrupt", func(t *testing.T) { t.Run("ErrFieldAttrStoreCorrupt", func(t *testing.T) {
h := test.MustOpenHolder() h := test.MustOpenHolder(t)
defer h.Close() defer h.Close()
if idx, err := h.CreateIndex("foo", pilosa.IndexOptions{}); err != nil { var idx *pilosa.Index
var err error
if idx, err = h.CreateIndex("foo", pilosa.IndexOptions{}); err != nil {
t.Fatal(err) t.Fatal(err)
} else if _, err := idx.CreateField("bar", pilosa.OptFieldTypeDefault()); err != nil { }
if _, err := idx.CreateField("bar", pilosa.OptFieldTypeDefault()); err != nil {
t.Fatal(err) t.Fatal(err)
} else if err := h.Holder.Close(); err != nil { } else if err := h.Holder.Close(); err != nil {
t.Fatal(err) t.Fatal(err)
} else if err := os.Truncate(filepath.Join(h.Path, "foo", "bar", ".data"), 2); err != nil { } else if err := os.Truncate(filepath.Join(h.Path(), "foo", "bar", ".data"), 2); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -153,43 +165,70 @@ func TestHolder_Open(t *testing.T) {
}) })
t.Run("ErrFragmentStoragePermission", func(t *testing.T) { t.Run("ErrFragmentStoragePermission", func(t *testing.T) {
roaringOnlyTest(t)
if os.Geteuid() == 0 { if os.Geteuid() == 0 {
t.Skip("Skipping permissions test since user is root.") t.Skip("Skipping permissions test since user is root.")
} }
h := test.MustOpenHolder() h := test.MustOpenHolder(t)
defer h.Close() defer h.Close()
if idx, err := h.CreateIndex("foo", pilosa.IndexOptions{}); err != nil { var idx *pilosa.Index
var err error
if idx, err = h.CreateIndex("foo", pilosa.IndexOptions{}); err != nil {
t.Fatal(err) t.Fatal(err)
} else if field, err := idx.CreateField("bar", pilosa.OptFieldTypeDefault()); err != nil { }
var shard uint64
tx := idx.Txf().NewTx(pilosa.Txo{Write: writable, Index: idx, Shard: shard})
defer tx.Rollback()
if field, err := idx.CreateField("bar", pilosa.OptFieldTypeDefault()); err != nil {
t.Fatal(err) t.Fatal(err)
} else if _, err := field.SetBit(0, 0, nil); err != nil { } else if _, err := field.SetBit(tx, 0, 0, nil); err != nil {
t.Fatal(err)
} else if err := tx.Commit(); err != nil {
t.Fatal(err) t.Fatal(err)
} else if err := h.Holder.Close(); err != nil { } else if err := h.Holder.Close(); err != nil {
t.Fatal(err) t.Fatal(err)
} else if err := os.Chmod(filepath.Join(h.Path, "foo", "bar", "views", "standard", "fragments", "0"), 0000); err != nil { } else if err := os.Chmod(filepath.Join(h.Path(), "foo", "bar", "views", "standard", "fragments", "0"), 0000); err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer func() { defer func() {
_ = os.Chmod(filepath.Join(h.Path, "foo", "bar", "views", "standard", "fragments", "0"), 0644) _ = os.Chmod(filepath.Join(h.Path(), "foo", "bar", "views", "standard", "fragments", "0"), 0644)
}() }()
if err := h.Reopen(); err == nil || !strings.Contains(err.Error(), "permission denied") { if err := h.Reopen(); err == nil || !strings.Contains(err.Error(), "permission denied") {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
}) })
t.Run("ErrFragmentStorageCorrupt", func(t *testing.T) { t.Run("ErrFragmentStorageCorrupt", func(t *testing.T) {
h := test.MustOpenHolder() roaringOnlyTest(t)
h := test.MustOpenHolder(t)
defer h.Close() defer h.Close()
if idx, err := h.CreateIndex("foo", pilosa.IndexOptions{}); err != nil { var idx *pilosa.Index
var err error
if idx, err = h.CreateIndex("foo", pilosa.IndexOptions{}); err != nil {
t.Fatal(err) t.Fatal(err)
} else if field, err := idx.CreateField("bar", pilosa.OptFieldTypeDefault()); err != nil { }
var shard uint64
tx := idx.Txf().NewTx(pilosa.Txo{Write: writable, Index: idx, Shard: shard})
if err != nil {
t.Fatal(err) t.Fatal(err)
} else if _, err := field.SetBit(0, 0, nil); err != nil { }
defer tx.Rollback()
if field, err := idx.CreateField("bar", pilosa.OptFieldTypeDefault()); err != nil {
t.Fatal(err)
} else if _, err := field.SetBit(tx, 0, 0, nil); err != nil {
t.Fatal(err)
} else if err := tx.Commit(); err != nil {
t.Fatal(err) t.Fatal(err)
} else if err := h.Holder.Close(); err != nil { } else if err := h.Holder.Close(); err != nil {
t.Fatal(err) t.Fatal(err)
} else if err := os.Truncate(filepath.Join(h.Path, "foo", "bar", "views", "standard", "fragments", "0"), 2); err != nil { } else if err := os.Truncate(filepath.Join(h.Path(), "foo", "bar", "views", "standard", "fragments", "0"), 2); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -197,12 +236,125 @@ func TestHolder_Open(t *testing.T) {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
}) })
t.Run("ErrFragmentStorageRecoverable", func(t *testing.T) {
roaringOnlyTest(t)
h := test.MustOpenHolder(t)
defer h.Close()
idx, err := h.CreateIndex("foo", pilosa.IndexOptions{})
if err != nil {
t.Fatal(err)
}
var shard uint64
tx := idx.Txf().NewTx(pilosa.Txo{Write: writable, Index: idx, Shard: shard})
defer tx.Rollback()
if field, err := idx.CreateField("bar", pilosa.OptFieldTypeDefault()); err != nil {
t.Fatal(err)
} else if _, err := field.SetBit(tx, 0, 0, nil); err != nil {
t.Fatal(err)
} else if err := tx.Commit(); err != nil {
t.Fatal(err)
} else if err := h.Holder.Close(); err != nil {
t.Fatal(err)
} else if err := os.Truncate(filepath.Join(h.Path(), "foo", "bar", "views", "standard", "fragments", "0"), 20); err != nil {
t.Fatal(err)
}
if err := h.Reopen(); err != nil {
t.Fatalf("unexpected error: %s", err)
}
})
t.Run("ForeignIndex", func(t *testing.T) {
t.Run("ErrForeignIndexNotFound", func(t *testing.T) {
h := test.MustOpenHolder(t)
defer h.Close()
if idx, err := h.CreateIndex("foo", pilosa.IndexOptions{}); err != nil {
t.Fatal(err)
} else {
_, err := idx.CreateField("bar", pilosa.OptFieldTypeInt(0, 100), pilosa.OptFieldForeignIndex("nonexistent"))
if err == nil {
t.Fatalf("expected error: %s", pilosa.ErrForeignIndexNotFound)
} else if errors.Cause(err) != pilosa.ErrForeignIndexNotFound {
t.Fatalf("expected error: %s, but got: %s", pilosa.ErrForeignIndexNotFound, err)
}
}
})
// Foreign index zzz is opened after foo/bar.
t.Run("ForeignIndexNotOpenYet", func(t *testing.T) {
h := test.MustOpenHolder(t)
defer h.Close()
if _, err := h.CreateIndex("zzz", pilosa.IndexOptions{}); err != nil {
t.Fatal(err)
} else if idx, err := h.CreateIndex("foo", pilosa.IndexOptions{}); err != nil {
t.Fatal(err)
} else if _, err := idx.CreateField("bar", pilosa.OptFieldTypeInt(0, 100), pilosa.OptFieldForeignIndex("zzz")); err != nil {
t.Fatal(err)
} else if err := h.Holder.Close(); err != nil {
t.Fatal(err)
}
if err := h.Reopen(); err != nil {
t.Fatalf("unexpected error: %s", err)
}
})
// Foreign index aaa is opened before foo/bar.
t.Run("ForeignIndexIsOpen", func(t *testing.T) {
h := test.MustOpenHolder(t)
defer h.Close()
if _, err := h.CreateIndex("aaa", pilosa.IndexOptions{}); err != nil {
t.Fatal(err)
} else if idx, err := h.CreateIndex("foo", pilosa.IndexOptions{}); err != nil {
t.Fatal(err)
} else if _, err := idx.CreateField("bar", pilosa.OptFieldTypeInt(0, 100), pilosa.OptFieldForeignIndex("aaa")); err != nil {
t.Fatal(err)
} else if err := h.Holder.Close(); err != nil {
t.Fatal(err)
}
if err := h.Reopen(); err != nil {
t.Fatalf("unexpected error: %s", err)
}
})
// Try to re-create existing index
t.Run("CreateIndexIfNotExists", func(t *testing.T) {
h := test.MustOpenHolder(t)
defer h.Close()
idx1, err := h.CreateIndexIfNotExists("aaa", pilosa.IndexOptions{})
if err != nil {
t.Fatal(err)
}
if _, err = h.CreateIndex("aaa", pilosa.IndexOptions{}); err == nil {
t.Fatalf("expected: ConflictError, got: nil")
} else if _, ok := err.(pilosa.ConflictError); !ok {
t.Fatalf("expected: ConflictError, got: %s", err)
}
idx2, err := h.CreateIndexIfNotExists("aaa", pilosa.IndexOptions{})
if err != nil {
t.Fatal(err)
}
if idx1 != idx2 {
t.Fatalf("expected the same indexes, got: %s and %s", idx1.Name(), idx2.Name())
}
})
})
} }
func TestHolder_HasData(t *testing.T) { func TestHolder_HasData(t *testing.T) {
t.Run("IndexDirectory", func(t *testing.T) { t.Run("IndexDirectory", func(t *testing.T) {
h := test.MustOpenHolder() h := test.MustOpenHolder(t)
defer h.Close() defer h.Close()
if ok, err := h.HasData(); ok || err != nil { if ok, err := h.HasData(); ok || err != nil {
@ -219,7 +371,7 @@ func TestHolder_HasData(t *testing.T) {
}) })
t.Run("Peek", func(t *testing.T) { t.Run("Peek", func(t *testing.T) {
h := test.NewHolder() h := test.NewHolder(t)
if ok, err := h.HasData(); ok || err != nil { if ok, err := h.HasData(); ok || err != nil {
t.Fatal("expected HasData to return false, no err, but", ok, err) t.Fatal("expected HasData to return false, no err, but", ok, err)
@ -236,10 +388,12 @@ func TestHolder_HasData(t *testing.T) {
}) })
t.Run("Peek at missing directory", func(t *testing.T) { t.Run("Peek at missing directory", func(t *testing.T) {
h := test.NewHolder()
// Ensure that hasData is false when dir doesn't exist. // Ensure that hasData is false when dir doesn't exist.
h.Path = "bad-path"
// Note that we are intentionally not using test.NewHolder,
// because we want to create a Holder object with an invalid path,
// rather than creating a valid holder with a temporary path.
h := pilosa.NewHolder("bad-path", nil)
if ok, err := h.HasData(); ok || err != nil { if ok, err := h.HasData(); ok || err != nil {
t.Fatal("expected HasData to return false, no err, but", ok, err) t.Fatal("expected HasData to return false, no err, but", ok, err)
@ -249,7 +403,8 @@ func TestHolder_HasData(t *testing.T) {
// Ensure holder can delete an index and its underlying files. // Ensure holder can delete an index and its underlying files.
func TestHolder_DeleteIndex(t *testing.T) { func TestHolder_DeleteIndex(t *testing.T) {
hldr := test.MustOpenHolder()
hldr := test.MustOpenHolder(t)
defer hldr.Close() defer hldr.Close()
// Write bits to separate indexes. // Write bits to separate indexes.
@ -277,39 +432,43 @@ func TestHolder_DeleteIndex(t *testing.T) {
// Ensure holder can sync with a remote holder. // Ensure holder can sync with a remote holder.
func TestHolderSyncer_SyncHolder(t *testing.T) { func TestHolderSyncer_SyncHolder(t *testing.T) {
c := test.MustNewCluster(t, 2) c := test.MustNewCluster(t, 2)
c[0].Config.Cluster.ReplicaN = 2 c.GetNode(0).Config.Cluster.ReplicaN = 2
c[0].Config.AntiEntropy.Interval = 0 c.GetNode(0).Config.AntiEntropy.Interval = 0
c[1].Config.Cluster.ReplicaN = 2 c.GetNode(1).Config.Cluster.ReplicaN = 2
c[1].Config.AntiEntropy.Interval = 0 c.GetNode(1).Config.AntiEntropy.Interval = 0
err := c.Start() err := c.Start()
if err != nil { if err != nil {
t.Fatalf("starting cluster: %v", err) t.Fatalf("starting cluster: %v", err)
} }
defer c.Close() defer c.Close()
_, err = c[0].API.CreateIndex(context.Background(), "i", pilosa.IndexOptions{}) _, err = c.GetNode(0).API.CreateIndex(context.Background(), "i", pilosa.IndexOptions{})
if err != nil { if err != nil {
t.Fatalf("creating index i: %v", err) t.Fatalf("creating index i: %v", err)
} }
_, err = c[0].API.CreateIndex(context.Background(), "y", pilosa.IndexOptions{}) _, err = c.GetNode(0).API.CreateIndex(context.Background(), "y", pilosa.IndexOptions{})
if err != nil { if err != nil {
t.Fatalf("creating index y: %v", err) t.Fatalf("creating index y: %v", err)
} }
_, err = c[0].API.CreateField(context.Background(), "i", "f", pilosa.OptFieldTypeSet(pilosa.DefaultCacheType, pilosa.DefaultCacheSize)) _, err = c.GetNode(0).API.CreateField(context.Background(), "i", "f", pilosa.OptFieldTypeSet(pilosa.DefaultCacheType, pilosa.DefaultCacheSize))
if err != nil { if err != nil {
t.Fatalf("creating field f: %v", err) t.Fatalf("creating field f: %v", err)
} }
_, err = c[0].API.CreateField(context.Background(), "i", "f0", pilosa.OptFieldTypeSet(pilosa.DefaultCacheType, pilosa.DefaultCacheSize)) _, err = c.GetNode(0).API.CreateField(context.Background(), "i", "f0", pilosa.OptFieldTypeSet(pilosa.DefaultCacheType, pilosa.DefaultCacheSize))
if err != nil { if err != nil {
t.Fatalf("creating field f0: %v", err) t.Fatalf("creating field f0: %v", err)
} }
_, err = c[0].API.CreateField(context.Background(), "y", "z", pilosa.OptFieldTypeSet(pilosa.DefaultCacheType, pilosa.DefaultCacheSize)) _, err = c.GetNode(0).API.CreateField(context.Background(), "y", "z", pilosa.OptFieldTypeMutex(pilosa.DefaultCacheType, pilosa.DefaultCacheSize))
if err != nil { if err != nil {
t.Fatalf("creating field z in y: %v", err) t.Fatalf("creating field z in y: %v", err)
} }
_, err = c.GetNode(0).API.CreateField(context.Background(), "y", "b", pilosa.OptFieldTypeBool())
if err != nil {
t.Fatalf("creating field b in y: %v", err)
}
hldr0 := &test.Holder{Holder: c[0].Server.Holder()} hldr0 := &test.Holder{Holder: c.GetNode(0).Server.Holder()}
hldr1 := &test.Holder{Holder: c[1].Server.Holder()} hldr1 := &test.Holder{Holder: c.GetNode(1).Server.Holder()}
// Set data on the local holder. // Set data on the local holder.
hldr0.SetBit("i", "f", 0, 10) hldr0.SetBit("i", "f", 0, 10)
@ -321,6 +480,7 @@ func TestHolderSyncer_SyncHolder(t *testing.T) {
// Set a bit to create the fragment. // Set a bit to create the fragment.
hldr0.SetBit("y", "z", 0, 0) hldr0.SetBit("y", "z", 0, 0)
hldr0.SetBit("y", "b", 0, 0) // rowID = 0 means false
// Set data on the remote holder. // Set data on the remote holder.
hldr1.SetBit("i", "f", 0, 4000) hldr1.SetBit("i", "f", 0, 4000)
@ -331,11 +491,15 @@ func TestHolderSyncer_SyncHolder(t *testing.T) {
hldr1.SetBit("y", "z", 10, (3*ShardWidth)+5) hldr1.SetBit("y", "z", 10, (3*ShardWidth)+5)
hldr1.SetBit("y", "z", 10, (3*ShardWidth)+7) hldr1.SetBit("y", "z", 10, (3*ShardWidth)+7)
err = c[0].Server.SyncData() hldr1.SetBit("y", "b", 1, (3*ShardWidth)+4) // true
hldr1.SetBit("y", "b", 0, (3*ShardWidth)+5) // false
hldr1.SetBit("y", "b", 1, (3*ShardWidth)+7) // true
err = c.GetNode(0).Server.SyncData()
if err != nil { if err != nil {
t.Fatalf("syncing node 0: %v", err) t.Fatalf("syncing node 0: %v", err)
} }
err = c[1].Server.SyncData() err = c.GetNode(1).Server.SyncData()
if err != nil { if err != nil {
t.Fatalf("syncing node 1: %v", err) t.Fatalf("syncing node 1: %v", err)
} }
@ -365,16 +529,128 @@ func TestHolderSyncer_SyncHolder(t *testing.T) {
if a := hldr.Row("y", "z", 10).Columns(); !reflect.DeepEqual(a, []uint64{(3 * ShardWidth) + 4, (3 * ShardWidth) + 5, (3 * ShardWidth) + 7}) { if a := hldr.Row("y", "z", 10).Columns(); !reflect.DeepEqual(a, []uint64{(3 * ShardWidth) + 4, (3 * ShardWidth) + 5, (3 * ShardWidth) + 7}) {
t.Errorf("unexpected columns(%d/y/z): %+v", i, a) t.Errorf("unexpected columns(%d/y/z): %+v", i, a)
} }
if a := hldr.Row("y", "b", 0).Columns(); !reflect.DeepEqual(a, []uint64{0, (3 * ShardWidth) + 5}) {
t.Errorf("unexpected false columns(%d/y/b): %+v", i, a)
}
if a := hldr.Row("y", "b", 1).Columns(); !reflect.DeepEqual(a, []uint64{(3 * ShardWidth) + 4, (3 * ShardWidth) + 7}) {
t.Errorf("unexpected true columns(%d/y/b): %+v", i, a)
}
}
}
// Ensure holder can sync with a remote holder and respects
// the row boundaries of the block.
func TestHolderSyncer_BlockIteratorLimits(t *testing.T) {
c := test.MustNewCluster(t, 3)
c.GetNode(0).Config.Cluster.ReplicaN = 3
c.GetNode(0).Config.AntiEntropy.Interval = 0
c.GetNode(1).Config.Cluster.ReplicaN = 3
c.GetNode(1).Config.AntiEntropy.Interval = 0
err := c.Start()
if err != nil {
t.Fatalf("starting cluster: %v", err)
}
defer c.Close()
_, err = c.GetNode(0).API.CreateIndex(context.Background(), "i", pilosa.IndexOptions{})
if err != nil {
t.Fatalf("creating index i: %v", err)
}
_, err = c.GetNode(0).API.CreateField(context.Background(), "i", "f", pilosa.OptFieldTypeSet(pilosa.DefaultCacheType, pilosa.DefaultCacheSize))
if err != nil {
t.Fatalf("creating field f: %v", err)
}
blockEdge := uint64(pilosa.HashBlockSize)
hldr0 := &test.Holder{Holder: c.GetNode(0).Server.Holder()}
hldr1 := &test.Holder{Holder: c.GetNode(1).Server.Holder()}
hldr2 := &test.Holder{Holder: c.GetNode(2).Server.Holder()}
// Set data on the local holder.
hldr0.SetBit("i", "f", blockEdge-1, 10)
hldr0.SetBit("i", "f", blockEdge, 20)
// Set the same data on one of the replicas
// so that we have a quorum.
hldr1.SetBit("i", "f", blockEdge-1, 10)
hldr1.SetBit("i", "f", blockEdge, 20)
// Leave the third replica empty to force a block merge.
//
err = c.GetNode(0).Server.SyncData()
if err != nil {
t.Fatalf("syncing node 0: %v", err)
}
// Verify data is the same on all nodes.
for i, hldr := range []*test.Holder{hldr0, hldr1, hldr2} {
if a := hldr.Row("i", "f", blockEdge-1).Columns(); !reflect.DeepEqual(a, []uint64{10}) {
t.Errorf("unexpected columns(%d/block 0): %+v", i, a)
}
if a := hldr.Row("i", "f", blockEdge).Columns(); !reflect.DeepEqual(a, []uint64{20}) {
t.Errorf("unexpected columns(%d/block 1): %+v", i, a)
}
}
}
// Ensure holder correctly handles clears during block sync.
func TestHolderSyncer_Clears(t *testing.T) {
c := test.MustNewCluster(t, 3)
c.GetNode(0).Config.Cluster.ReplicaN = 3
c.GetNode(0).Config.AntiEntropy.Interval = 0
c.GetNode(1).Config.Cluster.ReplicaN = 3
c.GetNode(1).Config.AntiEntropy.Interval = 0
err := c.Start()
if err != nil {
t.Fatalf("starting cluster: %v", err)
}
defer c.Close()
_, err = c.GetNode(0).API.CreateIndex(context.Background(), "i", pilosa.IndexOptions{})
if err != nil {
t.Fatalf("creating index i: %v", err)
}
_, err = c.GetNode(0).API.CreateField(context.Background(), "i", "f", pilosa.OptFieldTypeSet(pilosa.DefaultCacheType, pilosa.DefaultCacheSize))
if err != nil {
t.Fatalf("creating field f: %v", err)
}
hldr0 := &test.Holder{Holder: c.GetNode(0).Server.Holder()}
hldr1 := &test.Holder{Holder: c.GetNode(1).Server.Holder()}
hldr2 := &test.Holder{Holder: c.GetNode(2).Server.Holder()}
// Set data on the local holder that should be cleared
// because it's the only instance of this value.
hldr0.SetBit("i", "f", 0, 30)
// Set similar data on the replicas, but
// different from what's on local. This should end
// up being set on all replicas
hldr1.SetBit("i", "f", 0, 20)
hldr2.SetBit("i", "f", 0, 20)
err = c.GetNode(0).Server.SyncData()
if err != nil {
t.Fatalf("syncing node 0: %v", err)
}
// Verify data is the same on all nodes.
for i, hldr := range []*test.Holder{hldr0, hldr1, hldr2} {
if a := hldr.Row("i", "f", 0).Columns(); !reflect.DeepEqual(a, []uint64{20}) {
t.Errorf("unexpected columns(%d): %+v", i, a)
}
} }
} }
// Ensure holder can sync time quantum views with a remote holder. // Ensure holder can sync time quantum views with a remote holder.
func TestHolderSyncer_TimeQuantum(t *testing.T) { func TestHolderSyncer_TimeQuantum(t *testing.T) {
c := test.MustNewCluster(t, 2) c := test.MustNewCluster(t, 2)
c[0].Config.Cluster.ReplicaN = 2 c.GetNode(0).Config.Cluster.ReplicaN = 2
c[0].Config.AntiEntropy.Interval = 0 c.GetNode(0).Config.AntiEntropy.Interval = 0
c[1].Config.Cluster.ReplicaN = 2 c.GetNode(1).Config.Cluster.ReplicaN = 2
c[1].Config.AntiEntropy.Interval = 0 c.GetNode(1).Config.AntiEntropy.Interval = 0
err := c.Start() err := c.Start()
if err != nil { if err != nil {
t.Fatalf("starting cluster: %v", err) t.Fatalf("starting cluster: %v", err)
@ -383,17 +659,17 @@ func TestHolderSyncer_TimeQuantum(t *testing.T) {
quantum := "D" quantum := "D"
_, err = c[0].API.CreateIndex(context.Background(), "i", pilosa.IndexOptions{}) _, err = c.GetNode(0).API.CreateIndex(context.Background(), "i", pilosa.IndexOptions{})
if err != nil { if err != nil {
t.Fatalf("creating index i: %v", err) t.Fatalf("creating index i: %v", err)
} }
_, err = c[0].API.CreateField(context.Background(), "i", "f", pilosa.OptFieldTypeTime(pilosa.TimeQuantum(quantum))) _, err = c.GetNode(0).API.CreateField(context.Background(), "i", "f", pilosa.OptFieldTypeTime(pilosa.TimeQuantum(quantum)))
if err != nil { if err != nil {
t.Fatalf("creating field f: %v", err) t.Fatalf("creating field f: %v", err)
} }
hldr0 := &test.Holder{Holder: c[0].Server.Holder()} hldr0 := &test.Holder{Holder: c.GetNode(0).Server.Holder()}
hldr1 := &test.Holder{Holder: c[1].Server.Holder()} hldr1 := &test.Holder{Holder: c.GetNode(1).Server.Holder()}
// Set data on the local holder for node0. // Set data on the local holder for node0.
t1 := time.Date(2018, 8, 1, 12, 30, 0, 0, time.UTC) t1 := time.Date(2018, 8, 1, 12, 30, 0, 0, time.UTC)
@ -404,7 +680,7 @@ func TestHolderSyncer_TimeQuantum(t *testing.T) {
// Set data on node1. // Set data on node1.
hldr1.SetBitTime("i", "f", 0, 22, &t2) hldr1.SetBitTime("i", "f", 0, 22, &t2)
err = c[0].Server.SyncData() err = c.GetNode(0).Server.SyncData()
if err != nil { if err != nil {
t.Fatalf("syncing node 0: %v", err) t.Fatalf("syncing node 0: %v", err)
} }
@ -419,3 +695,129 @@ func TestHolderSyncer_TimeQuantum(t *testing.T) {
} }
} }
} }
// Ensure holder can sync integer views with a remote holder.
func TestHolderSyncer_IntField(t *testing.T) {
t.Run("BasicSync", func(t *testing.T) {
c := test.MustNewCluster(t, 2)
c.GetNode(0).Config.Cluster.ReplicaN = 2
c.GetNode(0).Config.AntiEntropy.Interval = 0
c.GetNode(1).Config.Cluster.ReplicaN = 2
c.GetNode(1).Config.AntiEntropy.Interval = 0
err := c.Start()
if err != nil {
t.Fatalf("starting cluster: %v", err)
}
defer c.Close()
var idx0 *pilosa.Index
_ = idx0
idx0, err = c.GetNode(0).API.CreateIndex(context.Background(), "i", pilosa.IndexOptions{})
_ = idx0
if err != nil {
t.Fatalf("creating index i: %v", err)
}
_, err = c.GetNode(0).API.CreateField(context.Background(), "i", "f", pilosa.OptFieldTypeInt(0, 100))
if err != nil {
t.Fatalf("creating field f: %v", err)
}
hldr0 := &test.Holder{Holder: c.GetNode(0).Server.Holder()}
hldr1 := &test.Holder{Holder: c.GetNode(1).Server.Holder()}
// Set data on the local holder for node0. columnID=1, value=1
hldr0.SetValue("i", "f", 1, 1)
// in c0 expect the 1 bit
// Set data on node1. columnID=2, value=2
idx1 := hldr1.SetValue("i", "f", 2, 2)
_ = idx1
err = c.GetNode(0).Server.SyncData()
if err != nil {
t.Fatalf("syncing node 0: %v", err)
}
// expect 3 rows, the 1 bit + 2 rows for the 2 value as BSI. But, we only see that c0 overwrote c1.
// Problem is: data at c1 was replaced by c0, instead of being merged with existing c1.
// Problem is: data at c0 did not receive and merge the c1 data.
// Verify data is the same on both nodes.
for i, hldr := range []*test.Holder{hldr0, hldr1} {
if a, exists := hldr.Value("i", "f", 1); !exists || a != 1 {
// expects exists==true, a==1
t.Errorf("unexpected value(node%d/0): a:%d, exists: %v", i, a, exists)
}
if a, exists := hldr.Value("i", "f", 2); exists {
t.Errorf("unexpected value(node%d/1): a:%d, exists: %v", i, a, exists)
}
}
})
t.Run("MultiShard", func(t *testing.T) {
c := test.MustNewCluster(t, 2)
c.GetNode(0).Config.Cluster.ReplicaN = 2
c.GetNode(0).Config.AntiEntropy.Interval = 0
c.GetNode(1).Config.Cluster.ReplicaN = 2
c.GetNode(1).Config.AntiEntropy.Interval = 0
err := c.Start()
if err != nil {
t.Fatalf("starting cluster: %v", err)
}
defer c.Close()
var idx0 *pilosa.Index
_ = idx0
idx0, err = c.GetNode(0).API.CreateIndex(context.Background(), "i", pilosa.IndexOptions{})
_ = idx0
if err != nil {
t.Fatalf("creating index i: %v", err)
}
_, err = c.GetNode(0).API.CreateField(context.Background(), "i", "f", pilosa.OptFieldTypeInt(math.MinInt64, math.MaxInt64))
if err != nil {
t.Fatalf("creating field f: %v", err)
}
hldr0 := &test.Holder{Holder: c.GetNode(0).Server.Holder()}
hldr1 := &test.Holder{Holder: c.GetNode(1).Server.Holder()}
// Set data on the local holder for node0.
hldr0.SetValue("i", "f", 1*pilosa.ShardWidth, 11)
hldr0.SetValue("i", "f", 3*pilosa.ShardWidth, 32)
hldr0.SetValue("i", "f", 4*pilosa.ShardWidth, math.MinInt32)
hldr0.SetValue("i", "f", 7*pilosa.ShardWidth, math.MinInt32)
// Set data on node1.
hldr1.SetValue("i", "f", 0*pilosa.ShardWidth, 2)
hldr1.SetValue("i", "f", 2*pilosa.ShardWidth, 22)
hldr1.SetValue("i", "f", 4*pilosa.ShardWidth, math.MaxInt32)
hldr1.SetValue("i", "f", 7*pilosa.ShardWidth, math.MaxInt32)
// Primary for shards (for index "i"):
// node0: [0,3,7]
// node1: [1,2,4]
err = c.GetNode(0).Server.SyncData()
if err != nil {
t.Fatalf("syncing node 0: %v", err)
}
err = c.GetNode(1).Server.SyncData()
if err != nil {
t.Fatalf("syncing node 1: %v", err)
}
// dump the badger keys for both c0 and c1
// Verify data is the same on both nodes.
for i, hldr := range []*test.Holder{hldr0, hldr1} {
if a := hldr.Range("i", "f", pql.GT, 0); !reflect.DeepEqual(a.Columns(), []uint64{2 * pilosa.ShardWidth, 3 * pilosa.ShardWidth, 4 * pilosa.ShardWidth}) {
t.Errorf("unexpected columns(node%d/0): %d", i, a.Columns())
}
if a := hldr.Range("i", "f", pql.LT, 0); !reflect.DeepEqual(a.Columns(), []uint64{7 * pilosa.ShardWidth}) {
t.Errorf("unexpected columns(node%d/0): %d", i, a.Columns())
}
}
})
}

View file

@ -26,6 +26,7 @@ import (
"net/url" "net/url"
"sort" "sort"
"strconv" "strconv"
"time"
"github.com/pilosa/pilosa/v2" "github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/encoding/proto" "github.com/pilosa/pilosa/v2/encoding/proto"
@ -290,6 +291,7 @@ func (c *InternalClient) QueryNode(ctx context.Context, uri *pilosa.URI, index s
req.Header.Set("Content-Length", strconv.Itoa(len(buf))) req.Header.Set("Content-Length", strconv.Itoa(len(buf)))
req.Header.Set("Content-Type", "application/x-protobuf") req.Header.Set("Content-Type", "application/x-protobuf")
req.Header.Set("Accept", "application/x-protobuf") req.Header.Set("Accept", "application/x-protobuf")
req.Header.Set("X-Pilosa-Row", "roaring")
req.Header.Set("User-Agent", "pilosa/"+pilosa.Version) req.Header.Set("User-Agent", "pilosa/"+pilosa.Version)
// Execute request against the host. // Execute request against the host.
@ -392,6 +394,13 @@ func (c *InternalClient) ImportK(ctx context.Context, index, field string, bits
// Get the coordinator node; all bits are sent to the // Get the coordinator node; all bits are sent to the
// primary translate store (i.e. coordinator). // primary translate store (i.e. coordinator).
// TODO... is that right^^?
// RESPONSE: It looks like in ctl/import.go, we could change the
// logic in ImportCommand.importBits() to only use ImportK
// when useRowKeys = true. It's no longer necessary to
// send column key translations to the coordinator (although
// it should still work). As far as I know, the only thing
// that uses ImportK is the pilosa import sub-command.
nodes, err := c.Nodes(ctx) nodes, err := c.Nodes(ctx)
if err != nil { if err != nil {
return fmt.Errorf("getting nodes: %s", err) return fmt.Errorf("getting nodes: %s", err)
@ -487,6 +496,7 @@ func (c *InternalClient) importNode(ctx context.Context, node *pilosa.Node, inde
req.Header.Set("Content-Length", strconv.Itoa(len(buf))) req.Header.Set("Content-Length", strconv.Itoa(len(buf)))
req.Header.Set("Content-Type", "application/x-protobuf") req.Header.Set("Content-Type", "application/x-protobuf")
req.Header.Set("Accept", "application/x-protobuf") req.Header.Set("Accept", "application/x-protobuf")
req.Header.Set("X-Pilosa-Row", "roaring")
req.Header.Set("User-Agent", "pilosa/"+pilosa.Version) req.Header.Set("User-Agent", "pilosa/"+pilosa.Version)
// Execute request against the host. // Execute request against the host.
@ -553,6 +563,35 @@ func (c *InternalClient) ImportValue(ctx context.Context, index, field string, s
return nil return nil
} }
// ImportValue2 is a simplified ImportValue method which just uses the
// ImportValueRequest instead of splitting up ImportValue and
// ImportValueK... it also supports importing float values. The idea
// being that (assuming it works) this will become the default (and be
// renamed) for 2.0, and we can deprecate the other methods.
func (c *InternalClient) ImportValue2(ctx context.Context, req *pilosa.ImportValueRequest, options *pilosa.ImportOptions) error {
span, ctx := tracing.StartSpanFromContext(ctx, "InternalClient.NewImportValue")
defer span.Finish()
buf, err := c.serializer.Marshal(req)
if err != nil {
return errors.Errorf("marshal import request: %s", err)
}
// Retrieve a list of nodes that own the shard.
nodes, err := c.FragmentNodes(ctx, req.Index, req.Shard)
if err != nil {
return errors.Errorf("shard nodes: %s", err)
}
// Import to each node.
for _, node := range nodes {
if err := c.importNode(ctx, node, req.Index, req.Field, buf, options); err != nil {
return errors.Errorf("import node: host=%s, err=%s", node.URI, err)
}
}
return nil
}
// ImportValueK bulk imports keyed field values to a host. // ImportValueK bulk imports keyed field values to a host.
func (c *InternalClient) ImportValueK(ctx context.Context, index, field string, vals []pilosa.FieldValue, opts ...pilosa.ImportOption) error { func (c *InternalClient) ImportValueK(ctx context.Context, index, field string, vals []pilosa.FieldValue, opts ...pilosa.ImportOption) error {
span, ctx := tracing.StartSpanFromContext(ctx, "InternalClient.ImportValueK") span, ctx := tracing.StartSpanFromContext(ctx, "InternalClient.ImportValueK")
@ -645,6 +684,7 @@ func (c *InternalClient) ImportRoaring(ctx context.Context, uri *pilosa.URI, ind
} }
httpReq.Header.Set("Content-Type", "application/x-protobuf") httpReq.Header.Set("Content-Type", "application/x-protobuf")
httpReq.Header.Set("Accept", "application/x-protobuf") httpReq.Header.Set("Accept", "application/x-protobuf")
httpReq.Header.Set("X-Pilosa-Row", "roaring")
httpReq.Header.Set("User-Agent", "pilosa/"+pilosa.Version) httpReq.Header.Set("User-Agent", "pilosa/"+pilosa.Version)
// Execute request against the host. // Execute request against the host.
@ -667,6 +707,57 @@ func (c *InternalClient) ImportRoaring(ctx context.Context, uri *pilosa.URI, ind
return nil return nil
} }
// ImportColumnAttrs does bulk import of column attrs
func (c *InternalClient) ImportColumnAttrs(ctx context.Context, uri *pilosa.URI, index string, req *pilosa.ImportColumnAttrsRequest) error {
span, ctx := tracing.StartSpanFromContext(ctx, "InternalClient.ImportRoaring")
defer span.Finish()
if index == "" {
return pilosa.ErrIndexRequired
}
if uri == nil {
uri = c.defaultURI
}
url := fmt.Sprintf("%s/index/%s/import-column-attrs", uri, index)
// Marshal data to protobuf.
data, err := c.serializer.Marshal(req)
if err != nil {
return errors.Wrap(err, "marshal import-column-attrs request")
}
// Generate HTTP request.
httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
if err != nil {
return errors.Wrap(err, "creating request")
}
httpReq.Header.Set("Content-Type", "application/x-protobuf")
httpReq.Header.Set("Accept", "application/x-protobuf")
httpReq.Header.Set("X-Pilosa-Row", "roaring")
httpReq.Header.Set("User-Agent", "pilosa/"+pilosa.Version)
// Execute request against the host.
resp, err := c.executeRequest(httpReq.WithContext(ctx))
if err != nil {
return err
}
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
rbody := &pilosa.ImportResponse{}
err = dec.Decode(rbody)
// Decode can return EOF when no error occurred. helpful!
if err != nil && err != io.EOF {
return errors.Wrap(err, "decoding response body")
}
if rbody.Err != "" {
return errors.Wrap(errors.New(rbody.Err), "importing roaring")
}
return nil
}
// ExportCSV bulk exports data for a single shard from a host to CSV format. // ExportCSV bulk exports data for a single shard from a host to CSV format.
func (c *InternalClient) ExportCSV(ctx context.Context, index, field string, shard uint64, w io.Writer) error { func (c *InternalClient) ExportCSV(ctx context.Context, index, field string, shard uint64, w io.Writer) error {
span, ctx := tracing.StartSpanFromContext(ctx, "InternalClient.ExportCSV") span, ctx := tracing.StartSpanFromContext(ctx, "InternalClient.ExportCSV")
@ -791,18 +882,28 @@ func (c *InternalClient) CreateFieldWithOptions(ctx context.Context, index, fiel
} }
// convert pilosa.FieldOptions to fieldOptions // convert pilosa.FieldOptions to fieldOptions
//
// TODO this kind of sucks because it's one more place that needs
// changes when we change anything with field options (and there
// are a lot of places already). It's not clear to me that this is
// providing a lot of value, but I think this kind of validation
// should probably happen in the field anyway??
fieldOpt := fieldOptions{ fieldOpt := fieldOptions{
Type: opt.Type, Type: opt.Type,
Keys: &opt.Keys, Keys: &opt.Keys,
} }
if fieldOpt.Type == "set" { if fieldOpt.Type == pilosa.FieldTypeSet {
fieldOpt.CacheType = &opt.CacheType fieldOpt.CacheType = &opt.CacheType
fieldOpt.CacheSize = &opt.CacheSize fieldOpt.CacheSize = &opt.CacheSize
} else if fieldOpt.Type == "int" { } else if fieldOpt.Type == pilosa.FieldTypeInt {
fieldOpt.Min = &opt.Min fieldOpt.Min = &opt.Min
fieldOpt.Max = &opt.Max fieldOpt.Max = &opt.Max
} else if fieldOpt.Type == "time" { } else if fieldOpt.Type == pilosa.FieldTypeTime {
fieldOpt.TimeQuantum = &opt.TimeQuantum fieldOpt.TimeQuantum = &opt.TimeQuantum
} else if fieldOpt.Type == pilosa.FieldTypeDecimal {
fieldOpt.Min = &opt.Min
fieldOpt.Max = &opt.Max
fieldOpt.Scale = &opt.Scale
} }
// TODO: remove buf completely? (depends on whether importer needs to create specific field types) // TODO: remove buf completely? (depends on whether importer needs to create specific field types)
@ -909,6 +1010,7 @@ func (c *InternalClient) BlockData(ctx context.Context, uri *pilosa.URI, index,
req.Header.Set("Content-Type", "application/protobuf") req.Header.Set("Content-Type", "application/protobuf")
req.Header.Set("Content-Length", strconv.Itoa(len(buf))) req.Header.Set("Content-Length", strconv.Itoa(len(buf)))
req.Header.Set("Accept", "application/protobuf") req.Header.Set("Accept", "application/protobuf")
req.Header.Set("X-Pilosa-Row", "roaring")
req.Header.Set("User-Agent", "pilosa/"+pilosa.Version) req.Header.Set("User-Agent", "pilosa/"+pilosa.Version)
resp, err := c.executeRequest(req.WithContext(ctx)) resp, err := c.executeRequest(req.WithContext(ctx))
@ -999,7 +1101,7 @@ func (c *InternalClient) RowAttrDiff(ctx context.Context, uri *pilosa.URI, index
resp, err := c.executeRequest(req.WithContext(ctx)) resp, err := c.executeRequest(req.WithContext(ctx))
if err != nil { if err != nil {
if resp != nil && resp.StatusCode == http.StatusNotFound { if resp != nil && resp.StatusCode == http.StatusNotFound {
return nil, pilosa.ErrFieldNotFound return nil, errors.Wrap(pilosa.ErrFieldNotFound, field)
} }
return nil, err return nil, err
} }
@ -1026,21 +1128,291 @@ func (c *InternalClient) SendMessage(ctx context.Context, uri *pilosa.URI, msg [
req.Header.Set("Content-Type", "application/x-protobuf") req.Header.Set("Content-Type", "application/x-protobuf")
req.Header.Set("User-Agent", "pilosa/"+pilosa.Version) req.Header.Set("User-Agent", "pilosa/"+pilosa.Version)
req.Header.Set("Accept", "application/json") req.Header.Set("Accept", "application/json")
req.Header.Set("Connection", "keep-alive")
// Execute request. // Execute request.
resp, err := c.executeRequest(req.WithContext(ctx)) resp, err := c.executeRequest(req.WithContext(ctx))
if err != nil { if err != nil {
return errors.Wrap(err, "executing request") return errors.Wrap(err, "executing request")
} }
return errors.Wrap(resp.Body.Close(), "closing response body") defer resp.Body.Close()
_, err = io.Copy(ioutil.Discard, resp.Body)
return errors.Wrap(err, "draining SendMessage response body")
}
// TranslateKeysNode function is mainly called to translate keys from coordinator node.
// If coordinator node returns 404 error the function wraps it with pilosa.ErrTranslatingKeyNotFound.
func (c *InternalClient) TranslateKeysNode(ctx context.Context, uri *pilosa.URI, index, field string, keys []string, writable bool) ([]uint64, error) {
span, ctx := tracing.StartSpanFromContext(ctx, "TranslateKeysNode")
defer span.Finish()
if index == "" {
return nil, pilosa.ErrIndexRequired
}
buf, err := c.serializer.Marshal(&pilosa.TranslateKeysRequest{
Index: index,
Field: field,
Keys: keys,
NotWritable: !writable,
})
if err != nil {
return nil, errors.Wrap(err, "marshaling TranslateKeysRequest")
}
// Create HTTP request.
u := uri.Path("/internal/translate/keys")
req, err := http.NewRequest("POST", u, bytes.NewReader(buf))
if err != nil {
return nil, errors.Wrap(err, "creating request")
}
req.Header.Set("Content-Length", strconv.Itoa(len(buf)))
req.Header.Set("Content-Type", "application/x-protobuf")
req.Header.Set("Accept", "application/x-protobuf")
req.Header.Set("X-Pilosa-Row", "roaring")
req.Header.Set("User-Agent", "pilosa/"+pilosa.Version)
// Execute request against the host.
resp, err := c.executeRequest(req.WithContext(ctx))
if err != nil {
if resp != nil && resp.StatusCode == http.StatusNotFound {
return nil, errors.Wrap(pilosa.ErrTranslatingKeyNotFound, err.Error())
}
return nil, err
}
defer resp.Body.Close()
// Read body and unmarshal response.
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "reading")
}
tkresp := &pilosa.TranslateKeysResponse{}
if err := c.serializer.Unmarshal(body, tkresp); err != nil {
return nil, fmt.Errorf("unmarshal response: %s", err)
}
return tkresp.IDs, nil
}
// TranslateIDsNode sends an id translation request to a specific node.
func (c *InternalClient) TranslateIDsNode(ctx context.Context, uri *pilosa.URI, index, field string, ids []uint64) ([]string, error) {
span, ctx := tracing.StartSpanFromContext(ctx, "TranslateIDsNode")
defer span.Finish()
if index == "" {
return nil, pilosa.ErrIndexRequired
}
buf, err := c.serializer.Marshal(&pilosa.TranslateIDsRequest{
Index: index,
Field: field,
IDs: ids,
})
if err != nil {
return nil, errors.Wrap(err, "marshaling TranslateIDsRequest")
}
// Create HTTP request.
u := uri.Path("/internal/translate/ids")
req, err := http.NewRequest("POST", u, bytes.NewReader(buf))
if err != nil {
return nil, errors.Wrap(err, "creating request")
}
req.Header.Set("Content-Length", strconv.Itoa(len(buf)))
req.Header.Set("Content-Type", "application/x-protobuf")
req.Header.Set("Accept", "application/x-protobuf")
req.Header.Set("X-Pilosa-Row", "roaring")
req.Header.Set("User-Agent", "pilosa/"+pilosa.Version)
// Execute request against the host.
resp, err := c.executeRequest(req.WithContext(ctx))
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Read body and unmarshal response.
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "reading")
}
tkresp := &pilosa.TranslateIDsResponse{}
if err := c.serializer.Unmarshal(body, tkresp); err != nil {
return nil, fmt.Errorf("unmarshal response: %s", err)
}
return tkresp.Keys, nil
}
func (c *InternalClient) Transactions(ctx context.Context) (map[string]*pilosa.Transaction, error) {
span, ctx := tracing.StartSpanFromContext(ctx, "InternalClient.Transactions")
defer span.Finish()
u := uriPathToURL(c.defaultURI, "/transactions")
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, errors.Wrap(err, "creating transactions request")
}
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "pilosa/"+pilosa.Version)
resp, err := c.executeRequest(req.WithContext(ctx))
if err != nil {
return nil, errors.Wrap(err, "executing request")
}
defer func() {
_, _ = io.Copy(ioutil.Discard, resp.Body)
_ = resp.Body.Close()
}()
trnsMap := make(map[string]*pilosa.Transaction)
err = json.NewDecoder(resp.Body).Decode(&trnsMap)
return trnsMap, errors.Wrap(err, "json decoding")
}
func (c *InternalClient) StartTransaction(ctx context.Context, id string, timeout time.Duration, exclusive bool) (*pilosa.Transaction, error) {
span, ctx := tracing.StartSpanFromContext(ctx, "InternalClient.StartTransaction")
defer span.Finish()
buf, err := json.Marshal(&pilosa.Transaction{
ID: id,
Timeout: timeout,
Exclusive: exclusive,
})
if err != nil {
return nil, errors.Wrap(err, "marshalling payload")
}
// We're using the defaultURI here because this is only used by
// tests, and we want to test requests against all hosts. A robust
// client implementation would ensure that these requests go to
// the coordinator.
u := uriPathToURL(c.defaultURI, "/transaction/"+id)
req, err := http.NewRequest("POST", u.String(), bytes.NewReader(buf))
if err != nil {
return nil, errors.Wrap(err, "creating post transaction request")
}
req.Header.Set("Content-Length", strconv.Itoa(len(buf)))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "pilosa/"+pilosa.Version)
resp, err := c.executeRequest(req.WithContext(ctx), giveRawResponse(true))
if err != nil {
return nil, errors.Wrap(err, "executing request")
}
defer func() {
_, _ = io.Copy(ioutil.Discard, resp.Body)
_ = resp.Body.Close()
}()
tr := &TransactionResponse{}
err = json.NewDecoder(resp.Body).Decode(tr)
if err != nil {
return nil, errors.Wrap(err, "decoding response")
}
if resp.StatusCode == 409 {
err = pilosa.ErrTransactionExclusive
} else if tr.Error != "" {
err = errors.New(tr.Error)
}
return tr.Transaction, err
}
func (c *InternalClient) FinishTransaction(ctx context.Context, id string) (*pilosa.Transaction, error) {
span, ctx := tracing.StartSpanFromContext(ctx, "InternalClient.FinishTransaction")
defer span.Finish()
u := uriPathToURL(c.defaultURI, "/transaction/"+id+"/finish")
req, err := http.NewRequest("POST", u.String(), nil)
if err != nil {
return nil, errors.Wrap(err, "creating finish transaction request")
}
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "pilosa/"+pilosa.Version)
resp, err := c.executeRequest(req.WithContext(ctx), giveRawResponse(true))
if err != nil {
return nil, errors.Wrap(err, "executing request")
}
defer func() {
_, _ = io.Copy(ioutil.Discard, resp.Body)
_ = resp.Body.Close()
}()
tr := &TransactionResponse{}
err = json.NewDecoder(resp.Body).Decode(tr)
if err != nil {
return nil, errors.Wrap(err, "decoding response")
}
if tr.Error != "" {
err = errors.New(tr.Error)
}
return tr.Transaction, err
}
func (c *InternalClient) GetTransaction(ctx context.Context, id string) (*pilosa.Transaction, error) {
span, ctx := tracing.StartSpanFromContext(ctx, "InternalClient.GetTransaction")
defer span.Finish()
// We're using the defaultURI here because this is only used by
// tests, and we want to test requests against all hosts. A robust
// client implementation would ensure that these requests go to
// the coordinator.
u := uriPathToURL(c.defaultURI, "/transaction/"+id)
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, errors.Wrap(err, "creating get transaction request")
}
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "pilosa/"+pilosa.Version)
resp, err := c.executeRequest(req.WithContext(ctx), giveRawResponse(true))
if err != nil {
return nil, errors.Wrap(err, "executing request")
}
defer func() {
_, _ = io.Copy(ioutil.Discard, resp.Body)
_ = resp.Body.Close()
}()
tr := &TransactionResponse{}
err = json.NewDecoder(resp.Body).Decode(tr)
if err != nil {
return nil, errors.Wrap(err, "decoding response")
}
if tr.Error != "" {
err = errors.New(tr.Error)
}
return tr.Transaction, err
}
type executeOpts struct {
// giveRawResponse instructs executeRequest not to process the
// respStatusCode and try to extract errors or whatever.
giveRawResponse bool
}
type executeRequestOption func(*executeOpts)
func giveRawResponse(b bool) executeRequestOption {
return func(eo *executeOpts) {
eo.giveRawResponse = b
}
} }
// executeRequest executes the given request and checks the Response. For // executeRequest executes the given request and checks the Response. For
// responses with non-2XX status, the body is read and closed, and an error is // responses with non-2XX status, the body is read and closed, and an error is
// returned. If the error is nil, the caller must ensure that the response body // returned. If the error is nil, the caller must ensure that the response body
// is closed. // is closed.
func (c *InternalClient) executeRequest(req *http.Request) (*http.Response, error) { func (c *InternalClient) executeRequest(req *http.Request, opts ...executeRequestOption) (*http.Response, error) {
eo := &executeOpts{}
for _, opt := range opts {
opt(eo)
}
tracing.GlobalTracer.InjectHTTPHeaders(req) tracing.GlobalTracer.InjectHTTPHeaders(req)
req.Close = false
resp, err := c.httpClient.Do(req) resp, err := c.httpClient.Do(req)
if err != nil { if err != nil {
if resp != nil { if resp != nil {
@ -1048,6 +1420,9 @@ func (c *InternalClient) executeRequest(req *http.Request) (*http.Response, erro
} }
return nil, errors.Wrap(err, "getting response") return nil, errors.Wrap(err, "getting response")
} }
if eo.giveRawResponse {
return resp, nil
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
defer resp.Body.Close() defer resp.Body.Close()
buf, err := ioutil.ReadAll(resp.Body) buf, err := ioutil.ReadAll(resp.Body)
@ -1278,3 +1653,103 @@ func nodePathToURL(node *pilosa.Node, path string) url.URL {
Path: path, Path: path,
} }
} }
// RetrieveTranslatePartitionFromURI returns a ReadCloser which contains the data of the
// specified translate partition from the specified node. Caller *must* close the returned
// ReadCloser or risk leaking goroutines/tcp connections.
func (c *InternalClient) RetrieveTranslatePartitionFromURI(ctx context.Context, index string, partition int, uri pilosa.URI) (io.ReadCloser, error) {
span, ctx := tracing.StartSpanFromContext(ctx, "InternalClient.RetrieveTranslatePartitionFromURI")
defer span.Finish()
node := &pilosa.Node{
URI: uri,
}
u := nodePathToURL(node, "/internal/translate/data")
u.RawQuery = url.Values{
"index": {index},
"partition": {strconv.FormatInt(int64(partition), 10)},
}.Encode()
// Build request.
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, errors.Wrap(err, "creating request")
}
req.Header.Set("User-Agent", "pilosa/"+pilosa.Version)
// Execute request.
resp, err := c.executeRequest(req.WithContext(ctx))
if err != nil {
if resp != nil && resp.StatusCode == http.StatusNotFound {
return nil, pilosa.ErrFragmentNotFound
}
return nil, err
}
return resp.Body, nil
}
func (c *InternalClient) ImportIndexKeys(ctx context.Context, uri *pilosa.URI, index string, partitionID int, remote bool, rddbdata io.Reader) error {
span, ctx := tracing.StartSpanFromContext(ctx, "InternalClient.ImportIndexKeys")
defer span.Finish()
if index == "" {
return pilosa.ErrIndexRequired
}
if uri == nil {
uri = c.defaultURI
}
vals := url.Values{}
vals.Set("remote", strconv.FormatBool(remote))
url := fmt.Sprintf("%s/internal/translate/index/%s/%d", uri, index, partitionID)
// Generate HTTP request.
httpReq, err := http.NewRequest("POST", url, rddbdata)
if err != nil {
return errors.Wrap(err, "creating request")
}
httpReq.Header.Set("User-Agent", "pilosa/"+pilosa.Version)
// Execute request against the host.
resp, err := c.executeRequest(httpReq.WithContext(ctx))
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
func (c *InternalClient) ImportFieldKeys(ctx context.Context, uri *pilosa.URI, index, field string, remote bool, rddbdata io.Reader) error {
span, ctx := tracing.StartSpanFromContext(ctx, "InternalClient.ImportFieldKeys")
defer span.Finish()
if index == "" {
return pilosa.ErrIndexRequired
}
if uri == nil {
uri = c.defaultURI
}
vals := url.Values{}
vals.Set("remote", strconv.FormatBool(remote))
url := fmt.Sprintf("%s/internal/translate/field/%s/%s", uri, index, field)
// Generate HTTP request.
httpReq, err := http.NewRequest("POST", url, rddbdata)
if err != nil {
return errors.Wrap(err, "creating request")
}
httpReq.Header.Set("User-Agent", "pilosa/"+pilosa.Version)
// Execute request against the host.
resp, err := c.executeRequest(httpReq.WithContext(ctx))
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -22,6 +22,7 @@ import (
"testing" "testing"
"github.com/pilosa/pilosa/v2" "github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/pql"
) )
// Test custom UnmarshalJSON for postIndexRequest object // Test custom UnmarshalJSON for postIndexRequest object
@ -99,8 +100,8 @@ func stringPtr(s string) *string {
return &s return &s
} }
func int64Ptr(i int64) *int64 { func decimalPtr(d pql.Decimal) *pql.Decimal {
return &i return &d
} }
// Test fieldOption validation. // Test fieldOption validation.
@ -135,10 +136,10 @@ func TestFieldOptionValidation(t *testing.T) {
// FieldType: Int // FieldType: Int
{json: `{"options": {"type": "int"}}`, err: "min is required for field type int"}, {json: `{"options": {"type": "int"}}`, err: "min is required for field type int"},
{json: `{"options": {"type": "int", "min": 0}}`, err: "max is required for field type int"}, {json: `{"options": {"type": "int", "min": 0}}`, err: "max is required for field type int"},
{json: `{"options": {"type": "int", "min": 0, "max": 1000}}`, expected: postFieldRequest{Options: fieldOptions{ {json: `{"options": {"type": "int", "min": 0, "max": 1001}}`, expected: postFieldRequest{Options: fieldOptions{
Type: pilosa.FieldTypeInt, Type: pilosa.FieldTypeInt,
Min: int64Ptr(0), Min: decimalPtr(pql.NewDecimal(0, 0)),
Max: int64Ptr(1000), Max: decimalPtr(pql.NewDecimal(1001, 0)),
}}}, }}},
{json: `{"options": {"type": "int", "min": 0, "max": 1000, "cacheType": "ranked"}}`, err: "cacheType does not apply to field type int"}, {json: `{"options": {"type": "int", "min": 0, "max": 1000, "cacheType": "ranked"}}`, err: "cacheType does not apply to field type int"},
{json: `{"options": {"type": "int", "min": 0, "max": 1000, "cacheSize": 1000}}`, err: "cacheSize does not apply to field type int"}, {json: `{"options": {"type": "int", "min": 0, "max": 1000, "cacheSize": 1000}}`, err: "cacheSize does not apply to field type int"},

View file

@ -15,11 +15,13 @@
package http_test package http_test
import ( import (
"encoding/json"
"net" "net"
"testing" "testing"
"github.com/pilosa/pilosa/v2" "github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/http" "github.com/pilosa/pilosa/v2/http"
"github.com/pilosa/pilosa/v2/test"
) )
func TestHandlerOptions(t *testing.T) { func TestHandlerOptions(t *testing.T) {
@ -40,3 +42,39 @@ func TestHandlerOptions(t *testing.T) {
t.Fatalf("expected error making handler without options, got nil") t.Fatalf("expected error making handler without options, got nil")
} }
} }
func TestMarshalUnmarshalTransactionResponse(t *testing.T) {
tests := []struct {
name string
tr *http.TransactionResponse
}{
{
name: "nil transaction",
tr: &http.TransactionResponse{},
},
{
name: "empty transaction",
tr: &http.TransactionResponse{Transaction: &pilosa.Transaction{}},
},
}
for _, tst := range tests {
t.Run(tst.name, func(t *testing.T) {
data, err := json.Marshal(tst.tr)
if err != nil {
t.Fatalf("marshaling: %v", err)
}
mytr := &http.TransactionResponse{}
err = json.Unmarshal(data, mytr)
if err != nil {
t.Fatalf("unmarshalling: %v", err)
}
if mytr.Error != tst.tr.Error {
t.Errorf("errors mismatch:exp/got \n%v\n%v", tst.tr.Error, mytr.Error)
}
test.CompareTransactions(t, tst.tr.Transaction, mytr.Transaction)
})
}
}

View file

@ -22,19 +22,26 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"sync"
"github.com/pilosa/pilosa/v2" "github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/logger" "github.com/pilosa/pilosa/v2/logger"
) )
func GetOpenTranslateReaderFunc(client *http.Client) pilosa.OpenTranslateReaderFunc { func GetOpenTranslateReaderFunc(client *http.Client) pilosa.OpenTranslateReaderFunc {
return GetOpenTranslateReaderWithLockerFunc(client, nopLocker{})
}
func GetOpenTranslateReaderWithLockerFunc(client *http.Client, locker sync.Locker) pilosa.OpenTranslateReaderFunc {
return func(ctx context.Context, nodeURL string, offsets pilosa.TranslateOffsetMap) (pilosa.TranslateEntryReader, error) { return func(ctx context.Context, nodeURL string, offsets pilosa.TranslateOffsetMap) (pilosa.TranslateEntryReader, error) {
return openTranslateReader(ctx, nodeURL, offsets, client) return openTranslateReader(ctx, nodeURL, offsets, client, locker)
} }
} }
func openTranslateReader(ctx context.Context, nodeURL string, offsets pilosa.TranslateOffsetMap, client *http.Client) (pilosa.TranslateEntryReader, error) { func openTranslateReader(ctx context.Context, nodeURL string, offsets pilosa.TranslateOffsetMap, client *http.Client, locker sync.Locker) (pilosa.TranslateEntryReader, error) {
r := NewTranslateEntryReader(ctx, client) r := NewTranslateEntryReader(ctx, client)
r.locker = locker
r.URL = nodeURL + "/internal/translate/data" r.URL = nodeURL + "/internal/translate/data"
r.Offsets = offsets r.Offsets = offsets
if err := r.Open(); err != nil { if err := r.Open(); err != nil {
@ -43,9 +50,16 @@ func openTranslateReader(ctx context.Context, nodeURL string, offsets pilosa.Tra
return r, nil return r, nil
} }
type nopLocker struct{}
func (nopLocker) Lock() {}
func (nopLocker) Unlock() {}
// TranslateEntryReader represents an implementation of pilosa.TranslateEntryReader. // TranslateEntryReader represents an implementation of pilosa.TranslateEntryReader.
// It consolidates all index & field translate entries into a single reader. // It consolidates all index & field translate entries into a single reader.
type TranslateEntryReader struct { type TranslateEntryReader struct {
locker sync.Locker
ctx context.Context ctx context.Context
cancel func() cancel func()
@ -70,7 +84,7 @@ func NewTranslateEntryReader(ctx context.Context, client *http.Client) *Translat
if client == nil { if client == nil {
client = http.DefaultClient client = http.DefaultClient
} }
r := &TranslateEntryReader{HTTPClient: client, Logger: logger.NopLogger} r := &TranslateEntryReader{locker: nopLocker{}, HTTPClient: client, Logger: logger.NopLogger}
r.ctx, r.cancel = context.WithCancel(ctx) r.ctx, r.cancel = context.WithCancel(ctx)
return r return r
} }
@ -116,7 +130,10 @@ func (r *TranslateEntryReader) Close() error {
r.cancel() r.cancel()
} }
if r.body != nil { if r.body != nil {
return r.body.Close() r.locker.Lock()
err := r.body.Close()
r.locker.Unlock()
return err
} }
return nil return nil
} }
@ -124,5 +141,8 @@ func (r *TranslateEntryReader) Close() error {
// ReadEntry reads the next entry from the stream into entry. // ReadEntry reads the next entry from the stream into entry.
// Returns io.EOF at the end of the stream. // Returns io.EOF at the end of the stream.
func (r *TranslateEntryReader) ReadEntry(entry *pilosa.TranslateEntry) error { func (r *TranslateEntryReader) ReadEntry(entry *pilosa.TranslateEntry) error {
r.locker.Lock()
defer r.locker.Unlock()
return r.dec.Decode(&entry) return r.dec.Decode(&entry)
} }

View file

@ -17,6 +17,7 @@ package http_test
import ( import (
"context" "context"
"fmt" "fmt"
"sync"
"testing" "testing"
"time" "time"
@ -35,7 +36,7 @@ func TestTranslateStore_EntryReader(t *testing.T) {
cluster := test.MustRunCluster(t, 1) cluster := test.MustRunCluster(t, 1)
defer cluster.Close() defer cluster.Close()
primary := cluster[0] primary := cluster.GetNode(0)
hldr := test.Holder{Holder: primary.Server.Holder()} hldr := test.Holder{Holder: primary.Server.Holder()}
index := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{Keys: true}) index := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{Keys: true})
@ -151,3 +152,84 @@ func TestTranslateStore_EntryReader(t *testing.T) {
}) })
*/ */
} }
func benchmarkSetup(b *testing.B, ctx context.Context, key string, nkeys int) (string, pilosa.TranslateOffsetMap, func()) {
b.Helper()
cluster := test.MustRunCluster(b, 1)
primary := cluster.GetNode(0)
idx := primary.MustCreateIndex(b, "i", pilosa.IndexOptions{})
fld := primary.MustCreateField(b, idx.Name(), "f", pilosa.OptFieldKeys())
offset := make(pilosa.TranslateOffsetMap)
offset.SetIndexPartitionOffset(idx.Name(), 0, 1)
offset.SetFieldOffset(idx.Name(), fld.Name(), 1)
// Set data on the primary node.
for k := 0; k < nkeys; k++ {
if _, err := primary.API.Query(ctx, &pilosa.QueryRequest{
Index: idx.Name(),
Query: fmt.Sprintf(`Set(%d, %s="%s%[1]d")`, k, fld.Name(), key),
}); err != nil {
b.Fatalf("quering api: %+v", err)
}
}
return primary.URL(), offset, func() {
b.Helper()
if err := primary.API.DeleteIndex(ctx, idx.Name()); err != nil {
panic(err)
}
if err := cluster.Close(); err != nil {
panic(err)
}
}
}
func benchmarkReadEntry(b *testing.B, r pilosa.TranslateEntryReader, key string, nkeys int) {
var entry pilosa.TranslateEntry
for k := 0; k < nkeys; k++ {
if err := r.ReadEntry(&entry); err != nil {
b.Fatalf("reading entry: %+v", err)
}
if entry.Key != fmt.Sprintf("%s%d", key, k) {
b.Fatalf("got: %s, expected: %s%d", entry.Key, key, k)
}
}
}
const (
key = "foo"
nkeys = 1000
)
func BenchmarkReadEntryNoMutex(b *testing.B) {
ctx := context.Background()
url, offset, teardown := benchmarkSetup(b, ctx, key, nkeys)
defer teardown()
for n := 0; n < b.N; n++ {
r, err := http.GetOpenTranslateReaderFunc(nil)(ctx, url, offset)
if err != nil {
b.Fatalf("opening translate reader: %+v", err)
}
benchmarkReadEntry(b, r, key, nkeys)
r.Close()
}
}
func BenchmarkReadEntryWithMutex(b *testing.B) {
ctx := context.Background()
url, offset, teardown := benchmarkSetup(b, ctx, key, nkeys)
defer teardown()
for n := 0; n < b.N; n++ {
r, err := http.GetOpenTranslateReaderWithLockerFunc(nil, &sync.Mutex{})(ctx, url, offset)
if err != nil {
b.Fatalf("opening translate reader: %+v", err)
}
benchmarkReadEntry(b, r, key, nkeys)
r.Close()
}
}

Some files were not shown because too many files have changed in this diff Show more