NEO-115: serialize JsonSchema registry registration for parallel tests.
Recipe and reward-table loaders registered schemas without locking, corrupting SchemaRegistry.Global when xUnit spun up multiple WebApplicationFactory hosts concurrently in CI.pull/154/head
parent
ab315f326b
commit
5f1244843c
|
|
@ -0,0 +1,34 @@
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using Json.Schema;
|
||||||
|
|
||||||
|
namespace NeonSprawl.Server;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thread-safe registration for <see cref="SchemaRegistry.Global"/> (JsonSchema.Net registry is not concurrent).
|
||||||
|
/// Used by content catalog loaders when parallel integration tests spin up multiple hosts.
|
||||||
|
/// </summary>
|
||||||
|
internal static class CatalogSchemaRegistry
|
||||||
|
{
|
||||||
|
private static readonly Lock RegisterLock = new();
|
||||||
|
private static readonly ConcurrentDictionary<string, JsonSchema> SchemasByPath =
|
||||||
|
new(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
/// <summary>Parses <paramref name="schemaFilePath"/> once per process and registers it under a global lock.</summary>
|
||||||
|
public static JsonSchema GetOrRegisterFromFile(string schemaFilePath)
|
||||||
|
{
|
||||||
|
var fullPath = Path.GetFullPath(schemaFilePath);
|
||||||
|
if (SchemasByPath.TryGetValue(fullPath, out var cached))
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
lock (RegisterLock)
|
||||||
|
{
|
||||||
|
if (SchemasByPath.TryGetValue(fullPath, out cached))
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
var schema = JsonSchema.FromText(File.ReadAllText(fullPath));
|
||||||
|
SchemaRegistry.Global.Register(schema);
|
||||||
|
SchemasByPath[fullPath] = schema;
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -45,12 +45,8 @@ public static class RecipeDefinitionCatalogLoader
|
||||||
if (errors.Count > 0)
|
if (errors.Count > 0)
|
||||||
ThrowIfAny(errors);
|
ThrowIfAny(errors);
|
||||||
|
|
||||||
var ioSchemaText = File.ReadAllText(recipeIoRowSchemaPath);
|
var ioSchema = CatalogSchemaRegistry.GetOrRegisterFromFile(recipeIoRowSchemaPath);
|
||||||
var defSchemaText = File.ReadAllText(recipeDefSchemaPath);
|
var defSchema = CatalogSchemaRegistry.GetOrRegisterFromFile(recipeDefSchemaPath);
|
||||||
var ioSchema = JsonSchema.FromText(ioSchemaText);
|
|
||||||
SchemaRegistry.Global.Register(ioSchema);
|
|
||||||
var defSchema = JsonSchema.FromText(defSchemaText);
|
|
||||||
SchemaRegistry.Global.Register(defSchema);
|
|
||||||
|
|
||||||
var evalOptions = new EvaluationOptions { OutputFormat = OutputFormat.List };
|
var evalOptions = new EvaluationOptions { OutputFormat = OutputFormat.List };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,10 +44,8 @@ public static class RewardTableDefinitionCatalogLoader
|
||||||
if (errors.Count > 0)
|
if (errors.Count > 0)
|
||||||
ThrowIfAny(errors);
|
ThrowIfAny(errors);
|
||||||
|
|
||||||
var grantRowSchema = JsonSchema.FromText(File.ReadAllText(rewardGrantRowSchemaPath));
|
var grantRowSchema = CatalogSchemaRegistry.GetOrRegisterFromFile(rewardGrantRowSchemaPath);
|
||||||
SchemaRegistry.Global.Register(grantRowSchema);
|
var tableSchema = CatalogSchemaRegistry.GetOrRegisterFromFile(rewardTableSchemaPath);
|
||||||
var tableSchema = JsonSchema.FromText(File.ReadAllText(rewardTableSchemaPath));
|
|
||||||
SchemaRegistry.Global.Register(tableSchema);
|
|
||||||
|
|
||||||
var evalOptions = new EvaluationOptions { OutputFormat = OutputFormat.List };
|
var evalOptions = new EvaluationOptions { OutputFormat = OutputFormat.List };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ namespace NeonSprawl.Server.Game.Quests;
|
||||||
/// <summary>Loads and validates <c>content/quests/*_quests.json</c> using the same rules as <c>scripts/validate_content.py</c> (NEO-113).</summary>
|
/// <summary>Loads and validates <c>content/quests/*_quests.json</c> using the same rules as <c>scripts/validate_content.py</c> (NEO-113).</summary>
|
||||||
public static class QuestDefinitionCatalogLoader
|
public static class QuestDefinitionCatalogLoader
|
||||||
{
|
{
|
||||||
private static readonly Lock SchemaLoadLock = new();
|
|
||||||
private static JsonSchema? _questObjectiveDefSchema;
|
private static JsonSchema? _questObjectiveDefSchema;
|
||||||
private static JsonSchema? _questStepDefSchema;
|
private static JsonSchema? _questStepDefSchema;
|
||||||
private static JsonSchema? _questDefSchema;
|
private static JsonSchema? _questDefSchema;
|
||||||
|
|
@ -352,23 +351,12 @@ public static class QuestDefinitionCatalogLoader
|
||||||
string questStepDefSchemaPath,
|
string questStepDefSchemaPath,
|
||||||
string questDefSchemaPath)
|
string questDefSchemaPath)
|
||||||
{
|
{
|
||||||
lock (SchemaLoadLock)
|
if (_questDefSchema is not null)
|
||||||
{
|
return;
|
||||||
if (_questDefSchema is not null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var objectiveSchema = JsonSchema.FromText(File.ReadAllText(questObjectiveDefSchemaPath));
|
_questObjectiveDefSchema = CatalogSchemaRegistry.GetOrRegisterFromFile(questObjectiveDefSchemaPath);
|
||||||
SchemaRegistry.Global.Register(objectiveSchema);
|
_questStepDefSchema = CatalogSchemaRegistry.GetOrRegisterFromFile(questStepDefSchemaPath);
|
||||||
_questObjectiveDefSchema = objectiveSchema;
|
_questDefSchema = CatalogSchemaRegistry.GetOrRegisterFromFile(questDefSchemaPath);
|
||||||
|
|
||||||
var stepSchema = JsonSchema.FromText(File.ReadAllText(questStepDefSchemaPath));
|
|
||||||
SchemaRegistry.Global.Register(stepSchema);
|
|
||||||
_questStepDefSchema = stepSchema;
|
|
||||||
|
|
||||||
var defSchema = JsonSchema.FromText(File.ReadAllText(questDefSchemaPath));
|
|
||||||
SchemaRegistry.Global.Register(defSchema);
|
|
||||||
_questDefSchema = defSchema;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ThrowIfAny(List<string> errors)
|
private static void ThrowIfAny(List<string> errors)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue