1
- using System . Collections . Generic ;
1
+ using System ;
2
+ using System . Collections . Concurrent ;
2
3
using System . Linq ;
3
4
using System . Threading . Tasks ;
4
5
using SauceLabs . Visual . GraphQL ;
5
6
using SauceLabs . Visual . Utils ;
6
7
7
8
namespace SauceLabs . Visual
8
9
{
10
+ /// <summary>
11
+ /// Factory for creating and managing Visual builds.
12
+ /// This class is thread-safe and supports parallel execution.
13
+ /// </summary>
9
14
internal static class BuildFactory
10
15
{
11
- private static readonly Dictionary < string , ApiBuildPair > Builds = new Dictionary < string , ApiBuildPair > ( ) ;
16
+
17
+ private static readonly ConcurrentDictionary < BuildKey , Lazy < Task < ApiBuildPair > > > Builds =
18
+ new ConcurrentDictionary < BuildKey , Lazy < Task < ApiBuildPair > > > ( ) ;
12
19
13
20
/// <summary>
14
- /// <c>Get</c> returns the build matching with the requested region.
21
+ /// <c>Get</c> returns the build matching with the requested region or name .
15
22
/// If none is available, it returns a newly created build with <c>options</c>.
16
23
/// It will also clone the input <c>api</c> to be able to close the build later.
24
+ /// This method is thread-safe and ensures only one build is created when called concurrently with the same key.
17
25
/// </summary>
18
26
/// <param name="api">the api to use to create build</param>
19
27
/// <param name="options">the options to use when creating the build</param>
20
- /// <returns></returns>
28
+ /// <returns>A VisualBuild instance </returns>
21
29
internal static async Task < VisualBuild > Get ( VisualApi api , CreateBuildOptions options )
22
30
{
23
- // Check if there is already a build for the current region.
24
- if ( Builds . TryGetValue ( api . Region . Name , out var build ) )
31
+ BuildKey buildKey ;
32
+ if ( ! string . IsNullOrEmpty ( options . Name ) )
33
+ {
34
+ buildKey = BuildKey . OfBuildName ( options . Name ) ;
35
+ }
36
+ else
25
37
{
26
- return build . Build ;
38
+ buildKey = BuildKey . OfRegion ( api . Region ) ;
27
39
}
28
40
29
- var createdBuild = await Create ( api , options ) ;
30
- Builds [ api . Region . Name ] = new ApiBuildPair ( api . Clone ( ) , createdBuild ) ;
31
- return createdBuild ;
41
+ var lazyBuild = Builds . GetOrAdd ( buildKey , key => new Lazy < Task < ApiBuildPair > > ( async ( ) =>
42
+ {
43
+ var createdBuild = await Create ( api , options ) ;
44
+ return new ApiBuildPair ( api . Clone ( ) , createdBuild ) ;
45
+ } ) ) ;
46
+
47
+ try
48
+ {
49
+ ApiBuildPair storedBuildPair = await lazyBuild . Value ;
50
+ return storedBuildPair . Build ;
51
+ }
52
+ catch
53
+ {
54
+ // If resolving build fails, remove the lazy task
55
+ // so subsequent calls do not fail with the same result
56
+ Builds . TryRemove ( buildKey , out _ ) ;
57
+ throw ;
58
+ }
32
59
}
33
60
34
61
/// <summary>
35
- /// <c>FindRegionByBuild </c> returns the region matching the passed build.
62
+ /// <c>FindBuildKey </c> returns the key (build name or region) matching the passed build.
36
63
/// </summary>
37
64
/// <param name="build"></param>
38
- /// <returns>the matching region name </returns>
39
- private static string ? FindRegionByBuild ( VisualBuild build )
65
+ /// <returns>the matching build key </returns>
66
+ private static BuildKey ? FindBuildKey ( VisualBuild build )
40
67
{
41
- return Builds . Where ( n => n . Value . Build == build ) . Select ( n => n . Key ) . FirstOrDefault ( ) ;
68
+ return Builds . Where ( n => n . Value . IsValueCreated && n . Value . Value . IsCompleted && n . Value . Value . Result . Build == build )
69
+ . Select ( n => n . Key )
70
+ . FirstOrDefault ( ) ;
42
71
}
43
72
44
73
/// <summary>
45
- /// <c>Close</c> finishes and forget about <c>build</c>
74
+ /// <c>Close</c> finishes and removes the specified build from the cache.
75
+ /// This method is thread-safe and can be called from multiple threads.
46
76
/// </summary>
47
77
/// <param name="build">the build to finish</param>
48
78
internal static async Task Close ( VisualBuild build )
49
79
{
50
- var key = FindRegionByBuild ( build ) ;
51
- if ( key != null )
80
+ var key = FindBuildKey ( build ) ;
81
+ if ( key != null && Builds . TryGetValue ( key , out var lazyBuildPair ) && lazyBuildPair . IsValueCreated )
52
82
{
53
- await Close ( key , Builds [ key ] ) ;
83
+ var buildPair = await lazyBuildPair . Value ;
84
+ await Close ( key , buildPair ) ;
54
85
}
55
86
}
56
87
57
88
/// <summary>
58
- /// <c>Close</c> finishes and forget about <c> build</c>
89
+ /// <c>Close</c> finishes and removes the build from the cache.
59
90
/// </summary>
60
- /// <param name="region ">the build to finish</param>
91
+ /// <param name="buildKey ">the build key (name or region) to finish</param>
61
92
/// <param name="entry">the api/build pair</param>
62
- private static async Task Close ( string region , ApiBuildPair entry )
93
+ private static async Task Close ( BuildKey buildKey , ApiBuildPair entry )
63
94
{
64
95
if ( ! entry . Build . IsExternal )
65
96
{
66
97
await entry . Api . FinishBuild ( entry . Build . Id ) ;
67
98
}
68
- Builds . Remove ( region ) ;
99
+
100
+ Builds . TryRemove ( buildKey , out _ ) ;
69
101
entry . Api . Dispose ( ) ;
70
102
}
71
103
72
104
/// <summary>
73
- /// <c>CloseBuilds</c> closes all build that are still open.
105
+ /// <c>CloseBuilds</c> closes all builds that are still open.
106
+ /// This method is thread-safe and should be called during application shutdown.
74
107
/// </summary>
75
108
internal static async Task CloseBuilds ( )
76
109
{
77
- var regions = Builds . Keys ;
78
- foreach ( var region in regions )
110
+ var buildsToClose = Builds . ToArray ( ) ;
111
+ foreach ( var kvp in buildsToClose )
79
112
{
80
- await Close ( region , Builds [ region ] ) ;
113
+ if ( kvp . Value . IsValueCreated && kvp . Value . Value . IsCompleted )
114
+ {
115
+ var buildPair = await kvp . Value . Value ;
116
+ await Close ( kvp . Key , buildPair ) ;
117
+ }
81
118
}
82
119
}
83
120
@@ -189,4 +226,4 @@ private static async Task<VisualBuild> Create(VisualApi api, CreateBuildOptions
189
226
return null ;
190
227
}
191
228
}
192
- }
229
+ }
0 commit comments