mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
827 lines
26 KiB
Go
827 lines
26 KiB
Go
// Copyright 2022 Molecula Corp. (DBA FeatureBase).
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
package ingest
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math/bits"
|
|
"sort"
|
|
|
|
"github.com/featurebasedb/featurebase/v3/shardwidth"
|
|
)
|
|
|
|
type OpType uint8
|
|
|
|
const (
|
|
OpNone = OpType(iota)
|
|
OpSet
|
|
OpRemove
|
|
OpClear
|
|
OpWrite
|
|
OpDelete
|
|
)
|
|
|
|
var opNames = []string{
|
|
"none",
|
|
"set",
|
|
"remove",
|
|
"clear",
|
|
"write",
|
|
"delete",
|
|
}
|
|
|
|
type FieldType string
|
|
|
|
const (
|
|
FieldTypeSet = "set"
|
|
FieldTypeInt = "int"
|
|
FieldTypeTimeQuantum = "time"
|
|
FieldTypeTimeStamp = "timestamp"
|
|
FieldTypeDecimal = "decimal"
|
|
FieldTypeMutex = "mutex"
|
|
FieldTypeBool = "bool"
|
|
)
|
|
|
|
var fieldTypeSorts = map[FieldType]func(*FieldOperation){
|
|
FieldTypeSet: (*FieldOperation).SortByValues,
|
|
FieldTypeInt: (*FieldOperation).SortByRecords,
|
|
FieldTypeTimeQuantum: (*FieldOperation).SortByValues,
|
|
FieldTypeDecimal: (*FieldOperation).SortByRecords,
|
|
FieldTypeMutex: (*FieldOperation).SortByValues,
|
|
FieldTypeTimeStamp: (*FieldOperation).SortByRecords,
|
|
FieldTypeBool: (*FieldOperation).SortByValues,
|
|
}
|
|
|
|
func (o OpType) String() string {
|
|
if int(o) < len(opNames) {
|
|
return opNames[o]
|
|
}
|
|
return fmt.Sprintf("invalid-optype-%d", o)
|
|
}
|
|
|
|
func ParseOpType(s string) (OpType, error) {
|
|
for i, v := range opNames[1:] {
|
|
if s == v {
|
|
return OpType(i + 1), nil
|
|
}
|
|
}
|
|
return 0, fmt.Errorf("unknown operation type %q", s)
|
|
}
|
|
|
|
// Operation represents a single set of changes to make to
|
|
// the stored data, which means some combination of clearing
|
|
// columns, clearing individual bits, or setting bits or values.
|
|
// The same data structure can be used whether this represents the
|
|
// whole database operation or a single shard's values.
|
|
//
|
|
// Operations can specify individual per-field operations, which
|
|
// have maps of record IDs to values. They can also have a set of
|
|
// record IDs and fields to clear. A Clear operation will have only
|
|
// record IDs and fields, a Set or Remove will have only FieldOps,
|
|
// and a Write will have both -- populating the record IDs and fields
|
|
// from the fieldops.
|
|
//
|
|
// When we parse an operation, we assign each op a sequential ID
|
|
// within the overall request. We keep these IDs associated with ops
|
|
// when splitting them up across shards, so we can reverse this even
|
|
// if some shards don't get some ops.
|
|
type Operation struct {
|
|
OpType OpType
|
|
Seq int // sequence position within a chain of ops
|
|
ClearRecordIDs []uint64
|
|
ClearFields []string
|
|
FieldOps map[string]*FieldOperation
|
|
}
|
|
|
|
// Compare reports whether two Operations seem to be the same. While
|
|
// a FieldOperation can be "empty" and compare-equal-to a nil FieldOperation,
|
|
// no Operation is considered empty even if it has no FieldOps.
|
|
func (got *Operation) Compare(expected *Operation) error {
|
|
if got == nil && expected == nil {
|
|
return nil
|
|
}
|
|
if got == nil {
|
|
return fmt.Errorf("expected %q op, got nil", expected.OpType)
|
|
}
|
|
if expected == nil {
|
|
return fmt.Errorf("expected no op, got %q", got.OpType)
|
|
}
|
|
if got.OpType != expected.OpType {
|
|
return fmt.Errorf("operation type mismatch: expected %q, got %q", expected.OpType, got.OpType)
|
|
}
|
|
if len(got.ClearRecordIDs) != len(expected.ClearRecordIDs) {
|
|
return fmt.Errorf("clear record counts differ: expected %d, got %d", len(expected.ClearRecordIDs), len(got.ClearRecordIDs))
|
|
}
|
|
for i, v1 := range got.ClearRecordIDs {
|
|
v2 := expected.ClearRecordIDs[i]
|
|
if v1 != v2 {
|
|
return fmt.Errorf("clear record id %d differs: expected %d, got %d", i, v2, v1)
|
|
}
|
|
}
|
|
// don't assume consistent ordering for the fields, because they're
|
|
// coming out in arbitrary hash order
|
|
seenFields := make(map[string]struct{}, len(expected.ClearFields))
|
|
for _, v1 := range expected.ClearFields {
|
|
seenFields[v1] = struct{}{}
|
|
}
|
|
for _, v2 := range got.ClearFields {
|
|
if _, ok := seenFields[v2]; !ok {
|
|
return fmt.Errorf("field %q cleared unexpectedly", v2)
|
|
}
|
|
delete(seenFields, v2)
|
|
}
|
|
for v1 := range seenFields {
|
|
return fmt.Errorf("field %q should be cleared but wasn't", v1)
|
|
}
|
|
// We check compare even if the op we find on one side is nil, so
|
|
// a round-trip test will consider a missing op and an empty op to
|
|
// be interchangeable.
|
|
for k, fo1 := range got.FieldOps {
|
|
fo2 := expected.FieldOps[k]
|
|
if err := fo1.Compare(fo2); err != nil {
|
|
return fmt.Errorf("field %q mismatch: %w", k, err)
|
|
}
|
|
}
|
|
for k, fo2 := range expected.FieldOps {
|
|
fo1 := got.FieldOps[k]
|
|
if err := fo1.Compare(fo2); err != nil {
|
|
return fmt.Errorf("field %q mismatch: %w", k, err)
|
|
}
|
|
}
|
|
if expected.Seq != got.Seq {
|
|
return fmt.Errorf("sequence mismatch: expected %d, got %d", expected.Seq, got.Seq)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// FieldOperation is the specific set of changes to make to a given
|
|
// field.
|
|
//
|
|
// For a Clear operation, values can be an empty array. For Set or Remove
|
|
// operations on sets, RecordIDs can contain duplicates. Times should
|
|
// be empty except for time-quantum fields.
|
|
type FieldOperation struct {
|
|
RecordIDs []uint64
|
|
Values []uint64
|
|
// For int/timestamp/decimal, this is the value
|
|
// For time-quantum, this is the timestamp
|
|
// No field has both signed values and timestamps.
|
|
// This is not a place of honor.
|
|
Signed []int64
|
|
}
|
|
|
|
// Sort sorts the clear record IDs and field list.
|
|
func (o *Operation) Sort() {
|
|
// I am aware that this is a crime, but it avoids rewriting
|
|
// the code and justifies FieldOperation handling the "only record
|
|
// IDs" case.
|
|
f := FieldOperation{RecordIDs: o.ClearRecordIDs}
|
|
f.SortByRecords()
|
|
sort.Strings(o.ClearFields)
|
|
}
|
|
|
|
// clone makes a duplicate of the operation without shared storage
|
|
func (o *Operation) clone() *Operation {
|
|
o2 := &Operation{
|
|
OpType: o.OpType,
|
|
Seq: o.Seq,
|
|
ClearRecordIDs: append([]uint64{}, o.ClearRecordIDs...),
|
|
ClearFields: append([]string{}, o.ClearFields...),
|
|
FieldOps: make(map[string]*FieldOperation, len(o.FieldOps)),
|
|
}
|
|
for k, v := range o.FieldOps {
|
|
o2.FieldOps[k] = v.clone()
|
|
}
|
|
return o2
|
|
}
|
|
|
|
// merge merges the fieldops of the provided operation into this operation.
|
|
func (o *Operation) merge(o2 *Operation) {
|
|
o.ClearRecordIDs = append(o.ClearRecordIDs, o2.ClearRecordIDs...)
|
|
o.ClearFields = append(o.ClearFields, o2.ClearFields...)
|
|
for f, op := range o2.FieldOps {
|
|
dst := o.FieldOps[f]
|
|
if dst == nil {
|
|
o.FieldOps[f] = op
|
|
continue
|
|
}
|
|
// otherwise append any contents. dst is a pointer-to, so this
|
|
// updates the thing the map points to, we don't have to write back
|
|
// into the map.
|
|
dst.RecordIDs = append(dst.RecordIDs, op.RecordIDs...)
|
|
dst.Values = append(dst.Values, op.Values...)
|
|
dst.Signed = append(dst.Signed, op.Signed...)
|
|
}
|
|
}
|
|
|
|
type ShardedFieldOperation map[uint64]*FieldOperation
|
|
|
|
// ByShard() divides the FieldOperation's values up into corresponding chunks
|
|
// based on the shards of record IDs. Does not further sort IDs within those
|
|
// chunks.
|
|
func (f *FieldOperation) ByShard() ShardedFieldOperation {
|
|
if len(f.RecordIDs) == 0 {
|
|
return nil
|
|
}
|
|
return f.SortToShards()
|
|
}
|
|
|
|
// clone makes a duplicate of the operation without shared storage
|
|
func (f *FieldOperation) clone() *FieldOperation {
|
|
f2 := &FieldOperation{
|
|
RecordIDs: append([]uint64{}, f.RecordIDs...),
|
|
Values: append(([]uint64)(nil), f.Values...),
|
|
Signed: append(([]int64)(nil), f.Signed...),
|
|
}
|
|
return f2
|
|
}
|
|
|
|
func ShardIDs(ids []uint64) (out map[uint64][]uint64) {
|
|
shards, ends := shardwidth.FindShards(ids)
|
|
prev := 0
|
|
out = make(map[uint64][]uint64, len(shards))
|
|
for i, shard := range shards {
|
|
endIndex := ends[i]
|
|
out[shard] = ids[prev:endIndex]
|
|
prev = endIndex
|
|
}
|
|
return out
|
|
}
|
|
|
|
// SortToShards() uses a pseudo-radix-sort to divide inputs into
|
|
// shards; the individual shards are not sorted.
|
|
func (f *FieldOperation) SortToShards() ShardedFieldOperation {
|
|
if len(f.RecordIDs) == 0 {
|
|
return nil
|
|
}
|
|
diffMask := uint64(0)
|
|
prev := f.RecordIDs[0]
|
|
for _, r := range f.RecordIDs[1:] {
|
|
diffMask |= r ^ prev
|
|
prev = r
|
|
}
|
|
bitsRemaining := bits.Len64(diffMask)
|
|
if bitsRemaining <= shardwidth.Exponent {
|
|
return map[uint64]*FieldOperation{f.RecordIDs[0] >> shardwidth.Exponent: f}
|
|
}
|
|
output := make(ShardedFieldOperation)
|
|
sortToShardsInto(f, bitsRemaining-8, output)
|
|
return output
|
|
}
|
|
|
|
// sortToShardsInto puts the shards it finds into the given map, so that
|
|
// as we split off buckets, they can be inserted into the same map.
|
|
func sortToShardsInto(f *FieldOperation, shift int, into ShardedFieldOperation) {
|
|
if shift < shardwidth.Exponent {
|
|
shift = shardwidth.Exponent
|
|
}
|
|
nextShift := shift - 8
|
|
if nextShift < shardwidth.Exponent {
|
|
nextShift = shardwidth.Exponent
|
|
}
|
|
// count things that belong in each of the 256 buckets
|
|
var buckets [256]int
|
|
var starts [256]int
|
|
|
|
// compute the buckets ourselves
|
|
for _, r := range f.RecordIDs {
|
|
b := (r >> shift) & 0xFF
|
|
buckets[b]++
|
|
}
|
|
total := 0
|
|
// compute starting points of each bucket, converting the
|
|
// bucket counts into ends
|
|
for i := range buckets {
|
|
starts[i] = total
|
|
total += buckets[i]
|
|
buckets[i] = total
|
|
}
|
|
// starts[n] is the index of the first thing that should
|
|
// go in that bucket, buckets[n] is the index of the first
|
|
// thing that shouldn't
|
|
var bucketOp FieldOperation
|
|
for bucket, start := range starts {
|
|
end := buckets[bucket]
|
|
if end <= start {
|
|
continue
|
|
}
|
|
for j := start; j < end; j++ {
|
|
want := int((f.RecordIDs[j] >> shift) & 0xFF)
|
|
for want != bucket {
|
|
// move this to the beginning of the
|
|
// bucket it wants to be in, swapping
|
|
// the thing there here
|
|
dst := starts[want]
|
|
f.RecordIDs[j], f.RecordIDs[dst] = f.RecordIDs[dst], f.RecordIDs[j]
|
|
if f.Values != nil {
|
|
f.Values[j], f.Values[dst] = f.Values[dst], f.Values[j]
|
|
}
|
|
if f.Signed != nil {
|
|
f.Signed[j], f.Signed[dst] = f.Signed[dst], f.Signed[j]
|
|
}
|
|
starts[want]++
|
|
want = int((f.RecordIDs[j] >> shift) & 0xFF)
|
|
}
|
|
}
|
|
// If shift == shardwidth.Exponent, then this is a completed
|
|
// shard and can go into the sharded output. otherwise, we
|
|
// can subdivide it.
|
|
bucketOp.RecordIDs = f.RecordIDs[start:end]
|
|
if f.Values != nil {
|
|
bucketOp.Values = f.Values[start:end]
|
|
}
|
|
if f.Signed != nil {
|
|
bucketOp.Signed = f.Signed[start:end]
|
|
}
|
|
if shift == shardwidth.Exponent {
|
|
x := bucketOp
|
|
into[f.RecordIDs[start]>>shardwidth.Exponent] = &x
|
|
} else {
|
|
sortToShardsInto(&bucketOp, nextShift, into)
|
|
}
|
|
}
|
|
}
|
|
|
|
const shardMask = ((uint64(1) << shardwidth.Exponent) - 1)
|
|
|
|
// SortByValues sorts the operation by values first, then by record
|
|
// ID within each value. This is the best ordering for set/mutex fields,
|
|
// where we'll want to generate positions in that order. For these
|
|
// purposes, a time quantum or bool counts as a kind of a set.
|
|
func (f *FieldOperation) SortByValues() {
|
|
keys := make([]uint64, len(f.RecordIDs))
|
|
for i, v := range f.RecordIDs {
|
|
keys[i] = (f.Values[i] << shardwidth.Exponent) | (v & shardMask)
|
|
}
|
|
f.SortByKeys(keys)
|
|
}
|
|
|
|
// SortByRecords sorts the operation by record ID, and not by value at
|
|
// all. This makes the most sense for int fields and the like.
|
|
func (f *FieldOperation) SortByRecords() {
|
|
f.SortByKeys(f.RecordIDs)
|
|
}
|
|
|
|
// SortByKeys reorganizes the record IDs and values of f according to the
|
|
// corresponding members of keys.
|
|
func (f *FieldOperation) SortByKeys(keys []uint64) {
|
|
if len(f.RecordIDs) < 2 {
|
|
return
|
|
}
|
|
diffMask := uint64(0)
|
|
prev := keys[0]
|
|
for _, r := range keys[1:] {
|
|
diffMask |= r ^ prev
|
|
prev = r
|
|
}
|
|
bitsRemaining := bits.Len64(diffMask)
|
|
sortPartialByKeys(f, keys, bitsRemaining-8)
|
|
}
|
|
|
|
// simpleSort sorts a FieldOperation by external keys, or record IDs. It's a
|
|
// horribly naive bubble sort because N is small and a more complex algorithm
|
|
// doesn't help as much as you'd hope. This beats using stdlib sort by about
|
|
// a factor of two for those small N, for larger N we're using the radix sort
|
|
// that calls this.
|
|
//
|
|
// External keys exist only when we are sorting by value, which is to say,
|
|
// when we're using row-oriented formats (set, mutex, time quantum).
|
|
// For int/decimal/timestamp fields, we're sorting by record only.
|
|
// So, if keys is the same as f.RecordIDs, we're looking at an int field
|
|
// or equivalent, so Signed exists and Values doesn't exist.
|
|
// Otherwise, we might be looking at a time quantum field (both exist)
|
|
// or set/mutex (only Values exist).
|
|
func simpleSort(f *FieldOperation, keys []uint64) {
|
|
// keys might actually just point to record IDs, in which case, we don't
|
|
// want to shuffle the corresponding RecordIDs too, because that would just
|
|
// reverse our swaps. If they're different, we actually need to swap them
|
|
// both.
|
|
if &keys[0] != &f.RecordIDs[0] {
|
|
// sorting by record IDs
|
|
if f.Values != nil && f.Signed != nil { // time quantum field
|
|
for i := 1; i < len(keys); i++ {
|
|
for j := i; j > 0 && keys[j-1] > keys[j]; j-- {
|
|
keys[j-1], keys[j] = keys[j], keys[j-1]
|
|
f.RecordIDs[j-1], f.RecordIDs[j] = f.RecordIDs[j], f.RecordIDs[j-1]
|
|
f.Values[j-1], f.Values[j] = f.Values[j], f.Values[j-1]
|
|
f.Signed[j-1], f.Signed[j] = f.Signed[j], f.Signed[j-1]
|
|
|
|
}
|
|
}
|
|
} else if f.Values != nil { // set/mutex/bool
|
|
for i := 1; i < len(keys); i++ {
|
|
for j := i; j > 0 && keys[j-1] > keys[j]; j-- {
|
|
keys[j-1], keys[j] = keys[j], keys[j-1]
|
|
f.RecordIDs[j-1], f.RecordIDs[j] = f.RecordIDs[j], f.RecordIDs[j-1]
|
|
|
|
f.Values[j-1], f.Values[j] = f.Values[j], f.Values[j-1]
|
|
}
|
|
}
|
|
} else if f.Signed != nil { // can't-happen, we think
|
|
for i := 1; i < len(keys); i++ {
|
|
for j := i; j > 0 && keys[j-1] > keys[j]; j-- {
|
|
keys[j-1], keys[j] = keys[j], keys[j-1]
|
|
f.RecordIDs[j-1], f.RecordIDs[j] = f.RecordIDs[j], f.RecordIDs[j-1]
|
|
f.Signed[j-1], f.Signed[j] = f.Signed[j], f.Signed[j-1]
|
|
}
|
|
}
|
|
} else { // can't-happen, we think
|
|
for i := 1; i < len(keys); i++ {
|
|
for j := i; j > 0 && keys[j-1] > keys[j]; j-- {
|
|
keys[j-1], keys[j] = keys[j], keys[j-1]
|
|
f.RecordIDs[j-1], f.RecordIDs[j] = f.RecordIDs[j], f.RecordIDs[j-1]
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if f.Values == nil && f.Signed != nil {
|
|
for i := 1; i < len(f.RecordIDs); i++ {
|
|
for j := i; j > 0 && f.RecordIDs[j-1] > f.RecordIDs[j]; j-- {
|
|
f.RecordIDs[j-1], f.RecordIDs[j] = f.RecordIDs[j], f.RecordIDs[j-1]
|
|
f.Signed[j-1], f.Signed[j] = f.Signed[j], f.Signed[j-1]
|
|
}
|
|
}
|
|
} else if f.Values != nil && f.Signed != nil { // can't happen, we think
|
|
for i := 1; i < len(f.RecordIDs); i++ {
|
|
for j := i; j > 0 && f.RecordIDs[j-1] > f.RecordIDs[j]; j-- {
|
|
f.RecordIDs[j-1], f.RecordIDs[j] = f.RecordIDs[j], f.RecordIDs[j-1]
|
|
f.Values[j-1], f.Values[j] = f.Values[j], f.Values[j-1]
|
|
f.Signed[j-1], f.Signed[j] = f.Signed[j], f.Signed[j-1]
|
|
}
|
|
}
|
|
} else if f.Values != nil { // only happens during testing
|
|
for i := 1; i < len(f.RecordIDs); i++ {
|
|
for j := i; j > 0 && f.RecordIDs[j-1] > f.RecordIDs[j]; j-- {
|
|
f.RecordIDs[j-1], f.RecordIDs[j] = f.RecordIDs[j], f.RecordIDs[j-1]
|
|
f.Values[j-1], f.Values[j] = f.Values[j], f.Values[j-1]
|
|
}
|
|
}
|
|
} else { // should definitely not happen
|
|
for i := 1; i < len(f.RecordIDs); i++ {
|
|
for j := i; j > 0 && f.RecordIDs[j-1] > f.RecordIDs[j]; j-- {
|
|
f.RecordIDs[j-1], f.RecordIDs[j] = f.RecordIDs[j], f.RecordIDs[j-1]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func sortPartialByKeys(f *FieldOperation, keys []uint64, shift int) {
|
|
if shift < 0 {
|
|
shift = 0
|
|
}
|
|
externalKeys := &f.RecordIDs[0] != &keys[0]
|
|
nextShift := shift - 8
|
|
if nextShift < 0 {
|
|
nextShift = 0
|
|
}
|
|
// count things that belong in each of the 256 buckets
|
|
var buckets [256]int
|
|
var starts [256]int
|
|
// compute the buckets ourselves
|
|
for _, r := range keys {
|
|
b := (r >> shift) & 0xFF
|
|
buckets[b]++
|
|
}
|
|
total := 0
|
|
// compute starting points of each bucket, converting the
|
|
// bucket counts into ends
|
|
for i := range buckets {
|
|
starts[i] = total
|
|
total += buckets[i]
|
|
buckets[i] = total
|
|
}
|
|
// starts[n] is the index of the first thing that should
|
|
// go in that bucket, buckets[n] is the index of the first
|
|
// thing that shouldn't
|
|
// var newbuckets [256]int
|
|
var bucketOp FieldOperation
|
|
for bucket, start := range starts {
|
|
end := buckets[bucket]
|
|
if end <= start {
|
|
continue
|
|
}
|
|
for j := start; j < end; j++ {
|
|
want := int((keys[j] >> shift) & 0xFF)
|
|
for want != bucket {
|
|
// move this to the beginning of the
|
|
// bucket it wants to be in, swapping
|
|
// the thing there here
|
|
dst := starts[want]
|
|
keys[j], keys[dst] = keys[dst], keys[j]
|
|
// we do this to allow you to just pass in the records as keys
|
|
if externalKeys {
|
|
f.RecordIDs[j], f.RecordIDs[dst] = f.RecordIDs[dst], f.RecordIDs[j]
|
|
}
|
|
if f.Values != nil {
|
|
f.Values[j], f.Values[dst] = f.Values[dst], f.Values[j]
|
|
}
|
|
if f.Signed != nil {
|
|
f.Signed[j], f.Signed[dst] = f.Signed[dst], f.Signed[j]
|
|
}
|
|
starts[want]++
|
|
want = int((keys[j] >> shift) & 0xFF)
|
|
}
|
|
}
|
|
// If shift == shardwidth.Exponent, then this is a completed
|
|
// shard and can go into the sharded output. otherwise, we
|
|
// can subdivide it.
|
|
if shift > 0 {
|
|
bucketOp.RecordIDs = f.RecordIDs[start:end]
|
|
if f.Values != nil {
|
|
bucketOp.Values = f.Values[start:end]
|
|
}
|
|
if f.Signed != nil {
|
|
bucketOp.Signed = f.Signed[start:end]
|
|
}
|
|
// if there's not very many, sort naively instead
|
|
if end-start > 32 {
|
|
sortPartialByKeys(&bucketOp, keys[start:end], nextShift)
|
|
} else {
|
|
simpleSort(&bucketOp, keys[start:end])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// AddPair adds a record ID/value pair where the value is unsigned, as
|
|
// when used with set/mutex/time quantum fields.
|
|
func (f *FieldOperation) AddPair(rec uint64, value uint64) {
|
|
f.RecordIDs = append(f.RecordIDs, rec)
|
|
f.Values = append(f.Values, value)
|
|
}
|
|
|
|
// AddSignedPair adds a record ID/value pair where the value is signed,
|
|
// as when used with int/decimal/timestamp fields.
|
|
func (f *FieldOperation) AddSignedPair(rec uint64, value int64) {
|
|
f.RecordIDs = append(f.RecordIDs, rec)
|
|
f.Signed = append(f.Signed, value)
|
|
}
|
|
|
|
// AddStampedPair adds a record/value pair plus a time, which is just
|
|
// a Unix time in seconds. (Note, no scaling here; timestamp fields are
|
|
// scaled int fields, this is for time quantums.)
|
|
func (f *FieldOperation) AddStampedPair(rec uint64, value uint64, stamp int64) {
|
|
f.RecordIDs = append(f.RecordIDs, rec)
|
|
f.Values = append(f.Values, value)
|
|
f.Signed = append(f.Signed, stamp)
|
|
}
|
|
|
|
// Compare returns a diagnostic if the field operations do not seem
|
|
// equivalent.
|
|
func (got *FieldOperation) Compare(expected *FieldOperation) error {
|
|
if got == nil {
|
|
if expected == nil {
|
|
return nil
|
|
}
|
|
// We don't worry about non-empty Values or Signed here, because in theory
|
|
// RecordIDs are the Source of Truth as to what's in the op.
|
|
if len(expected.RecordIDs) == 0 {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("expected field operation with %d records, got nil", len(expected.RecordIDs))
|
|
}
|
|
if expected == nil {
|
|
if len(got.RecordIDs) == 0 {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("expected empty field operation, got %d records", len(got.RecordIDs))
|
|
}
|
|
if len(got.RecordIDs) != len(expected.RecordIDs) {
|
|
return fmt.Errorf("record counts differ: expected %d, got %d", len(expected.RecordIDs), len(got.RecordIDs))
|
|
}
|
|
for i, v1 := range got.RecordIDs {
|
|
v2 := expected.RecordIDs[i]
|
|
if v1 != v2 {
|
|
return fmt.Errorf("record id %d differs: expected %d, got %d", i, v2, v1)
|
|
}
|
|
}
|
|
if len(got.Values) != len(expected.Values) {
|
|
return fmt.Errorf("value counts differ: expected %d, got %d", len(expected.Values), len(got.Values))
|
|
}
|
|
for i, v1 := range got.Values {
|
|
v2 := expected.Values[i]
|
|
if v1 != v2 {
|
|
return fmt.Errorf("value %d differs: expected %d, got %d", i, v2, v1)
|
|
}
|
|
}
|
|
if len(got.Signed) != len(expected.Signed) {
|
|
return fmt.Errorf("signed value counts differ: expected %d, got %d", len(expected.Signed), len(got.Signed))
|
|
}
|
|
for i, v1 := range got.Signed {
|
|
v2 := expected.Signed[i]
|
|
if v1 != v2 {
|
|
return fmt.Errorf("signed value %d differs: expected %d, got %d", i, v2, v1)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ShardOperations is a set of Operations associated with a specific shard.
|
|
type ShardOperations struct {
|
|
Shard uint64
|
|
Ops []*Operation
|
|
}
|
|
|
|
// Request is a complete ingest request, which may be any combination
|
|
// of operations, which may apply to multiple shards.
|
|
type Request struct {
|
|
Ops []*Operation
|
|
}
|
|
|
|
// ShardedRequest is an ingest request, split up into individual per-shard
|
|
// operations.
|
|
type ShardedRequest struct {
|
|
Ops map[uint64][]*Operation
|
|
}
|
|
|
|
// ByShard converts a request into the same request, only sharded.
|
|
func (r *Request) ByShard(fields map[string]FieldType) (*ShardedRequest, error) {
|
|
if len(r.Ops) == 0 {
|
|
return &ShardedRequest{Ops: nil}, nil
|
|
}
|
|
req := make(map[uint64][]*Operation)
|
|
shards := make(map[uint64]*Operation)
|
|
// we're getting per-field things, which we want to divide per-shard,
|
|
// and return to per-shard sets of per-field things, so we're inverting
|
|
// the structure.
|
|
for _, op := range r.Ops {
|
|
// for clear and write ops, we also need to split up the
|
|
// ClearRecords values, which may be distinct from the set of
|
|
// records for any given field. For Write ops, we'll then end
|
|
// up adding in field values for some fields.
|
|
if op.OpType == OpClear || op.OpType == OpWrite || op.OpType == OpDelete {
|
|
sharded := ShardIDs(op.ClearRecordIDs)
|
|
for shard, data := range sharded {
|
|
shards[shard] = &Operation{OpType: op.OpType, Seq: op.Seq, ClearRecordIDs: data, ClearFields: op.ClearFields, FieldOps: map[string]*FieldOperation{}}
|
|
}
|
|
}
|
|
for field, fieldOp := range op.FieldOps {
|
|
sharded := fieldOp.ByShard()
|
|
sorter := fieldTypeSorts[fields[field]]
|
|
if sorter == nil {
|
|
sorter = (*FieldOperation).SortByRecords
|
|
}
|
|
for shard, data := range sharded {
|
|
sorter(data)
|
|
shardOp, ok := shards[shard]
|
|
if !ok {
|
|
if op.OpType == OpWrite {
|
|
return nil, fmt.Errorf("write operation has field operation data (%d items) for shard %d, but no clear data", len(data.RecordIDs), shard)
|
|
}
|
|
shardOp = &Operation{OpType: op.OpType, Seq: op.Seq}
|
|
shards[shard] = shardOp
|
|
shardOp.FieldOps = map[string]*FieldOperation{field: data}
|
|
} else {
|
|
shardOp.FieldOps[field] = data
|
|
}
|
|
}
|
|
}
|
|
for shard, shardOp := range shards {
|
|
req[shard] = append(req[shard], shardOp)
|
|
}
|
|
for k := range shards {
|
|
delete(shards, k)
|
|
}
|
|
|
|
}
|
|
return &ShardedRequest{Ops: req}, nil
|
|
}
|
|
|
|
// merge combines the components of a sharded request back into a single
|
|
// unsharded request, processing shards in numerical order.
|
|
func (s *ShardedRequest) merge() *Request {
|
|
req := &Request{}
|
|
if s == nil || len(s.Ops) == 0 {
|
|
return req
|
|
}
|
|
shards := make([]uint64, 0, len(s.Ops))
|
|
for shard := range s.Ops {
|
|
shards = append(shards, shard)
|
|
}
|
|
sort.Slice(shards, func(i, j int) bool { return shards[i] < shards[j] })
|
|
for _, shard := range shards {
|
|
ops := s.Ops[shard]
|
|
for _, op := range ops {
|
|
var _ *Operation
|
|
if op.Seq >= len(req.Ops) {
|
|
// Pad out with nil *Operations to the required length
|
|
req.Ops = append(req.Ops, make([]*Operation, op.Seq+1-len(req.Ops))...)
|
|
}
|
|
if req.Ops[op.Seq] == nil {
|
|
req.Ops[op.Seq] = op.clone()
|
|
continue
|
|
}
|
|
req.Ops[op.Seq].merge(op)
|
|
}
|
|
}
|
|
return req
|
|
}
|
|
|
|
func (r *Request) Dump(logf func(string, ...interface{})) {
|
|
logf("req: %#v", r)
|
|
for _, op := range r.Ops {
|
|
logf("op: %#v", op)
|
|
if len(op.ClearRecordIDs) > 0 {
|
|
if len(op.ClearRecordIDs) > 8 {
|
|
logf(" clearRecordIDs: %d...+%d", op.ClearRecordIDs[:8], len(op.ClearRecordIDs)-8)
|
|
} else {
|
|
logf(" clearRecordIDs: %d", op.ClearRecordIDs)
|
|
}
|
|
}
|
|
if len(op.ClearFields) > 0 {
|
|
if len(op.ClearFields) > 8 {
|
|
logf(" clearFields: %s...+%d", op.ClearFields[:8], len(op.ClearFields)-8)
|
|
} else {
|
|
logf(" clearFields: %s", op.ClearFields)
|
|
}
|
|
}
|
|
for field, fieldOp := range op.FieldOps {
|
|
if fieldOp != nil {
|
|
logf(" field %q: op (%d/%d/%d)", field, len(fieldOp.RecordIDs), len(fieldOp.Values), len(fieldOp.Signed))
|
|
if len(fieldOp.RecordIDs) > 0 {
|
|
if len(fieldOp.RecordIDs) > 8 {
|
|
logf(" records %d...+%d", fieldOp.RecordIDs[:8], len(fieldOp.RecordIDs)-8)
|
|
} else {
|
|
logf(" records %d", fieldOp.RecordIDs)
|
|
}
|
|
}
|
|
} else {
|
|
logf(" field %q: nil op", field)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *Request) Compare(other *Request) error {
|
|
if other == nil {
|
|
if r != nil && len(r.Ops) != 0 {
|
|
return errors.New("non-empty sharded request can't equal empty/nil sharded request")
|
|
}
|
|
// empty and nil are allowed
|
|
return nil
|
|
}
|
|
if r == nil {
|
|
if other != nil && len(other.Ops) != 0 {
|
|
return errors.New("non-empty sharded request can't equal empty/nil sharded request")
|
|
}
|
|
// empty and nil are allowed
|
|
return nil
|
|
}
|
|
ops := r.Ops
|
|
ops2 := other.Ops
|
|
if len(ops2) != len(ops) {
|
|
return fmt.Errorf("expected %d ops, got %d", len(ops), len(ops2))
|
|
}
|
|
for i, op := range ops {
|
|
if err := op.Compare(ops2[i]); err != nil {
|
|
return fmt.Errorf("op %d: %v", i, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Compare checks whether two ShardedRequest objects represent the same
|
|
// data. Empty shards shouldn't have entries in the map in the first place,
|
|
// so we don't accept a nil or 0-length slice of ops as equal to the
|
|
// shard key not existing, but we do accept nil or empty requests as
|
|
// equal to each other.
|
|
func (s *ShardedRequest) Compare(other *ShardedRequest) error {
|
|
if other == nil {
|
|
if s != nil && len(s.Ops) != 0 {
|
|
return errors.New("non-empty sharded request can't equal empty/nil sharded request")
|
|
}
|
|
// empty and nil are allowed
|
|
return nil
|
|
}
|
|
if s == nil {
|
|
if other != nil && len(other.Ops) != 0 {
|
|
return errors.New("non-empty sharded request can't equal empty/nil sharded request")
|
|
}
|
|
// empty and nil are allowed
|
|
return nil
|
|
}
|
|
for shard, ops := range s.Ops {
|
|
ops2, ok := other.Ops[shard]
|
|
if !ok {
|
|
return fmt.Errorf("shard %d missing in other", shard)
|
|
}
|
|
if len(ops2) != len(ops) {
|
|
return fmt.Errorf("shard %d: expected %d ops, got %d", shard, len(ops), len(ops2))
|
|
}
|
|
for i, op := range ops {
|
|
if err := op.Compare(ops2[i]); err != nil {
|
|
return fmt.Errorf("shard %d, op %d: %v", shard, i, err)
|
|
}
|
|
}
|
|
}
|
|
if len(other.Ops) != len(s.Ops) {
|
|
for shard := range other.Ops {
|
|
if _, ok := s.Ops[shard]; !ok {
|
|
return fmt.Errorf("shard %d missing in self", shard)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|