@@ -10,7 +10,9 @@ import (
10
10
"math/rand"
11
11
"sort"
12
12
13
+ "github.com/cockroachdb/cockroach/pkg/cmd/roachtest/option"
13
14
"github.com/cockroachdb/cockroach/pkg/cmd/roachtest/roachtestutil/clusterupgrade"
15
+ "github.com/cockroachdb/cockroach/pkg/roachprod/failureinjection/failures"
14
16
"github.com/cockroachdb/cockroach/pkg/roachprod/install"
15
17
"golang.org/x/exp/maps"
16
18
)
@@ -54,7 +56,7 @@ func (m preserveDowngradeOptionRandomizerMutator) Probability() float64 {
54
56
// mutations is always even.
55
57
func (m preserveDowngradeOptionRandomizerMutator ) Generate (
56
58
rng * rand.Rand , plan * TestPlan , planner * testPlanner ,
57
- ) []mutation {
59
+ ) ( []mutation , error ) {
58
60
var mutations []mutation
59
61
for _ , upgradeSelector := range randomUpgrades (rng , plan ) {
60
62
removeExistingStep := upgradeSelector .
@@ -99,7 +101,7 @@ func (m preserveDowngradeOptionRandomizerMutator) Generate(
99
101
mutations = append (mutations , addRandomly ... )
100
102
}
101
103
102
- return mutations
104
+ return mutations , nil
103
105
}
104
106
105
107
// randomUpgrades returns selectors for the steps of a random subset
@@ -223,7 +225,7 @@ func (m clusterSettingMutator) Probability() float64 {
223
225
// happen any time after cluster setup.
224
226
func (m clusterSettingMutator ) Generate (
225
227
rng * rand.Rand , plan * TestPlan , planner * testPlanner ,
226
- ) []mutation {
228
+ ) ( []mutation , error ) {
227
229
var mutations []mutation
228
230
229
231
// possiblePointsInTime is the list of steps in the plan that are
@@ -264,7 +266,7 @@ func (m clusterSettingMutator) Generate(
264
266
mutations = append (mutations , applyChange ... )
265
267
}
266
268
267
- return mutations
269
+ return mutations , nil
268
270
}
269
271
270
272
// clusterSettingChangeStep encapsulates the information necessary to
@@ -401,7 +403,7 @@ func (m panicNodeMutator) Probability() float64 {
401
403
402
404
func (m panicNodeMutator ) Generate (
403
405
rng * rand.Rand , plan * TestPlan , planner * testPlanner ,
404
- ) []mutation {
406
+ ) ( []mutation , error ) {
405
407
var mutations []mutation
406
408
upgrades := randomUpgrades (rng , plan )
407
409
idx := newStepIndex (plan )
@@ -411,8 +413,10 @@ func (m panicNodeMutator) Generate(
411
413
possiblePointsInTime := upgrade .
412
414
// We don't want to panic concurrently with other steps, and inserting before a concurrent step
413
415
// causes the step to run concurrently with that step, so we filter out any concurrent steps.
416
+ // We don't want to panic the system on a node while a system node is already down, as that could cause
417
+ // the cluster to lose quorum, so we filter out any steps with unavailable system nodes.
414
418
Filter (func (s * singleStep ) bool {
415
- return s .context .System .Stage >= InitUpgradeStage && ! idx .IsConcurrent (s )
419
+ return s .context .System .Stage >= InitUpgradeStage && ! idx .IsConcurrent (s ) && ! s . context . System . hasUnavailableNodes
416
420
})
417
421
418
422
targetNode := nodeList .SeededRandNode (rng )
@@ -441,7 +445,7 @@ func (m panicNodeMutator) Generate(
441
445
firstStepInConcurrentBlock = nil
442
446
}
443
447
444
- return restart || waitForStable || runHook
448
+ return restart || waitForStable || runHook || s . context . System . hasUnavailableNodes
445
449
}
446
450
447
451
// The node should be restarted after the panic, but before any steps that are
@@ -468,19 +472,183 @@ func (m panicNodeMutator) Generate(
468
472
addPanicStep := stepToPanic .
469
473
InsertBefore (panicNodeStep {planner .currentContext .System .Descriptor .Nodes [0 ], targetNode })
470
474
var addRestartStep []mutation
475
+ var restartStep stepSelector
471
476
// If validEndStep is nil, it means that there are no steps after the panic step that
472
477
// are compatible with a dead node, so we immediately restart the node after the panic.
473
478
if validEndStep == nil {
479
+ restartStep = cutStep
474
480
addRestartStep = cutStep .InsertBefore (restartNodeStep {planner .currentContext .System .Descriptor .Nodes [0 ], targetNode , planner .rt , restartDesc })
475
481
} else {
476
- addRestartStep = validEndStep .
477
- RandomStep ( rng ) .
482
+ restartStep = validEndStep .RandomStep ( rng )
483
+ addRestartStep = restartStep .
478
484
Insert (rng , restartNodeStep {planner .currentContext .System .Descriptor .Nodes [0 ], targetNode , planner .rt , restartDesc })
479
485
}
480
486
487
+ failureContextSteps , _ := validStartStep .CutBefore (func (s * singleStep ) bool {
488
+ return s == restartStep [0 ]
489
+ })
490
+ failureContextSteps .MarkNodesUnavailable (true , false )
491
+
481
492
mutations = append (mutations , addPanicStep ... )
482
493
mutations = append (mutations , addRestartStep ... )
483
494
}
484
495
485
- return mutations
496
+ return mutations , nil
497
+ }
498
+
499
+ type networkPartitionMutator struct {}
500
+
501
+ func (m networkPartitionMutator ) Name () string { return failures .IPTablesNetworkPartitionName }
502
+
503
+ func (m networkPartitionMutator ) Probability () float64 {
504
+ return 0.3
505
+ }
506
+
507
+ func (m networkPartitionMutator ) Generate (
508
+ rng * rand.Rand , plan * TestPlan , planner * testPlanner ,
509
+ ) ([]mutation , error ) {
510
+ var mutations []mutation
511
+ upgrades := randomUpgrades (rng , plan )
512
+ idx := newStepIndex (plan )
513
+ nodeList := planner .currentContext .System .Descriptor .Nodes
514
+
515
+ failure := failures .GetFailureRegistry ()
516
+ f , err := failure .GetFailer (planner .cluster .Name (), failures .IPTablesNetworkPartitionName , planner .logger )
517
+ if err != nil {
518
+ return nil , fmt .Errorf ("failed to get failer for %s: %w" , failures .IPTablesNetworkPartitionName , err )
519
+ }
520
+
521
+ for _ , upgrade := range upgrades {
522
+ possiblePointsInTime := upgrade .
523
+ Filter (func (s * singleStep ) bool {
524
+ // We don't want to set up a partition concurrently with other steps, and inserting
525
+ // before a concurrent step causes the step to run concurrently with that step, so
526
+ // we filter out any concurrent steps. We don't want to set up a partition while
527
+ // nodes are unavailable, as that could cause the cluster to lose quorum,
528
+ // so we filter out steps with unavailable nodes.
529
+ var unavailableNodes bool
530
+ if planner .isMultitenant () {
531
+ unavailableNodes = s .context .Tenant .hasUnavailableNodes || s .context .System .hasUnavailableNodes
532
+ } else {
533
+ unavailableNodes = s .context .System .hasUnavailableNodes
534
+ }
535
+ return s .context .System .Stage >= InitUpgradeStage && ! idx .IsConcurrent (s ) && ! unavailableNodes
536
+ })
537
+
538
+ stepToPartition := possiblePointsInTime .RandomStep (rng )
539
+ hasInvalidConcurrentStep := false
540
+ var firstStepInConcurrentBlock * singleStep
541
+
542
+ isInvalidRecoverStep := func (s * singleStep ) bool {
543
+ // Restarting a node in the middle of a network partition has a chance of
544
+ // loss of quorum, so we do should recover the network partition before this
545
+ // if the restarted node is not the node being partitioned.
546
+ // e.g. In a 4-node cluster, if node 1 is partitioned from nodes 2, 3, and
547
+ // 4, then restarting node 2 would cause a loss of quorum since 3 and 4
548
+ // cannot talk to 1.
549
+
550
+ // TODO: The partitioned node should be able to restart safely, provided
551
+ // the necessary steps are altered to allow it.
552
+
553
+ _ , restartSystem := s .impl .(restartWithNewBinaryStep )
554
+ _ , restartTenant := s .impl .(restartVirtualClusterStep )
555
+ // Many hook steps require communication between specific nodes, so we
556
+ // should recover the network partition before running them.
557
+ _ , runHook := s .impl .(runHookStep )
558
+
559
+ if idx .IsConcurrent (s ) {
560
+ if firstStepInConcurrentBlock == nil {
561
+ firstStepInConcurrentBlock = s
562
+ }
563
+ hasInvalidConcurrentStep = true
564
+ } else {
565
+ hasInvalidConcurrentStep = false
566
+ firstStepInConcurrentBlock = nil
567
+ }
568
+
569
+ var unavailableNodes bool
570
+ if planner .isMultitenant () {
571
+ unavailableNodes = s .context .Tenant .hasUnavailableNodes || s .context .System .hasUnavailableNodes
572
+ } else {
573
+ unavailableNodes = s .context .System .hasUnavailableNodes
574
+ }
575
+ return unavailableNodes || restartTenant || restartSystem || runHook
576
+ }
577
+
578
+ _ , validStartStep := upgrade .CutAfter (func (s * singleStep ) bool {
579
+ return s == stepToPartition [0 ]
580
+ })
581
+
582
+ validEndStep , _ , cutStep := validStartStep .Cut (func (s * singleStep ) bool {
583
+ return isInvalidRecoverStep (s )
584
+ })
585
+
586
+ // Inserting before a concurrent step will cause the step to run concurrently with that step,
587
+ // so we remove the concurrent steps from the list of possible insertions if they contain
588
+ // any invalid steps.
589
+ if hasInvalidConcurrentStep {
590
+ validEndStep , _ = validEndStep .CutAfter (func (s * singleStep ) bool {
591
+ return s == firstStepInConcurrentBlock
592
+ })
593
+ }
594
+
595
+ partitionedNode , leftPartition , rightPartition := selectPartitions (rng , nodeList )
596
+ partitionType := failures .AllPartitionTypes [rng .Intn (len (failures .AllPartitionTypes ))]
597
+
598
+ partition := failures.NetworkPartition {Source : leftPartition , Destination : rightPartition , Type : partitionType }
599
+
600
+ addPartition := stepToPartition .
601
+ InsertBefore (networkPartitionInjectStep {f , partition , partitionedNode })
602
+ var addRecoveryStep []mutation
603
+ var recoveryStep stepSelector
604
+ // If validEndStep is nil, it means that there are no steps after the partition step that are
605
+ // compatible with a network partition, so we immediately restart the node after the partition.
606
+ if validEndStep == nil {
607
+ recoveryStep = cutStep
608
+ addRecoveryStep = cutStep .InsertBefore (networkPartitionRecoveryStep {f , partition , partitionedNode })
609
+ } else {
610
+ recoveryStep = validEndStep .RandomStep (rng )
611
+ addRecoveryStep = recoveryStep .
612
+ Insert (rng , networkPartitionRecoveryStep {f , partition , partitionedNode })
613
+ }
614
+
615
+ failureContextSteps , _ := validStartStep .CutBefore (func (s * singleStep ) bool {
616
+ return s == recoveryStep [0 ]
617
+ })
618
+
619
+ failureContextSteps .MarkNodesUnavailable (true , true )
620
+
621
+ mutations = append (mutations , addPartition ... )
622
+ mutations = append (mutations , addRecoveryStep ... )
623
+ }
624
+
625
+ return mutations , nil
626
+ }
627
+ func selectPartitions (
628
+ rng * rand.Rand , nodeList option.NodeListOption ,
629
+ ) (option.NodeListOption , []install.Node , []install.Node ) {
630
+ rand .Shuffle (len (nodeList ), func (i , j int ) {
631
+ nodeList [i ], nodeList [j ] = nodeList [j ], nodeList [i ]
632
+ })
633
+ partitionedNode := nodeList [0 ]
634
+
635
+ leftPartition := []install.Node {install .Node (partitionedNode )}
636
+ var rightPartition []install.Node
637
+ // To make an even distribution of partial vs total partitions, 50% of the
638
+ // time we will default to a total partition, and the other 50% we will
639
+ // randomly choose which nodes to partition.
640
+ isTotalPartition := rng .Float64 () < 0.5
641
+ if isTotalPartition {
642
+ for _ , n := range nodeList [1 :] {
643
+ rightPartition = append (rightPartition , install .Node (n ))
644
+ }
645
+ } else {
646
+ rightPartition = append (rightPartition , install .Node (nodeList [1 ]))
647
+ for _ , n := range nodeList [2 :] {
648
+ if rng .Float64 () < 0.5 {
649
+ rightPartition = append (rightPartition , install .Node (n ))
650
+ }
651
+ }
652
+ }
653
+ return option.NodeListOption {partitionedNode }, leftPartition , rightPartition
486
654
}
0 commit comments