|
| 1 | +<script lang="ts"> |
| 2 | + import { posts, type post } from '../posts'; |
| 3 | + import PageSubtitle from '../../../../components/pageSubtitle.svelte'; |
| 4 | + import PageLayout from '../../../../components/layout/pageLayout.svelte'; |
| 5 | + import PageHeader from '../../../../components/pageHeader.svelte'; |
| 6 | + import PageParagraph from '../../../../components/pageParagraph.svelte'; |
| 7 | + import Code from '../../../../components/code.svelte'; |
| 8 | + import Emphasis from '../../../../components/emphasis.svelte'; |
| 9 | + import { base } from '$app/paths'; |
| 10 | + import { TableOfContents, tocCrawler } from '@skeletonlabs/skeleton'; |
| 11 | +
|
| 12 | + let p: post = posts[6]; |
| 13 | + let title = p.title; |
| 14 | + let date = p.date; |
| 15 | + let backText = 'blog'; |
| 16 | + let backHref = '/blog'; |
| 17 | +
|
| 18 | +let mapOfMutexes = ` |
| 19 | +import ( |
| 20 | + "fmt" |
| 21 | + "sync" |
| 22 | +) |
| 23 | +
|
| 24 | +// M wraps a map of mutexes. Each key locks separately. |
| 25 | +type M struct { |
| 26 | + ml sync.Mutex // lock for entry map |
| 27 | + ma map[interface{}]*mentry // entry map |
| 28 | +} |
| 29 | +
|
| 30 | +type mentry struct { |
| 31 | + m *M // point back to M, so we can synchronize removing this mentry when cnt==0 |
| 32 | + el sync.Mutex // entry-specific lock |
| 33 | + cnt int // reference count |
| 34 | + key interface{} // key in ma |
| 35 | +} |
| 36 | +
|
| 37 | +// Unlocker provides an Unlock method to release the lock. |
| 38 | +type Unlocker interface { |
| 39 | + Unlock() |
| 40 | +} |
| 41 | +
|
| 42 | +// NewMapOfMu returns an initalized M. |
| 43 | +func NewMapOfMu() *M { |
| 44 | + return &M{ma: make(map[interface{}]*mentry)} |
| 45 | +} |
| 46 | +
|
| 47 | +// Lock acquires a lock corresponding to this key. |
| 48 | +// This method will never return nil and Unlock() must be called |
| 49 | +// to release the lock when done. |
| 50 | +func (m *M) Lock(key interface{}) Unlocker { |
| 51 | +
|
| 52 | + // read or create entry for this key atomically |
| 53 | + m.ml.Lock() |
| 54 | + e, ok := m.ma[key] |
| 55 | + if !ok { |
| 56 | + e = &mentry{m: m, key: key} |
| 57 | + m.ma[key] = e |
| 58 | + } |
| 59 | + e.cnt++ // ref count |
| 60 | + m.ml.Unlock() |
| 61 | +
|
| 62 | + // acquire lock, will block here until e.cnt==1 |
| 63 | + e.el.Lock() |
| 64 | +
|
| 65 | + return e |
| 66 | +} |
| 67 | +
|
| 68 | +// Unlock releases the lock for this entry. |
| 69 | +func (me *mentry) Unlock() { |
| 70 | +
|
| 71 | + m := me.m |
| 72 | +
|
| 73 | + // decrement and if needed remove entry atomically |
| 74 | + m.ml.Lock() |
| 75 | + e, ok := m.ma[me.key] |
| 76 | + if !ok { // entry must exist |
| 77 | + m.ml.Unlock() |
| 78 | + panic(fmt.Errorf("Unlock requested for key=%v but no entry found", me.key)) |
| 79 | + } |
| 80 | + e.cnt-- // ref count |
| 81 | + if e.cnt < 1 { // if it hits zero then we own it and remove from map |
| 82 | + delete(m.ma, me.key) |
| 83 | + } |
| 84 | + m.ml.Unlock() |
| 85 | +
|
| 86 | + // now that map stuff is handled, we unlock and let |
| 87 | + // anything else waiting on this key through |
| 88 | + e.el.Unlock() |
| 89 | +
|
| 90 | +} |
| 91 | +` |
| 92 | +
|
| 93 | +let mapOfMutexesTests = ` |
| 94 | +import ( |
| 95 | + "math/rand" |
| 96 | + "strconv" |
| 97 | + "strings" |
| 98 | + "sync" |
| 99 | + "testing" |
| 100 | + "time" |
| 101 | +) |
| 102 | +
|
| 103 | +func TestM(t *testing.T) { |
| 104 | +
|
| 105 | + r := rand.New(rand.NewSource(42)) |
| 106 | +
|
| 107 | + m := NewMapOfMu() |
| 108 | + _ = m |
| 109 | +
|
| 110 | + keyCount := 20 |
| 111 | + iCount := 10000 |
| 112 | + out := make(chan string, iCount*2) |
| 113 | +
|
| 114 | + // run a bunch of concurrent requests for various keys, |
| 115 | + // the idea is to have a lot of lock contention |
| 116 | + var wg sync.WaitGroup |
| 117 | + wg.Add(iCount) |
| 118 | + for i := 0; i < iCount; i++ { |
| 119 | + go func(rn int) { |
| 120 | + defer wg.Done() |
| 121 | + key := strconv.Itoa(rn) |
| 122 | +
|
| 123 | + // you can prove the test works by commenting the locking out and seeing it fail |
| 124 | + l := m.Lock(key) |
| 125 | + defer l.Unlock() |
| 126 | +
|
| 127 | + out <- key + " A" |
| 128 | + time.Sleep(time.Microsecond) // make 'em wait a mo' |
| 129 | + out <- key + " B" |
| 130 | + }(r.Intn(keyCount)) |
| 131 | + } |
| 132 | + wg.Wait() |
| 133 | + close(out) |
| 134 | +
|
| 135 | + // verify the map is empty now |
| 136 | + if l := len(m.ma); l != 0 { |
| 137 | + t.Errorf("unexpected map length at test end: %v", l) |
| 138 | + } |
| 139 | +
|
| 140 | + // confirm that the output always produced the correct sequence |
| 141 | + outLists := make([][]string, keyCount) |
| 142 | + for s := range out { |
| 143 | + sParts := strings.Fields(s) |
| 144 | + kn, err := strconv.Atoi(sParts[0]) |
| 145 | + if err != nil { |
| 146 | + t.Fatal(err) |
| 147 | + } |
| 148 | + outLists[kn] = append(outLists[kn], sParts[1]) |
| 149 | + } |
| 150 | + for kn := 0; kn < keyCount; kn++ { |
| 151 | + l := outLists[kn] // list of output for this particular key |
| 152 | + for i := 0; i < len(l); i += 2 { |
| 153 | + if l[i] != "A" || l[i+1] != "B" { |
| 154 | + t.Errorf("For key=%v and i=%v got unexpected values %v and %v", kn, i, l[i], l[i+1]) |
| 155 | + break |
| 156 | + } |
| 157 | + } |
| 158 | + } |
| 159 | + if t.Failed() { |
| 160 | + t.Logf("Failed, outLists: %#v", outLists) |
| 161 | + } |
| 162 | +
|
| 163 | +} |
| 164 | +
|
| 165 | +func BenchmarkM(b *testing.B) { |
| 166 | +
|
| 167 | + m := NewMapOfMu() |
| 168 | +
|
| 169 | + b.ResetTimer() |
| 170 | + for i := 0; i < b.N; i++ { |
| 171 | + // run uncontended lock/unlock - should be quite fast |
| 172 | + m.Lock(i).Unlock() |
| 173 | + } |
| 174 | +
|
| 175 | +} |
| 176 | +
|
| 177 | +` |
| 178 | +
|
| 179 | +let use = `bundlePath := fmt.Sprintf("%s/%s/bundles/%s", BUNDLE_SCHEMA, ownerEntityID, bundleID) |
| 180 | +bundleLock := bundleMapOfMu.Lock(bundlePath) |
| 181 | +defer bundleLock.Unlock()` |
| 182 | +</script> |
| 183 | + |
| 184 | +<!-- POST 2 --> |
| 185 | + |
| 186 | +<PageLayout {backHref} {backText} {title} {date}> |
| 187 | + <PageSubtitle className="underline underline-offset-8 decoration-sky-500" |
| 188 | + >Synchronizing PwManager: Preventing Race Conditions When Sharing Password Bundles</PageSubtitle |
| 189 | + > |
| 190 | + <PageParagraph> |
| 191 | + PwManager provides a way for users to share their password bundles with other users. Note that a |
| 192 | + password bundle is a collection of password entries. If the password bundle is shared with admin |
| 193 | + permissions, the user can modify the password bundle, such as updating users. A race condition |
| 194 | + can occur when two admins try to update the same bundle at the same time from different |
| 195 | + goroutines. To prevent this race condition, synchronization through the use of mutexes is |
| 196 | + implemented. |
| 197 | + </PageParagraph> |
| 198 | + |
| 199 | + <PageParagraph> |
| 200 | + Since PwManager uses HashiCorp Vault and Vault is a KV store, the paths to the bundles are |
| 201 | + unique. To synchronize updates to bundles across goroutines, a map of mutexes is used. This |
| 202 | + allows a goroutine to acquire and lock the map mutex first, and then acquire and lock the bundle |
| 203 | + mutex second. There is a second advantage to this map of mutex pattern: it creates mutexes for |
| 204 | + bundle paths on demand and garbage collects the mutexes when no goroutine requires them. |
| 205 | + </PageParagraph> |
| 206 | + |
| 207 | + <PageParagraph> |
| 208 | + In some cases, you can find gems on Stack Overflow. However, rarely do the gems come with tests. |
| 209 | + The following map of mutexes code was pulled from this <a class="text-primary-500" |
| 210 | + href="https://stackoverflow.com/questions/40931373/how-to-gc-a-map-of-mutexes-in-go" |
| 211 | + target="_blank">SO</a |
| 212 | + > question and provided by <a class="text-primary-500" href="https://stackoverflow.com/users/961810/brad-peabody" target="_blank">Brad Peabody</a>. |
| 213 | + </PageParagraph> |
| 214 | + |
| 215 | + |
| 216 | + |
| 217 | + <PageParagraph> |
| 218 | + Map Of Mutexes: |
| 219 | + </PageParagraph> |
| 220 | + <Code code={mapOfMutexes} lang="go"></Code> |
| 221 | + |
| 222 | + <PageParagraph> |
| 223 | + Map Of Mutexes Tests: |
| 224 | + </PageParagraph> |
| 225 | + <Code code={mapOfMutexesTests} lang="go"></Code> |
| 226 | + |
| 227 | + <PageParagraph> |
| 228 | + Locking A Bundle: |
| 229 | + </PageParagraph> |
| 230 | + <Code code={use} lang="go"></Code> |
| 231 | + |
| 232 | + |
| 233 | +</PageLayout> |
0 commit comments