// Copyright 2021 Molecula Corp. All rights reserved. package pilosa_test import ( "context" "fmt" "testing" pilosa "github.com/molecula/featurebase/v3" "github.com/molecula/featurebase/v3/test" ) func queryIRABit(t *testing.T, m0api *pilosa.API, acctOwnerID uint64, iraField string, iraRowID uint64, index string) (bit bool) { query := fmt.Sprintf("Row(%v=%v)", iraField, iraRowID) // acctOwnerID) res, err := m0api.Query(context.Background(), &pilosa.QueryRequest{Index: index, Query: query}) if err != nil { t.Fatalf("querying IRA bit: %v", err) } cols := res.Results[0].(*pilosa.Row).Columns() for i := range cols { if cols[i] == acctOwnerID { return true } } return false } func mustQueryAcct(t *testing.T, m0api *pilosa.API, acctOwnerID uint64, fieldAcct0, index string) (acctBal int64) { query := fmt.Sprintf("FieldValue(field=%v, column=%v)", fieldAcct0, acctOwnerID) res, err := m0api.Query(context.Background(), &pilosa.QueryRequest{Index: index, Query: query}) if err != nil { t.Fatalf("querying account: %v", err) } if len(res.Results) == 0 { return 0 } valCount := res.Results[0].(pilosa.ValCount) return valCount.Val } func queryBalances(t *testing.T, m0api *pilosa.API, acctOwnerID uint64, fldAcct0, fldAcct1, index string) (acct0bal, acct1bal int64) { acct0bal = mustQueryAcct(t, m0api, acctOwnerID, fldAcct0, index) acct1bal = mustQueryAcct(t, m0api, acctOwnerID, fldAcct1, index) return } func TestAPI_ImportAtomicRecord(t *testing.T) { c := test.MustRunCluster(t, 1) defer c.Close() m0 := c.GetNode(0) m0api := m0.API ctx := context.Background() index := c.Idx() fieldAcct0 := "acct0" fieldAcct1 := "acct1" transferUSD := int64(100) _ = transferUSD opts := pilosa.OptFieldTypeInt(-1000, 1000) _, err := m0api.CreateIndex(ctx, index, pilosa.IndexOptions{}) if err != nil { t.Fatalf("creating index: %v", err) } _, err = m0api.CreateField(ctx, index, fieldAcct0, opts) if err != nil { t.Fatalf("creating fieldAcct0: %v", err) } _, err = m0api.CreateField(ctx, index, fieldAcct1, opts) if err != nil { t.Fatalf("creating fieldAcct1: %v", err) } iraField := "ira" // set field. iraRowID := uint64(3) _, err = m0api.CreateField(ctx, index, iraField) if err != nil { t.Fatalf("creating fieldIRA: %v", err) } acctOwnerID := uint64(78) // ColumnID shard := acctOwnerID / ShardWidth // setup 500 USD in acct1 and 700 USD in acct2. // transfer 100 USD. // should see 400 USD in acct, and 800 USD in acct2. // // setup initial balances createAIRUpdate := func(acct0bal, acct1bal int64) (air *pilosa.AtomicRecord) { ivr0 := &pilosa.ImportValueRequest{ Index: index, Field: fieldAcct0, Shard: shard, ColumnIDs: []uint64{acctOwnerID}, Values: []int64{acct0bal}, } ivr1 := &pilosa.ImportValueRequest{ Index: index, Field: fieldAcct1, Shard: shard, ColumnIDs: []uint64{acctOwnerID}, Values: []int64{acct1bal}, } ir0 := &pilosa.ImportRequest{ Index: index, Field: iraField, Shard: shard, ColumnIDs: []uint64{acctOwnerID}, RowIDs: []uint64{iraRowID}, } air = &pilosa.AtomicRecord{ Index: index, Shard: shard, Ivr: []*pilosa.ImportValueRequest{ ivr0, ivr1, }, Ir: []*pilosa.ImportRequest{ir0}, } return } expectedBalStartingAcct0 := int64(500) expectedBalStartingAcct1 := int64(700) air := createAIRUpdate(expectedBalStartingAcct0, expectedBalStartingAcct1) //vv("BEFORE the first ImportAtomicRecord!") qcx := m0api.Txf().NewQcx() if err := m0api.ImportAtomicRecord(ctx, qcx, air); err != nil { qcx.Abort() t.Fatal(err) } if err := qcx.Finish(); err != nil { t.Fatal(err) } //vv("AFTER the first ImportAtomicRecord!") iraBit := queryIRABit(t, m0api, acctOwnerID, iraField, iraRowID, index) if !iraBit { t.Fatal("IRA bit should have been set") } startingBalanceAcct0, startingBalanceAcct1 := queryBalances(t, m0api, acctOwnerID, fieldAcct0, fieldAcct1, index) //vv("starting balance: acct0=%v, acct1=%v", startingBalanceAcct0, startingBalanceAcct1) if startingBalanceAcct0 != expectedBalStartingAcct0 { t.Fatalf("expected %v, observed %v starting acct0 balance", expectedBalStartingAcct0, startingBalanceAcct0) } if startingBalanceAcct1 != expectedBalStartingAcct1 { t.Fatalf("expected %v, observed %v starting acct1 balance", expectedBalStartingAcct1, startingBalanceAcct1) } //vv("sad path: transferUSD %v from %v -> %v, with power loss half-way through", transferUSD, fieldAcct0, fieldAcct1) opt := func(o *pilosa.ImportOptions) error { o.SimPowerLossAfter = 1 return nil } expectedBalEndingAcct0 := expectedBalStartingAcct0 - 100 expectedBalEndingAcct1 := expectedBalStartingAcct1 + 100 air = createAIRUpdate(expectedBalEndingAcct0, expectedBalEndingAcct1) qcx = m0api.Txf().NewQcx() //vv("just before the SECOND ImportAtomicRecord, qcx is %p, should NOT BE NIL", qcx) err = m0api.ImportAtomicRecord(ctx, qcx, air.Clone(), opt) //err = m0api.ImportAtomicRecord(ctx, nil, air, opt) if err != pilosa.ErrAborted { t.Fatalf("expected ErrTxnAborted but got err='%#v'", err) } // sad path, cleanup qcx.Abort() qcx = nil b0, b1 := queryBalances(t, m0api, acctOwnerID, fieldAcct0, fieldAcct1, index) //vv("after power failure tx, balance: acct0=%v, acct1=%v", b0, b1) if b0 != expectedBalStartingAcct0 { t.Fatalf("expected %v, observed %v starting acct0 balance", expectedBalStartingAcct0, b0) } if b1 != expectedBalStartingAcct1 { t.Fatalf("expected %v, observed %v starting acct1 balance", expectedBalStartingAcct1, b1) } //vv("good: with power loss half-way, no change in account balances; acct0=%v; acct1=%v", b0, b1) // next part of the test, just make sure we do the update. //vv("happy path: transferUSD %v from %v -> %v, with no interruption.", transferUSD, fieldAcct0, fieldAcct1) // happy path with no power failure half-way through. qcx = m0api.Txf().NewQcx() err = m0api.ImportAtomicRecord(ctx, qcx, air.Clone()) if err != nil { t.Fatalf("importing record: %v", err) } if err := qcx.Finish(); err != nil { t.Fatal(err) } eb0, eb1 := queryBalances(t, m0api, acctOwnerID, fieldAcct0, fieldAcct1, index) // should have been applied this time. if eb0 != expectedBalEndingAcct0 || eb1 != expectedBalEndingAcct1 { t.Fatalf("problem: transaction did not get committed/applied. transferUSD=%v, but we see: startingBalanceAcct0=%v -> endingBalanceAcct0=%v; startingBalanceAcct1=%v -> endingBalanceAcct1=%v", transferUSD, startingBalanceAcct0, eb0, startingBalanceAcct1, eb1) } //vv("ending balance: acct0=%v, acct1=%v", eb0, eb1) // clear all the bits air.Ivr[0].Clear = true air.Ivr[1].Clear = true air.Ir[0].Clear = true qcx = m0api.Txf().NewQcx() err = m0api.ImportAtomicRecord(ctx, qcx, air) if err != nil { t.Fatalf("importing record: %v", err) } if err := qcx.Finish(); err != nil { t.Fatal(err) } eb0, eb1 = queryBalances(t, m0api, acctOwnerID, fieldAcct0, fieldAcct1, index) if eb0 != 0 || eb1 != 0 { t.Fatal("problem: bits did not clear") } //vv("cleared balances: acct0=%v, acct1=%v", eb0, eb1) iraBit = queryIRABit(t, m0api, acctOwnerID, iraField, iraRowID, index) if iraBit { t.Fatal("IRA bit should have been cleared") } }