github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ccl/changefeedccl/cdctest/nemeses.go (about) 1 // Copyright 2019 The Cockroach Authors. 2 // 3 // Licensed as a CockroachDB Enterprise file under the Cockroach Community 4 // License (the "License"); you may not use this file except in compliance with 5 // the License. You may obtain a copy of the License at 6 // 7 // https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt 8 9 package cdctest 10 11 import ( 12 "bytes" 13 "context" 14 gosql "database/sql" 15 "fmt" 16 "math/rand" 17 "strings" 18 19 "github.com/cockroachdb/cockroach/pkg/util/fsm" 20 "github.com/cockroachdb/cockroach/pkg/util/log" 21 "github.com/cockroachdb/cockroach/pkg/util/randutil" 22 "github.com/cockroachdb/errors" 23 ) 24 25 // RunNemesis runs a jepsen-style validation of whether a changefeed meets our 26 // user-facing guarantees. It's driven by a state machine with various nemeses: 27 // txn begin/commit/rollback, job pause/unpause. 28 // 29 // Changefeeds have a set of user-facing guarantees about ordering and 30 // duplicates, which the two cdctest.Validator implementations verify for the 31 // real output of a changefeed. The output rows and resolved timestamps of the 32 // tested feed are fed into them to check for anomalies. 33 func RunNemesis(f TestFeedFactory, db *gosql.DB, isSinkless bool) (Validator, error) { 34 // possible additional nemeses: 35 // - schema changes 36 // - merges 37 // - rebalancing 38 // - lease transfers 39 // - receiving snapshots 40 // mostly redundant with the pause/unpause nemesis, but might be nice to have: 41 // - crdb chaos 42 // - sink chaos 43 44 ctx := context.Background() 45 rng, _ := randutil.NewPseudoRand() 46 47 eventPauseCount := 10 48 if isSinkless { 49 // Disable eventPause for sinkless changefeeds because we currently do not 50 // have "correct" pause and unpause mechanisms for changefeeds that aren't 51 // based on the jobs infrastructure. Enabling it for sinkless might require 52 // using "AS OF SYSTEM TIME" for sinkless changefeeds. See #41006 for more 53 // details. 54 eventPauseCount = 0 55 } 56 ns := &nemeses{ 57 maxTestColumnCount: 10, 58 rowCount: 4, 59 db: db, 60 // eventMix does not have to add to 100 61 eventMix: map[fsm.Event]int{ 62 // We don't want `eventFinished` to ever be returned by `nextEvent` so we set 63 // its weight to 0. 64 eventFinished{}: 0, 65 66 // eventFeedMessage reads a message from the feed, or if the state machine 67 // thinks there will be no message available, it falls back to eventOpenTxn or 68 // eventCommit (if there is already a txn open). 69 eventFeedMessage{}: 50, 70 71 // eventSplit splits between two random rows (the split is a no-op if it 72 // already exists). 73 eventSplit{}: 5, 74 75 // TRANSACTIONS 76 // eventOpenTxn opens an UPSERT or DELETE transaction. 77 eventOpenTxn{}: 10, 78 79 // eventCommit commits the outstanding transaction. 80 eventCommit{}: 5, 81 82 // eventRollback simply rolls the outstanding transaction back. 83 eventRollback{}: 5, 84 85 // eventPush pushes every open transaction by running a high priority SELECT. 86 eventPush{}: 5, 87 88 // eventAbort aborts every open transaction by running a high priority DELETE. 89 eventAbort{}: 5, 90 91 // PAUSE / RESUME 92 // eventPause PAUSEs the changefeed. 93 eventPause{}: eventPauseCount, 94 95 // eventResume RESUMEs the changefeed. 96 eventResume{}: 50, 97 98 // SCHEMA CHANGES 99 // eventAddColumn performs a schema change by adding a new column with a default 100 // value in order to trigger a backfill. 101 eventAddColumn{ 102 CanAddColumnAfter: fsm.True, 103 }: 5, 104 105 eventAddColumn{ 106 CanAddColumnAfter: fsm.False, 107 }: 5, 108 109 // eventRemoveColumn performs a schema change by removing a column. 110 eventRemoveColumn{ 111 CanRemoveColumnAfter: fsm.True, 112 }: 5, 113 114 eventRemoveColumn{ 115 CanRemoveColumnAfter: fsm.False, 116 }: 5, 117 }, 118 } 119 120 // Create the table and set up some initial splits. 121 if _, err := db.Exec(`CREATE TABLE foo (id INT PRIMARY KEY, ts STRING DEFAULT '0')`); err != nil { 122 return nil, err 123 } 124 if _, err := db.Exec(`SET CLUSTER SETTING kv.range_merge.queue_enabled = false`); err != nil { 125 return nil, err 126 } 127 if _, err := db.Exec(`ALTER TABLE foo SPLIT AT VALUES ($1)`, ns.rowCount/2); err != nil { 128 return nil, err 129 } 130 131 // Initialize table rows by repeatedly running the `openTxn` transition, 132 // then randomly either committing or rolling back transactions. This will 133 // leave some committed rows. 134 for i := 0; i < ns.rowCount*5; i++ { 135 if err := openTxn(fsm.Args{Ctx: ctx, Extended: ns}); err != nil { 136 return nil, err 137 } 138 // Randomly commit or rollback, but commit at least one row to the table. 139 if rand.Intn(3) < 2 || i == 0 { 140 if err := commit(fsm.Args{Ctx: ctx, Extended: ns}); err != nil { 141 return nil, err 142 } 143 } else { 144 if err := rollback(fsm.Args{Ctx: ctx, Extended: ns}); err != nil { 145 return nil, err 146 } 147 } 148 } 149 150 foo, err := f.Feed(`CREATE CHANGEFEED FOR foo WITH updated, resolved, diff`) 151 if err != nil { 152 return nil, err 153 } 154 ns.f = foo 155 defer func() { _ = foo.Close() }() 156 157 // Create scratch table with a pre-specified set of test columns to avoid having to 158 // accommodate schema changes on-the-fly. 159 scratchTableName := `fprint` 160 var createFprintStmtBuf bytes.Buffer 161 fmt.Fprintf(&createFprintStmtBuf, `CREATE TABLE %s (id INT PRIMARY KEY, ts STRING)`, scratchTableName) 162 if _, err := db.Exec(createFprintStmtBuf.String()); err != nil { 163 return nil, err 164 } 165 baV, err := NewBeforeAfterValidator(db, `foo`) 166 if err != nil { 167 return nil, err 168 } 169 fprintV, err := NewFingerprintValidator(db, `foo`, scratchTableName, foo.Partitions(), ns.maxTestColumnCount) 170 if err != nil { 171 return nil, err 172 } 173 ns.v = MakeCountValidator(Validators{ 174 NewOrderValidator(`foo`), 175 baV, 176 fprintV, 177 }) 178 179 // Initialize the actual row count, overwriting what the initialization loop did. That 180 // loop has set this to the number of modified rows, which is correct during 181 // changefeed operation, but not for the initial scan, because some of the rows may 182 // have had the same primary key. 183 if err := db.QueryRow(`SELECT count(*) FROM foo`).Scan(&ns.availableRows); err != nil { 184 return nil, err 185 } 186 187 txnOpenBeforeInitialScan := false 188 // Maybe open an intent. 189 if rand.Intn(2) < 1 { 190 txnOpenBeforeInitialScan = true 191 if err := openTxn(fsm.Args{Ctx: ctx, Extended: ns}); err != nil { 192 return nil, err 193 } 194 } 195 196 // Run the state machine until it finishes. Exit criteria is in `nextEvent` 197 // and is based on the number of rows that have been resolved and the number 198 // of resolved timestamp messages. 199 initialState := stateRunning{ 200 FeedPaused: fsm.False, 201 TxnOpen: fsm.FromBool(txnOpenBeforeInitialScan), 202 CanAddColumn: fsm.True, 203 CanRemoveColumn: fsm.False, 204 } 205 m := fsm.MakeMachine(compiledStateTransitions, initialState, ns) 206 for { 207 state := m.CurState() 208 if _, ok := state.(stateDone); ok { 209 return ns.v, nil 210 } 211 event, err := ns.nextEvent(rng, state, foo, &m) 212 if err != nil { 213 return nil, err 214 } 215 if err := m.Apply(ctx, event); err != nil { 216 return nil, err 217 } 218 } 219 } 220 221 type openTxnType string 222 223 const ( 224 openTxnTypeUpsert openTxnType = `UPSERT` 225 openTxnTypeDelete openTxnType = `DELETE` 226 ) 227 228 type nemeses struct { 229 rowCount int 230 maxTestColumnCount int 231 eventMix map[fsm.Event]int 232 233 v *CountValidator 234 db *gosql.DB 235 f TestFeed 236 237 availableRows int 238 currentTestColumnCount int 239 txn *gosql.Tx 240 openTxnType openTxnType 241 openTxnID int 242 openTxnTs string 243 } 244 245 // nextEvent selects the next state transition. 246 func (ns *nemeses) nextEvent( 247 rng *rand.Rand, state fsm.State, f TestFeed, m *fsm.Machine, 248 ) (se fsm.Event, err error) { 249 if ns.v.NumResolvedWithRows >= 6 && ns.v.NumResolvedRows >= 10 { 250 return eventFinished{}, nil 251 } 252 possibleEvents, ok := compiledStateTransitions.GetExpanded()[state] 253 if !ok { 254 return nil, errors.Errorf(`unknown state: %T %s`, state, state) 255 } 256 mixTotal := 0 257 for event := range possibleEvents { 258 weight, ok := ns.eventMix[event] 259 if !ok { 260 return nil, errors.Errorf(`unknown event: %T`, event) 261 } 262 mixTotal += weight 263 } 264 r, t := rng.Intn(mixTotal), 0 265 for event := range possibleEvents { 266 t += ns.eventMix[event] 267 if r >= t { 268 continue 269 } 270 if _, ok := event.(eventFeedMessage); ok { 271 // If there are no available rows, openTxn or commit outstanding txn instead 272 // of reading. 273 if ns.availableRows < 1 { 274 s := state.(stateRunning) 275 if s.TxnOpen.Get() { 276 return eventCommit{}, nil 277 } 278 return eventOpenTxn{}, nil 279 } 280 return eventFeedMessage{}, nil 281 } 282 if e, ok := event.(eventAddColumn); ok { 283 e.CanAddColumnAfter = fsm.FromBool(ns.currentTestColumnCount < ns.maxTestColumnCount-1) 284 return e, nil 285 } 286 if e, ok := event.(eventRemoveColumn); ok { 287 e.CanRemoveColumnAfter = fsm.FromBool(ns.currentTestColumnCount > 1) 288 return e, nil 289 } 290 return event, nil 291 } 292 293 panic(`unreachable`) 294 } 295 296 type stateRunning struct { 297 FeedPaused fsm.Bool 298 TxnOpen fsm.Bool 299 CanRemoveColumn fsm.Bool 300 CanAddColumn fsm.Bool 301 } 302 type stateDone struct{} 303 304 func (stateRunning) State() {} 305 func (stateDone) State() {} 306 307 type eventOpenTxn struct{} 308 type eventFeedMessage struct{} 309 type eventPause struct{} 310 type eventResume struct{} 311 type eventCommit struct{} 312 type eventPush struct{} 313 type eventAbort struct{} 314 type eventRollback struct{} 315 type eventSplit struct{} 316 type eventAddColumn struct { 317 CanAddColumnAfter fsm.Bool 318 } 319 type eventRemoveColumn struct { 320 CanRemoveColumnAfter fsm.Bool 321 } 322 type eventFinished struct{} 323 324 func (eventOpenTxn) Event() {} 325 func (eventFeedMessage) Event() {} 326 func (eventPause) Event() {} 327 func (eventResume) Event() {} 328 func (eventCommit) Event() {} 329 func (eventPush) Event() {} 330 func (eventAbort) Event() {} 331 func (eventRollback) Event() {} 332 func (eventSplit) Event() {} 333 func (eventAddColumn) Event() {} 334 func (eventRemoveColumn) Event() {} 335 func (eventFinished) Event() {} 336 337 var stateTransitions = fsm.Pattern{ 338 stateRunning{ 339 FeedPaused: fsm.Var("FeedPaused"), 340 TxnOpen: fsm.Var("TxnOpen"), 341 CanAddColumn: fsm.Var("CanAddColumn"), 342 CanRemoveColumn: fsm.Var("CanRemoveColumn"), 343 }: { 344 eventSplit{}: { 345 Next: stateRunning{ 346 FeedPaused: fsm.Var("FeedPaused"), 347 TxnOpen: fsm.Var("TxnOpen"), 348 CanAddColumn: fsm.Var("CanAddColumn"), 349 CanRemoveColumn: fsm.Var("CanRemoveColumn")}, 350 Action: logEvent(split), 351 }, 352 eventFinished{}: { 353 Next: stateDone{}, 354 Action: logEvent(cleanup), 355 }, 356 }, 357 stateRunning{ 358 FeedPaused: fsm.Var("FeedPaused"), 359 TxnOpen: fsm.False, 360 CanAddColumn: fsm.True, 361 CanRemoveColumn: fsm.Any, 362 }: { 363 eventAddColumn{ 364 CanAddColumnAfter: fsm.Var("CanAddColumnAfter"), 365 }: { 366 Next: stateRunning{ 367 FeedPaused: fsm.Var("FeedPaused"), 368 TxnOpen: fsm.False, 369 CanAddColumn: fsm.Var("CanAddColumnAfter"), 370 CanRemoveColumn: fsm.True}, 371 Action: logEvent(addColumn), 372 }, 373 }, 374 stateRunning{ 375 FeedPaused: fsm.Var("FeedPaused"), 376 TxnOpen: fsm.False, 377 CanAddColumn: fsm.Any, 378 CanRemoveColumn: fsm.True, 379 }: { 380 eventRemoveColumn{ 381 CanRemoveColumnAfter: fsm.Var("CanRemoveColumnAfter"), 382 }: { 383 Next: stateRunning{ 384 FeedPaused: fsm.Var("FeedPaused"), 385 TxnOpen: fsm.False, 386 CanAddColumn: fsm.True, 387 CanRemoveColumn: fsm.Var("CanRemoveColumnAfter")}, 388 Action: logEvent(removeColumn), 389 }, 390 }, 391 stateRunning{ 392 FeedPaused: fsm.False, 393 TxnOpen: fsm.False, 394 CanAddColumn: fsm.Var("CanAddColumn"), 395 CanRemoveColumn: fsm.Var("CanRemoveColumn"), 396 }: { 397 eventFeedMessage{}: { 398 Next: stateRunning{ 399 FeedPaused: fsm.False, 400 TxnOpen: fsm.False, 401 CanAddColumn: fsm.Var("CanAddColumn"), 402 CanRemoveColumn: fsm.Var("CanRemoveColumn")}, 403 Action: logEvent(noteFeedMessage), 404 }, 405 }, 406 stateRunning{ 407 FeedPaused: fsm.Var("FeedPaused"), 408 TxnOpen: fsm.False, 409 CanAddColumn: fsm.Var("CanAddColumn"), 410 CanRemoveColumn: fsm.Var("CanRemoveColumn"), 411 }: { 412 eventOpenTxn{}: { 413 Next: stateRunning{ 414 FeedPaused: fsm.Var("FeedPaused"), 415 TxnOpen: fsm.True, 416 CanAddColumn: fsm.Var("CanAddColumn"), 417 CanRemoveColumn: fsm.Var("CanRemoveColumn")}, 418 Action: logEvent(openTxn), 419 }, 420 }, 421 stateRunning{ 422 FeedPaused: fsm.Var("FeedPaused"), 423 TxnOpen: fsm.True, 424 CanAddColumn: fsm.Var("CanAddColumn"), 425 CanRemoveColumn: fsm.Var("CanRemoveColumn"), 426 }: { 427 eventCommit{}: { 428 Next: stateRunning{ 429 FeedPaused: fsm.Var("FeedPaused"), 430 TxnOpen: fsm.False, 431 CanAddColumn: fsm.Var("CanAddColumn"), 432 CanRemoveColumn: fsm.Var("CanRemoveColumn")}, 433 Action: logEvent(commit), 434 }, 435 eventRollback{}: { 436 Next: stateRunning{ 437 FeedPaused: fsm.Var("FeedPaused"), 438 TxnOpen: fsm.False, 439 CanAddColumn: fsm.Var("CanAddColumn"), 440 CanRemoveColumn: fsm.Var("CanRemoveColumn")}, 441 Action: logEvent(rollback), 442 }, 443 eventAbort{}: { 444 Next: stateRunning{ 445 FeedPaused: fsm.Var("FeedPaused"), 446 TxnOpen: fsm.True, 447 CanAddColumn: fsm.Var("CanAddColumn"), 448 CanRemoveColumn: fsm.Var("CanRemoveColumn")}, 449 Action: logEvent(abort), 450 }, 451 eventPush{}: { 452 Next: stateRunning{ 453 FeedPaused: fsm.Var("FeedPaused"), 454 TxnOpen: fsm.True, 455 CanAddColumn: fsm.Var("CanAddColumn"), 456 CanRemoveColumn: fsm.Var("CanRemoveColumn")}, 457 Action: logEvent(push), 458 }, 459 }, 460 stateRunning{ 461 FeedPaused: fsm.False, 462 TxnOpen: fsm.Var("TxnOpen"), 463 CanAddColumn: fsm.Var("CanAddColumn"), 464 CanRemoveColumn: fsm.Var("CanRemoveColumn"), 465 }: { 466 eventPause{}: { 467 Next: stateRunning{ 468 FeedPaused: fsm.True, 469 TxnOpen: fsm.Var("TxnOpen"), 470 CanAddColumn: fsm.Var("CanAddColumn"), 471 CanRemoveColumn: fsm.Var("CanRemoveColumn")}, 472 Action: logEvent(pause), 473 }, 474 }, 475 stateRunning{ 476 FeedPaused: fsm.True, 477 TxnOpen: fsm.Var("TxnOpen"), 478 CanAddColumn: fsm.Var("CanAddColumn"), 479 CanRemoveColumn: fsm.Var("CanRemoveColumn"), 480 }: { 481 eventResume{}: { 482 Next: stateRunning{ 483 FeedPaused: fsm.False, 484 TxnOpen: fsm.Var("TxnOpen"), 485 CanAddColumn: fsm.Var("CanAddColumn"), 486 CanRemoveColumn: fsm.Var("CanRemoveColumn")}, 487 Action: logEvent(resume), 488 }, 489 }, 490 } 491 492 var compiledStateTransitions = fsm.Compile(stateTransitions) 493 494 func logEvent(fn func(fsm.Args) error) func(fsm.Args) error { 495 return func(a fsm.Args) error { 496 if log.V(1) { 497 log.Infof(a.Ctx, "%#v\n", a.Event) 498 } 499 return fn(a) 500 } 501 } 502 503 func cleanup(a fsm.Args) error { 504 if txn := a.Extended.(*nemeses).txn; txn != nil { 505 return txn.Rollback() 506 } 507 return nil 508 } 509 510 func openTxn(a fsm.Args) error { 511 ns := a.Extended.(*nemeses) 512 513 const noDeleteSentinel = int(-1) 514 // 10% of the time attempt a DELETE. 515 deleteID := noDeleteSentinel 516 if rand.Intn(10) == 0 { 517 rows, err := ns.db.Query(`SELECT id FROM foo ORDER BY random() LIMIT 1`) 518 if err != nil { 519 return err 520 } 521 defer func() { _ = rows.Close() }() 522 if rows.Next() { 523 if err := rows.Scan(&deleteID); err != nil { 524 return err 525 } 526 } 527 // If there aren't any rows, skip the DELETE this time. 528 } 529 530 txn, err := ns.db.Begin() 531 if err != nil { 532 return err 533 } 534 if deleteID == noDeleteSentinel { 535 if err := txn.QueryRow( 536 `UPSERT INTO foo VALUES ((random() * $1)::int, cluster_logical_timestamp()::string) RETURNING id, ts`, 537 ns.rowCount, 538 ).Scan(&ns.openTxnID, &ns.openTxnTs); err != nil { 539 return err 540 } 541 ns.openTxnType = openTxnTypeUpsert 542 } else { 543 if err := txn.QueryRow( 544 `DELETE FROM foo WHERE id = $1 RETURNING id, ts`, deleteID, 545 ).Scan(&ns.openTxnID, &ns.openTxnTs); err != nil { 546 return err 547 } 548 ns.openTxnType = openTxnTypeDelete 549 } 550 ns.txn = txn 551 return nil 552 } 553 554 func commit(a fsm.Args) error { 555 ns := a.Extended.(*nemeses) 556 defer func() { ns.txn = nil }() 557 if err := ns.txn.Commit(); err != nil { 558 // Don't error out if we got pushed, but don't increment availableRows no 559 // matter what error was hit. 560 if strings.Contains(err.Error(), `restart transaction`) { 561 return nil 562 } 563 } 564 ns.availableRows++ 565 return nil 566 } 567 568 func rollback(a fsm.Args) error { 569 ns := a.Extended.(*nemeses) 570 defer func() { ns.txn = nil }() 571 return ns.txn.Rollback() 572 } 573 574 func addColumn(a fsm.Args) error { 575 ns := a.Extended.(*nemeses) 576 577 if ns.currentTestColumnCount >= ns.maxTestColumnCount { 578 return errors.AssertionFailedf(`addColumn should be called when`+ 579 `there are less than %d columns.`, ns.maxTestColumnCount) 580 } 581 582 if _, err := ns.db.Exec(fmt.Sprintf(`ALTER TABLE foo ADD COLUMN test%d STRING DEFAULT 'x'`, 583 ns.currentTestColumnCount)); err != nil { 584 return err 585 } 586 ns.currentTestColumnCount++ 587 var rows int 588 // Adding a column should trigger a full table scan. 589 if err := ns.db.QueryRow(`SELECT count(*) FROM foo`).Scan(&rows); err != nil { 590 return err 591 } 592 // We expect one table scan that corresponds to the schema change backfill, and one 593 // scan that corresponds to the changefeed level backfill. 594 ns.availableRows += 2 * rows 595 return nil 596 } 597 598 func removeColumn(a fsm.Args) error { 599 ns := a.Extended.(*nemeses) 600 601 if ns.currentTestColumnCount == 0 { 602 return errors.AssertionFailedf(`removeColumn should be called with` + 603 `at least one test column.`) 604 } 605 if _, err := ns.db.Exec(fmt.Sprintf(`ALTER TABLE foo DROP COLUMN test%d`, 606 ns.currentTestColumnCount-1)); err != nil { 607 return err 608 } 609 ns.currentTestColumnCount-- 610 var rows int 611 // Dropping a column should trigger a full table scan. 612 if err := ns.db.QueryRow(`SELECT count(*) FROM foo`).Scan(&rows); err != nil { 613 return err 614 } 615 // We expect one table scan that corresponds to the schema change backfill, and one 616 // scan that corresponds to the changefeed level backfill. 617 ns.availableRows += 2 * rows 618 return nil 619 } 620 621 func noteFeedMessage(a fsm.Args) error { 622 ns := a.Extended.(*nemeses) 623 624 if ns.availableRows <= 0 { 625 return errors.AssertionFailedf(`noteFeedMessage should be called with at` + 626 `least one available row.`) 627 } 628 m, err := ns.f.Next() 629 if err != nil { 630 return err 631 } else if m == nil { 632 return errors.Errorf(`expected another message`) 633 } 634 635 if len(m.Resolved) > 0 { 636 _, ts, err := ParseJSONValueTimestamps(m.Resolved) 637 if err != nil { 638 return err 639 } 640 log.Infof(a.Ctx, "%v", string(m.Resolved)) 641 return ns.v.NoteResolved(m.Partition, ts) 642 } 643 ts, _, err := ParseJSONValueTimestamps(m.Value) 644 if err != nil { 645 return err 646 } 647 648 ns.availableRows-- 649 log.Infof(a.Ctx, "%s->%s", m.Key, m.Value) 650 return ns.v.NoteRow(m.Partition, string(m.Key), string(m.Value), ts) 651 } 652 653 func pause(a fsm.Args) error { 654 return a.Extended.(*nemeses).f.Pause() 655 } 656 657 func resume(a fsm.Args) error { 658 return a.Extended.(*nemeses).f.Resume() 659 } 660 661 func abort(a fsm.Args) error { 662 ns := a.Extended.(*nemeses) 663 const delete = `BEGIN TRANSACTION PRIORITY HIGH; ` + 664 `SELECT count(*) FROM [DELETE FROM foo RETURNING *]; ` + 665 `COMMIT` 666 var deletedRows int 667 if err := ns.db.QueryRow(delete).Scan(&deletedRows); err != nil { 668 return err 669 } 670 ns.availableRows += deletedRows 671 return nil 672 } 673 674 func push(a fsm.Args) error { 675 ns := a.Extended.(*nemeses) 676 _, err := ns.db.Exec(`BEGIN TRANSACTION PRIORITY HIGH; SELECT * FROM foo; COMMIT`) 677 return err 678 } 679 680 func split(a fsm.Args) error { 681 ns := a.Extended.(*nemeses) 682 _, err := ns.db.Exec(`ALTER TABLE foo SPLIT AT VALUES ((random() * $1)::int)`, ns.rowCount) 683 return err 684 }