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 }