Skip to content

Commit 20d2dc2

Browse files
committed
Windows CI exposing some problems, try to fix...
1 parent 4771ae1 commit 20d2dc2

2 files changed

Lines changed: 51 additions & 120 deletions

File tree

‎.github/workflows/CI.yml‎

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ on:
66

77
env:
88
DOTNET_VERSION: '6.0.428'
9+
DOTNET_SDK_VERSION: '10.0.x'
910

1011
jobs:
1112
buildAndTestWindows:
@@ -16,9 +17,13 @@ jobs:
1617
steps:
1718
- uses: actions/checkout@v4
1819
- name: Setup .NET
19-
uses: actions/setup-dotnet@v3
20+
uses: actions/setup-dotnet@v4
2021
with:
21-
dotnet-version: ${{ env.DOTNET_VERSION }}
22+
# .NET 6 runtime for the FAKE build tool (net6.0); .NET 10 SDK for the net10.0 targets
23+
# and to satisfy global.json. Installed explicitly rather than relying on the runner image.
24+
dotnet-version: |
25+
${{ env.DOTNET_VERSION }}
26+
${{ env.DOTNET_SDK_VERSION }}
2227
- name: Build & Run Tests
2328
run: |
2429
.\build.cmd
@@ -41,9 +46,13 @@ jobs:
4146
steps:
4247
- uses: actions/checkout@v4
4348
- name: Setup .NET
44-
uses: actions/setup-dotnet@v3
49+
uses: actions/setup-dotnet@v4
4550
with:
46-
dotnet-version: ${{ env.DOTNET_VERSION }}
51+
# .NET 6 runtime for the FAKE build tool (net6.0); .NET 10 SDK for the net10.0 targets
52+
# and to satisfy global.json. Installed explicitly rather than relying on the runner image.
53+
dotnet-version: |
54+
${{ env.DOTNET_VERSION }}
55+
${{ env.DOTNET_SDK_VERSION }}
4756
- name: Build & Run Tests
4857
run: |
4958
./build.sh
@@ -66,9 +75,13 @@ jobs:
6675
steps:
6776
- uses: actions/checkout@v4
6877
- name: Setup .NET
69-
uses: actions/setup-dotnet@v3
78+
uses: actions/setup-dotnet@v4
7079
with:
71-
dotnet-version: ${{ env.DOTNET_VERSION }}
80+
# .NET 6 runtime for the FAKE build tool (net6.0); .NET 10 SDK for the net10.0 targets
81+
# and to satisfy global.json. Installed explicitly rather than relying on the runner image.
82+
dotnet-version: |
83+
${{ env.DOTNET_VERSION }}
84+
${{ env.DOTNET_SDK_VERSION }}
7285
- name: Build & Run Tests
7386
run: |
7487
./build.sh

‎src/SQLProvider.DesignTime/SqlDesignTime.fs‎

Lines changed: 32 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -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-
13061291
module 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 * 10L))
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

Comments
 (0)