From 386ef193346265e3f8cc60df9bf8641450e4edf0 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 30 Oct 2025 11:31:11 -0700 Subject: [PATCH 1/2] Always check refCount after acquiring lock --- internal/project/extendedconfigcache.go | 18 +++++++++++++++++- internal/project/parsecache.go | 18 +++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/internal/project/extendedconfigcache.go b/internal/project/extendedconfigcache.go index 38654c55ae..3e6977272d 100644 --- a/internal/project/extendedconfigcache.go +++ b/internal/project/extendedconfigcache.go @@ -38,6 +38,17 @@ func (c *extendedConfigCache) Acquire(fh FileHandle, path tspath.Path, parse fun func (c *extendedConfigCache) Ref(path tspath.Path) { if entry, ok := c.entries.Load(path); ok { entry.mu.Lock() + if entry.refCount <= 0 { + // Entry was deleted while we were acquiring the lock + newEntry, loaded := c.loadOrStoreNewLockedEntry(path) + if !loaded { + newEntry.entry = entry.entry + newEntry.hash = entry.hash + } + entry.mu.Unlock() + newEntry.mu.Unlock() + return + } entry.refCount++ entry.mu.Unlock() } @@ -48,10 +59,10 @@ func (c *extendedConfigCache) Deref(path tspath.Path) { entry.mu.Lock() entry.refCount-- remove := entry.refCount <= 0 - entry.mu.Unlock() if remove { c.entries.Delete(path) } + entry.mu.Unlock() } } @@ -68,6 +79,11 @@ func (c *extendedConfigCache) loadOrStoreNewLockedEntry(path tspath.Path) (*exte entry.mu.Lock() if existing, loaded := c.entries.LoadOrStore(path, entry); loaded { existing.mu.Lock() + if existing.refCount <= 0 { + // Entry was deleted while we were acquiring the lock + existing.mu.Unlock() + return c.loadOrStoreNewLockedEntry(path) + } existing.refCount++ return existing, true } diff --git a/internal/project/parsecache.go b/internal/project/parsecache.go index caf8851584..f0a3e34023 100644 --- a/internal/project/parsecache.go +++ b/internal/project/parsecache.go @@ -63,6 +63,17 @@ func (c *ParseCache) Ref(file *ast.SourceFile) { key := newParseCacheKey(file.ParseOptions(), file.ScriptKind) if entry, ok := c.entries.Load(key); ok { entry.mu.Lock() + if entry.refCount <= 0 { + // Entry was deleted while we were acquiring the lock + newEntry, loaded := c.loadOrStoreNewLockedEntry(key) + if !loaded { + newEntry.sourceFile = entry.sourceFile + newEntry.hash = entry.hash + } + entry.mu.Unlock() + newEntry.mu.Unlock() + return + } entry.refCount++ entry.mu.Unlock() } else { @@ -76,10 +87,10 @@ func (c *ParseCache) Deref(file *ast.SourceFile) { entry.mu.Lock() entry.refCount-- remove := entry.refCount <= 0 - entry.mu.Unlock() if !c.Options.DisableDeletion && remove { c.entries.Delete(key) } + entry.mu.Unlock() } } @@ -92,6 +103,11 @@ func (c *ParseCache) loadOrStoreNewLockedEntry(key parseCacheKey) (*parseCacheEn existing, loaded := c.entries.LoadOrStore(key, entry) if loaded { existing.mu.Lock() + if existing.refCount <= 0 { + // Existing entry was deleted while we were acquiring the lock + existing.mu.Unlock() + return c.loadOrStoreNewLockedEntry(key) + } existing.refCount++ return existing, true } From 67ec2350fcc58e98acf9ba810f3df1041436e7ee Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 30 Oct 2025 12:13:40 -0700 Subject: [PATCH 2/2] Disable new logic when DisableDeletion is enabled --- internal/project/parsecache.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/project/parsecache.go b/internal/project/parsecache.go index f0a3e34023..48ccf8a9e7 100644 --- a/internal/project/parsecache.go +++ b/internal/project/parsecache.go @@ -63,7 +63,7 @@ func (c *ParseCache) Ref(file *ast.SourceFile) { key := newParseCacheKey(file.ParseOptions(), file.ScriptKind) if entry, ok := c.entries.Load(key); ok { entry.mu.Lock() - if entry.refCount <= 0 { + if entry.refCount <= 0 && !c.Options.DisableDeletion { // Entry was deleted while we were acquiring the lock newEntry, loaded := c.loadOrStoreNewLockedEntry(key) if !loaded { @@ -103,7 +103,7 @@ func (c *ParseCache) loadOrStoreNewLockedEntry(key parseCacheKey) (*parseCacheEn existing, loaded := c.entries.LoadOrStore(key, entry) if loaded { existing.mu.Lock() - if existing.refCount <= 0 { + if existing.refCount <= 0 && !c.Options.DisableDeletion { // Existing entry was deleted while we were acquiring the lock existing.mu.Unlock() return c.loadOrStoreNewLockedEntry(key)