Skip to content

Commit c77ed66

Browse files
add post 7
1 parent 85ffdff commit c77ed66

File tree

2 files changed

+239
-0
lines changed

2 files changed

+239
-0
lines changed
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
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>

src/routes/(main)/blog/posts.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,11 @@ export const posts: post[] = [
4040
title: 'Sharing Password Bundles',
4141
description: `In this demo, I will showcase new features, including creating a new bundle and sharing bundles with other users.`,
4242
date: '2025-03-13'
43+
},
44+
{
45+
id: '7',
46+
title: 'Synchronizing PwManager: Preventing Race Conditions When Sharing Password Bundles',
47+
description: `This blog post explores how PwManager handles synchronization when sharing password bundles to prevent race conditions. It explains the concept of password bundles and the potential issues when multiple admins try to modify the same bundle simultaneously. The post details how mutexes are used to synchronize updates across goroutines and prevent conflicts. Additionally, it discusses the benefits of a dynamic map of mutexes and how it efficiently manages lock acquisition and garbage collection. Finally, the post touches on a practical example sourced from Stack Overflow, provided by Brad Peabody.`,
48+
date: '2025-03-15'
4349
}
4450
];

0 commit comments

Comments
 (0)