featurebase/fragment.go
Travis 04aa8b18fc Add DAX - full list of squashed commits below
In this commit, the Directive is mocked; it doesn't actually reach out
to a controller.

Limits key translation to only those partitions (per index) specified in
the Directive. Attempting to create or find a key (or ID) for a
partition which is not handled by this node will result in an error;
translation requests are no longer forwarded to other nodes.

Limits import into only those shards specified, per index, by the
Directive. Attempting to import into a shard which is not handled by
this node will result in an error.

Stub out /directive endpoint

The `applyDirective()` method still needs to be implemented.

Update mds references to use the new /mds/types structure

In mds, we moved the shared types to mds/types. FeatureBase needs to
reference those instead.

This also bumps the mds version in go.mod.

Implement the Add/Remove Index part of Holder.ApplyDirective()

This adds functionality to `Holder.ApplyDirective()` which adds or
removes indexes (tables) based on those provided in the Directive. Still
to be implemented here: shards and partitions.

WIP: remove client from Batch

Move Batch into its own package: batch

Also, in order to avoid import loops, this introduces packages:
/batch/types
/client/types

Reorganize the Importer-related code

Moved the Importer interface to package: batch
Move the "pilosa client" implementation of the Importer interface to
package: client

Modify batch.NewBatch to take an Importer (not client)

This commit modifies the batch.NewBatch() function to use a functinal
option on Batch to inject an Importer into the Batch. Prior to this,
NewBatch() took a pointer to a client, which was a little too
restrictive. Now, MDS can implement an Importer which uses information
from MDS to determine to which node(s) the import calls should be directed.

Add client.SetAuthToken() method to satisfy SchemaManager interface

Update ApplyDirective logic to include fields.

This needs more work, but it was enough to get a basic test passing.

Move Transaction type into /types package.

Add interface check on batch.Importer no-op implementation

Updated ApplyDirective to create all currently support Field types

There are still the following TODOs:
- [ ] impolement field options (ex: decimal scale, int min/max, etc).
- [ ] `time` fields

Added support for Decimal.Scale in ApplyDirective

Update mds dependency

Add /health endpoint

Update to use dax (dax/mds) instead of mds.

After moving the mds repository into the dax repository as a
sub-package, this commit changes everything in FeatureBase to use the
dax repo instead of the now abandoned mds repo.

Introduce and implment the WriteLogger interfaces.

This adds both a `WriteLogReader` and `WriteLogWriter` interface. They
are both implemented by the implementation: `fileWriteLogger`. The
`fileWriteLogger` uses the dax/writelogger API to append log messages to
files on disk.

Add WriteLogWriter.ImportRoaring method to interface

This commit adds the `ImportRoaring` method to the `WriteLogWriter`
interface. Still to implement are the `Import` and `ImportValue`
methods.

Reorganize the ApplyDirective code

The primary goal was to cache the incoming Directive on the Holder prior
to applying all of the changes in the directive (i.e. loading data from
the WriteLogger) because applying those changes often validated against
the accepted state of the node.

Implement all of the WriteLogger read/write methods

Implement the HTTP WriteLogger implementation

WIP: Introduce shard.Version. Implement snapshotter.

Add HTTP Snapshotter implementation

This also recofigures server to use the HTTPSnapshotter instead of the
FileSnapshotter.

Implement snapshotter: TableKeys

Implement snapshotter: FieldKeys

Dependency dance

last of the dependency dance

Add support for prototype

This adds the Makefile targets to build the docker container and push it
to ECR.

SQL3 changes which break with dax changes

Missed TODO: implement FieldVersion version to WriteLogger

Address bug causing missing TranslateStores to error

Originally, we tried to limit the TranslateStores which get allocated to
only those for which the node is responsible. This works when adding a
new table. But if a table already exists, there's no logic to start
missing TranslateStores.

This reverts back to the old FeatureBase logic which brutishly allocates
a TranslateStore for every partition, even if one is not needed.

We need to address this by allowing the ApplyDirective logic to
initialize TranslateStores when they don't yet exist.

Move the ImportRoaringShardRequest type to the types package

Since the ImportRoaringShardRequest object is part of the Importer
interface, we need to move it to a non-root (i.e. pilosa) package. All
the other interface types are either concrete types or part of a
sub-package (such as roaring). We do this to prevent an implementer of
the interface from having to import the entire pilosa package and risk
circular imports.

buncha changes to support latest dax stuff

Move dax related types to /dax sub-package

This commit moves all the common "dax" types into the /dax sub-package.
The idea is to ensure that featurebase does not import dax at all.
It's ok if dax imports featurebase.
In the future, we might need to split the dax sub-package (common data
types used by muliple molecula data-plan services) into it's own repo.

Add type: dax.Schema

This isn't currently being used; I started to use is as a replacement
for pilosa_client.Schema, but then deferred that. But we'll need to do
it eventually, so it doesn't hurt to have this type in place.

Export RowIDs.Merge() method for use in orchestrator.

Add CreateSQL method to dax.Table type

The CreateSQL() method will return the "CREATE TABLE" statement required
to create the dax.Table.

Comment out confusing writelogger log message.

We need to revisit this, but for now, this log message is confusing.

Also, rename daxSharder to versionStore.

Remove hard-coded AWS account

Implement more FieldOptions such as Epoch

Some of the FieldOption logic was stubbed out in the dax package. This
commit fills that out more; specifically, it adds the
dax.Field.Options.Epoch parameter.

export stuff needed for TopK in orchestrator

export ValCount stuff to implement Percentile in orchestrator

export more stuff to support less code in orchestrator, shared objs

Port dax repo over to featurebase/dax (run all as sub-services)

This commit does ALOT. Sorry.
It introduces a `featurebase dax` sub-command which can be configured to
run the various dax services as sub-services within the same process, or
individually as the lone service in process.

It also changes all the URL paths to be prefixed with the service name.
So for example, instead of calling localhost:8080/status, you would now
call localhost:8080/featurebase/status.

Also, note that all services provide a /health endpoint to confirm they
are running in process.

Clean up integration tests. Remove PILOSA_ config prefix.

Remove duplicate clients (mistake from porting dax to featurebase)

Rename sub-service "featurebase" to "computer"

In the places where we have hard-coded the sub-service name into a URI
path, I've tried to tag the line with a comment containing:
`// #SERVICEPATHPREFIX`

Update copilot manifest files to reference "computer"

Port dax/README.md from dax repository

Separate (toml) Queryer Config from Injections

We needed to separate the toml config from the configuration required to
inject sub-services into the Queryer. I'm not sure this is the best
solution, but it's *a* solution. So here we are.

Clean up (i.e. remove) the queryer "implementations" package

Remove old test file

Run WriteLogger and Snapshotter as local sub-services.

Prior to this commit, the writelogger and snapshotter services only
worked when run as separate services. This allows them to be run in the
same process as all the other dax services.

There is still some naming issues that we should address, but it's
functional for now.

Clean up (i.e. organize) the intra-service interfaces.

Implement alpha Director for local messages from MDS to Computer

Prior to this commit, messages from MDS to the computer service were
still going over http. This commit introduces an interface
implementation which registers the local computer command, and use that
command's API to directly reference methods used by the Director.

Clean up a few more interface names

Add Queryer OpenAPI document.

Update copilot manifests to reflect latest changes

Add OpenAPI documents for WriteLogger and Snapshotter

Add OpenAPI document for MDS service

Add OpenAPI document for Computer service

Consolidate errors to use fb/errors package.

This commit is a first pass at trying to ensure that all of the DAX code
uses:
"github.com/molecula/featurebase/v3/errors"

This package is a wrapper for "github.com/pkg/errors", so going forward
we want to avoid importing that package.

The only method which isn't backward-compatible is `New()`; the
New() method in the featurebase/errors package takes an errors.Code. If
this becomes a problem, we could change this by reverting New() and then
introducing something like NewCoded(). But for now I think it might
actually discourage someone from just creating a New() error without
thinking about how it should be coded.

Introduce VersionStore interface

Move the existing VersionStore code to the `inmem` package as the
in-memory implementation of the new dax.VersionStore interface.

Introduce NodeService interface

With this, the Controller can maintain a registry of nodes by using this
NodeService interface as opposed to an in-memory map of nodes on the
Controller struct.

This also adds an inmem implementation of the NodeService interface.

Introduce controller.Balancer interface

This moves the existing balancer package to controller/naive package.
The idea is to allow us to add a different Balancer implementation in
the future.

Introduce DirectiveVersion interface

This commit also includes *A LOT* of refactoring to use dax.Worker and
dax.Job types everywhere instead of strings.

Introduce Schemar interface

The previous `Schemar` struct was moved to the `schemar/inmem` package,
and `Schemar` is now an interface implemented by that inmem package.

Remove unused type `nUnit`

Add boltdb implementation of VersionStore interface.

This removed the previous sqlite implementation; we decided not to use
sqlite for now (as a basic, local disk implementation) because it
requires CGO.

--------------------------------------------
No longer applicable:

Add sqlite implementation of VersionStore interface.

This commit implements the VersionStore interface using sqlite. Sqlite
requires CGO, so this may not be something we want to include, but it's
implemented here to get a feel for how an external implementation might
be used; the next step will be to determine how the user configured
FeatureBase to run using sqlite as a backing store for services like
MDS.

Add boltdb implementation of NodeService and DirectiveVersion interfaces.

Add boltdb implementation of naive Balancer interfaces.

This includes the two interfaces defined in `naive/balancer.go`:
- WorkerJobService
- FreeJobService

Add boltdb implementation of Schemar interface.

clean up a linter issue

Thread context.Context through all the interfaces.

Some of the interface implementations are going to use context, so we
need to make that part of the interface. The boltdb implementations, for
example, take a context. This is probably so we can do things like
cancel or timeout operations.

Update interfaces to return error; remove `panic(err)` everywhere.

Down-rev grpc version to 1.38.0

Later versions (after 1.42.0?) cause MustRunCluster.Close() in tests to
deadlock.

This commit also adds an `isComputeNode` feature flag around some of the
write log and shard/partition check functionality so that it doesn't run
under normal conditions (this is excercised by running the sql3 tests
for example).

Add MDS_Persistence test to cover meta data persistence

This adds a basic test which configures the MDS container to use boltdb
as its persistence storage, saved on a docker volume. Then, the mds
container is stopped/replaced, and we confirm that the data stored on
the volume is availble to the new MDS container.

Fix a few things after rebase with sql-experiment branch

The lastest version of sql-experiment contains a fairly significan
refactor of the way query iteration works. This commit adjusts for those
changes.

pull dax IDK changes in to FB IDK (#2177)

* pull dax IDK changes in to FB IDK

* Move docker-related IDK build stuff to featurebase root

Building the docker image required the root level go.mod and vendor
directory. This change moves the make targets to the root level
Makefile, and the Dockerfiles now copy the root level vendor directory
(and everything else in the root for that matter).

* Fix batch- and client-related tests

* InitializePoller on MDS restart/replacement

Prior to this change, if MDS was restarted, its internal poller (which
maintains an in-memory list of nodes to poll) is empty. This is bad,
because it doesn't know about nodes that it should be polling.

This change fixes that. Upon MDS startup, it intializes the poller with
the list of nodes that MDS keeps in persistent storage (currently:
boltdb).

* Add EFS volume to MDS Copilot manifest

This allows us to use MDS's persistent storage (via boltdb) in the
Copilot demo by saving metadata in a boltdb file on EFS.

* Thread logger.Logger through all dax components

* Revert some of the breaking changes from DAX development.

When we first started prototyping DAX, we made changes to the
featurebase core code which would have broken the existing featurebase
functionality. This commit reverts some of those changes. Anywhere that
we need to modify core featurebase functionilty, we put it behind some
kind of feature flag. This flag is typically determined by whether the
running node is a "compute" node (i.e. DAX.COMPUTER.RUN = true).

Co-authored-by: Travis Turner <travis@molecula.com>

add packaging for DAX

need cgo for datagen build

bind to 0.0.0.0, pass GOOS and GOARCH explicitly

not sure if the explicit GOOS/GOARCH is actually necessary...

Get INSERT INTO (aka ingest) working through SQL3

This commit does a few things which I'll try do describe here.

- Introduces a Qctx interface. The existing Qcx is an implementation of
  this interface, and can be used exactly how it has been. But this
  allows us to abstract away the notion of Qcx in the Queryer (which is
  handling SQL3) until we're ready to address that. As an example, the
  Qcx has a notion of a featurebase Holder, but that doesn't make sense
  when we're at the Queryer layer. For now, the Qctx used in the Queryer
  is a no-op.

- Adds a ComputeAPI interface implementation for the Queryer. This is
  effectively the Import() and ImportValues() methods used for ingest.
  The logic here handles the incoming ImportRequest by first doing any
  necessary column and row translation for the entire request, then it
  splits the records by shard, and generates a new ImportRequest per
  shard with only the shard-appropriate records.

- Changes the mds.Importer to take an MDS interface implementation
  (which can be an mds client) instead of an mdsAddress. This allows us
  to use a localy MDS implementation rather than assuming we need a
  client to make calls over a network.

Add queryer.Importer interface to handle ingest via SQL (#2203)

* Add queryer.Importer interface to handle ingest via SQL

This is meant to support ingest through SQL when the queryer and the
compute services are running in the same process, or when they are on
seperate processes and need to talk via http client.

* remove datagen from RPM

was originally added as a convenience to generate test data, but is
unused and annoying because datagen doesn't easily cross-compile due
to cgo

* add marshalUnmarshal to controller to avoid passing pointers

passing pointers across API boundaries can cause unpredictable things
in local vs remote configurations.

Co-authored-by: Matthew Jaffee <jaffee@pilosa.com>

"fix" a few issues with wrong default partition numbers

these still need to be properly fixed and actually get the correct
data from MDS

go mod tidy

Introduce TableQualifier (OrganizationID/DatabaseID) (#2220)

* add check in ApplyDirective that version is increasing

fix TestAPIDirective to make version always increasing

* fix docker image build and break out dax test in CI

We have to run the DAX integration tests separately as they call out
to Docker, and so it isn't easy to run them in a Docker container as
the other tests do. So we run them directly on the CI runner which has
Docker and Go installed.

We also explicitly exclude these tests from running during the other
tests.

Also my editor was automatically reformatting some comments badly
which is why I added the "data" thing in those two places

* add timeout to poller

* give Poller a default Logger

apparently we can NPE sometimes, seen in CI: https://gitlab.com/molecula/featurebase/-/jobs/3028286364

* bunch of testing fixes, mostly IDK/DAX related

make MDS error if sendDirectives errors, don't just
log. sendDirectives can error if computer nodes disagree about the
validity of a schema (for example), in which case it might need to get
deleted and user notified somehow. very messy, needs more thought.

re-introduce old env prefix to maintain compatibility with master
branch

make self-contained dax container for IDK testing

build IDK images from source (now that all the source is available
since it's in the same repo)

catch errors in DoExtractQuery in idktest.go

fix IDK bug where prefix path was hardcoded in all cases rather than
only when useMDS was true

fix TestBatchTargetMDS... needed to add field options and catch error
when creating table. also needed an _id field

* fix env prefix in tests

* WIP getting tests to pass, wanna see CI

* don't error if we get a zero version directive and we don't have a

directive yet

* cleanup debugging junk

* "fix" future.rename thing, run IDK tests

* Introduce TableQualifier (OrganizationID/DatabaseID)

This commit introduces a lot of new types (in dax/table.go) related to
TableQualifer (which is made up of OrganizationID and DatabaseID), as
well as things like TableID and TableKey.

For the most part, we try to thread a QualifiedTableID through the
entirety of DAX. There are some places (for example in the Balancers,
which are just aware of string keys) which use a string TableKey
(tbl__org__db__tableid).

* Remove some debugging comments

* Add Org/DB support to CLI.

This commit adds support for special commands:

SET
SET ORG acme
SET DB db1
USE db1

* remove ".pulled" from IDK Makefile

I don't think we need it any more as most things can be built
locally. I think it was only there to refresh the FeatureBase images
that were tagged as master, but we don't need to do that any more.

* Change DAX json tags to kebab-case (i.e. hyphenated)

This commit also renames some struct arguments to more accurately
reflect their type: for example, renaming `Table` to `TableKey` when the
type is TableKey.

* Return DAX TableName in SHOW TABLES (instead of Index.Name)

There are cases where SchemaAPI is used to return DAX friendly table
names (as opposed to featurebase index names, which are DAX TableKey).

This is an attempt to do that. With that said, it's not ideal because
anything could call those API methods and expect the other type.

* Fix a bug which wasn't completely dropping a table.

When using boltdb as a backend, DROP TABLE wasn't removing the
reverse-lookup key for the table in boltdb.

* Remove idk/testenv/certs which got accidentally committed.

also update .gitignore to include those.

* Fix IDK ingest tests to be TableQualifier aware.

* Add example Table types to dax/table.com godoc.

* ignore idk.Main fields for flags, upgrade commandeer

* go mod tidy

* Fix DAX integration tests: ingester using wrong ENV VARs

We change from ORGANIZATION_ID to ORG_ID
and from DATABASE_ID to DB_ID

* Clarify things around idk (docker) tests

* Stop running TestKafkaSourceIntegration with t.Parallel()

This test can't be run in parallel as it's currently written. Doing so
allows for interleaving of messages to the same kafka topic between
tests.

I didn't attempt to modify the test so it could be run in parallel. That
could be done, but left for someone more ambitious.

Co-authored-by: Matthew Jaffee <jaffee@pilosa.com>

Require Directive.Version be a non-zero value. (#2227)

Because the directive cached on the holder is not a pointer, its default
version is 0. In order to avoid having to compare against that, we just
require that Directive.Version start at 1.

General, non-invasive code cleanup and comment adjustment.

Move ImportRoaringShardRequest out of the types package

Early on in the DAX development, I moved ImportRoaringShardRequest into
a types package. There must have been some import loop going on, but
since that is not longer the case, it's safe to move this back into the
core featurebase (er... pilosa) package.

Move Transaction struct back into the pilosa package (from types)

Revert some name changes (cli -> client)

Add DAX Handler CloseTimeout

This was implemented in htt_handler.go, but it had been commented out in
the DAX handler. This just uncomments that and finishes the
implementation.

Remove Qcx from queryer.Importer interface

This sets us up to revert the Qctx interface that was initially
introduced to allow us to abstract away the need for a Qcx when calling
the ComputeAPI from a remote service (i.e. the queryer).

Add some go-doc comments and remove unused code.

Move SchemaManager setup from datagen to idk.Main (#2233)

The set for idk.SchemaManager (for dax implementations) was previously
in datagen. This may have been because of some import loop problem
during development, but that's no longer an issue.

The setup for this should be in idk.Main so anything using that can
leverage the MDS-specific SchemaManager setup.

Fix issues around nil TxFactory

First, don't return a nil. Rather return a new *TxFactory (with no
holder).

Second, don't call `f.holder` in the testhook outside of checking if
`f.holder` is nil.

Wrap all bare errors

Make service prefixes constants

Instead of having `"computer"` throughout the code, use instead a
constant: `dax.ServicePrefixComputer`.

MDS skip errors when sending empty directives

also add in the docker-login and ecr-push changes for serverless DAX

Fix the logic in Directive.IsEmpty() (#2236)

Update the cached value for Index.translatePartitions

In the case where a node already knows about an index, but its
assignment of partitions for that index changes (for example, when
another node goes down and the node in question is now responsible for
more partitions than it previously was), then we need to update the
cached value of Index.translatePartitions because that's used in
translation checks.

minor fixes for IDK-related bugs

WIP: tokenize CLI to access cloud

FB CLI cloud support with automatic token refresh

Also adds support for a GET command which allows making HTTP GET
queries to cloud CP which can be handy for debugging stuff. E.g. GET /v2/databases

buncha little fixes working on writelogger stuff

fix writelogger/snapshotter setup bugs

implement writelogging for importRoaringShard

add debug endpoint to MDS

use shard transactional endpoint in MDS datagen

add debugging to API related to writelogger

revert handleroption change

clean up big PR

remove "GET" command from CLI for making arbitrary HTTP request to
cloud control plane (was a messy hack and not that useful)

remove json tags from FB objects where we had to duplicate the object
elsewhere due to import loops and weren't actually json encoding it

unexport handlerOption which was exported to try to avoid doing
certain things if we're in DAX mode, but I didn't end up merging that code.

remove (hopefully) unecessary extra call to api.indexField

fix some formatting, unexport some vars, godoc, etc

oops, fix build failure

Update FeatureBase CLI to support a standard deployment

The standard deployment uses a different endpoint and request payload.
This commit tries to detect is the standard deployment is being used,
and if so, it uses a standard-specific FBQueryer.

It also modifies the auto-detection logic to try standard featurebase
and dax ports in the case where a port was not provided.

MDS API refactor (#2259)

* MDS API refactor

table IDs are exposed but only created server side

also cleaned up dax Makefile

* clean up review feedback

Co-authored-by: Travis Turner <travis@pilosa.com>

* remove TablesByName

* rip out inmem implementations and use boltdb everywhere

* remove inmem balancer, create bolt tempfile by default on startup

* WIP on snapshot table impl and test

* Minor comment and code layout adjustments.

This commit also adds the `Equals` method to `QualifiedTableID` for
equality comparisons. It's no longer safe to compare struct (two structs
might still be equal even if one of the structs doesn't have a `Name`
value.

* Use a unique docker network for each dax test

Ocassionally we would see some test failures due to a network already
existing. This shouldn't happen, but to avoid that, this commit
generates a unique name for each sub test (which gets deleted at the end
of every test).

* Fix one instance of NewQualifiedTableID losing Name

We should probably check the other instances and see if Name is getting
lost.

* simplify unique network stuff and fix api directive tests

* Remove TableIDRequest and TableIDResponse types for /table-id (#2267)

For the mds/table-id http requests, just use dax.QualifiedTableID as
both the request and response types.

* remove lattice from dax, no error on node re-reg, dax docker-compose

* various updates

* WIP: mds-refactor branch review

* no-op on SnapshotTableKeys if table is not keyed

* Makefile helpers

* add doWeCare so controller doesn't fail unnecessarily

* clean up table creation (#2272)

* Strip underscores from TableID stub name

* fix boltdb versionstore tests: generate unique, sorted tables

* fix controller test related to reregistering a node

* JobSet -> generic Set

Co-authored-by: Travis Turner <travis@pilosa.com>
Co-authored-by: Travis Turner <travis@molecula.com>

Cleanup after rebase on master

The latest rebase on master entailed all the client/batch changes as
well as some of the qcx refactoring. It made for a hairy rebase. This
commit fixes some of the tests that were failing after that rebase.

Fix batch/client import loop missed during rebase (#2280)

It's not surprising that `batch` can't import `client`. It was doing
that here (importing an error type from the `client` package). What is
surprising is that it's okay for `batch_test.go` to import `client` even
though `batch_test.go` is an internal test and therefore part of the
`batch` package.

different boltDB's for schemar/controller, explicit balancers

nice helpers for dax docker-compose, make build really fast

build FB binary outside of docker, then create Docker image with its
working dir in an empty subdirectory so it doesn't send a GB of
context to the daemon.

error on unassigned jobs and use client with timeout

fix CR feedback

deregister batch of nodes

also make removal faster via director dial timeout

implement WorkersForJobPrefix so orchestrator doesn't make up shards

also fix some godocs and remove unused method

Run sub-tasks of a Directive concurrently in a worker pool. (#2275)

* Run sub-tasks of a Directive concurrently in a worker pool.

This allows the compute node to concurrently load shapshot and writelog
data concurrently, instead of one keyset/partition/shard at a time.

It introduces a config parameter called `DirectiveWorkerPoolSize`.

* code review cleanup

* Use unique container names in DAX integration tests

We were seeing "container already exists" errors in CI, so just to be
safe, this commit constructs a unique container name for every container
in the DAX integration test run.

Stub in SystemAPI to Queryer (note: will not work if used)

This just makes is so that dax can compile. Actually implementing
system-table functionality for dax will take some planning.

Tlt/dax merge prep (#2282)

* Remove copilot directory

* Remove Dockerfile-datagen-long

* Remove orphaned RegisterNodeRequest

This type is not defined in the dax/mds/http package.

* implement TIMEQUANTUM and TTL in Table.Field type

* Remove the "service" misdirection in queryer/writelogger/snapshotter.

We had originally used an additional layer, er.. package, for a "service".
The main distinction was that the Config differed in that it was
internal, unlike the Config that we need to provide for the top-level
server config (i.e. toml). Having that additional layer just to support
a different Config seemed premature at best. So I'm removing it.

* Remove dax docker containers no longer used in tests

Since we run everything as "featurebase", we don't have multiple
container types anymore.

* Some minor comment updates

* Remove nfpm stuff related to dax

* Fix linter issues

Fix "duplicate" issues raised by sonarcloud.

run docker components of dax integration tests with coverage

trying to get dax integration coverage

add coverate volume mounts throughout dax integration tests

add a lock, tweak dax Makefile, remote flag on query handler

remove some unused code

convert batch tests to use clustertests to get coverage

maybe fix clustertests

more authclustertests fixes, test is failing locally

but also seems to have been silently failing in CI prior to these
changes... let's see if it's still silent

fix some lint to kick CI

just re-running the job wasn't working... strange behavior

remove RetryLogic test and pipe which don't work

RetryLogic test removed due to etcd changes. Seebs thinks we shouldn't
test this here.

Pipe was being ignored since we're no longer using "bash -c" to
execute the command. If we need to generate that output file we'll
either have to reintroduce bash -c and set -o pipefail so that it
actually fails properly, or figure out some other solution.

shooting into the dark...

first cut at bulk node registration

remove unused stuff from batch tests, set coverpkg to ../...

batch registration timeout and fix tests

disable most tests and don't run fb background batch test

debuggin!!!!!!!!!

and then he tried this....

Implement importer (for INSERT INTO) in the Queryer

Prior to this, we we passing a nil value in for the importer to the
planner.NewExecutionPlanner in the Queryer. This meant that INSERT INTO
statements didn't work. Now they should.

It uses the importer that we build for IDK in /idk/mds/importer.go, and
wrapps that with a type that can determine if the provided string
"index" is of the form indexName or TableKey.

turn off debug mode, fix log saving

Run sql3 test definitions in a dax integration test

There are currently 22 tests which are not passing. They are skipped in
the "skips" slice.

WIP, not working, pql queries to tests

Add TableQualifier to PQL query logic in the Queryer

Add more PQL tests to the keyed table

Allow instant node registration if registration-batch-timeout=0

When running dax services in process, we don't want to wait 3s for the
compute node to register; we know it's there because it's in the same
process.

Fixes related to IncludesColumn PQL test.

Tests for ConstRow and FieldValue

cleanup

add UnionRows and Options, better error reporting on bad queries

delete unused schemar client.go, clean up unused in batch test CI

move test timeouts into more reasonable territory

apparently this had already been done, but got merge-stommped at some point

move dax bolt test helpers into dax package

Add computer CheckIn routine (#2296)

* Add computer CheckIn routine

This adds a background routine which sends a "check-in" request to MDS
every <interval>. This is to address the case where the poller has
removed a computer node from the node list (due to a network fault, for
example), but the node is still healthy and becomes available again. In
that case, the node needs to "check-in" to tell MDS it is still there.
MDS will likely send the node a new directive with Method=reset telling
the node to delete all of its data an apply the latest directive.

* Don't send directives to Deregistered (i.e. removed) nodes

We have an issue where we're locking on sendDirective in the
controller, and when the node is unavailable, the send hangs and never
releases the lock. This is a temporary fix for that until we address the
real problem.

Fix .gitlab-ci.yml after rebase

fix some indentation shenanigans

(cherry picked from commit 20a8b5713a)
2022-12-12 09:01:20 -08:00

2992 lines
84 KiB
Go

// Copyright 2022 Molecula Corp. (DBA FeatureBase).
// SPDX-License-Identifier: Apache-2.0
package pilosa
import (
"archive/tar"
"bytes"
"container/heap"
"context"
"fmt"
"io"
"math"
"math/bits"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/featurebasedb/featurebase/v3/logger"
"github.com/featurebasedb/featurebase/v3/pb"
"github.com/featurebasedb/featurebase/v3/pql"
"github.com/featurebasedb/featurebase/v3/roaring"
"github.com/featurebasedb/featurebase/v3/shardwidth"
"github.com/featurebasedb/featurebase/v3/stats"
"github.com/featurebasedb/featurebase/v3/testhook"
"github.com/featurebasedb/featurebase/v3/tracing"
"github.com/featurebasedb/featurebase/v3/vprint"
"github.com/gogo/protobuf/proto"
"github.com/pkg/errors"
)
const (
// ShardWidth is the number of column IDs in a shard. It must be a power of 2 greater than or equal to 16.
// shardWidthExponent = 20 // set in shardwidthNN.go files
ShardWidth = 1 << shardwidth.Exponent
// shardVsContainerExponent is the power of 2 of ShardWith minus the power
// of two of roaring container width (which is 16).
// 2^shardVsContainerExponent is the number of containers in a shard row.
//
// It is represented in this rather awkward way because calculating the row
// which a given container is in means dividing by the number of rows per
// container which is performantly expressed as a right shift by this
// exponent.
shardVsContainerExponent = shardwidth.Exponent - 16
// width of roaring containers is 2^16
containerWidth = 1 << 16
// cacheExt is the file extension for persisted cache ids.
cacheExt = ".cache"
// HashBlockSize is the number of rows in a merkle hash block.
HashBlockSize = 100
// Row ids used for boolean fields.
falseRowID = uint64(0)
trueRowID = uint64(1)
// BSI bits used to check existence & sign.
bsiExistsBit = 0
bsiSignBit = 1
bsiOffsetBit = 2
)
func (f *fragment) index() string {
return f.idx.name
}
func (f *fragment) field() string {
return f.fld.name
}
func (f *fragment) view() string {
return f._view.name
}
func (f *fragment) path() string {
return filepath.Join(f._view.path, "fragments", strconv.FormatUint(f.shard, 10))
}
// fragment represents the intersection of a field and shard in an index.
type fragment struct {
mu sync.RWMutex
// We save 20GB worth strings on some data sets by not duplicating
// the path, index, field, view strings on every fragment.
// Instead assemble strings on demand in field(), view(), path(), index().
fld *Field
_view *view
shard uint64
// idx cached to avoid repeatedly looking it up everywhere.
idx *Index
// parent holder
holder *Holder
// Cache for row counts.
CacheType string // passed in by field
// cache keeps a local rowid,count ranking:
// telling us which is the most populated rows in that field.
// Is only on "set fields" with rowCache enabled. So
// BSI, mutex, bool fields do not have this.
// Good: it Only has a string and a count, so cannot use Tx memory.
cache cache
CacheSize uint32
// Cached checksums for each block.
checksums map[int][]byte
// Logger used for out-of-band log entries.
Logger logger.Logger
// mutexVector is used for mutex field types. It's checked for an
// existing value (to clear) prior to setting a new value.
mutexVector vector
stats stats.StatsClient
}
// newFragment returns a new instance of fragment.
func newFragment(holder *Holder, idx *Index, fld *Field, vw *view, shard uint64) *fragment {
checkIdx := holder.Index(idx.name)
if checkIdx == nil {
vprint.PanicOn(fmt.Sprintf("got nil idx back for '%v' from holder!", idx.Name()))
}
f := &fragment{
_view: vw,
fld: fld,
shard: shard,
idx: idx,
CacheType: DefaultCacheType,
CacheSize: DefaultCacheSize,
holder: holder,
stats: stats.NopStatsClient,
}
return f
}
// cachePath returns the path to the fragment's cache data.
func (f *fragment) cachePath() string { return f.path() + cacheExt }
func (f *fragment) bitDepth() (uint64, error) {
f.mu.RLock()
defer f.mu.RUnlock()
tx, err := f.holder.BeginTx(false, f.idx, f.shard)
if err != nil {
return 0, errors.Wrapf(err, "beginning new tx(false, %s, %d)", f.index(), f.shard)
}
defer tx.Rollback()
maxRowID, _, err := f.maxRow(tx, nil)
if err != nil {
return 0, errors.Wrapf(err, "getting fragment max row id")
}
if maxRowID+1 > bsiOffsetBit {
return maxRowID + 1 - bsiOffsetBit, nil
}
return 0, nil
}
type FragmentInfo struct {
BitmapInfo roaring.BitmapInfo
}
func (f *fragment) Index() *Index {
return f.holder.Index(f.index())
}
// Open opens the underlying storage.
func (f *fragment) Open() error {
f.mu.Lock()
defer f.mu.Unlock()
if err := func() error {
// Fill cache with rows persisted to disk.
f.holder.Logger.Debugf("open cache for index/field/view/fragment: %s/%s/%s/%d", f.index(), f.field(), f.view(), f.shard)
if err := f.openCache(); err != nil {
return errors.Wrap(err, "opening cache")
}
// Clear checksums.
f.checksums = make(map[int][]byte)
return nil
}(); err != nil {
f.close()
return err
}
_ = testhook.Opened(f.holder.Auditor, f, nil)
f.holder.Logger.Debugf("successfully opened index/field/view/fragment: %s/%s/%s/%d", f.index(), f.field(), f.view(), f.shard)
return nil
}
// openCache initializes the cache from row ids persisted to disk.
func (f *fragment) openCache() error {
// Determine cache type from field name.
switch f.CacheType {
case CacheTypeRanked:
f.cache = NewRankCache(f.CacheSize)
case CacheTypeLRU:
f.cache = newLRUCache(f.CacheSize)
case CacheTypeNone:
f.cache = globalNopCache
return nil
default:
return ErrInvalidCacheType
}
// Read cache data from disk.
path := f.cachePath()
buf, err := os.ReadFile(path)
if os.IsNotExist(err) {
return nil
} else if err != nil {
return fmt.Errorf("open cache: %s", err)
}
// Unmarshal cache data.
var pb pb.Cache
if err := proto.Unmarshal(buf, &pb); err != nil {
f.holder.Logger.Errorf("error unmarshaling cache data, skipping: path=%s, err=%s", path, err)
return nil
}
tx := f.idx.holder.txf.NewTx(Txo{Write: !writable, Index: f.idx, Fragment: f, Shard: f.shard})
defer tx.Rollback()
// Read in all rows by ID.
// This will cause them to be added to the cache.
for _, id := range pb.IDs {
n, err := tx.CountRange(f.index(), f.field(), f.view(), f.shard, id*ShardWidth, (id+1)*ShardWidth)
if err != nil {
return errors.Wrap(err, "CountRange")
}
f.cache.BulkAdd(id, n)
}
f.cache.Invalidate()
return nil
}
// Close flushes the underlying storage, closes the file and unlocks it.
func (f *fragment) Close() error {
f.mu.Lock()
defer f.mu.Unlock()
defer func() {
_ = testhook.Closed(f.holder.Auditor, f, nil)
}()
return f.close()
}
func (f *fragment) close() error {
// Flush cache if closing gracefully.
if err := f.flushCache(); err != nil {
f.holder.Logger.Errorf("fragment: error flushing cache on close: err=%s, path=%s", err, f.path())
return errors.Wrap(err, "flushing cache")
}
// Remove checksums.
f.checksums = nil
return nil
}
// mutexCheck checks for any entries in fragment which violate the mutex
// property of having only one value set for a given column ID.
func (f *fragment) mutexCheck(tx Tx, details bool, limit int) (map[uint64][]uint64, error) {
dup := roaring.NewBitmapMutexDupFilter(f.shard<<shardwidth.Exponent, details, limit)
err := tx.ApplyFilter(f.index(), f.field(), f.view(), f.shard, 0, dup)
if err != nil {
return nil, err
}
return dup.Report(), nil
}
// row returns a row by ID.
func (f *fragment) row(tx Tx, rowID uint64) (*Row, error) {
f.mu.RLock()
defer f.mu.RUnlock()
return f.unprotectedRow(tx, rowID)
}
// mustRow returns a row by ID. Panic on error. Only used for testing.
func (f *fragment) mustRow(tx Tx, rowID uint64) *Row {
row, err := f.row(tx, rowID)
if err != nil {
vprint.PanicOn(err)
}
return row
}
// unprotectedRow returns a row from the row cache if available or from storage
// (updating the cache).
func (f *fragment) unprotectedRow(tx Tx, rowID uint64) (*Row, error) {
row, err := f.rowFromStorage(tx, rowID)
if err != nil {
return nil, err
}
return row, nil
}
// rowFromStorage clones a row data out of fragment storage and returns it as a
// Row object.
func (f *fragment) rowFromStorage(tx Tx, rowID uint64) (*Row, error) {
// Only use a subset of the containers.
// NOTE: The start & end ranges must be divisible by container width.
//
// Note that OffsetRange now returns a new bitmap which uses frozen
// containers which will use copy-on-write semantics. The actual bitmap
// and Containers object are new and not shared, but the containers are
// shared.
data, err := tx.OffsetRange(f.index(), f.field(), f.view(), f.shard, f.shard*ShardWidth, rowID*ShardWidth, (rowID+1)*ShardWidth)
if err != nil {
return nil, err
}
row := &Row{
Segments: []RowSegment{{
data: data,
shard: f.shard,
writable: true,
}},
}
row.invalidateCount()
return row, nil
}
// setBit sets a bit for a given column & row within the fragment.
// This updates both the on-disk storage and the in-cache bitmap.
func (f *fragment) setBit(tx Tx, rowID, columnID uint64) (changed bool, err error) {
f.mu.Lock() // controls access to the file.
defer f.mu.Unlock()
doSetFunc := func() error {
// handle mutux field type
if f.mutexVector != nil {
if err := f.handleMutex(tx, rowID, columnID); err != nil {
return errors.Wrap(err, "handling mutex")
}
}
changed, err = f.unprotectedSetBit(tx, rowID, columnID)
return err
}
err = doSetFunc()
return changed, err
}
// handleMutex will clear an existing row and store the new row
// in the vector.
func (f *fragment) handleMutex(tx Tx, rowID, columnID uint64) error {
if existingRowID, found, err := f.mutexVector.Get(tx, columnID); err != nil {
return errors.Wrap(err, "getting mutex vector data")
} else if found && existingRowID != rowID {
if _, err := f.unprotectedClearBit(tx, existingRowID, columnID); err != nil {
return errors.Wrap(err, "clearing mutex value")
}
}
return nil
}
// unprotectedSetBit TODO should be replaced by an invocation of importPositions with a single bit to set.
func (f *fragment) unprotectedSetBit(tx Tx, rowID, columnID uint64) (changed bool, err error) {
// Determine the position of the bit in the storage.
pos, err := f.pos(rowID, columnID)
if err != nil {
return false, errors.Wrap(err, "getting bit pos")
}
// Write to storage.
changeCount := 0
changeCount, err = tx.Add(f.index(), f.field(), f.view(), f.shard, pos)
changed = changeCount > 0
if err != nil {
return false, errors.Wrap(err, "writing")
}
// Don't update the cache if nothing changed.
if !changed {
return changed, nil
}
// Invalidate block checksum.
delete(f.checksums, int(rowID/HashBlockSize))
// If we're using a cache, update it. Otherwise skip the
// possibly-expensive count operation.
if f.CacheType != CacheTypeNone {
n, err := tx.CountRange(f.index(), f.field(), f.view(), f.shard, rowID*ShardWidth, (rowID+1)*ShardWidth)
if err != nil {
return false, err
}
f.cache.Add(rowID, n)
}
f.stats.Count(MetricSetBit, 1, 1.0)
return changed, nil
}
// clearBit clears a bit for a given column & row within the fragment.
// This updates both the on-disk storage and the in-cache bitmap.
func (f *fragment) clearBit(tx Tx, rowID, columnID uint64) (changed bool, err error) {
f.mu.Lock()
defer f.mu.Unlock()
return f.unprotectedClearBit(tx, rowID, columnID)
}
// unprotectedClearBit TODO should be replaced by an invocation of
// importPositions with a single bit to clear.
func (f *fragment) unprotectedClearBit(tx Tx, rowID, columnID uint64) (changed bool, err error) {
changed = false
// Determine the position of the bit in the storage.
pos, err := f.pos(rowID, columnID)
if err != nil {
return false, errors.Wrap(err, "getting bit pos")
}
// Write to storage.
changeCount := 0
if changeCount, err = tx.Remove(f.index(), f.field(), f.view(), f.shard, pos); err != nil {
return false, errors.Wrap(err, "writing")
}
// Don't update the cache if nothing changed.
if changeCount <= 0 {
return false, nil
} else {
changed = true
}
// Invalidate block checksum.
delete(f.checksums, int(rowID/HashBlockSize))
// If we're using a cache, update it. Otherwise skip the
// possibly-expensive count operation.
if f.CacheType != CacheTypeNone {
n, err := tx.CountRange(f.index(), f.field(), f.view(), f.shard, rowID*ShardWidth, (rowID+1)*ShardWidth)
if err != nil {
return changed, err
}
f.cache.Add(rowID, n)
}
f.stats.Count(MetricClearBit, 1, 1.0)
return changed, nil
}
// setRow replaces an existing row (specified by rowID) with the given
// Row. This updates both the on-disk storage and the in-cache bitmap.
func (f *fragment) setRow(tx Tx, row *Row, rowID uint64) (changed bool, err error) {
f.mu.Lock()
defer f.mu.Unlock()
return f.unprotectedSetRow(tx, row, rowID)
}
func (f *fragment) unprotectedSetRow(tx Tx, row *Row, rowID uint64) (changed bool, err error) {
// TODO: In order to return `changed`, we need to first compare
// the existing row with the given row. Determine if the overhead
// of this is worth having `changed`.
// For now we will assume changed is always true.
changed = true
// First container of the row in storage.
headContainerKey := rowID << shardVsContainerExponent
// Remove every existing container in the row.
for i := uint64(0); i < (1 << shardVsContainerExponent); i++ {
if err := tx.RemoveContainer(f.index(), f.field(), f.view(), f.shard, headContainerKey+i); err != nil {
return changed, err
}
}
// From the given row, get the rowSegment for this shard.
seg := row.segment(f.shard)
if seg != nil {
// Put each container from rowSegment to fragment storage.
citer, _ := seg.data.Containers.Iterator(f.shard << shardVsContainerExponent)
for citer.Next() {
k, c := citer.Value()
if err := tx.PutContainer(f.index(), f.field(), f.view(), f.shard, headContainerKey+(k%(1<<shardVsContainerExponent)), c); err != nil {
return changed, err
}
}
// Update the row in cache.
if f.CacheType != CacheTypeNone {
n, err := tx.CountRange(f.index(), f.field(), f.view(), f.shard, rowID*ShardWidth, (rowID+1)*ShardWidth)
if err != nil {
return changed, err
}
f.cache.BulkAdd(rowID, n)
}
} else {
if f.CacheType != CacheTypeNone {
f.cache.BulkAdd(rowID, 0)
}
}
f.stats.Count("setRow", 1, 1.0)
return changed, nil
}
// clearRow clears a row for a given rowID within the fragment.
// This updates both the on-disk storage and the in-cache bitmap.
func (f *fragment) clearRow(tx Tx, rowID uint64) (changed bool, err error) {
f.mu.Lock()
defer f.mu.Unlock()
return f.unprotectedClearRow(tx, rowID)
}
func (f *fragment) unprotectedClearRow(tx Tx, rowID uint64) (changed bool, err error) {
changed = false
// First container of the row in storage.
headContainerKey := rowID << shardVsContainerExponent
// Remove every container in the row.
for i := uint64(0); i < (1 << shardVsContainerExponent); i++ {
k := headContainerKey + i
// Technically we could bypass the Get() call and only
// call Remove(), but the Get() gives us the ability
// to return true if any existing data was removed.
if cont, err := tx.Container(f.index(), f.field(), f.view(), f.shard, k); err != nil {
return changed, err
} else if cont != nil {
if err := tx.RemoveContainer(f.index(), f.field(), f.view(), f.shard, k); err != nil {
return changed, err
}
changed = true
}
}
// Clear the row in cache.
f.cache.Add(rowID, 0)
return changed, nil
}
// clearBlock clears all rows for a given block.
// This updates both the on-disk storage and the in-cache bitmap.
func (f *fragment) clearBlock(tx Tx, block int) (changed bool, err error) {
f.mu.Lock()
defer f.mu.Unlock()
firstRow := uint64(block * HashBlockSize)
err = func() error {
var rowChanged bool
for rowID := uint64(firstRow); rowID < firstRow+HashBlockSize; rowID++ {
if chang, err := f.unprotectedClearRow(tx, rowID); err != nil {
return errors.Wrapf(err, "clearing row: %d", rowID)
} else if chang {
rowChanged = true
}
}
changed = rowChanged
return nil
}()
return changed, err
}
func (f *fragment) bit(tx Tx, rowID, columnID uint64) (bool, error) {
pos, err := f.pos(rowID, columnID)
if err != nil {
return false, err
}
return tx.Contains(f.index(), f.field(), f.view(), f.shard, pos)
}
// value uses a column of bits to read a multi-bit value.
func (f *fragment) value(tx Tx, columnID uint64, bitDepth uint64) (value int64, exists bool, err error) {
f.mu.Lock()
defer f.mu.Unlock()
// If existence bit is unset then ignore remaining bits.
if v, err := f.bit(tx, bsiExistsBit, columnID); err != nil {
return 0, false, errors.Wrap(err, "getting existence bit")
} else if !v {
return 0, false, nil
}
// Compute other bits into a value.
for i := uint64(0); i < bitDepth; i++ {
if v, err := f.bit(tx, uint64(bsiOffsetBit+i), columnID); err != nil {
return 0, false, errors.Wrapf(err, "getting value bit %d", i)
} else if v {
value |= (1 << i)
}
}
// Negate if sign bit set.
if v, err := f.bit(tx, bsiSignBit, columnID); err != nil {
return 0, false, errors.Wrap(err, "getting sign bit")
} else if v {
value = -value
}
return value, true, nil
}
// clearValue uses a column of bits to clear a multi-bit value.
func (f *fragment) clearValue(tx Tx, columnID uint64, bitDepth uint64, value int64) (changed bool, err error) {
return f.setValueBase(tx, columnID, bitDepth, value, true)
}
// setValue uses a column of bits to set a multi-bit value.
func (f *fragment) setValue(tx Tx, columnID uint64, bitDepth uint64, value int64) (changed bool, err error) {
return f.setValueBase(tx, columnID, bitDepth, value, false)
}
func (f *fragment) positionsForValue(columnID uint64, bitDepth uint64, value int64, clear bool, toSet, toClear []uint64) ([]uint64, []uint64, error) {
// Convert value to an unsigned representation.
uvalue := uint64(value)
if value < 0 {
uvalue = uint64(-value)
}
// Mark value as set.
if bit, err := f.pos(bsiExistsBit, columnID); err != nil {
return toSet, toClear, errors.Wrap(err, "getting not-null pos")
} else if clear {
toClear = append(toClear, bit)
} else {
toSet = append(toSet, bit)
}
// Mark sign.
if bit, err := f.pos(bsiSignBit, columnID); err != nil {
return toSet, toClear, errors.Wrap(err, "getting sign pos")
} else if value >= 0 || clear {
toClear = append(toClear, bit)
} else {
toSet = append(toSet, bit)
}
for i := uint64(0); i < bitDepth; i++ {
bit, err := f.pos(uint64(bsiOffsetBit+i), columnID)
if err != nil {
return toSet, toClear, errors.Wrap(err, "getting pos")
}
if uvalue&(1<<i) != 0 {
toSet = append(toSet, bit)
} else {
toClear = append(toClear, bit)
}
}
return toSet, toClear, nil
}
// TODO get rid of this and use positionsForValue to generate a single write op, and set that with importPositions.
func (f *fragment) setValueBase(tx Tx, columnID uint64, bitDepth uint64, value int64, clear bool) (changed bool, err error) {
f.mu.Lock()
defer f.mu.Unlock()
err = func() error {
// Convert value to an unsigned representation.
uvalue := uint64(value)
if value < 0 {
uvalue = uint64(-value)
}
for i := uint64(0); i < bitDepth; i++ {
if uvalue&(1<<i) != 0 {
if c, err := f.unprotectedSetBit(tx, uint64(bsiOffsetBit+i), columnID); err != nil {
return err
} else if c {
changed = true
}
} else {
if c, err := f.unprotectedClearBit(tx, uint64(bsiOffsetBit+i), columnID); err != nil {
return err
} else if c {
changed = true
}
}
}
// Mark value as set (or cleared).
if clear {
if c, err := f.unprotectedClearBit(tx, uint64(bsiExistsBit), columnID); err != nil {
return errors.Wrap(err, "clearing not-null")
} else if c {
changed = true
}
} else {
if c, err := f.unprotectedSetBit(tx, uint64(bsiExistsBit), columnID); err != nil {
return errors.Wrap(err, "marking not-null")
} else if c {
changed = true
}
}
// Mark sign bit (or clear).
if value >= 0 || clear {
if c, err := f.unprotectedClearBit(tx, uint64(bsiSignBit), columnID); err != nil {
return errors.Wrap(err, "clearing sign")
} else if c {
changed = true
}
} else {
if c, err := f.unprotectedSetBit(tx, uint64(bsiSignBit), columnID); err != nil {
return errors.Wrap(err, "marking sign")
} else if c {
changed = true
}
}
return nil
}()
return changed, err
}
// sum returns the sum of a given bsiGroup as well as the number of columns involved.
// A bitmap can be passed in to optionally filter the computed columns.
func (f *fragment) sum(tx Tx, filter *Row, bitDepth uint64) (sum int64, count uint64, err error) {
// If there's a provided filter, but it has no contents for this particular
// shard, we're done and can return early. If there's no provided filter,
// though, we want to run with no-filter, as opposed to an empty filter.
var filterData *roaring.Bitmap
if filter != nil {
for _, seg := range filter.Segments {
if seg.shard == f.shard {
filterData = seg.data
break
}
}
// if filter is empty, we're done
if filterData == nil {
return 0, 0, nil
}
}
bsiFilt := roaring.NewBitmapBSICountFilter(filterData)
err = tx.ApplyFilter(f.index(), f.field(), f.view(), f.shard, 0, bsiFilt)
if err != nil && err != io.EOF {
return sum, count, errors.Wrap(err, "finding existing positions")
}
c32, sum := bsiFilt.Total()
return sum, uint64(c32), nil
}
// min returns the min of a given bsiGroup as well as the number of columns involved.
// A bitmap can be passed in to optionally filter the computed columns.
func (f *fragment) min(tx Tx, filter *Row, bitDepth uint64) (min int64, count uint64, err error) {
consider, err := f.row(tx, bsiExistsBit)
if err != nil {
return min, count, err
} else if filter != nil {
consider = consider.Intersect(filter)
}
// If there are no columns to consider, return early.
if consider.Count() == 0 {
return 0, 0, nil
}
// If we have negative values, we should find the highest unsigned value
// from that set, then negate it, and return it. For example, if values
// (-1, -2) exist, they are stored unsigned (1,2) with a negative sign bit
// set. We take the highest of that set (2) and negate it and return it.
if row, err := f.row(tx, bsiSignBit); err != nil {
return min, count, err
} else if row = row.Intersect(consider); row.Any() {
min, count, err := f.maxUnsigned(tx, row, bitDepth)
return -min, count, err
}
// Otherwise find lowest positive number.
return f.minUnsigned(tx, consider, bitDepth)
}
// minUnsigned the lowest value without considering the sign bit. Filter is required.
func (f *fragment) minUnsigned(tx Tx, filter *Row, bitDepth uint64) (min int64, count uint64, err error) {
count = filter.Count()
for i := int(bitDepth - 1); i >= 0; i-- {
row, err := f.row(tx, uint64(bsiOffsetBit+i))
if err != nil {
return min, count, err
}
row = filter.Difference(row)
count = row.Count()
if count > 0 {
filter = row
} else {
min += (1 << uint(i))
if i == 0 {
count = filter.Count()
}
}
}
return min, count, nil
}
// max returns the max of a given bsiGroup as well as the number of columns involved.
// A bitmap can be passed in to optionally filter the computed columns.
func (f *fragment) max(tx Tx, filter *Row, bitDepth uint64) (max int64, count uint64, err error) {
consider, err := f.row(tx, bsiExistsBit)
if err != nil {
return max, count, err
} else if filter != nil {
consider = consider.Intersect(filter)
}
// If there are no columns to consider, return early.
if !consider.Any() {
return 0, 0, nil
}
// Find lowest negative number w/o sign and negate, if no positives are available.
row, err := f.row(tx, bsiSignBit)
if err != nil {
return max, count, err
}
pos := consider.Difference(row)
if !pos.Any() {
max, count, err = f.minUnsigned(tx, consider, bitDepth)
return -max, count, err
}
// Otherwise find highest positive number.
return f.maxUnsigned(tx, pos, bitDepth)
}
// maxUnsigned the highest value without considering the sign bit. Filter is required.
func (f *fragment) maxUnsigned(tx Tx, filter *Row, bitDepth uint64) (max int64, count uint64, err error) {
count = filter.Count()
for i := int(bitDepth - 1); i >= 0; i-- {
row, err := f.row(tx, uint64(bsiOffsetBit+i))
if err != nil {
return max, count, err
}
row = row.Intersect(filter)
count = row.Count()
if count > 0 {
max += (1 << uint(i))
filter = row
} else if i == 0 {
count = filter.Count()
}
}
return max, count, nil
}
// minRow returns minRowID of the rows in the filter and its count.
// if filter is nil, it returns fragment.minRowID, 1
// if fragment has no rows, it returns 0, 0
func (f *fragment) minRow(tx Tx, filter *Row) (uint64, uint64, error) {
minRowID, hasRowID, err := f.minRowID(tx)
if err != nil {
return 0, 0, err
}
if hasRowID {
if filter == nil {
return minRowID, 1, nil
}
// Read last bit to determine max row.
maxRowID, err := f.maxRowID(tx)
if err != nil {
return 0, 0, err
}
// iterate from min row ID and return the first that intersects with filter.
for i := minRowID; i <= maxRowID; i++ {
row, err := f.row(tx, i)
if err != nil {
return 0, 0, err
}
row = row.Intersect(filter)
count := row.Count()
if count > 0 {
return i, count, nil
}
}
}
return 0, 0, nil
}
// maxRow returns maxRowID of the rows in the filter and its count.
// if filter is nil, it returns fragment.maxRowID, 1
// if fragment has no rows, it returns 0, 0
func (f *fragment) maxRow(tx Tx, filter *Row) (uint64, uint64, error) {
minRowID, hasRowID, err := f.minRowID(tx)
if err != nil {
return 0, 0, err
}
if hasRowID {
maxRowID, err := f.maxRowID(tx)
if err != nil {
return 0, 0, err
}
if filter == nil {
return maxRowID, 1, nil
}
// iterate back from max row ID and return the first that intersects with filter.
// TODO: implement reverse container iteration to improve performance here for sparse data. --Jaffee
for i := maxRowID; i >= minRowID; i-- {
row, err := f.row(tx, i)
if err != nil {
return 0, 0, err
}
row = row.Intersect(filter)
count := row.Count()
if count > 0 {
return i, count, nil
}
}
}
return 0, 0, nil
}
// maxRowID determines the field's maxRowID value based
// on the contents of its storage, and sets the struct argument.
func (f *fragment) maxRowID(tx Tx) (_ uint64, err error) {
max, err := tx.Max(f.index(), f.field(), f.view(), f.shard)
if err != nil {
return 0, err
}
return max / ShardWidth, nil
}
// rangeOp returns bitmaps with a bsiGroup value encoding matching the predicate.
func (f *fragment) rangeOp(tx Tx, op pql.Token, bitDepth uint64, predicate int64) (*Row, error) {
switch op {
case pql.EQ:
return f.rangeEQ(tx, bitDepth, predicate)
case pql.NEQ:
return f.rangeNEQ(tx, bitDepth, predicate)
case pql.LT, pql.LTE:
return f.rangeLT(tx, bitDepth, predicate, op == pql.LTE)
case pql.GT, pql.GTE:
return f.rangeGT(tx, bitDepth, predicate, op == pql.GTE)
default:
return nil, ErrInvalidRangeOperation
}
}
func absInt64(v int64) uint64 {
switch {
case v > 0:
return uint64(v)
case v == -9223372036854775808:
return 9223372036854775808
default:
return uint64(-v)
}
}
func (f *fragment) rangeEQ(tx Tx, bitDepth uint64, predicate int64) (*Row, error) {
// Start with set of columns with values set.
b, err := f.row(tx, bsiExistsBit)
if err != nil {
return nil, err
}
upredicate := absInt64(predicate)
if uint64(bits.Len64(upredicate)) > bitDepth {
// Predicate is out of range.
return NewRow(), nil
}
// Filter to only positive/negative numbers.
r, err := f.row(tx, bsiSignBit)
if err != nil {
return nil, err
}
if predicate < 0 {
b = b.Intersect(r) // only negatives
} else {
b = b.Difference(r) // only positives
}
// Filter any bits that don't match the current bit value.
for i := int(bitDepth - 1); i >= 0; i-- {
row, err := f.row(tx, uint64(bsiOffsetBit+i))
if err != nil {
return nil, err
}
bit := (upredicate >> uint(i)) & 1
if bit == 1 {
b = b.Intersect(row)
} else {
b = b.Difference(row)
}
}
return b, nil
}
func (f *fragment) rangeNEQ(tx Tx, bitDepth uint64, predicate int64) (*Row, error) {
// Start with set of columns with values set.
b, err := f.row(tx, bsiExistsBit)
if err != nil {
return nil, err
}
// Get the equal bitmap.
eq, err := f.rangeEQ(tx, bitDepth, predicate)
if err != nil {
return nil, err
}
// Not-null minus the equal bitmap.
b = b.Difference(eq)
return b, nil
}
func (f *fragment) rangeLT(tx Tx, bitDepth uint64, predicate int64, allowEquality bool) (*Row, error) {
if predicate == 1 && !allowEquality {
predicate, allowEquality = 0, true
}
// Start with set of columns with values set.
b, err := f.row(tx, bsiExistsBit)
if err != nil {
return nil, err
}
// Get the sign bit row.
sign, err := f.row(tx, bsiSignBit)
if err != nil {
return nil, err
}
// Create predicate without sign bit.
upredicate := absInt64(predicate)
switch {
case predicate == 0 && !allowEquality:
// Match all negative integers.
return b.Intersect(sign), nil
case predicate == 0 && allowEquality:
// Match all integers that are either negative or 0.
zeroes, err := f.rangeEQ(tx, bitDepth, 0)
if err != nil {
return nil, err
}
return b.Intersect(sign).Union(zeroes), nil
case predicate < 0:
// Match all every negative number beyond the predicate.
return f.rangeGTUnsigned(tx, b.Intersect(sign), bitDepth, upredicate, allowEquality)
default:
// Match positive numbers less than the predicate, and all negatives.
pos, err := f.rangeLTUnsigned(tx, b.Difference(sign), bitDepth, upredicate, allowEquality)
if err != nil {
return nil, err
}
neg := b.Intersect(sign)
return pos.Union(neg), nil
}
}
// rangeLTUnsigned returns all bits LT/LTE the predicate without considering the sign bit.
func (f *fragment) rangeLTUnsigned(tx Tx, filter *Row, bitDepth uint64, predicate uint64, allowEquality bool) (*Row, error) {
switch {
case uint64(bits.Len64(predicate)) > bitDepth:
fallthrough
case predicate == (1<<bitDepth)-1 && allowEquality:
// This query matches all possible values.
return filter, nil
case predicate == (1<<bitDepth)-1 && !allowEquality:
// This query matches everything that is not (1<<bitDepth)-1.
matches := NewRow()
for i := uint64(0); i < bitDepth; i++ {
row, err := f.row(tx, uint64(bsiOffsetBit+i))
if err != nil {
return nil, err
}
matches = matches.Union(filter.Difference(row))
}
return matches, nil
case allowEquality:
predicate++
}
// Compare intermediate bits.
matched := NewRow()
remaining := filter
for i := int(bitDepth - 1); i >= 0 && predicate > 0 && remaining.Any(); i-- {
row, err := f.row(tx, uint64(bsiOffsetBit+i))
if err != nil {
return nil, err
}
zeroes := remaining.Difference(row)
switch (predicate >> uint(i)) & 1 {
case 1:
// Match everything with a zero bit here.
matched = matched.Union(zeroes)
predicate &^= 1 << uint(i)
case 0:
// Discard everything with a one bit here.
remaining = zeroes
}
}
return matched, nil
}
func (f *fragment) rangeGT(tx Tx, bitDepth uint64, predicate int64, allowEquality bool) (*Row, error) {
if predicate == -1 && !allowEquality {
predicate, allowEquality = 0, true
}
b, err := f.row(tx, bsiExistsBit)
if err != nil {
return nil, err
}
// Create predicate without sign bit.
upredicate := absInt64(predicate)
sign, err := f.row(tx, bsiSignBit)
if err != nil {
return nil, err
}
switch {
case predicate == 0 && !allowEquality:
// Match all positive numbers except zero.
nonzero, err := f.rangeNEQ(tx, bitDepth, 0)
if err != nil {
return nil, err
}
b = nonzero
fallthrough
case predicate == 0 && allowEquality:
// Match all positive numbers.
return b.Difference(sign), nil
case predicate >= 0:
// Match all positive numbers greater than the predicate.
return f.rangeGTUnsigned(tx, b.Difference(sign), bitDepth, upredicate, allowEquality)
default:
// Match all positives and greater negatives.
neg, err := f.rangeLTUnsigned(tx, b.Intersect(sign), bitDepth, upredicate, allowEquality)
if err != nil {
return nil, err
}
pos := b.Difference(sign)
return pos.Union(neg), nil
}
}
func (f *fragment) rangeGTUnsigned(tx Tx, filter *Row, bitDepth uint64, predicate uint64, allowEquality bool) (*Row, error) {
prep:
switch {
case predicate == 0 && allowEquality:
// This query matches all possible values.
return filter, nil
case predicate == 0 && !allowEquality:
// This query matches everything that is not 0.
matches := NewRow()
for i := uint64(0); i < bitDepth; i++ {
row, err := f.row(tx, uint64(bsiOffsetBit+i))
if err != nil {
return nil, err
}
matches = matches.Union(filter.Intersect(row))
}
return matches, nil
case !allowEquality && uint64(bits.Len64(predicate)) > bitDepth:
// The predicate is bigger than the BSI width, so nothing can be bigger.
return NewRow(), nil
case allowEquality:
predicate--
allowEquality = false
goto prep
}
// Compare intermediate bits.
matched := NewRow()
remaining := filter
predicate |= (^uint64(0)) << bitDepth
for i := int(bitDepth - 1); i >= 0 && predicate < ^uint64(0) && remaining.Any(); i-- {
row, err := f.row(tx, uint64(bsiOffsetBit+i))
if err != nil {
return nil, err
}
ones := remaining.Intersect(row)
switch (predicate >> uint(i)) & 1 {
case 1:
// Discard everything with a zero bit here.
remaining = ones
case 0:
// Match everything with a one bit here.
matched = matched.Union(ones)
predicate |= 1 << uint(i)
}
}
return matched, nil
}
// notNull returns the exists row.
func (f *fragment) notNull(tx Tx) (*Row, error) {
return f.row(tx, bsiExistsBit)
}
// rangeBetween returns bitmaps with a bsiGroup value encoding matching any value between predicateMin and predicateMax.
func (f *fragment) rangeBetween(tx Tx, bitDepth uint64, predicateMin, predicateMax int64) (*Row, error) {
b, err := f.row(tx, bsiExistsBit)
if err != nil {
return nil, err
}
// Convert predicates to unsigned values.
upredicateMin, upredicateMax := absInt64(predicateMin), absInt64(predicateMax)
switch {
case predicateMin == predicateMax:
return f.rangeEQ(tx, bitDepth, predicateMin)
case predicateMin >= 0:
// Handle positive-only values.
r, err := f.row(tx, bsiSignBit)
if err != nil {
return nil, err
}
return f.rangeBetweenUnsigned(tx, b.Difference(r), bitDepth, upredicateMin, upredicateMax)
case predicateMax < 0:
// Handle negative-only values. Swap unsigned min/max predicates.
r, err := f.row(tx, bsiSignBit)
if err != nil {
return nil, err
}
return f.rangeBetweenUnsigned(tx, b.Intersect(r), bitDepth, upredicateMax, upredicateMin)
default:
// If predicate crosses positive/negative boundary then handle separately and union.
r0, err := f.row(tx, bsiSignBit)
if err != nil {
return nil, err
}
pos, err := f.rangeLTUnsigned(tx, b.Difference(r0), bitDepth, upredicateMax, true)
if err != nil {
return nil, err
}
r1, err := f.row(tx, bsiSignBit)
if err != nil {
return nil, err
}
neg, err := f.rangeLTUnsigned(tx, b.Intersect(r1), bitDepth, upredicateMin, true)
if err != nil {
return nil, err
}
return pos.Union(neg), nil
}
}
// rangeBetweenUnsigned returns BSI columns for a range of values. Disregards the sign bit.
func (f *fragment) rangeBetweenUnsigned(tx Tx, filter *Row, bitDepth uint64, predicateMin, predicateMax uint64) (*Row, error) {
switch {
case predicateMax > (1<<bitDepth)-1:
// The upper bound cannot be violated.
return f.rangeGTUnsigned(tx, filter, bitDepth, predicateMin, true)
case predicateMin == 0:
// The lower bound cannot be violated.
return f.rangeLTUnsigned(tx, filter, bitDepth, predicateMax, true)
}
// Compare any upper bits which are equal.
diffLen := bits.Len64(predicateMax ^ predicateMin)
remaining := filter
for i := int(bitDepth - 1); i >= diffLen; i-- {
row, err := f.row(tx, uint64(bsiOffsetBit+i))
if err != nil {
return nil, err
}
switch (predicateMin >> uint(i)) & 1 {
case 1:
remaining = remaining.Intersect(row)
case 0:
remaining = remaining.Difference(row)
}
}
// Clear the bits we just compared.
equalMask := (^uint64(0)) << diffLen
predicateMin &^= equalMask
predicateMax &^= equalMask
var err error
remaining, err = f.rangeGTUnsigned(tx, remaining, uint64(diffLen), predicateMin, true)
if err != nil {
return nil, err
}
remaining, err = f.rangeLTUnsigned(tx, remaining, uint64(diffLen), predicateMax, true)
if err != nil {
return nil, err
}
return remaining, nil
}
// pos translates the row ID and column ID into a position in the storage bitmap.
func (f *fragment) pos(rowID, columnID uint64) (uint64, error) {
// Return an error if the column ID is out of the range of the fragment's shard.
minColumnID := f.shard * ShardWidth
if columnID < minColumnID || columnID >= minColumnID+ShardWidth {
return 0, errors.Errorf("column:%d out of bounds for shard %d", columnID, f.shard)
}
return pos(rowID, columnID), nil
}
// top returns the top rows from the fragment.
// If opt.Src is specified then only rows which intersect src are returned.
func (f *fragment) top(tx Tx, opt topOptions) ([]Pair, error) {
// Retrieve pairs. If no row ids specified then return from cache.
pairs, err := f.topBitmapPairs(tx, opt.RowIDs)
if err != nil {
return nil, err
}
// If row ids are provided, we don't want to truncate the result set
if len(opt.RowIDs) > 0 {
opt.N = 0
}
// Use `tanimotoThreshold > 0` to indicate whether or not we are considering Tanimoto.
var tanimotoThreshold uint64
var minTanimoto, maxTanimoto float64
var srcCount uint64
if opt.TanimotoThreshold > 0 && opt.Src != nil {
tanimotoThreshold = opt.TanimotoThreshold
srcCount = opt.Src.Count()
minTanimoto = float64(srcCount*tanimotoThreshold) / 100
maxTanimoto = float64(srcCount*100) / float64(tanimotoThreshold)
}
// Iterate over rankings and add to results until we have enough.
results := &pairHeap{}
for _, pair := range pairs {
rowID, cnt := pair.ID, pair.Count
// Ignore empty rows.
if cnt == 0 {
continue
}
// Check against either Tanimoto threshold or minimum threshold.
if tanimotoThreshold > 0 {
// Ignore counts outside of the Tanimoto min/max values.
if float64(cnt) <= minTanimoto || float64(cnt) >= maxTanimoto {
continue
}
} else {
// Ignore counts less than MinThreshold.
if cnt < opt.MinThreshold {
continue
}
}
// The initial n pairs should simply be added to the results.
if opt.N == 0 || results.Len() < opt.N {
// Calculate count and append.
count := cnt
if opt.Src != nil {
r, err := f.row(tx, rowID)
if err != nil {
return nil, err
}
count = opt.Src.intersectionCount(r)
}
if count == 0 {
continue
}
// Check against either Tanimoto threshold or minimum threshold.
if tanimotoThreshold > 0 {
tanimoto := math.Ceil(float64(count*100) / float64(cnt+srcCount-count))
if tanimoto <= float64(tanimotoThreshold) {
continue
}
} else {
if count < opt.MinThreshold {
continue
}
}
heap.Push(results, Pair{ID: rowID, Count: count})
// If we reach the requested number of pairs and we are not computing
// intersections then simply exit. If we are intersecting then sort
// and then only keep pairs that are higher than the lowest count.
if opt.N > 0 && results.Len() == opt.N {
if opt.Src == nil {
break
}
}
continue
}
// Retrieve the lowest count we have.
// If it's too low then don't try finding anymore pairs.
threshold := results.Pairs[0].Count
// If the row doesn't have enough columns set before the intersection
// then we can assume that any remaining rows also have a count too low.
if threshold < opt.MinThreshold || cnt < threshold {
break
}
// Calculate the intersecting column count and skip if it's below our
// last row in our current result set.
r, err := f.row(tx, rowID)
if err != nil {
return nil, err
}
count := opt.Src.intersectionCount(r)
if count < threshold {
continue
}
heap.Push(results, Pair{ID: rowID, Count: count})
}
//Pop first opt.N elements out of heap
r := make(Pairs, results.Len())
x := results.Len()
i := 1
for results.Len() > 0 {
r[x-i] = heap.Pop(results).(Pair)
i++
}
return r, nil
}
func (f *fragment) topBitmapPairs(tx Tx, rowIDs []uint64) ([]bitmapPair, error) {
// Don't retrieve from storage if CacheTypeNone.
if f.CacheType == CacheTypeNone {
return f.cache.Top(), nil
}
// If no specific rows are requested, retrieve top rows.
if len(rowIDs) == 0 {
f.mu.Lock()
defer f.mu.Unlock()
f.cache.Invalidate()
return f.cache.Top(), nil
}
// Otherwise retrieve specific rows.
pairs := make([]bitmapPair, 0, len(rowIDs))
for _, rowID := range rowIDs {
// Look up cache first, if available.
if n := f.cache.Get(rowID); n > 0 {
pairs = append(pairs, bitmapPair{
ID: rowID,
Count: n,
})
continue
}
row, err := f.row(tx, rowID)
if err != nil {
return nil, err
}
if row.Count() > 0 {
// Otherwise load from storage.
pairs = append(pairs, bitmapPair{
ID: rowID,
Count: row.Count(),
})
}
}
sortPairs := bitmapPairs(pairs)
sort.Sort(&sortPairs)
return pairs, nil
}
// topOptions represents options passed into the Top() function.
type topOptions struct {
// Number of rows to return.
N int
// Bitmap to intersect with.
Src *Row
// Specific rows to filter against.
RowIDs []uint64
MinThreshold uint64
TanimotoThreshold uint64
}
// bulkImport bulk imports a set of bits.
// The cache is updated to reflect the new data.
func (f *fragment) bulkImport(tx Tx, rowIDs, columnIDs []uint64, options *ImportOptions) error {
// Verify that there are an equal number of row ids and column ids.
if len(rowIDs) != len(columnIDs) {
return fmt.Errorf("mismatch of row/column len: %d != %d", len(rowIDs), len(columnIDs))
}
if f.mutexVector != nil && !options.Clear {
return f.bulkImportMutex(tx, rowIDs, columnIDs, options)
}
return f.bulkImportStandard(tx, rowIDs, columnIDs, options)
}
// rowColumnSet is a sortable set of row and column IDs which
// correspond, allowing us to ensure that we produce values in
// a predictable order
type rowColumnSet struct {
r []uint64
c []uint64
}
func (r rowColumnSet) Len() int {
return len(r.r)
}
func (r rowColumnSet) Swap(i, j int) {
r.r[i], r.r[j] = r.r[j], r.r[i]
r.c[i], r.c[j] = r.c[j], r.c[i]
}
// Sort by row ID first, column second, to sort by fragment position
func (r rowColumnSet) Less(i, j int) bool {
if r.r[i] < r.r[j] {
return true
}
if r.r[i] > r.r[j] {
return false
}
return r.c[i] < r.c[j]
}
// bulkImportStandard performs a bulk import on a standard fragment. May mutate
// its rowIDs and columnIDs arguments.
func (f *fragment) bulkImportStandard(tx Tx, rowIDs, columnIDs []uint64, options *ImportOptions) (err error) {
// rowSet maintains the set of rowIDs present in this import. It allows the
// cache to be updated once per row, instead of once per bit. TODO: consider
// sorting by rowID/columnID first and avoiding the map allocation here. (we
// could reuse rowIDs to store the list of unique row IDs)
rowSet := make(map[uint64]struct{})
lastRowID := uint64(1 << 63)
// It's possible for the ingest API to have already sorted things in
// the row-first order we want for this import.
if !options.fullySorted {
sort.Sort(rowColumnSet{r: rowIDs, c: columnIDs})
}
// replace columnIDs with calculated positions to avoid allocation.
prevRow, prevCol := ^uint64(0), ^uint64(0)
next := 0
for i := 0; i < len(columnIDs); i++ {
rowID, columnID := rowIDs[i], columnIDs[i]
if rowID == prevRow && columnID == prevCol {
continue
}
prevRow, prevCol = rowID, columnID
pos, err := f.pos(rowID, columnID)
if err != nil {
return err
}
columnIDs[next] = pos
next++
// Add row to rowSet.
if rowID != lastRowID {
lastRowID = rowID
rowSet[rowID] = struct{}{}
}
}
positions := columnIDs[:next]
f.mu.Lock()
defer f.mu.Unlock()
if options.Clear {
err = f.importPositions(tx, nil, positions, rowSet)
} else {
err = f.importPositions(tx, positions, nil, rowSet)
}
return errors.Wrap(err, "bulkImportStandard")
}
// parallelSlices provides a sort.Interface for corresponding slices of
// column and row values, and also allows pruning of duplicate values,
// meaning values for the same column, as you might want for a mutex.
//
// The slices should be parallel and of the same length.
type parallelSlices struct {
cols, rows []uint64
}
// prune eliminates values which have the same column key and are
// adjacent in the slice. It doesn't handle non-adjacent keys, but
// does report whether it saw any. See fullPrune for what you probably
// want to be using.
func (p *parallelSlices) prune() (unsorted bool) {
l := len(p.cols)
if l == 0 {
return
}
n := 0
prev := p.cols[0]
// At any point, n is the index of the last value we wrote
// (we pretend we copied 0 to 0 before starting). If the next
// value would have the same column ID, it should replace
// the one we just wrote, otherwise we move n to point to a
// new slot before writing. If there are no duplicates, n is
// always equal to i. we don't check for this because skipping
// those writes would require an extra branch...
for i := 1; i < l; i++ {
next := p.cols[i]
if next < prev {
unsorted = true
}
if next != prev {
n++
}
prev = next
p.cols[n] = p.cols[i]
p.rows[n] = p.rows[i]
}
p.rows = p.rows[:n+1]
p.cols = p.cols[:n+1]
return unsorted
}
// fullPrune trims any adjacent values with identical column keys (and
// the corresponding row values), and if it notices that anything was unsorted,
// does a stable sort by column key and tries that again, ensuring that
// there's no items with the same column key. The last entry with a given
// column key wins.
func (p *parallelSlices) fullPrune() {
if len(p.cols) == 0 {
return
}
if len(p.rows) != len(p.cols) {
vprint.PanicOn("parallelSlices must have same length for rows and columns")
}
unsorted := p.prune()
if unsorted {
// Q: why sort.Stable instead of sort.Sort?
//
// A: Because we need to ensure that the last entry
// is the one that wins. The last entry is the most recent update, and
// so the mutex field should reflect that one and not earlier
// updates. Mutex fields are special in that only 1 bit can
// be hot (set to 1), so the last update overrides all the others. We
// exploit this to eliminate irrelevant earlier writes.
//
// So if the input columns were {1, 2, 1},
// and input rows were {3, 4, 5},
// we need to be sure that we end up with cols:{1, 2}
// rows:{5, 4} // right, last update won.
// and not rows:{3, 4} // wrong, first update won.
//
// illustrated: (dk = don't know state)
//
// the new, raw data before later updates "win":
//
// col0 col1 col2
// row3 dk 1 dk
// row4 dk dk 1
// row5 dk 1 dk
//
// after last one wins, to be written to the backend:
//
// col0 col1 col2
// row3 dk 0 0
// row4 dk 0 1
// row5 dk 1 0
// ^
// \-- on col1. The last one won, b/c its a mutex all other rows go to 0 for that column.
//
// Which means we need a stable sort, ensuring that if two things
// have the same column key, they stay in the same relative order,
// and then the prune algorithm always keeps the last.
sort.Stable(p)
_ = p.prune()
}
}
func (p *parallelSlices) Len() int {
return len(p.cols)
}
func (p *parallelSlices) Less(i, j int) bool {
return p.cols[i] < p.cols[j]
}
func (p parallelSlices) Swap(i, j int) {
p.cols[i], p.cols[j] = p.cols[j], p.cols[i]
p.rows[i], p.rows[j] = p.rows[j], p.rows[i]
}
// importPositions takes slices of positions within the fragment to set and
// clear in storage. One must also pass in the set of unique rows which are
// affected by the set and clear operations. It is unprotected (f.mu must be
// locked when calling it). No position should appear in both set and clear.
//
// importPositions tries to intelligently decide whether or not to do a full
// snapshot of the fragment or just do in-memory updates while appending
// operations to the op log.
func (f *fragment) importPositions(tx Tx, set, clear []uint64, rowSet map[uint64]struct{}) error {
if len(set) > 0 {
f.stats.Count(MetricImportingN, int64(len(set)), 1)
// TODO benchmark Add/RemoveN behavior with sorted/unsorted positions
changedN, err := tx.Add(f.index(), f.field(), f.view(), f.shard, set...)
if err != nil {
return errors.Wrap(err, "adding positions")
}
f.stats.Count(MetricImportedN, int64(changedN), 1)
}
if len(clear) > 0 {
f.stats.Count(MetricClearingN, int64(len(clear)), 1)
changedN, err := tx.Remove(f.index(), f.field(), f.view(), f.shard, clear...)
if err != nil {
return errors.Wrap(err, "clearing positions")
}
f.stats.Count(MetricClearedN, int64(changedN), 1)
}
return f.updateCaching(tx, rowSet)
}
// updateCaching clears checksums for rows, and clears any existing TopN
// cache for them, and marks the cache for needing updates. I'm not sure
// that's correct. This was originally the tail end of importPositions, but
// we want to be able to access the same logic from elsewhere.
func (f *fragment) updateCaching(tx Tx, rowSet map[uint64]struct{}) error {
// Update cache counts for all affected rows.
for rowID := range rowSet {
// Invalidate block checksum.
delete(f.checksums, int(rowID/HashBlockSize))
if f.CacheType != CacheTypeNone {
start := rowID * ShardWidth
end := (rowID + 1) * ShardWidth
n, err := tx.CountRange(f.index(), f.field(), f.view(), f.shard, start, end)
if err != nil {
return errors.Wrap(err, "CountRange")
}
f.cache.BulkAdd(rowID, n)
}
}
if f.CacheType != CacheTypeNone {
f.cache.Invalidate()
}
return nil
}
// sliceDifference removes everything from original that's found in remove,
// updating the slice in place, and returns the compacted slice. The input
// sets should be sorted.
func sliceDifference(original, remove []uint64) []uint64 {
if len(remove) == 0 {
return original
}
rn := 0
rv := remove[rn]
on := 0
ov := uint64(0)
n := 0
for on, ov = range original {
for rv < ov {
rn++
if rn >= len(remove) {
return append(original[:n], original[on:]...)
}
rv = remove[rn]
}
if rv != ov {
original[n] = ov
n++
}
}
return original[:n]
}
// bulkImportMutex performs a bulk import on a fragment while ensuring
// mutex restrictions. Because the mutex requirements must be checked
// against storage, this method must acquire a write lock on the fragment
// during the entire process, and it handles every bit independently.
func (f *fragment) bulkImportMutex(tx Tx, rowIDs, columnIDs []uint64, options *ImportOptions) error {
f.mu.Lock()
defer f.mu.Unlock()
// if ingest promises that this is "fully sorted", then we have been
// promised that (1) there's no duplicate entries that need to be
// pruned, (2) the input is sorted by row IDs and then column IDs,
// meaning that we will generate positions in strictly sequential order.
if !options.fullySorted {
p := parallelSlices{cols: columnIDs, rows: rowIDs}
p.fullPrune()
columnIDs = p.cols
rowIDs = p.rows
}
// create a mask of columns we care about
columns := roaring.NewSliceBitmap(columnIDs...)
// we now need to find existing rows for these bits.
rowSet := make(map[uint64]struct{}, len(rowIDs))
unsorted := false
prev := uint64(0)
for i := range rowIDs {
rowID, columnID := rowIDs[i], columnIDs[i]
rowSet[rowID] = struct{}{}
pos, err := f.pos(rowID, columnID)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("finding pos for row %d, col %d", rowID, columnID))
}
// positions are sorted by columns, but not by absolute
// position. we might want them sorted, though.
if pos < prev {
if options.fullySorted {
fmt.Printf("HELP! was promised fully sorted input, but previous position was %d, now generated %d\n",
prev, pos)
}
unsorted = true
}
prev = pos
rowIDs[i] = pos
}
toSet := rowIDs
if unsorted {
sort.Slice(toSet, func(i, j int) bool { return toSet[i] < toSet[j] })
}
nextKey := toSet[0] >> 16
scratchContainer := roaring.NewContainerArray([]uint16{})
rewriteExisting := roaring.NewBitmapBitmapTrimmer(columns, func(key roaring.FilterKey, data *roaring.Container, filter *roaring.Container, writeback roaring.ContainerWriteback) error {
var inserting []uint64
for roaring.FilterKey(nextKey) < key {
thisKey := roaring.FilterKey(nextKey)
inserting, toSet, nextKey = roaring.GetMatchingKeysFrom(toSet, nextKey)
scratchContainer = roaring.RemakeContainerFrom(scratchContainer, inserting)
err := writeback(thisKey, scratchContainer)
if err != nil {
return err
}
}
// we only wanted to insert the data we had before the end, there's
// no actual data here to modify.
if data == nil {
return nil
}
if roaring.FilterKey(nextKey) > key {
// simple path: we only have to remove things from the filter,
// if there are any.
if filter.N() == 0 {
return nil
}
existing := data.N()
data = data.DifferenceInPlace(filter)
if data.N() != existing {
rowSet[key.Row()] = struct{}{}
return writeback(key, data)
}
return nil
}
// nextKey has to be the same as key. we have values to insert, and
// necessarily have a filter to remove which matches them. so we're
// going to remove everything in the filter, then add all the values
// we have to insert. but! in the case where a bit is already set,
// and we remove it and re-add it, we don't want to count that.
inserting, toSet, nextKey = roaring.GetMatchingKeysFrom(toSet, nextKey)
existing := data.N()
// so, we want to remove anything that's in the filter, *but*, if a
// thing is in the filter, and we then add it back, that doesn't
// count. but if a thing is in the filter, but *wasn't originally
// there*, that counts. But we can't check that *after* we compute
// the difference, so...
reAdds := 0
for _, v := range inserting {
if filter.Contains(uint16(v)) && data.Contains(uint16(v)) {
reAdds++
}
}
data = data.DifferenceInPlace(filter)
removes := int(existing - data.N())
var changed bool
adds := 0
for _, v := range inserting {
data, changed = data.Add(uint16(v))
if changed {
adds++
}
}
// if we added more things than were being readded, or removed more
// things than were being readded, we changed something.
if adds > reAdds || removes > reAdds {
rowSet[key.Row()] = struct{}{}
return writeback(key, data)
}
return nil
})
err := tx.ApplyRewriter(f.index(), f.field(), f.view(), f.shard, 0, rewriteExisting)
if err != nil {
return err
}
return f.updateCaching(tx, rowSet)
}
// ClearRecords deletes all bits for the given records. It's basically
// the remove-only part of setting a mutex.
func (f *fragment) ClearRecords(tx Tx, recordIDs []uint64) (bool, error) {
// create a mask of columns we care about
columns := roaring.NewSliceBitmap(recordIDs...)
return f.clearRecordsByBitmap(tx, columns)
}
func (f *fragment) clearRecordsByBitmap(tx Tx, columns *roaring.Bitmap) (changed bool, err error) {
f.mu.Lock()
defer f.mu.Unlock()
return f.unprotectedClearRecordsByBitmap(tx, columns)
}
// clearRecordsByBitmap clears bits in a fragment that correspond to those
// positions within the bitmap.
func (f *fragment) unprotectedClearRecordsByBitmap(tx Tx, columns *roaring.Bitmap) (changed bool, err error) {
rowSet := make(map[uint64]struct{})
rewriteExisting := roaring.NewBitmapBitmapTrimmer(columns, func(key roaring.FilterKey, data *roaring.Container, filter *roaring.Container, writeback roaring.ContainerWriteback) error {
if filter.N() == 0 {
return nil
}
existing := data.N()
// nothing to delete. this can't happen normally, but the rewriter calls
// us with an empty data container when it's done.
if existing == 0 {
return nil
}
data = data.DifferenceInPlace(filter)
if data.N() != existing {
rowSet[key.Row()] = struct{}{}
changed = true
return writeback(key, data)
}
return nil
})
err = tx.ApplyRewriter(f.index(), f.field(), f.view(), f.shard, 0, rewriteExisting)
if err != nil {
return false, err
}
return changed, f.updateCaching(tx, rowSet)
}
// importValue bulk imports a set of range-encoded values.
func (f *fragment) importValue(tx Tx, columnIDs []uint64, values []int64, bitDepth uint64, clear bool) error {
f.mu.Lock()
defer f.mu.Unlock()
// Verify that there are an equal number of column ids and values.
if len(columnIDs) != len(values) {
return fmt.Errorf("mismatch of column/value len: %d != %d", len(columnIDs), len(values))
}
positionsByDepth := make([][]uint64, bitDepth+2)
toSetByDepth := make([]int, bitDepth+2)
toClearByDepth := make([]int, bitDepth+2)
batchSize := len(columnIDs)
if batchSize > 65536 {
batchSize = 65536
}
for i := 0; i < int(bitDepth)+2; i++ {
positionsByDepth[i] = make([]uint64, batchSize)
toClearByDepth[i] = batchSize
}
row := 0
columnID := uint64(0)
value := int64(0)
// arbitrarily set prev to be not equal to the first column ID
// we will encounter.
prev := columnIDs[len(columnIDs)-1] + 1
for len(columnIDs) > 0 {
downTo := len(columnIDs) - batchSize
if downTo < 0 {
downTo = 0
}
for i := range positionsByDepth {
toSetByDepth[i] = 0
toClearByDepth[i] = batchSize
}
for i := len(columnIDs) - 1; i >= downTo; i-- {
columnID, value = columnIDs[i], values[i]
columnID = columnID % ShardWidth
if columnID == prev {
continue
}
prev = columnID
row = 0
if clear {
toClearByDepth[row]--
positionsByDepth[row][toClearByDepth[row]] = columnID
} else {
positionsByDepth[row][toSetByDepth[row]] = columnID
toSetByDepth[row]++
}
row++
columnID += ShardWidth
if value < 0 {
positionsByDepth[row][toSetByDepth[row]] = columnID
toSetByDepth[row]++
value *= -1
} else {
toClearByDepth[row]--
positionsByDepth[row][toClearByDepth[row]] = columnID
}
row++
columnID += ShardWidth
for j := 0; j < int(bitDepth); j++ {
if value&1 != 0 {
positionsByDepth[row][toSetByDepth[row]] = columnID
toSetByDepth[row]++
} else {
toClearByDepth[row]--
positionsByDepth[row][toClearByDepth[row]] = columnID
}
row++
columnID += ShardWidth
value >>= 1
}
}
for i := range positionsByDepth {
err := f.importPositions(tx, positionsByDepth[i][:toSetByDepth[i]], positionsByDepth[i][toClearByDepth[i]:], nil)
if err != nil {
return errors.Wrap(err, "importing positions")
}
}
columnIDs = columnIDs[:downTo]
}
return nil
}
// importRoaring imports from the official roaring data format defined at
// https://github.com/RoaringBitmap/RoaringFormatSpec or from pilosa's version
// of the roaring format. The cache is updated to reflect the new data.
func (f *fragment) importRoaring(ctx context.Context, tx Tx, data []byte, clear bool) error {
span, ctx := tracing.StartSpanFromContext(ctx, "fragment.importRoaring")
defer span.Finish()
rowSet, updateCache, err := f.doImportRoaring(ctx, tx, data, clear)
if err != nil {
return errors.Wrap(err, "doImportRoaring")
}
if updateCache {
return f.updateCachePostImport(ctx, rowSet)
}
return nil
}
// ImportRoaringClearAndSet simply clears the bits in clear and sets the bits in set.
func (f *fragment) ImportRoaringClearAndSet(ctx context.Context, tx Tx, clear, set []byte) error {
clearIter, err := roaring.NewContainerIterator(clear)
if err != nil {
return errors.Wrap(err, "getting clear iterator")
}
setIter, err := roaring.NewContainerIterator(set)
if err != nil {
return errors.Wrap(err, "getting set iterator")
}
rewriter, err := roaring.NewClearAndSetRewriter(clearIter, setIter)
if err != nil {
return errors.Wrap(err, "getting rewriter")
}
err = tx.ApplyRewriter(f.index(), f.field(), f.view(), f.shard, 0, rewriter)
if err != nil {
errors.Wrap(err, "applying rewriter")
}
if f.CacheType != CacheTypeNone {
// TODO this may be quite a bit slower than the way
// importRoaring does it as it tracks the number of bits
// changed per row. We could do that, but I think it'd require
// significant changes to the Rewriter API.
f.mu.Lock()
defer f.mu.Unlock()
return f.rebuildRankCache(ctx, tx)
}
return nil
}
// ImportRoaringBSI interprets "clear" as a single row specifying
// records to be cleared, and "set" as specifying the values to be set
// which implies clearing any other values in those columns.
func (f *fragment) ImportRoaringBSI(ctx context.Context, tx Tx, clear, set []byte) error {
// In this first block, we take the first row of clear as records
// we want to unconditionally clear, and the first row of set as
// records we also want to clear because they're going to get set
// and Union the two together into a single clearing iterator.
clearclearIter, err := roaring.NewRepeatedRowIteratorFromBytes(clear)
if err != nil {
return errors.Wrap(err, "getting clear iterator")
}
setClearIter, err := roaring.NewRepeatedRowIteratorFromBytes(set)
if err != nil {
return errors.Wrap(err, "getting set/clear iterator")
}
clearIter := roaring.NewUnionContainerIterator(clearclearIter, setClearIter)
// Then we get the set iterator and create the rewriter.
setIter, err := roaring.NewContainerIterator(set)
if err != nil {
return errors.Wrap(err, "getting set iterator")
}
rewriter, err := roaring.NewClearAndSetRewriter(clearIter, setIter)
if err != nil {
return errors.Wrap(err, "getting rewriter")
}
err = tx.ApplyRewriter(f.index(), f.field(), f.view(), f.shard, 0, rewriter)
return errors.Wrap(err, "applying rewriter")
}
// ImportRoaringSingleValued treats "clear" as a single row and clears
// all the columns specified, then sets all the bits in set. It's very
// similar to ImportRoaringBSI, but doesn't treate the first row of
// "set" as the existence row to also be cleared. Essentially it's for
// FieldTypeMutex.
func (f *fragment) ImportRoaringSingleValued(ctx context.Context, tx Tx, clear, set []byte) error {
clearIter, err := roaring.NewRepeatedRowIteratorFromBytes(clear)
if err != nil {
return errors.Wrap(err, "getting cleariterator")
}
setIter, err := roaring.NewContainerIterator(set)
if err != nil {
return errors.Wrap(err, "getting set iterator")
}
rewriter, err := roaring.NewClearAndSetRewriter(clearIter, setIter)
if err != nil {
return errors.Wrap(err, "getting rewriter")
}
err = tx.ApplyRewriter(f.index(), f.field(), f.view(), f.shard, 0, rewriter)
return errors.Wrap(err, "applying rewriter")
}
func (f *fragment) doImportRoaring(ctx context.Context, tx Tx, data []byte, clear bool) (map[uint64]int, bool, error) {
f.mu.RLock()
defer f.mu.RUnlock()
rowSize := uint64(1 << shardVsContainerExponent)
span, _ := tracing.StartSpanFromContext(ctx, "importRoaring.ImportRoaringBits")
defer span.Finish()
var rowSet map[uint64]int
err := func() (err error) {
var rit roaring.RoaringIterator
rit, err = roaring.NewRoaringIterator(data)
if err != nil {
return err
}
_, rowSet, err = tx.ImportRoaringBits(f.index(), f.field(), f.view(), f.shard, rit, clear, true, rowSize)
return err
}()
if err != nil {
return nil, false, err
}
updateCache := f.CacheType != CacheTypeNone
return rowSet, updateCache, err
}
func (f *fragment) updateCachePostImport(ctx context.Context, rowSet map[uint64]int) error {
f.mu.Lock()
defer f.mu.Unlock()
anyChanged := false
for rowID, changes := range rowSet {
if changes == 0 {
continue
}
anyChanged = true
if changes < 0 {
absChanges := uint64(-1 * changes)
if absChanges <= f.cache.Get(rowID) {
f.cache.BulkAdd(rowID, f.cache.Get(rowID)-absChanges)
} else {
f.cache.BulkAdd(rowID, 0)
}
} else {
f.cache.BulkAdd(rowID, f.cache.Get(rowID)+uint64(changes))
}
}
// we only set this if we need to update the cache
if anyChanged {
f.cache.Invalidate()
}
return nil
}
// importRoaringOverwrite overwrites the specified block with the provided data.
func (f *fragment) importRoaringOverwrite(ctx context.Context, tx Tx, data []byte, block int) error {
// Clear the existing data from fragment block.
if _, err := f.clearBlock(tx, block); err != nil {
return errors.Wrapf(err, "clearing block: %d", block)
}
// Union the new block data with the fragment data.
return f.importRoaring(ctx, tx, data, false)
}
// RecalculateCache rebuilds the cache regardless of invalidate time delay.
func (f *fragment) RecalculateCache() {
f.mu.Lock()
f.cache.Recalculate()
f.mu.Unlock()
}
// FlushCache writes the cache data to disk.
func (f *fragment) FlushCache() error {
f.mu.Lock()
defer f.mu.Unlock()
return f.flushCache()
}
func (f *fragment) rebuildRankCache(ctx context.Context, tx Tx) error {
if f.CacheType != CacheTypeRanked {
return nil // only rebuild ranked caches
}
f.cache.Clear()
rows, err := f.unprotectedRows(ctx, tx, uint64(0))
if err != nil {
return err
}
for _, id := range rows {
n, err := tx.CountRange(f.index(), f.field(), f.view(), f.shard, id*ShardWidth, (id+1)*ShardWidth)
if err != nil {
return errors.Wrap(err, "CountRange")
}
f.cache.BulkAdd(id, n)
}
f.cache.Invalidate()
return nil
}
func (f *fragment) RebuildRankCache(ctx context.Context) error {
if f.CacheType != CacheTypeRanked {
return nil //only rebuild ranked caches
}
f.mu.Lock()
defer f.mu.Unlock()
tx, err := f.holder.BeginTx(false, f.idx, f.shard)
if err != nil {
return err
}
defer tx.Rollback()
return f.rebuildRankCache(ctx, tx)
}
func (f *fragment) flushCache() error {
if f.cache == nil {
return nil
}
if f.CacheType == CacheTypeNone {
return nil
}
// Retrieve a list of row ids from the cache.
ids := f.cache.IDs()
// Marshal cache data to bytes.
buf, err := proto.Marshal(&pb.Cache{IDs: ids})
if err != nil {
return errors.Wrap(err, "marshalling")
}
if err := os.MkdirAll(filepath.Dir(f.cachePath()), 0750); err != nil {
return errors.Wrap(err, "mkdir")
}
// Write to disk.
if err := os.WriteFile(f.cachePath(), buf, 0600); err != nil {
return errors.Wrap(err, "writing")
}
return nil
}
// WriteTo writes the fragment's data to w.
func (f *fragment) WriteTo(w io.Writer) (n int64, err error) {
// Force cache flush.
if err := f.FlushCache(); err != nil {
return 0, errors.Wrap(err, "flushing cache")
}
// Write out data and cache to a tar archive.
tw := tar.NewWriter(w)
if err := f.writeStorageToArchive(tw); err != nil {
return 0, fmt.Errorf("write storage: %s", err)
}
if err := f.writeCacheToArchive(tw); err != nil {
return 0, fmt.Errorf("write cache: %s", err)
}
return 0, nil
}
// used in shipping the slices across the network for a resize.
func (f *fragment) writeStorageToArchive(tw *tar.Writer) error {
tx := f.idx.holder.txf.NewTx(Txo{Write: !writable, Index: f.idx, Shard: f.shard})
defer tx.Rollback()
rbm, err := tx.RoaringBitmap(f.index(), f.field(), f.view(), f.shard)
if err != nil {
return errors.Wrap(err, "RoaringBitmapReader RoaringBitmap")
}
var buf bytes.Buffer
sz, err := rbm.WriteTo(&buf)
if err != nil {
return errors.Wrap(err, "RoaringBitmapReader rbm.WriteTo(buf)")
}
// Write archive header.
if err := tw.WriteHeader(&tar.Header{
Name: "data",
Mode: 0600,
Size: sz,
ModTime: time.Now(),
}); err != nil {
return errors.Wrap(err, "writing header")
}
// Copy the file up to the last known size.
// This is done outside the lock because the storage format is append-only.
if _, err := io.CopyN(tw, &buf, sz); err != nil {
return errors.Wrap(err, "copying")
}
return nil
}
func (f *fragment) writeCacheToArchive(tw *tar.Writer) error {
f.mu.Lock()
defer f.mu.Unlock()
// Read cache into buffer.
buf, err := os.ReadFile(f.cachePath())
if os.IsNotExist(err) {
return nil
} else if err != nil {
return errors.Wrap(err, "reading cache")
}
// Write archive header.
if err := tw.WriteHeader(&tar.Header{
Name: "cache",
Mode: 0600,
Size: int64(len(buf)),
ModTime: time.Now(),
}); err != nil {
return errors.Wrap(err, "writing header")
}
// Write data to archive.
if _, err := tw.Write(buf); err != nil {
return errors.Wrap(err, "writing")
}
return nil
}
// ReadFrom reads a data file from r and loads it into the fragment.
func (f *fragment) ReadFrom(r io.Reader) (n int64, err error) {
f.mu.Lock()
defer f.mu.Unlock()
tr := tar.NewReader(r)
for {
// Read next tar header.
hdr, err := tr.Next()
if err == io.EOF {
break
} else if err != nil {
return 0, errors.Wrap(err, "opening")
}
// Process file based on file name.
switch hdr.Name {
case "data":
tx := f.holder.txf.NewTx(Txo{Write: writable, Index: f.idx, Fragment: f, Shard: f.shard})
defer tx.Rollback()
if err := f.fillFragmentFromArchive(tx, tr); err != nil {
return 0, errors.Wrap(err, "reading storage")
}
if err := tx.Commit(); err != nil {
return 0, errors.Wrap(err, "Commit after tx.ReadFragmentFromArchive")
}
case "cache":
if err := f.readCacheFromArchive(tr); err != nil {
return 0, errors.Wrap(err, "reading cache")
}
default:
return 0, fmt.Errorf("invalid fragment archive file: %s", hdr.Name)
}
}
return 0, nil
}
// should be morally equivalent to fragment.readStorageFromArchive()
// below for RoaringTx, but also work on any Tx because it uses
// tx.ImportRoaringBits().
func (f *fragment) fillFragmentFromArchive(tx Tx, r io.Reader) error {
// this is reading from inside a tarball, so definitely no need
// to close it here.
data, err := io.ReadAll(r)
if err != nil {
return errors.Wrap(err, "fillFragmentFromArchive io.ReadAll(r)")
}
if len(data) == 0 {
return nil
}
// For reference, compare to what fragment.go:313 fragment.importStorage() does.
clear := false
log := false
rowSize := uint64(0)
itr, err := roaring.NewRoaringIterator(data)
if err != nil {
return errors.Wrap(err, "fillFragmentFromArchive NewRoaringIterator")
}
changed, rowSet, err := tx.ImportRoaringBits(f.index(), f.field(), f.view(), f.shard, itr, clear, log, rowSize)
_, _ = changed, rowSet
if err != nil {
return errors.Wrap(err, "fillFragmentFromArchive ImportRoaringBits")
}
return nil
}
func (f *fragment) readCacheFromArchive(r io.Reader) error {
// Slurp data from reader and write to disk.
buf, err := io.ReadAll(r)
if err != nil {
return errors.Wrap(err, "reading")
} else if err := os.WriteFile(f.cachePath(), buf, 0600); err != nil {
return errors.Wrap(err, "writing")
}
// Re-open cache.
if err := f.openCache(); err != nil {
return errors.Wrap(err, "opening")
}
return nil
}
func (f *fragment) minRowID(tx Tx) (uint64, bool, error) {
min, ok, err := tx.Min(f.index(), f.field(), f.view(), f.shard)
return min / ShardWidth, ok, err
}
// rows returns all rows starting from 'start'. Filters will be applied in
// order. All filters must return true to include the row. Once a row is
// included, further containers in that row will be skipped. So, for a row to be
// included, there must be one container in that row where all filters return
// true. For a row to be skipped, at least one filter must return false for each
// container in that row (it need not be the same filter for each). Any filter
// returning done == true will cause processing to stop after all filters for
// this container have been processed. The rows accumulated up to this point
// (including this row if all filters passed) will be returned.
func (f *fragment) rows(ctx context.Context, tx Tx, start uint64, filters ...roaring.BitmapFilter) ([]uint64, error) {
f.mu.RLock()
defer f.mu.RUnlock()
return f.unprotectedRows(ctx, tx, start, filters...)
}
// unprotectedRows calls rows without grabbing the mutex.
func (f *fragment) unprotectedRows(ctx context.Context, tx Tx, start uint64, filters ...roaring.BitmapFilter) ([]uint64, error) {
var rows []uint64
cb := func(row uint64) error {
rows = append(rows, row)
return nil
}
startKey := rowToKey(start)
filter := roaring.NewBitmapRowFilter(cb, filters...)
err := tx.ApplyFilter(f.index(), f.field(), f.view(), f.shard, startKey, filter)
if err != nil {
return nil, err
} else {
return rows, nil
}
}
// unionRows yields the union of the given rows in this fragment
func (f *fragment) unionRows(ctx context.Context, tx Tx, rows []uint64) (*Row, error) {
f.mu.RLock()
defer f.mu.RUnlock()
return f.unprotectedUnionRows(ctx, tx, rows)
}
// unprotectedRows calls rows without grabbing the mutex.
func (f *fragment) unprotectedUnionRows(ctx context.Context, tx Tx, rows []uint64) (*Row, error) {
filter := roaring.NewBitmapRowsUnion(rows)
err := tx.ApplyFilter(f.index(), f.field(), f.view(), f.shard, 0, filter)
if err != nil {
return nil, err
} else {
row := &Row{
Segments: []RowSegment{{
data: filter.Results(f.shard),
shard: f.shard,
writable: true,
}},
}
row.invalidateCount()
return row, nil
}
}
type rowIterator interface {
// TODO(kuba) linter suggests to use io.Seeker
// Seek(offset int64, whence int) (int64, error)
Seek(uint64)
Next() (*Row, uint64, *int64, bool, error)
}
func (f *fragment) rowIterator(tx Tx, wrap bool, filters ...roaring.BitmapFilter) (rowIterator, error) {
if strings.HasPrefix(f.view(), viewBSIGroupPrefix) {
return f.intRowIterator(tx, wrap, filters...)
}
// viewStandard
// TODO(kuba) - IMHO we should check if f.view() is viewStandard,
// but because of testing the function returns set iterator as default one.
return f.setRowIterator(tx, wrap, filters...)
}
type timeRowIterator struct {
tx Tx
cur int
wrap bool
allRowIDs []uint64
rowIDToFragments map[uint64][]*fragment
}
func timeFragmentsRowIterator(fragments []*fragment, tx Tx, wrap bool, filters ...roaring.BitmapFilter) (rowIterator, error) {
if len(fragments) == 0 {
return nil, fmt.Errorf("there should be at least 1 fragment")
} else if len(fragments) == 1 {
return fragments[0].setRowIterator(tx, wrap, filters...)
}
it := &timeRowIterator{
tx: tx,
cur: 0,
wrap: wrap,
}
// create a sort of inverted index that maps each
// rowID back to the fragments that have that rowID
rowIDToFragments := make(map[uint64][]*fragment)
for _, f := range fragments {
rowIDs, err := f.rows(context.Background(), tx, 0, filters...)
if err != nil {
return nil, err
}
for _, rowID := range rowIDs {
fs := append(rowIDToFragments[rowID], f)
rowIDToFragments[rowID] = fs
}
}
// if len(rowIDToFragments) == 0 what to do ??
// ie all fragments returned empty rowIDs, is this possible
// is this an error
// collect all rowIDs from inverted index to a slice
allRowIDs := make([]uint64, len(rowIDToFragments))
i := 0
for rowID := range rowIDToFragments {
allRowIDs[i] = rowID
i++
}
sort.Slice(allRowIDs, func(i, j int) bool { return allRowIDs[i] < allRowIDs[j] })
it.rowIDToFragments = rowIDToFragments
it.allRowIDs = allRowIDs
return it, nil
}
func (it *timeRowIterator) Seek(rowID uint64) {
idx := sort.Search(len(it.allRowIDs), func(i int) bool {
return it.allRowIDs[i] >= rowID
})
it.cur = idx
}
func (it *timeRowIterator) Next() (r *Row, rowID uint64, _ *int64, wrapped bool, err error) {
if it.cur >= len(it.allRowIDs) {
if !it.wrap || len(it.allRowIDs) == 0 {
return nil, 0, nil, true, nil
}
it.Seek(0)
wrapped = true
}
// gather rows
rowID = it.allRowIDs[it.cur]
fragments := it.rowIDToFragments[rowID]
rows := make([]*Row, 0, len(fragments))
for _, fragment := range fragments {
row, err := fragment.row(it.tx, rowID)
if err != nil {
return row, rowID, nil, wrapped, err
}
rows = append(rows, row)
}
// union rows
r = rows[0].Union(rows[1:]...)
it.cur++
return r, rowID, nil, wrapped, nil
}
type intRowIterator struct {
f *fragment
values int64Slice // sorted slice of int values
colIDs map[int64][]uint64 // [int value] -> [column IDs]
cur int // current value index() (rowID)
wrap bool
}
func (f *fragment) intRowIterator(tx Tx, wrap bool, filters ...roaring.BitmapFilter) (rowIterator, error) {
it := intRowIterator{
f: f,
colIDs: make(map[int64][]uint64),
cur: 0,
wrap: wrap,
}
// accumulator [column ID] -> [int value]
acc := make(map[uint64]int64)
f.mu.RLock()
defer f.mu.RUnlock()
callback := func(rid uint64) error {
// skip exist(0) and sign(1) rows
if rid == bsiExistsBit || rid == bsiSignBit {
return nil
}
val := int64(1 << (rid - bsiOffsetBit))
r, err := f.unprotectedRow(tx, rid)
if err != nil {
return err
}
for _, cid := range r.Columns() {
acc[cid] |= val
}
return nil
}
if err := f.foreachRow(tx, filters, callback); err != nil {
return nil, err
}
// apply exist and sign bits
r0, err := f.unprotectedRow(tx, 0)
if err != nil {
return nil, err
}
allCols := r0.Columns()
r1, err := f.unprotectedRow(tx, 1)
if err != nil {
return nil, err
}
signCols := r1.Columns()
signIdx, signLen := 0, len(signCols)
// all distinct values
values := make(map[int64]struct{})
for _, cid := range allCols {
// apply sign bit
if signIdx < signLen && cid == signCols[signIdx] {
if tmp, ok := acc[cid]; ok {
acc[cid] = -tmp
}
signIdx++
}
val := acc[cid]
it.colIDs[val] = append(it.colIDs[val], cid)
if _, ok := values[val]; !ok {
it.values = append(it.values, val)
values[val] = struct{}{}
}
}
sort.Sort(it.values)
return &it, nil
}
func (f *fragment) foreachRow(tx Tx, filters []roaring.BitmapFilter, fn func(rid uint64) error) error {
filter := roaring.NewBitmapRowFilter(fn, filters...)
return tx.ApplyFilter(f.index(), f.field(), f.view(), f.shard, 0, filter)
}
func (it *intRowIterator) Seek(rowID uint64) {
idx := sort.Search(len(it.values), func(i int) bool {
return it.values[i] >= it.values[rowID]
})
it.cur = idx
}
func (it *intRowIterator) Next() (r *Row, rowID uint64, value *int64, wrapped bool, err error) {
if it.cur >= len(it.values) {
if !it.wrap || len(it.values) == 0 {
return nil, 0, nil, true, nil
}
wrapped = true
it.cur = 0
}
if it.cur >= 0 {
rowID = uint64(it.cur)
value = &it.values[rowID]
r = NewRow(it.colIDs[*value]...)
}
it.cur++
return r, rowID, value, wrapped, nil
}
type setRowIterator struct {
tx Tx
f *fragment
rowIDs []uint64
cur int
wrap bool
}
func (f *fragment) setRowIterator(tx Tx, wrap bool, filters ...roaring.BitmapFilter) (rowIterator, error) {
rows, err := f.rows(context.Background(), tx, 0, filters...)
if err != nil {
return nil, err
}
return &setRowIterator{
tx: tx,
f: f,
rowIDs: rows, // TODO: this may be memory intensive in high cardinality cases
wrap: wrap,
}, nil
}
func (it *setRowIterator) Seek(rowID uint64) {
idx := sort.Search(len(it.rowIDs), func(i int) bool {
return it.rowIDs[i] >= rowID
})
it.cur = idx
}
func (it *setRowIterator) Next() (r *Row, rowID uint64, _ *int64, wrapped bool, err error) {
if it.cur >= len(it.rowIDs) {
if !it.wrap || len(it.rowIDs) == 0 {
return nil, 0, nil, true, nil
}
it.Seek(0)
wrapped = true
}
id := it.rowIDs[it.cur]
r, err = it.f.row(it.tx, id)
if err != nil {
return r, rowID, nil, wrapped, err
}
rowID = id
it.cur++
return r, rowID, nil, wrapped, nil
}
// pos returns the row position of a row/column pair.
func pos(rowID, columnID uint64) uint64 {
return (rowID * ShardWidth) + (columnID % ShardWidth)
}
// vector stores the mapping of colID to rowID.
// It's used for a mutex field type.
type vector interface {
Get(tx Tx, colID uint64) (uint64, bool, error)
}
// rowsVector implements the vector interface by looking
// at row data as needed.
type rowsVector struct {
f *fragment
}
// newRowsVector returns a rowsVector for a given fragment.
func newRowsVector(f *fragment) *rowsVector {
return &rowsVector{
f: f,
}
}
// Get returns the rowID associated to the given colID.
// Additionally, it returns true if a value was found,
// otherwise it returns false. Ensure that you already
// have the mutex before calling this.
func (v *rowsVector) Get(tx Tx, colID uint64) (uint64, bool, error) {
rows, err := v.f.unprotectedRows(context.Background(), tx, 0, roaring.NewBitmapColumnFilter(colID))
if err != nil {
return 0, false, err
} else if len(rows) > 1 {
return 0, false, errors.New("found multiple row values for column")
} else if len(rows) == 1 {
return rows[0], true, nil
}
return 0, false, nil
}
// rowToKey converts a Pilosa row ID to the key of the container which starts
// that row in the bitmap which represents this entire fragment. A fragment is
// all the rows within a shard within a field concatenated together.
func rowToKey(rowID uint64) (key uint64) {
return rowID * (ShardWidth / containerWidth)
}
// boolVector implements the vector interface by looking
// at data in rows 0 and 1.
type boolVector struct {
f *fragment
}
// newBoolVector returns a boolVector for a given fragment.
func newBoolVector(f *fragment) *boolVector {
return &boolVector{
f: f,
}
}
// Get returns the rowID associated to the given colID.
// Additionally, it returns true if a value was found,
// otherwise it returns false. Ensure that you already
// have the fragment mutex before calling this.
func (v *boolVector) Get(tx Tx, colID uint64) (uint64, bool, error) {
rows, err := v.f.unprotectedRows(context.Background(), tx, 0, roaring.NewBitmapColumnFilter(colID))
if err != nil {
return 0, false, err
} else if len(rows) > 1 {
return 0, false, errors.New("found multiple row values for column")
} else if len(rows) == 1 {
switch rows[0] {
case falseRowID, trueRowID:
return rows[0], true, nil
default:
return 0, false, errors.New("found non-boolean value")
}
}
return 0, false, nil
}
// FormatQualifiedFragmentName generates a qualified name for the fragment to be used with Tx operations.
func FormatQualifiedFragmentName(index, field, view string, shard uint64) string {
return fmt.Sprintf("%s\x00%s\x00%s\x00%d", index, field, view, shard)
}
// ParseQualifiedFragmentName parses a qualified name into its parts.
func ParseQualifiedFragmentName(name string) (index, field, view string, shard uint64, err error) {
a := strings.Split(name, "\x00")
if len(a) < 4 {
return "", "", "", 0, fmt.Errorf("invalid qualified name: %q", name)
}
index, field, view = string(a[0]), string(a[1]), string(a[2])
if shard, err = strconv.ParseUint(a[3], 10, 64); err != nil {
return "", "", "", 0, fmt.Errorf("invalid qualified name: %q", name)
}
return index, field, view, shard, nil
}
type RowKV struct {
RowID uint64 `json:"id"`
Value interface{} `json:"value"`
}
func (r *RowKV) Compare(o RowKV, desc bool) (bool, bool) {
switch val := r.Value.(type) {
case string:
if oVal, ok := o.Value.(string); ok {
return desc != (val < oVal), true
}
return desc, false
case bool:
if oVal, ok := o.Value.(bool); ok {
return desc != oVal, true
}
return desc, false
case int64:
if oVal, ok := o.Value.(int64); ok {
return desc != (val < oVal), true
}
return desc, false
default:
return desc, false
}
}
// sortBSIData, fetches the rows and seperates the positive and negetive values.
// these values and sorted seperately and appended
func (f *fragment) sortBsiData(tx Tx, filter *Row, bitDepth uint64, sort_desc bool) (*SortedRow, error) {
consider, err := f.row(tx, bsiExistsBit)
if err != nil {
return nil, err
} else if filter != nil {
consider = consider.Intersect(filter)
}
row, err := f.row(tx, bsiSignBit)
if err != nil {
return nil, err
}
pos := consider.Difference(row)
neg := consider.Difference(pos)
var sortedRowIds []RowKV
f.flattenRowValues(tx, &sortedRowIds, neg, bitDepth, -1)
ok := true
f.flattenRowValues(tx, &sortedRowIds, pos, bitDepth, 1)
sort.SliceStable(sortedRowIds, func(i, j int) bool {
if c, k := sortedRowIds[i].Compare(sortedRowIds[j], sort_desc); k {
return c
} else {
ok = false
return !k
}
})
if !ok {
return nil, errors.New("Couldn't compare field type for sorting")
}
return &SortedRow{
Row: consider,
RowKVs: sortedRowIds,
}, nil
}
func (f *fragment) flattenRowValues(tx Tx, sortedRowIds *[]RowKV, filter *Row, bitDepth uint64, sign int64) error {
m := make(map[uint64]int64)
for i := int(bitDepth - 1); i >= 0; i-- {
row, err := f.row(tx, uint64(bsiOffsetBit+i))
if err != nil {
return err
}
row = row.Intersect(filter)
for _, v := range row.Columns() {
if val, ok := m[v]; ok {
m[v] = val | (1 << i)
} else {
m[v] = (1 << i)
}
}
}
for k, v := range m {
*sortedRowIds = append(*sortedRowIds, RowKV{
RowID: k,
Value: (v * sign),
})
}
return nil
}