github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/pkg/causality/internal/slots.go (about)

     1  // Copyright 2022 PingCAP, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package internal
    15  
    16  import (
    17  	"math"
    18  	"sort"
    19  	"sync"
    20  )
    21  
    22  type slot struct {
    23  	nodes map[uint64]*Node
    24  	mu    sync.Mutex
    25  }
    26  
    27  // Slots implements slot-based conflict detection.
    28  // It holds references to Node, which can be used to build
    29  // a DAG of dependency.
    30  type Slots struct {
    31  	slots    []slot
    32  	numSlots uint64
    33  }
    34  
    35  // NewSlots creates a new Slots.
    36  func NewSlots(numSlots uint64) *Slots {
    37  	slots := make([]slot, numSlots)
    38  	for i := uint64(0); i < numSlots; i++ {
    39  		slots[i].nodes = make(map[uint64]*Node, 8)
    40  	}
    41  	return &Slots{
    42  		slots:    slots,
    43  		numSlots: numSlots,
    44  	}
    45  }
    46  
    47  // AllocNode allocates a new node and initializes it with the given hashes.
    48  // TODO: reuse node if necessary. Currently it's impossible if async-notify is used.
    49  // The reason is a node can step functions `assignTo`, `Remove`, `free`, then `assignTo`.
    50  // again. In the last `assignTo`, it can never know whether the node has been reused
    51  // or not.
    52  func (s *Slots) AllocNode(hashes []uint64) *Node {
    53  	return &Node{
    54  		id:                  genNextNodeID(),
    55  		sortedDedupKeysHash: sortAndDedupHashes(hashes, s.numSlots),
    56  		assignedTo:          unassigned,
    57  	}
    58  }
    59  
    60  // Add adds an elem to the slots and calls DependOn for elem.
    61  func (s *Slots) Add(elem *Node) {
    62  	hashes := elem.sortedDedupKeysHash
    63  	dependencyNodes := make(map[int64]*Node, len(hashes))
    64  
    65  	var lastSlot uint64 = math.MaxUint64
    66  	for _, hash := range hashes {
    67  		// lock the slot that the node belongs to.
    68  		slotIdx := getSlot(hash, s.numSlots)
    69  		if lastSlot != slotIdx {
    70  			s.slots[slotIdx].mu.Lock()
    71  			lastSlot = slotIdx
    72  		}
    73  
    74  		// If there is a node occpuied the same hash slot, we may have conflict with it.
    75  		// Add the conflict node to the dependencyNodes.
    76  		if prevNode, ok := s.slots[slotIdx].nodes[hash]; ok {
    77  			prevID := prevNode.nodeID()
    78  			// If there are multiple hashes conflicts with the same node, we only need to
    79  			// depend on the node once.
    80  			dependencyNodes[prevID] = prevNode
    81  		}
    82  		// Add this node to the slot, make sure new coming nodes with the same hash should
    83  		// depend on this node.
    84  		s.slots[slotIdx].nodes[hash] = elem
    85  	}
    86  
    87  	// Construct the dependency graph based on collected `dependencyNodes` and with corresponding
    88  	// slots locked.
    89  	elem.dependOn(dependencyNodes)
    90  
    91  	// Lock those slots one by one and then unlock them one by one, so that
    92  	// we can avoid 2 transactions get executed interleaved.
    93  	lastSlot = math.MaxUint64
    94  	for _, hash := range hashes {
    95  		slotIdx := getSlot(hash, s.numSlots)
    96  		if lastSlot != slotIdx {
    97  			s.slots[slotIdx].mu.Unlock()
    98  			lastSlot = slotIdx
    99  		}
   100  	}
   101  }
   102  
   103  // Remove removes an element from the Slots.
   104  func (s *Slots) Remove(elem *Node) {
   105  	elem.remove()
   106  	hashes := elem.sortedDedupKeysHash
   107  	for _, hash := range hashes {
   108  		slotIdx := getSlot(hash, s.numSlots)
   109  		s.slots[slotIdx].mu.Lock()
   110  		// Remove the node from the slot.
   111  		// If the node is not in the slot, it means the node has been replaced by new node with the same hash,
   112  		// in this case we don't need to remove it from the slot.
   113  		if tail, ok := s.slots[slotIdx].nodes[hash]; ok && tail.nodeID() == elem.nodeID() {
   114  			delete(s.slots[slotIdx].nodes, hash)
   115  		}
   116  		s.slots[slotIdx].mu.Unlock()
   117  	}
   118  }
   119  
   120  func getSlot(hash, numSlots uint64) uint64 {
   121  	return hash % numSlots
   122  }
   123  
   124  // Sort and dedup hashes.
   125  // Sort hashes by `hash % numSlots` to avoid deadlock, and then dedup
   126  // hashes, so the same node will not check confict with the same hash
   127  // twice to prevent potential cyclic self dependency in the causality
   128  // dependency graph.
   129  func sortAndDedupHashes(hashes []uint64, numSlots uint64) []uint64 {
   130  	if len(hashes) == 0 {
   131  		return nil
   132  	}
   133  
   134  	// Sort hashes by `hash % numSlots` to avoid deadlock.
   135  	sort.Slice(hashes, func(i, j int) bool { return hashes[i]%numSlots < hashes[j]%numSlots })
   136  
   137  	// Dedup hashes
   138  	last := hashes[0]
   139  	j := 1
   140  	for i, hash := range hashes {
   141  		if i == 0 {
   142  			// skip first one, start checking duplication from 2nd one
   143  			continue
   144  		}
   145  		if hash == last {
   146  			continue
   147  		}
   148  		last = hash
   149  		hashes[j] = hash
   150  		j++
   151  	}
   152  	hashes = hashes[:j]
   153  
   154  	return hashes
   155  }