mirror of
1
Fork 0
forgejo/vendor/github.com/pingcap/go-themis/themis_txn.go

797 lines
22 KiB
Go

package themis
import (
"fmt"
"sync"
"time"
"github.com/juju/errors"
"github.com/ngaut/log"
"github.com/pingcap/go-hbase"
"github.com/pingcap/go-themis/oracle"
)
type TxnConfig struct {
ConcurrentPrewriteAndCommit bool
WaitSecondaryCommit bool
TTLInMs uint64
MaxRowsInOneTxn int
// options below is for debugging and testing
brokenPrewriteSecondaryTest bool
brokenPrewriteSecondaryAndRollbackTest bool
brokenCommitPrimaryTest bool
brokenCommitSecondaryTest bool
}
var defaultTxnConf = TxnConfig{
ConcurrentPrewriteAndCommit: true,
WaitSecondaryCommit: false,
MaxRowsInOneTxn: 50000,
TTLInMs: 5 * 1000, // default txn TTL: 5s
brokenPrewriteSecondaryTest: false,
brokenPrewriteSecondaryAndRollbackTest: false,
brokenCommitPrimaryTest: false,
brokenCommitSecondaryTest: false,
}
type themisTxn struct {
client hbase.HBaseClient
rpc *themisRPC
lockCleaner LockManager
oracle oracle.Oracle
mutationCache *columnMutationCache
startTs uint64
commitTs uint64
primaryRow *rowMutation
primary *hbase.ColumnCoordinate
secondaryRows []*rowMutation
secondary []*hbase.ColumnCoordinate
primaryRowOffset int
singleRowTxn bool
secondaryLockBytes []byte
conf TxnConfig
hooks *txnHook
}
var _ Txn = (*themisTxn)(nil)
var (
// ErrSimulated is used when maybe rollback occurs error too.
ErrSimulated = errors.New("simulated error")
maxCleanLockRetryCount = 30
pauseTime = 300 * time.Millisecond
)
func NewTxn(c hbase.HBaseClient, oracle oracle.Oracle) (Txn, error) {
return NewTxnWithConf(c, defaultTxnConf, oracle)
}
func NewTxnWithConf(c hbase.HBaseClient, conf TxnConfig, oracle oracle.Oracle) (Txn, error) {
var err error
txn := &themisTxn{
client: c,
mutationCache: newColumnMutationCache(),
oracle: oracle,
primaryRowOffset: -1,
conf: conf,
rpc: newThemisRPC(c, oracle, conf),
hooks: newHook(),
}
txn.startTs, err = txn.oracle.GetTimestamp()
if err != nil {
return nil, errors.Trace(err)
}
txn.lockCleaner = newThemisLockManager(txn.rpc, c)
return txn, nil
}
func (txn *themisTxn) setHook(hooks *txnHook) {
txn.hooks = hooks
}
func (txn *themisTxn) Gets(tbl string, gets []*hbase.Get) ([]*hbase.ResultRow, error) {
results, err := txn.rpc.themisBatchGet([]byte(tbl), gets, txn.startTs, false)
if err != nil {
return nil, errors.Trace(err)
}
var ret []*hbase.ResultRow
hasLock := false
for _, r := range results {
// if this row is locked, try clean lock and get again
if isLockResult(r) {
hasLock = true
err = txn.constructLockAndClean([]byte(tbl), r.SortedColumns)
if err != nil {
// TODO if it's a conflict error, it means this lock
// isn't expired, maybe we can retry or return partial results.
return nil, errors.Trace(err)
}
}
// it's OK, because themisBatchGet doesn't return nil value.
ret = append(ret, r)
}
if hasLock {
// after we cleaned locks, try to get again.
ret, err = txn.rpc.themisBatchGet([]byte(tbl), gets, txn.startTs, true)
if err != nil {
return nil, errors.Trace(err)
}
}
return ret, nil
}
func (txn *themisTxn) Get(tbl string, g *hbase.Get) (*hbase.ResultRow, error) {
r, err := txn.rpc.themisGet([]byte(tbl), g, txn.startTs, false)
if err != nil {
return nil, errors.Trace(err)
}
// contain locks, try to clean and get again
if r != nil && isLockResult(r) {
r, err = txn.tryToCleanLockAndGetAgain([]byte(tbl), g, r.SortedColumns)
if err != nil {
return nil, errors.Trace(err)
}
}
return r, nil
}
func (txn *themisTxn) Put(tbl string, p *hbase.Put) {
// add mutation to buffer
for _, e := range getEntriesFromPut(p) {
txn.mutationCache.addMutation([]byte(tbl), p.Row, e.Column, e.typ, e.value, false)
}
}
func (txn *themisTxn) Delete(tbl string, p *hbase.Delete) error {
entries, err := getEntriesFromDel(p)
if err != nil {
return errors.Trace(err)
}
for _, e := range entries {
txn.mutationCache.addMutation([]byte(tbl), p.Row, e.Column, e.typ, e.value, false)
}
return nil
}
func (txn *themisTxn) Commit() error {
if txn.mutationCache.getMutationCount() == 0 {
return nil
}
if txn.mutationCache.getRowCount() > txn.conf.MaxRowsInOneTxn {
return ErrTooManyRows
}
txn.selectPrimaryAndSecondaries()
err := txn.prewritePrimary()
if err != nil {
// no need to check wrong region here, hbase client will retry when
// occurs single row NotInRegion error.
log.Error(errors.ErrorStack(err))
// it's safe to retry, because this transaction is not committed.
return ErrRetryable
}
err = txn.prewriteSecondary()
if err != nil {
if isWrongRegionErr(err) {
log.Warn("region info outdated")
// reset hbase client buffered region info
txn.client.CleanAllRegionCache()
}
return ErrRetryable
}
txn.commitTs, err = txn.oracle.GetTimestamp()
if err != nil {
log.Error(errors.ErrorStack(err))
return ErrRetryable
}
err = txn.commitPrimary()
if err != nil {
// commit primary error, rollback
log.Error("commit primary row failed", txn.startTs, err)
txn.rollbackRow(txn.primaryRow.tbl, txn.primaryRow)
txn.rollbackSecondaryRow(len(txn.secondaryRows) - 1)
return ErrRetryable
}
txn.commitSecondary()
log.Debug("themis txn commit successfully", txn.startTs, txn.commitTs)
return nil
}
func (txn *themisTxn) commitSecondary() {
if bypass, _, _ := txn.hooks.beforeCommitSecondary(txn, nil); !bypass {
return
}
if txn.conf.brokenCommitSecondaryTest {
txn.brokenCommitSecondary()
return
}
if txn.conf.ConcurrentPrewriteAndCommit {
txn.batchCommitSecondary(txn.conf.WaitSecondaryCommit)
} else {
txn.commitSecondarySync()
}
}
func (txn *themisTxn) commitSecondarySync() {
for _, r := range txn.secondaryRows {
err := txn.rpc.commitSecondaryRow(r.tbl, r.row, r.mutationList(false), txn.startTs, txn.commitTs)
if err != nil {
// fail of secondary commit will not stop the commits of next
// secondaries
log.Warning(err)
}
}
}
func (txn *themisTxn) batchCommitSecondary(wait bool) error {
//will batch commit all rows in a region
rsRowMap, err := txn.groupByRegion()
if err != nil {
return errors.Trace(err)
}
wg := sync.WaitGroup{}
for _, regionRowMap := range rsRowMap {
wg.Add(1)
_, firstRowM := getFirstEntity(regionRowMap)
go func(cli *themisRPC, tbl string, rMap map[string]*rowMutation, startTs, commitTs uint64) {
defer wg.Done()
err := cli.batchCommitSecondaryRows([]byte(tbl), rMap, startTs, commitTs)
if err != nil {
// fail of secondary commit will not stop the commits of next
// secondaries
if isWrongRegionErr(err) {
txn.client.CleanAllRegionCache()
log.Warn("region info outdated when committing secondary rows, don't panic")
}
}
}(txn.rpc, string(firstRowM.tbl), regionRowMap, txn.startTs, txn.commitTs)
}
if wait {
wg.Wait()
}
return nil
}
func (txn *themisTxn) groupByRegion() (map[string]map[string]*rowMutation, error) {
rsRowMap := make(map[string]map[string]*rowMutation)
for _, rm := range txn.secondaryRows {
region, err := txn.client.LocateRegion(rm.tbl, rm.row, true)
if err != nil {
return nil, errors.Trace(err)
}
key := getBatchGroupKey(region, string(rm.tbl))
if _, exists := rsRowMap[key]; !exists {
rsRowMap[key] = map[string]*rowMutation{}
}
rsRowMap[key][string(rm.row)] = rm
}
return rsRowMap, nil
}
func (txn *themisTxn) commitPrimary() error {
if txn.conf.brokenCommitPrimaryTest {
return txn.brokenCommitPrimary()
}
return txn.rpc.commitRow(txn.primary.Table, txn.primary.Row,
txn.primaryRow.mutationList(false),
txn.startTs, txn.commitTs, txn.primaryRowOffset)
}
func (txn *themisTxn) selectPrimaryAndSecondaries() {
txn.secondary = nil
for tblName, rowMutations := range txn.mutationCache.mutations {
for _, rowMutation := range rowMutations {
row := rowMutation.row
findPrimaryInRow := false
for i, mutation := range rowMutation.mutationList(true) {
colcord := hbase.NewColumnCoordinate([]byte(tblName), row, mutation.Family, mutation.Qual)
// set the first column as primary if primary is not set by user
if txn.primaryRowOffset == -1 &&
(txn.primary == nil || txn.primary.Equal(colcord)) {
txn.primary = colcord
txn.primaryRowOffset = i
txn.primaryRow = rowMutation
findPrimaryInRow = true
} else {
txn.secondary = append(txn.secondary, colcord)
}
}
if !findPrimaryInRow {
txn.secondaryRows = append(txn.secondaryRows, rowMutation)
}
}
}
// hook for test
if bypass, _, _ := txn.hooks.afterChoosePrimaryAndSecondary(txn, nil); !bypass {
return
}
if len(txn.secondaryRows) == 0 {
txn.singleRowTxn = true
}
// construct secondary lock
secondaryLock := txn.constructSecondaryLock(hbase.TypePut)
if secondaryLock != nil {
txn.secondaryLockBytes = secondaryLock.Encode()
} else {
txn.secondaryLockBytes = nil
}
}
func (txn *themisTxn) constructSecondaryLock(typ hbase.Type) *themisSecondaryLock {
if txn.primaryRow.getSize() <= 1 && len(txn.secondaryRows) == 0 {
return nil
}
l := newThemisSecondaryLock()
l.primaryCoordinate = txn.primary
l.ts = txn.startTs
// TODO set client addr
return l
}
func (txn *themisTxn) constructPrimaryLock() *themisPrimaryLock {
l := newThemisPrimaryLock()
l.typ = txn.primaryRow.getType(txn.primary.Column)
l.ts = txn.startTs
for _, c := range txn.secondary {
l.addSecondary(c, txn.mutationCache.getMutation(c).typ)
}
return l
}
func (txn *themisTxn) constructLockAndClean(tbl []byte, lockKvs []*hbase.Kv) error {
locks, err := getLocksFromResults([]byte(tbl), lockKvs, txn.rpc)
if err != nil {
return errors.Trace(err)
}
for _, lock := range locks {
err := txn.cleanLockWithRetry(lock)
if err != nil {
return errors.Trace(err)
}
}
return nil
}
func (txn *themisTxn) tryToCleanLockAndGetAgain(tbl []byte, g *hbase.Get, lockKvs []*hbase.Kv) (*hbase.ResultRow, error) {
// try to clean locks
err := txn.constructLockAndClean(tbl, lockKvs)
if err != nil {
return nil, errors.Trace(err)
}
// get again, ignore lock
r, err := txn.rpc.themisGet([]byte(tbl), g, txn.startTs, true)
if err != nil {
return nil, errors.Trace(err)
}
return r, nil
}
func (txn *themisTxn) commitSecondaryAndCleanLock(lock *themisSecondaryLock, commitTs uint64) error {
cc := lock.Coordinate()
mutation := &columnMutation{
Column: &cc.Column,
mutationValuePair: &mutationValuePair{
typ: lock.typ,
},
}
err := txn.rpc.commitSecondaryRow(cc.Table, cc.Row,
[]*columnMutation{mutation}, lock.Timestamp(), commitTs)
if err != nil {
return errors.Trace(err)
}
return nil
}
func (txn *themisTxn) cleanLockWithRetry(lock Lock) error {
for i := 0; i < maxCleanLockRetryCount; i++ {
if exists, err := txn.lockCleaner.IsLockExists(lock.Coordinate(), 0, lock.Timestamp()); err != nil || !exists {
return errors.Trace(err)
}
log.Warnf("lock exists txn: %v lock-txn: %v row: %q", txn.startTs, lock.Timestamp(), lock.Coordinate().Row)
// try clean lock
err := txn.tryToCleanLock(lock)
if errorEqual(err, ErrLockNotExpired) {
log.Warn("sleep a while, and retry clean lock", txn.startTs)
// TODO(dongxu) use cleverer retry sleep time interval
time.Sleep(pauseTime)
continue
} else if err != nil {
return errors.Trace(err)
}
// lock cleaned successfully
return nil
}
return ErrCleanLockFailed
}
func (txn *themisTxn) tryToCleanLock(lock Lock) error {
// if it's secondary lock, first we'll check if its primary lock has been released.
if lock.Role() == RoleSecondary {
// get primary lock
pl := lock.Primary()
// check primary lock is exists
exists, err := txn.lockCleaner.IsLockExists(pl.Coordinate(), 0, pl.Timestamp())
if err != nil {
return errors.Trace(err)
}
if !exists {
// primary row is committed, commit this row
cc := pl.Coordinate()
commitTs, err := txn.lockCleaner.GetCommitTimestamp(cc, pl.Timestamp())
if err != nil {
return errors.Trace(err)
}
if commitTs > 0 {
// if this transction has been committed
log.Info("txn has been committed, ts:", commitTs, "prewriteTs:", pl.Timestamp())
// commit secondary rows
err := txn.commitSecondaryAndCleanLock(lock.(*themisSecondaryLock), commitTs)
if err != nil {
return errors.Trace(err)
}
return nil
}
}
}
expired, err := txn.rpc.checkAndSetLockIsExpired(lock)
if err != nil {
return errors.Trace(err)
}
// only clean expired lock
if expired {
// try to clean primary lock
pl := lock.Primary()
commitTs, cleanedLock, err := txn.lockCleaner.CleanLock(pl.Coordinate(), pl.Timestamp())
if err != nil {
return errors.Trace(err)
}
if cleanedLock != nil {
pl = cleanedLock
}
log.Info("try clean secondary locks", pl.Timestamp())
// clean secondary locks
// erase lock and data if commitTs is 0; otherwise, commit it.
for k, v := range pl.(*themisPrimaryLock).secondaries {
cc := &hbase.ColumnCoordinate{}
if err = cc.ParseFromString(k); err != nil {
return errors.Trace(err)
}
if commitTs == 0 {
// commitTs == 0, means clean primary lock successfully
// expire trx havn't committed yet, we must delete lock and
// dirty data
err = txn.lockCleaner.EraseLockAndData(cc, pl.Timestamp())
if err != nil {
return errors.Trace(err)
}
} else {
// primary row is committed, so we must commit other
// secondary rows
mutation := &columnMutation{
Column: &cc.Column,
mutationValuePair: &mutationValuePair{
typ: v,
},
}
err = txn.rpc.commitSecondaryRow(cc.Table, cc.Row,
[]*columnMutation{mutation}, pl.Timestamp(), commitTs)
if err != nil {
return errors.Trace(err)
}
}
}
} else {
return ErrLockNotExpired
}
return nil
}
func (txn *themisTxn) batchPrewriteSecondaryRowsWithLockClean(tbl []byte, rowMs map[string]*rowMutation) error {
locks, err := txn.batchPrewriteSecondaryRows(tbl, rowMs)
if err != nil {
return errors.Trace(err)
}
// lock clean
if locks != nil && len(locks) > 0 {
// hook for test
if bypass, _, err := txn.hooks.onSecondaryOccursLock(txn, locks); !bypass {
return errors.Trace(err)
}
// try one more time after clean lock successfully
for _, lock := range locks {
err = txn.cleanLockWithRetry(lock)
if err != nil {
return errors.Trace(err)
}
// prewrite all secondary rows
locks, err = txn.batchPrewriteSecondaryRows(tbl, rowMs)
if err != nil {
return errors.Trace(err)
}
if len(locks) > 0 {
for _, l := range locks {
log.Errorf("can't clean lock, column:%q; conflict lock: %+v, lock ts: %d", l.Coordinate(), l, l.Timestamp())
}
return ErrRetryable
}
}
}
return nil
}
func (txn *themisTxn) prewriteRowWithLockClean(tbl []byte, mutation *rowMutation, containPrimary bool) error {
lock, err := txn.prewriteRow(tbl, mutation, containPrimary)
if err != nil {
return errors.Trace(err)
}
// lock clean
if lock != nil {
// hook for test
if bypass, _, err := txn.hooks.beforePrewriteLockClean(txn, lock); !bypass {
return errors.Trace(err)
}
err = txn.cleanLockWithRetry(lock)
if err != nil {
return errors.Trace(err)
}
// try one more time after clean lock successfully
lock, err = txn.prewriteRow(tbl, mutation, containPrimary)
if err != nil {
return errors.Trace(err)
}
if lock != nil {
log.Errorf("can't clean lock, column:%q; conflict lock: %+v, lock ts: %d", lock.Coordinate(), lock, lock.Timestamp())
return ErrRetryable
}
}
return nil
}
func (txn *themisTxn) batchPrewriteSecondaryRows(tbl []byte, rowMs map[string]*rowMutation) (map[string]Lock, error) {
return txn.rpc.batchPrewriteSecondaryRows(tbl, rowMs, txn.startTs, txn.secondaryLockBytes)
}
func (txn *themisTxn) prewriteRow(tbl []byte, mutation *rowMutation, containPrimary bool) (Lock, error) {
// hook for test
if bypass, ret, err := txn.hooks.onPrewriteRow(txn, []interface{}{mutation, containPrimary}); !bypass {
return ret.(Lock), errors.Trace(err)
}
if containPrimary {
// try to get lock
return txn.rpc.prewriteRow(tbl, mutation.row,
mutation.mutationList(true),
txn.startTs,
txn.constructPrimaryLock().Encode(),
txn.secondaryLockBytes, txn.primaryRowOffset)
}
return txn.rpc.prewriteSecondaryRow(tbl, mutation.row,
mutation.mutationList(true),
txn.startTs,
txn.secondaryLockBytes)
}
func (txn *themisTxn) prewritePrimary() error {
// hook for test
if bypass, _, err := txn.hooks.beforePrewritePrimary(txn, nil); !bypass {
return err
}
err := txn.prewriteRowWithLockClean(txn.primary.Table, txn.primaryRow, true)
if err != nil {
log.Debugf("prewrite primary %v %q failed: %v", txn.startTs, txn.primaryRow.row, err.Error())
return errors.Trace(err)
}
log.Debugf("prewrite primary %v %q successfully", txn.startTs, txn.primaryRow.row)
return nil
}
func (txn *themisTxn) prewriteSecondary() error {
// hook for test
if bypass, _, err := txn.hooks.beforePrewriteSecondary(txn, nil); !bypass {
return err
}
if txn.conf.brokenPrewriteSecondaryTest {
return txn.brokenPrewriteSecondary()
}
if txn.conf.ConcurrentPrewriteAndCommit {
return txn.batchPrewriteSecondaries()
}
return txn.prewriteSecondarySync()
}
func (txn *themisTxn) prewriteSecondarySync() error {
for i, mu := range txn.secondaryRows {
err := txn.prewriteRowWithLockClean(mu.tbl, mu, false)
if err != nil {
// rollback
txn.rollbackRow(txn.primaryRow.tbl, txn.primaryRow)
txn.rollbackSecondaryRow(i)
return errors.Trace(err)
}
}
return nil
}
// just for test
func (txn *themisTxn) brokenCommitPrimary() error {
// do nothing
log.Warn("Simulating primary commit failed")
return nil
}
// just for test
func (txn *themisTxn) brokenCommitSecondary() {
// do nothing
log.Warn("Simulating secondary commit failed")
}
func (txn *themisTxn) brokenPrewriteSecondary() error {
log.Warn("Simulating prewrite secondary failed")
for i, rm := range txn.secondaryRows {
if i == len(txn.secondary)-1 {
if !txn.conf.brokenPrewriteSecondaryAndRollbackTest {
// simulating prewrite failed, need rollback
txn.rollbackRow(txn.primaryRow.tbl, txn.primaryRow)
txn.rollbackSecondaryRow(i)
}
// maybe rollback occurs error too
return ErrSimulated
}
txn.prewriteRowWithLockClean(rm.tbl, rm, false)
}
return nil
}
func (txn *themisTxn) batchPrewriteSecondaries() error {
wg := sync.WaitGroup{}
//will batch prewrite all rows in a region
rsRowMap, err := txn.groupByRegion()
if err != nil {
return errors.Trace(err)
}
errChan := make(chan error, len(rsRowMap))
defer close(errChan)
successChan := make(chan map[string]*rowMutation, len(rsRowMap))
defer close(successChan)
for _, regionRowMap := range rsRowMap {
wg.Add(1)
_, firstRowM := getFirstEntity(regionRowMap)
go func(tbl []byte, rMap map[string]*rowMutation) {
defer wg.Done()
err := txn.batchPrewriteSecondaryRowsWithLockClean(tbl, rMap)
if err != nil {
errChan <- err
} else {
successChan <- rMap
}
}(firstRowM.tbl, regionRowMap)
}
wg.Wait()
if len(errChan) != 0 {
// occur error, clean success prewrite mutations
log.Warnf("batch prewrite secondary rows error, rolling back %d %d", len(successChan), txn.startTs)
txn.rollbackRow(txn.primaryRow.tbl, txn.primaryRow)
L:
for {
select {
case succMutMap := <-successChan:
{
for _, rowMut := range succMutMap {
txn.rollbackRow(rowMut.tbl, rowMut)
}
}
default:
break L
}
}
err := <-errChan
if err != nil {
log.Error("batch prewrite secondary rows error, txn:", txn.startTs, err)
}
return errors.Trace(err)
}
return nil
}
func getFirstEntity(rowMap map[string]*rowMutation) (string, *rowMutation) {
for row, rowM := range rowMap {
return row, rowM
}
return "", nil
}
func getBatchGroupKey(rInfo *hbase.RegionInfo, tblName string) string {
return rInfo.Server + "_" + rInfo.Name
}
func (txn *themisTxn) rollbackRow(tbl []byte, mutation *rowMutation) error {
l := fmt.Sprintf("\nrolling back %q %d {\n", mutation.row, txn.startTs)
for _, v := range mutation.getColumns() {
l += fmt.Sprintf("\t%s:%s\n", string(v.Family), string(v.Qual))
}
l += "}\n"
log.Warn(l)
for _, col := range mutation.getColumns() {
cc := &hbase.ColumnCoordinate{
Table: tbl,
Row: mutation.row,
Column: col,
}
err := txn.lockCleaner.EraseLockAndData(cc, txn.startTs)
if err != nil {
return errors.Trace(err)
}
}
return nil
}
func (txn *themisTxn) rollbackSecondaryRow(successIndex int) error {
for i := successIndex; i >= 0; i-- {
r := txn.secondaryRows[i]
err := txn.rollbackRow(r.tbl, r)
if err != nil {
return errors.Trace(err)
}
}
return nil
}
func (txn *themisTxn) GetScanner(tbl []byte, startKey, endKey []byte, batchSize int) *ThemisScanner {
scanner := newThemisScanner(tbl, txn, batchSize, txn.client)
if startKey != nil {
scanner.setStartRow(startKey)
}
if endKey != nil {
scanner.setStopRow(endKey)
}
return scanner
}
func (txn *themisTxn) Release() {
txn.primary = nil
txn.primaryRow = nil
txn.secondary = nil
txn.secondaryRows = nil
txn.startTs = 0
txn.commitTs = 0
}
func (txn *themisTxn) String() string {
return fmt.Sprintf("%d", txn.startTs)
}
func (txn *themisTxn) GetCommitTS() uint64 {
return txn.commitTs
}
func (txn *themisTxn) GetStartTS() uint64 {
return txn.startTs
}
func (txn *themisTxn) LockRow(tbl string, rowkey []byte) error {
g := hbase.NewGet(rowkey)
r, err := txn.Get(tbl, g)
if err != nil {
log.Warnf("get row error, table:%s, row:%q, error:%v", tbl, rowkey, err)
return errors.Trace(err)
}
if r == nil {
log.Warnf("has not data to lock, table:%s, row:%q", tbl, rowkey)
return nil
}
for _, v := range r.Columns {
txn.mutationCache.addMutation([]byte(tbl), rowkey, &v.Column, hbase.TypeMinimum, nil, true)
}
return nil
}