diff --git a/syncs/tsync/tsync.go b/syncs/tsync/tsync.go new file mode 100644 index 000000000..0cda18be8 --- /dev/null +++ b/syncs/tsync/tsync.go @@ -0,0 +1 @@ +package tsync diff --git a/syncs/tsync/tsync_noop.go b/syncs/tsync/tsync_noop.go new file mode 100644 index 000000000..a536a9064 --- /dev/null +++ b/syncs/tsync/tsync_noop.go @@ -0,0 +1,10 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//go:build !ts_tsync_test + +package tsync + +import "sync" + +type Mutex = sync.Mutex diff --git a/syncs/tsync/tsync_real.go b/syncs/tsync/tsync_real.go new file mode 100644 index 000000000..b81346fd2 --- /dev/null +++ b/syncs/tsync/tsync_real.go @@ -0,0 +1,108 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//go:build ts_tsync_test + +package tsync + +import ( + "fmt" + "sync" + "weak" +) + +type Mutex struct { + mu sync.Mutex +} + +func (m *Mutex) Lock() { + noteLock(m) + m.mu.Lock() +} + +func (m *Mutex) Unlock() { + noteUnlock(m) + m.mu.Unlock() +} + +func (m *Mutex) TryLock() bool { + locked := m.mu.TryLock() + if locked { + noteLock(m) + } + return locked +} + +type lockInfo struct { + locked bool +} + +var ( + // TODO: this should be a per-G datastructure + locksMu sync.Mutex + locks map[weak.Pointer[Mutex]]*lockInfo +) + +func panicLocked(format string, args ...any) { + msg := fmt.Sprintf(format, args...) + + // First, gather all current (non-GCed) locks. + type lockEntry struct { + wp weak.Pointer[Mutex] + li *lockInfo + } + var currLocks []lockEntry + for wp, li := range locks { + if wp.Value() != nil { + currLocks = append(currLocks, lockEntry{wp, li}) + } + } + + msg += fmt.Sprintf("\ncurrent locks (%d):", len(currLocks)) + for _, cl := range currLocks { + if cl.li.locked { + msg += fmt.Sprintf("\n\tlocked: %p", cl.wp.Value()) + } else { + msg += fmt.Sprintf("\n\tunlocked: %p", cl.wp.Value()) + } + } + panic(msg) +} + +func noteLock(m *Mutex) { + locksMu.Lock() + defer locksMu.Unlock() + if locks == nil { + locks = make(map[weak.Pointer[Mutex]]*lockInfo) + } + + wp := weak.Make(m) + li, ok := locks[wp] + if !ok { + locks[wp] = &lockInfo{ + locked: true, + } + return + } + + li.locked = true + + // TODO: additional checks here + // TODO: clear things out of the locks map when GCed +} + +func noteUnlock(m *Mutex) { + locksMu.Lock() + defer locksMu.Unlock() + + wp := weak.Make(m) + li, ok := locks[wp] + if !ok { + panicLocked("unknown Unlock on mutex %p", m) + } + + li.locked = false + + // TODO: additional checks here + // TODO: clear things out of the locks map when GCed +}