@@ -1288,23 +1288,22 @@ type SqlRuntimeInfo (config : TypeProviderConfig) =
12881288 //| Choice2Of2(paths, errors) -> Assembly.GetExecutingAssembly()
12891289 member __.RuntimeAssembly = runtimeAssembly
12901290
1291- /// One generation of provided types for one set of static parameters.
1292- /// Reference equality so that ConcurrentDictionary.TryUpdate swaps compare generations,
1293- /// not their (concurrently mutated) field values.
1294- [<ReferenceEquality>]
1295- type DesignCacheEntry =
1296- { /// The provided root type, when it was built, and how old the build may get before a
1297- /// background refresh is started (adapted to build duration on slow systems).
1298- Root : Lazy < ProvidedTypeDefinition * DateTime * TimeSpan >
1299- /// Last access in UTC ticks. Updated lock-free on every instantiation request;
1300- /// entries idle longer than the expiration are dropped to free memory.
1301- mutable LastAccess : int64
1302- /// 1 while a background refresh build is in flight: at most one refresh per entry,
1303- /// no matter how many Visual Studio threads request the type concurrently.
1304- mutable Refreshing : int }
1305-
13061291module DesignTimeCache =
1307- let cache = System.Collections.Concurrent.ConcurrentDictionary< DesignCacheKey, DesignCacheEntry>()
1292+ /// One lazily-built generation of provided types per set of static parameters, memoized for the
1293+ /// life of the type-provider instance (the compilation, or the IDE session).
1294+ ///
1295+ /// It is deliberately NEVER rebuilt, swapped or evicted while the process lives. Erased provided
1296+ /// types are compared by reference identity, so if one compilation ever bound some sites to one
1297+ /// generation and later/other-thread sites to a rebuilt one, the two identical-looking types
1298+ /// (e.g. 'CustomersEntity', or 'dataContext.mainSchema' vs 'readDataContext.mainSchema') would
1299+ /// fail to unify (FS0001/FS0193). Both the old background "stale-while-revalidate" swap and the
1300+ /// idle eviction rebuilt the tree on a timer, and a slow compilation (Windows CI builds net48
1301+ /// and net10.0 in parallel) crossed that timer mid-build and produced a second generation that
1302+ /// parallel compiler threads then mixed. The pre-7ffdce22 provider avoided this by keeping its
1303+ /// provided types alive for the whole process too (DesignTimeCacheSchema); this restores that,
1304+ /// for the entire type tree. Schema refresh is explicit (ClearDatabaseSchemaCache), which is the
1305+ /// only way to guarantee a single stable generation per compilation.
1306+ let cache = System.Collections.Concurrent.ConcurrentDictionary< DesignCacheKey, Lazy< ProvidedTypeDefinition>>()
13081307
13091308/// The idea of this is trying to avoid case where compile-time has loaded non-runtime assembly. (Happens in .NET 8.0, not in .NET Framework.)
13101309/// So let's load compile-time (and design-time) manually the required runtime assembly.
@@ -1523,15 +1522,8 @@ type public SqlTypeProvider(config: TypeProviderConfig) as this =
15231522 args.[ 12 ] :?> string, // SSDT Path
15241523 typeName)
15251524
1526- // Entries idle longer than this are dropped to free memory. An actively used entry is
1527- // never dropped on a timer; instead it is refreshed in the background once its build is
1528- // older than its staleness interval, so database schema changes are still picked up
1529- // without IntelliSense ever stalling on a synchronous rebuild.
1530- let idleExpiration = TimeSpan.FromMinutes 3.0
1531-
15321525 let buildRoot ( args : DesignCacheKey ) =
15331526 let struct ( _ , _ , _ , _ , _ , _ , _ , _ , _ , _ , _ , _ , _ , rootTypeName ) = args
1534- let buildStarted = DateTime.UtcNow
15351527
15361528 let rootType = ProvidedTypeDefinition( sqlRuntimeInfo.RuntimeAssembly, FSHARP_ DATA_ SQL, rootTypeName, Some typeof< obj>, isErased= true )
15371529 let serviceType = ProvidedTypeDefinition( " dataContext" , Some typeof< obj>, isErased= true )
@@ -1540,100 +1532,26 @@ type public SqlTypeProvider(config: TypeProviderConfig) as this =
15401532 createTypes rootType serviceType readServiceType config sqlRuntimeInfo invalidate registerDispose args
15411533 createConstructors config ( rootType, serviceType, readServiceType, args)
15421534
1543- // Refresh no sooner than the idle expiration, and on a slow system no sooner than
1544- // 10x the time a build takes. Fetching the full schema of a big database is heavy;
1545- // the staleness window must stay well above the fetch time so a refresh always
1546- // finishes long before the next one is due. Otherwise refreshes (each followed by a
1547- // re-check) would overlap and build back-pressure that eventually stalls the UI.
1548- // The window is derived from this build's own duration, so it adapts as the schema grows.
1549- let buildDuration = DateTime.UtcNow - buildStarted
1550- let staleAfter = max idleExpiration ( TimeSpan.FromTicks( buildDuration.Ticks * 10 L))
1551- rootType, DateTime.UtcNow, staleAfter
1552-
1553- let dropDesignTimeDcProvider ( key : DesignCacheKey ) =
1554- // Release the design-time data context provider (used by the Individuals feature)
1555- // together with its type tree generation, so it cannot pin stale schema in memory.
1556- let struct ( _ , _ , _ , _ , _ , _ , _ , _ , _ , _ , _ , _ , _ , rootTypeName ) = key
1557- DcCache.providerCache.TryRemove rootTypeName |> ignore
1558-
1559- let rec idleWatcher ( key : DesignCacheKey ) =
1560- async {
1561- do ! Async.Sleep ( int idleExpiration.TotalMilliseconds)
1562- match DesignTimeCache.cache.TryGetValue key with
1563- | true , entry ->
1564- let lastAccess = DateTime( System.Threading.Interlocked.Read(& entry.LastAccess), DateTimeKind.Utc)
1565- if DateTime.UtcNow - lastAccess >= idleExpiration then
1566- DesignTimeCache.cache.TryRemove key |> ignore
1567- dropDesignTimeDcProvider key
1568- else
1569- do ! idleWatcher key
1570- | _ -> ()
1571- }
1572-
1573- let addCache ( key : DesignCacheKey ) =
1574- { Root =
1575- lazy
1576- let generation = buildRoot key
1577- // Only reclaim idle entries in a live host (IDE). In a batch compile
1578- // (fsc/CI, IsInvalidationSupported=false) the process is short-lived and an
1579- // eviction+rebuild would hand out a second generation of erased provided
1580- // types, which then fail to unify against the first.
1581- if config.IsInvalidationSupported then idleWatcher key |> Async.Start
1582- generation
1583- LastAccess = DateTime.UtcNow.Ticks
1584- Refreshing = 0 }
1535+ rootType
15851536
1586- try
1587- let entry = DesignTimeCache.cache.GetOrAdd( arguments, addCache)
1588- System.Threading.Interlocked.Exchange(& entry.LastAccess, DateTime.UtcNow.Ticks) |> ignore
1589-
1590- // Stale-while-revalidate: always serve the current tree; if it has gone stale,
1591- // rebuild it once in the background and swap the entry atomically. Callers never
1592- // wait on a refresh, and concurrent VS threads can never start a second one.
1593- //
1594- // Only ever swap generations in a live host (IDE, IsInvalidationSupported=true).
1595- // In a batch compile (fsc/CI) the swap is unsafe: erased provided types have
1596- // reference identity, so if a long compilation binds some sites to the original
1597- // generation and later sites to the swapped-in one, the two identical-looking
1598- // 'CustomersEntity' types fail to unify (FS0001/FS0193). Windows CI builds two
1599- // target frameworks and so is slow enough to cross the staleness window mid-build,
1600- // which is why it fails there while Linux/Mac and warm local builds pass.
1601- if config.IsInvalidationSupported && entry.Root.IsValueCreated then
1602- let _ , builtAt , staleAfter = entry.Root.Value
1603- if DateTime.UtcNow - builtAt >= staleAfter
1604- && System.Threading.Interlocked.CompareExchange(& entry.Refreshing, 1 , 0 ) = 0 then
1605- async {
1606- try
1607- try
1608- // 1. Fetch/build the new generation fully in the background. Nothing
1609- // waits on it: on-demand callers keep getting the previous tree, so
1610- // a slow big-database schema fetch never blocks the editor.
1611- let freshGeneration = buildRoot arguments
1612- let freshEntry =
1613- { Root = Lazy<_>. CreateFromValue freshGeneration
1614- LastAccess = DateTime.UtcNow.Ticks
1615- Refreshing = 0 }
1616- // 2. Only now that the new generation is ready, invalidate. This makes
1617- // the host re-check bind straight to the finished tree instead of
1618- // driving a fresh blocking build on demand. It fires at most once per
1619- // staleness window (>= 10x the build time), so it cannot churn.
1620- this.Invalidate()
1621- // 3. Publish it. Invalidate() only schedules a later re-check, so the
1622- // atomic swap always lands before the host re-reads the cache; the
1623- // re-check then finds a fresh (non-stale) entry and triggers no rebuild.
1624- if DesignTimeCache.cache.TryUpdate( arguments, freshEntry, entry) then
1625- dropDesignTimeDcProvider arguments
1626- with
1627- | _ -> () // keep serving the previous generation; retried on a later access
1628- finally
1629- System.Threading.Interlocked.Exchange(& entry.Refreshing, 0 ) |> ignore
1630- } |> Async.Start
1537+ // Memoize one generation per set of static parameters and always return that same one.
1538+ // See DesignTimeCache for why it is never rebuilt/swapped/evicted while the process lives.
1539+ let addCache ( key : DesignCacheKey ) = lazy ( buildRoot key)
16311540
1632- let root , _ , _ = entry.Root.Value
1633- root
1541+ // GetOrAdd only allocates the entry (its Lazy is not forced here), so only the build below
1542+ // needs the poisoned-entry cleanup.
1543+ let entry = DesignTimeCache.cache.GetOrAdd( arguments, addCache)
1544+ try
1545+ entry.Value
16341546 with
1635- | e ->
1636- DesignTimeCache.cache.TryRemove( arguments) |> ignore
1547+ | _ ->
1548+ // The build threw and its Lazy has cached that exception. Drop the poisoned entry so a
1549+ // genuine transient failure can be retried — but only if it is still the entry we hold,
1550+ // so we never evict a newer generation another thread has since rebuilt at this key.
1551+ match DesignTimeCache.cache.TryGetValue arguments with
1552+ | true , current when System.Object.ReferenceEquals( current, entry) ->
1553+ DesignTimeCache.cache.TryRemove arguments |> ignore
1554+ | _ -> ()
16371555 reraise()
16381556 )
16391557
0 commit comments