// Copyright 2021 Molecula Corp. All rights reserved. package pilosa import ( "context" "fmt" "io" "os" "path/filepath" "strings" "sync" "github.com/apache/arrow/go/v10/arrow" "github.com/apache/arrow/go/v10/arrow/array" "github.com/apache/arrow/go/v10/arrow/memory" "github.com/gomem/gomem/pkg/dataframe" "github.com/molecula/featurebase/v3/pql" "github.com/molecula/featurebase/v3/tracing" "github.com/molecula/featurebase/v3/vprint" "github.com/pkg/errors" ivy "robpike.io/ivy/arrow" config "robpike.io/ivy/config" "robpike.io/ivy/exec" "robpike.io/ivy/parse" "robpike.io/ivy/run" "robpike.io/ivy/scan" "robpike.io/ivy/value" ) type ( ApplyResult *arrow.Column ) func runIvyString(context value.Context, str string) (ok bool, err error) { defer func() { if r := recover(); r != nil { err = r.(value.Error) } }() scanner := scan.New(context, "", strings.NewReader(str)) parser := parse.NewParser("", scanner, context) ok = run.Run(parser, context, false) return } // Possibly combine all arrays together then apply some interesting // computation at the end? func IvyReduce(reduceCode string, opCode string, opt *ExecOptions) (func(ctx context.Context, prev, v interface{}) interface{}, func() (*dataframe.DataFrame, error)) { var accumulator value.Value mu := &sync.Mutex{} concat := value.BinaryOps[opCode] conf := getDefaultConfig() ctxIvy := exec.NewContext(&conf) // concat returned results at coordinating node. reduceFn := func(ctx context.Context, prev, v interface{}) interface{} { if v == nil { return prev } if accumulator == nil { switch val := v.(type) { case *dataframe.DataFrame: col := val.ColumnAt(0) resolver := dataframe.NewChunkResolver(col) accumulator = value.NewArrowVector(col, &conf, &resolver) case value.Value: accumulator = v.(value.Value) default: return errors.New(fmt.Sprintf("ivy reduction failed first unexpected type %T", v)) } return nil } switch val := v.(type) { case *dataframe.DataFrame: col := val.ColumnAt(0) resolver := dataframe.NewChunkResolver(col) x := value.NewArrowVector(col, &conf, &resolver) mu.Lock() // i'm being overyerly cautious..need to confirm this can be concurrent accumulator = concat.EvalBinary(ctxIvy, accumulator, x) mu.Unlock() case value.Value: mu.Lock() accumulator = concat.EvalBinary(ctxIvy, accumulator, val) mu.Unlock() default: return errors.New(fmt.Sprintf("ivy reduction failed unexpected type %T", v)) } return nil } tablerFn := func() (*dataframe.DataFrame, error) { pool := memory.NewGoAllocator() // TODO(twg) 2022/09/01 singledton? if opt.Remote { col := value.ToArrowColumn(accumulator, pool) return dataframe.NewDataFrameFromColumns(pool, []arrow.Column{*col}) } // only acutally reduce on the initiating node i hate the network // over head but oh well ctxIvy.AssignGlobal("_", accumulator) ok, err := runIvyString(ctxIvy, reduceCode) if err != nil { return nil, err } if ok { v := ctxIvy.Global("_") if v == nil { return nil, errors.New("ivy reduction no result ") } col := value.ToArrowColumn(ctxIvy.Global("_"), pool) return dataframe.NewDataFrameFromColumns(pool, []arrow.Column{*col}) } return nil, errors.New("ivy reduction failed ") } return reduceFn, tablerFn } // executeApply executes a Apply() call. func (e *executor) executeApply(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (*dataframe.DataFrame, error) { if !e.dataframeEnabled { return nil, errors.New("Dataframe support not enabled") } span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeMax") defer span.Finish() if _, err := c.FirstStringArg("_ivy"); err != nil { return nil, errors.Wrap(err, " no ivy program supplied") } if len(c.Children) > 1 { return nil, errors.New("Apply() only accepts a single bitmap input filter") } // Execute calls in bulk on each remote node and merge. mapFn := func(ctx context.Context, shard uint64, mopt *mapOptions) (_ interface{}, err error) { return e.executeApplyShard(ctx, qcx, index, c, shard) } ivyReduce, ok, err := c.StringArg("_ivyReduce") if err != nil { return nil, err } reduceFn, tablerFn := IvyReduce("_", ",", opt) if ok { reduceFn, tablerFn = IvyReduce(ivyReduce, ",", opt) } _, err = e.mapReduce(ctx, index, shards, c, opt, mapFn, reduceFn) if err != nil { return nil, err } return tablerFn() } func getDefaultConfig() config.Config { maxbits := uint(1e9) // "maximum size of an integer, in bits; 0 means no limit") maxdigits := uint(1e4) // "above this many `digits`, integers print as floating point; 0 disables") maxstack := uint(100000) origin := 1 // "set index origin to `n` (must be 0 or 1)") prompt := "" // flag.String("prompt", "", "command `prompt`") format := "" // debugFlag := "" // flag.String("debug", "", "comma-separated `names` of debug settings to enable") conf := config.Config{} conf.SetFormat(format) conf.SetMaxBits(maxbits) conf.SetMaxDigits(maxdigits) conf.SetMaxStack(maxstack) conf.SetOrigin(origin) conf.SetPrompt(prompt) conf.SetOutput(io.Discard) conf.SetErrOutput(io.Discard) conf.SetEmbedded(true) // needed to propagate panic return conf } func filterDataframe(resolver dataframe.Resolver, pool memory.Allocator, filter []int64) (*dataframe.IndexResolver, error) { if resolver.NumRows() == 0 { return nil, errors.New("No data") } indexResolver := dataframe.NewIndexResolver(len(filter), uint32(ShardWidth-1)) for i, id := range filter { if int(id) >= resolver.NumRows() { continue } c, o := resolver.Resolve(int(id)) indexResolver.Set(i, c, o) } return indexResolver, nil } func (e *executor) executeApplyShard(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shard uint64) (value.Value, error) { span, _ := tracing.StartSpanFromContext(ctx, "Executor.executeApplyShard") defer span.Finish() ivyProgram, ok, err := c.StringArg("_ivy") if err != nil || !ok { return nil, errors.Wrap(err, "finding ivy program") } var filter *Row if len(c.Children) == 1 { row, err := e.executeBitmapCallShard(ctx, qcx, index, c.Children[0], shard) if err != nil { return nil, err } filter = row if !filter.Any() { // no need to actuall run the query for its not operating against any values return value.NewVector([]value.Value{}), nil } } // pool := memory.NewGoAllocator() // TODO(twg) 2022/09/01 singledton? ids := filter.ShardColumns() // needs to be shard columns // Fetch index. idx := e.Holder.Index(index) if idx == nil { return nil, newNotFoundError(ErrIndexNotFound, index) } fname := idx.GetDataFramePath(shard) if !e.dataFrameExists(fname) { return value.NewVector([]value.Value{}), nil } table, err := e.getDataTable(ctx, fname, pool) if err != nil { return nil, err } defer table.Release() df, err := dataframe.NewDataFrameFromTable(pool, table) if err != nil { return nil, err } p := dataframe.NewChunkResolver(df.ColumnAt(0)) var resolver dataframe.Resolver resolver = &p if filter != nil { if len(ids) == 0 { return value.NewVector([]value.Value{}), nil } resolver, err = filterDataframe(resolver, pool, ids) if err != nil { return nil, err } } conf := getDefaultConfig() context, err := ivy.RunArrow(dataframe.NewTableFacade(df), ivyProgram, conf, resolver) if err != nil { return nil, fmt.Errorf("ivy map error: %w", err) } return context.Global("_"), nil } // /////////////////////////////////////////////////////// // all the ingest supporting functions // /////////////////////////////////////////////////////// func NewShardFile(ctx context.Context, name string, mem memory.Allocator, e *executor) (*ShardFile, error) { if !e.dataFrameExists(name) { return &ShardFile{dest: name, executor: e}, nil } // else read in existing table, err := e.getDataTable(ctx, name, mem) if err != nil { return nil, err } return &ShardFile{table: table, schema: table.Schema(), dest: name, executor: e}, nil } type NameType struct { Name string DataType arrow.DataType } type ChangesetRequest struct { ShardIds []int64 // only shardwidth bits to provide 0 indexing inside shard file Columns []interface{} SimpleSchema []NameType } // TODO(twg) 2022/09/30 Needs a refactor func cast(v interface{}) arrow.DataType { switch v.(type) { case *arrow.Int64Type: return arrow.PrimitiveTypes.Int64 case int64: return arrow.PrimitiveTypes.Int64 case *arrow.Float64Type: return arrow.PrimitiveTypes.Float64 case float64: return arrow.PrimitiveTypes.Float64 default: vprint.VV("%T .... %v", v, v) } return arrow.PrimitiveTypes.Int64 } func (cr *ChangesetRequest) ArrowSchema() *arrow.Schema { fields := make([]arrow.Field, len(cr.SimpleSchema)) for i := range cr.SimpleSchema { fields[i] = arrow.Field{Name: cr.SimpleSchema[i].Name, Type: cast(cr.SimpleSchema[i].DataType)} } return arrow.NewSchema(fields, nil) } type ShardFile struct { table arrow.Table schema *arrow.Schema beforeRows int64 added int64 columns []interface{} dest string executor *executor } func compareSchema(s1, s2 *arrow.Schema) bool { if s1 == nil || s2 == nil { return false } if len(s1.Fields()) != len(s2.Fields()) { return false } for i := 0; i < len(s1.Fields()); i++ { f1 := s1.Field(i) f2 := s2.Field(i) if f1.Name != f2.Name { return false } if f1.Type != f2.Type { return false } } return true } func (sf *ShardFile) EnsureSchema(cs *ChangesetRequest) error { schema := cs.ArrowSchema() if sf.schema == nil { sf.schema = schema } else { if !compareSchema(sf.schema, schema) { vprint.VV("incomeing schema", schema) vprint.VV("existing schema", sf.schema) return errors.New("dataframe schema's don't match") } } sf.columns = make([]interface{}, len(sf.schema.Fields())) return nil } func (sf *ShardFile) buildAppenders(maxid int64) { if sf.table != nil { sf.beforeRows = sf.table.NumRows() } if maxid < sf.beforeRows { // no need to add new rows return } newSize := maxid - sf.beforeRows + 1 for i := 0; i < len(sf.schema.Fields()); i++ { switch sf.schema.Field(i).Type { case arrow.PrimitiveTypes.Int64: sf.columns[i] = make([]int64, newSize) case arrow.PrimitiveTypes.Float64: sf.columns[i] = make([]float64, newSize) } } sf.added = newSize } // the row offset must be reset to 0 for the slices being appended func (sf *ShardFile) SetIntValue(col int, row int64, val int64) { v := sf.columns[col].([]int64) v[row-sf.beforeRows] = val } func (sf *ShardFile) SetFloatValue(col int, row int64, val float64) { v := sf.columns[col].([]float64) v[row-sf.beforeRows] = val } func (sf *ShardFile) Process(cs *ChangesetRequest) error { err := sf.process(cs) if err != nil { return err } rtemp := sf.dest + ".temp" err = sf.Save(rtemp) if err != nil { return err } return os.Rename(rtemp+sf.executor.TableExtension(), sf.dest+sf.executor.TableExtension()) } func (sf *ShardFile) process(cs *ChangesetRequest) error { offset := 0 if sf.table != nil { column := sf.table.Column(0) resolver := dataframe.NewChunkResolver(column) for i, rowid := range cs.ShardIds { offset = i if rowid >= sf.table.NumRows() { break } chunk, l := resolver.Resolve(int(rowid)) for col := 0; col < len(sf.schema.Fields()); col++ { column := sf.table.Column(col) switch column.DataType() { case arrow.PrimitiveTypes.Int64: v := column.Data().Chunk(chunk).(*array.Int64).Int64Values() v[l] = cs.Columns[col].([]int64)[i] case arrow.PrimitiveTypes.Float64: v := column.Data().Chunk(chunk).(*array.Float64).Float64Values() v[l] = cs.Columns[col].([]float64)[i] default: panic(fmt.Sprintf("Unknown Type %v", column.DataType())) } } } } max := cs.ShardIds[len(cs.ShardIds)-1] sf.buildAppenders(max) // need to check if only replace and no apend if sf.added > 0 { for i, rowid := range cs.ShardIds[offset:] { i += offset for col := 0; col < len(sf.schema.Fields()); col++ { switch sf.schema.Field(col).Type { case arrow.PrimitiveTypes.Int64: sf.SetIntValue(col, rowid, cs.Columns[col].([]int64)[i]) case arrow.PrimitiveTypes.Float64: sf.SetFloatValue(col, rowid, cs.Columns[col].([]float64)[i]) default: panic(fmt.Sprintf("2 Unknown Type %v", sf.schema.Field(col).Type)) } } } } return nil } func (sf *ShardFile) Save(name string) error { parts := make([]arrow.Array, 0) mem := memory.NewGoAllocator() for col := 0; col < len(sf.schema.Fields()); col++ { chunks := make([]arrow.Array, 0) if sf.table != nil { // we append if there was existing parquet file column := sf.table.Column(col) chunks = append(chunks, column.Data().Chunks()...) } switch sf.schema.Field(col).Type { case arrow.PrimitiveTypes.Int64: // case *arrow.Int64Type: if sf.added > 0 { ibuild := array.NewInt64Builder(mem) ibuild.AppendValues(sf.columns[col].([]int64), nil) // TODO(twg) 2022/09/28 need to handle null newChunk := ibuild.NewArray() chunks = append(chunks, newChunk) } record, err := array.Concatenate(chunks, mem) if err != nil { return err } parts = append(parts, record) case arrow.PrimitiveTypes.Float64: // case *arrow.Float64Type: if sf.added > 0 { fbuild := array.NewFloat64Builder(mem) fbuild.AppendValues(sf.columns[col].([]float64), nil) // TODO(twg) 2022/09/28 need to handle null newChunk := fbuild.NewArray() chunks = append(chunks, newChunk) } record, err := array.Concatenate(chunks, mem) if err != nil { return err } parts = append(parts, record) default: vprint.VV("UNKNOWN %T", sf.schema.Field(col).Type) } } rec := array.NewRecord(sf.schema, parts, sf.beforeRows+sf.added) table := array.NewTableFromRecords(sf.schema, []arrow.Record{rec}) return sf.executor.SaveTable(name, table, mem) } // TODO(twg) 2022/10/03 Not a huge fan of the global variable will look at adding to executor structure // when dataframe is fully integrated var ( dataframeShardLocks map[uint64]*sync.Mutex muWriteDataframe sync.Mutex ) func init() { dataframeShardLocks = make(map[uint64]*sync.Mutex) } func getDataframeWritelock(shard uint64) *sync.Mutex { muWriteDataframe.Lock() defer muWriteDataframe.Unlock() lock, ok := dataframeShardLocks[shard] if ok { return lock } newLock := sync.Mutex{} dataframeShardLocks[shard] = &newLock return &newLock } func (api *API) ApplyDataframeChangeset(ctx context.Context, index string, cs *ChangesetRequest, shard uint64) error { // TODO(twg) 2022/09/29 need to validate api call idx := api.Holder().Index(index) // check if dataframe exists fname := idx.GetDataFramePath(shard) // only 1 shard writer allowed at at time so wait for it to be available mu := getDataframeWritelock(shard) mu.Lock() defer mu.Unlock() mem := memory.NewGoAllocator() shardFile, err := NewShardFile(ctx, fname, mem, api.server.executor) if err != nil { return err } err = shardFile.EnsureSchema(cs) if err != nil { return err } return shardFile.Process(cs) } type column struct { Name string Type string } func (api *API) GetDataframeSchema(ctx context.Context, indexName string) (interface{}, error) { idx, err := api.Index(ctx, indexName) if err != nil { return nil, err } base := idx.DataframesPath() dir, _ := os.Open(base) files, _ := dir.Readdir(0) parts := make([]column, 0) mem := memory.NewGoAllocator() for i := range files { file := files[i] name := file.Name() if api.server.executor.IsDataframeFile(name) { // strip off the parquet extenison name = strings.TrimSuffix(name, filepath.Ext(name)) // read the parquet file and extract the schema fname := filepath.Join(base, name) table, err := api.server.executor.getDataTable(ctx, fname, mem) if err != nil { return nil, err } for i := 0; i < int(table.NumCols()); i++ { col := table.Column(i) part := column{Name: col.Name(), Type: col.DataType().String()} parts = append(parts, part) } break // only go on first file } } return parts, nil }