diff --git a/src/ApiService/ApiService/onefuzzlib/JobOperations.cs b/src/ApiService/ApiService/onefuzzlib/JobOperations.cs index 5510e1dfb..d664a0048 100644 --- a/src/ApiService/ApiService/onefuzzlib/JobOperations.cs +++ b/src/ApiService/ApiService/onefuzzlib/JobOperations.cs @@ -1,4 +1,5 @@ -using ApiService.OneFuzzLib.Orm; +using System.Threading.Tasks; +using ApiService.OneFuzzLib.Orm; namespace Microsoft.OneFuzz.Service; @@ -6,15 +7,19 @@ public interface IJobOperations : IStatefulOrm { Async.Task Get(Guid jobId); Async.Task OnStart(Job job); IAsyncEnumerable SearchExpired(); - Async.Task Stopping(Job job); - Async.Task Init(Job job); IAsyncEnumerable SearchState(IEnumerable states); Async.Task StopNeverStartedJobs(); Async.Task StopIfAllDone(Job job); + + // state transitions + Async.Task Init(Job job); + Async.Task Enabled(Job job); + Async.Task Stopping(Job job); + Async.Task Stopped(Job job); } public class JobOperations : StatefulOrm, IJobOperations { - private static TimeSpan JOB_NEVER_STARTED_DURATION = TimeSpan.FromDays(30); + private static readonly TimeSpan JOB_NEVER_STARTED_DURATION = TimeSpan.FromDays(30); public JobOperations(ILogTracer logTracer, IOnefuzzContext context) : base(logTracer, context) { } @@ -111,4 +116,14 @@ public class JobOperations : StatefulOrm, IJobOper throw new Exception($"Failed to save job {job.JobId} : {result.ErrorV}"); } } + + public Task Enabled(Job job) { + // nothing to do + return Async.Task.FromResult(job); + } + + public Task Stopped(Job job) { + // nothing to do + return Async.Task.FromResult(job); + } } diff --git a/src/ApiService/ApiService/onefuzzlib/NodeMessageOperations.cs b/src/ApiService/ApiService/onefuzzlib/NodeMessageOperations.cs new file mode 100644 index 000000000..0859b868e --- /dev/null +++ b/src/ApiService/ApiService/onefuzzlib/NodeMessageOperations.cs @@ -0,0 +1,46 @@ +using ApiService.OneFuzzLib.Orm; +using Microsoft.OneFuzz.Service.OneFuzzLib.Orm; + +namespace Microsoft.OneFuzz.Service; + +//# this isn't anticipated to be needed by the client, hence it not +//# being in onefuzztypes +public record NodeMessage( + [PartitionKey] Guid MachineId, + [RowKey] string MessageId, + NodeCommand Message +) : EntityBase { + public NodeMessage(Guid machineId, NodeCommand message) : this(machineId, NewSortedKey, message) { } +}; + +public interface INodeMessageOperations : IOrm { + IAsyncEnumerable GetMessage(Guid machineId); + Async.Task ClearMessages(Guid machineId); + + Async.Task SendMessage(Guid machineId, NodeCommand message, string? messageId = null); +} + +public class NodeMessageOperations : Orm, INodeMessageOperations { + + public NodeMessageOperations(ILogTracer log, IOnefuzzContext context) + : base(log, context) { } + + public IAsyncEnumerable GetMessage(Guid machineId) + => QueryAsync(Query.PartitionKey(machineId.ToString())); + + public async Async.Task ClearMessages(Guid machineId) { + _logTracer.Info($"clearing messages for node {machineId}"); + + await foreach (var message in GetMessage(machineId)) { + var r = await Delete(message); + if (!r.IsOk) { + _logTracer.WithHttpStatus(r.ErrorV).Error($"failed to delete message for node {machineId}"); + } + } + } + + public async Async.Task SendMessage(Guid machineId, NodeCommand message, string? messageId = null) { + messageId = messageId ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(); + await Insert(new NodeMessage(machineId, messageId, message)); + } +} diff --git a/src/ApiService/ApiService/onefuzzlib/NodeOperations.cs b/src/ApiService/ApiService/onefuzzlib/NodeOperations.cs index cf174d079..558aee2a3 100644 --- a/src/ApiService/ApiService/onefuzzlib/NodeOperations.cs +++ b/src/ApiService/ApiService/onefuzzlib/NodeOperations.cs @@ -1,7 +1,6 @@ using System.Threading.Tasks; using ApiService.OneFuzzLib.Orm; using Azure.Data.Tables; -using Microsoft.OneFuzz.Service.OneFuzzLib.Orm; namespace Microsoft.OneFuzz.Service; @@ -55,6 +54,17 @@ public interface INodeOperations : IStatefulOrm { IAsyncEnumerable SearchByPoolName(PoolName poolName); Async.Task SetShutdown(Node node); + + // state transitions: + Async.Task Init(Node node); + Async.Task Free(Node node); + Async.Task SettingUp(Node node); + Async.Task Rebooting(Node node); + Async.Task Ready(Node node); + Async.Task Busy(Node node); + Async.Task Done(Node node); + Async.Task Shutdown(Node node); + Async.Task Halt(Node node); } @@ -572,108 +582,49 @@ public class NodeOperations : StatefulOrm, INod await Stop(node, done: done); return true; } -} - -public interface INodeTasksOperations : IStatefulOrm { - IAsyncEnumerable GetNodesByTaskId(Guid taskId); - IAsyncEnumerable GetNodeAssignments(Guid taskId); - IAsyncEnumerable GetByMachineId(Guid machineId); - IAsyncEnumerable GetByTaskId(Guid taskId); - Async.Task ClearByMachineId(Guid machineId); -} - -public class NodeTasksOperations : StatefulOrm, INodeTasksOperations { - - ILogTracer _log; - - public NodeTasksOperations(ILogTracer log, IOnefuzzContext context) - : base(log, context) { - _log = log; + public Task Init(Node node) { + // nothing to do + return Async.Task.FromResult(node); } - //TODO: suggest by Cheick: this can probably be optimize by query all NodesTasks then query the all machine in single request - - public async IAsyncEnumerable GetNodesByTaskId(Guid taskId) { - await foreach (var entry in QueryAsync(Query.RowKey(taskId.ToString()))) { - var node = await _context.NodeOperations.GetByMachineId(entry.MachineId); - if (node is not null) { - yield return node; - } - } + public Task Free(Node node) { + // nothing to do + return Async.Task.FromResult(node); } - public async IAsyncEnumerable GetNodeAssignments(Guid taskId) { - - await foreach (var entry in QueryAsync(Query.RowKey(taskId.ToString()))) { - var node = await _context.NodeOperations.GetByMachineId(entry.MachineId); - if (node is not null) { - var nodeAssignment = new NodeAssignment(node.MachineId, node.ScalesetId, entry.State); - yield return nodeAssignment; - } - } + public Task SettingUp(Node node) { + // nothing to do + return Async.Task.FromResult(node); } - public IAsyncEnumerable GetByMachineId(Guid machineId) { - return QueryAsync(Query.PartitionKey(machineId.ToString())); + public Task Rebooting(Node node) { + // nothing to do + return Async.Task.FromResult(node); } - public IAsyncEnumerable GetByTaskId(Guid taskId) { - return QueryAsync(Query.RowKey(taskId.ToString())); + public Task Ready(Node node) { + // nothing to do + return Async.Task.FromResult(node); } - public async Async.Task ClearByMachineId(Guid machineId) { - _logTracer.Info($"clearing tasks for node {machineId}"); - await foreach (var entry in GetByMachineId(machineId)) { - var res = await Delete(entry); - if (!res.IsOk) { - _logTracer.WithHttpStatus(res.ErrorV).Error($"failed to delete node task entry for machine_id: {entry.MachineId}"); - } - } - } -} - -//# this isn't anticipated to be needed by the client, hence it not -//# being in onefuzztypes -public record NodeMessage( - [PartitionKey] Guid MachineId, - [RowKey] string MessageId, - NodeCommand Message -) : EntityBase { - public NodeMessage(Guid machineId, NodeCommand message) : this(machineId, NewSortedKey, message) { } -}; - -public interface INodeMessageOperations : IOrm { - IAsyncEnumerable GetMessage(Guid machineId); - Async.Task ClearMessages(Guid machineId); - - Async.Task SendMessage(Guid machineId, NodeCommand message, string? messageId = null); -} - - -public class NodeMessageOperations : Orm, INodeMessageOperations { - - private readonly ILogTracer _log; - public NodeMessageOperations(ILogTracer log, IOnefuzzContext context) : base(log, context) { - _log = log; - } - - public IAsyncEnumerable GetMessage(Guid machineId) - => QueryAsync(Query.PartitionKey(machineId.ToString())); - - public async Async.Task ClearMessages(Guid machineId) { - _logTracer.Info($"clearing messages for node {machineId}"); - - await foreach (var message in GetMessage(machineId)) { - var r = await Delete(message); - if (!r.IsOk) { - _logTracer.WithHttpStatus(r.ErrorV).Error($"failed to delete message for node {machineId}"); - } - } - } - - public async Async.Task SendMessage(Guid machineId, NodeCommand message, string? messageId = null) { - messageId = messageId ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(); - await Insert(new NodeMessage(machineId, messageId, message)); + public Task Busy(Node node) { + // nothing to do + return Async.Task.FromResult(node); + } + + public Task Done(Node node) { + // nothing to do + return Async.Task.FromResult(node); + } + + public Task Shutdown(Node node) { + // nothing to do + return Async.Task.FromResult(node); + } + + public Task Halt(Node node) { + // nothing to do + return Async.Task.FromResult(node); } } diff --git a/src/ApiService/ApiService/onefuzzlib/NodeTasksOperations.cs b/src/ApiService/ApiService/onefuzzlib/NodeTasksOperations.cs new file mode 100644 index 000000000..59fb82f64 --- /dev/null +++ b/src/ApiService/ApiService/onefuzzlib/NodeTasksOperations.cs @@ -0,0 +1,79 @@ +using System.Threading.Tasks; +using ApiService.OneFuzzLib.Orm; + +namespace Microsoft.OneFuzz.Service; + +public interface INodeTasksOperations : IStatefulOrm { + IAsyncEnumerable GetNodesByTaskId(Guid taskId); + IAsyncEnumerable GetNodeAssignments(Guid taskId); + IAsyncEnumerable GetByMachineId(Guid machineId); + IAsyncEnumerable GetByTaskId(Guid taskId); + Async.Task ClearByMachineId(Guid machineId); + + // state transitions: + Async.Task Init(NodeTasks nodeTasks); + Async.Task SettingUp(NodeTasks nodeTasks); + Async.Task Running(NodeTasks nodeTasks); +} + +public class NodeTasksOperations : StatefulOrm, INodeTasksOperations { + + public NodeTasksOperations(ILogTracer log, IOnefuzzContext context) + : base(log, context) { + } + + //TODO: suggest by Cheick: this can probably be optimize by query all NodesTasks then query the all machine in single request + + public async IAsyncEnumerable GetNodesByTaskId(Guid taskId) { + await foreach (var entry in QueryAsync(Query.RowKey(taskId.ToString()))) { + var node = await _context.NodeOperations.GetByMachineId(entry.MachineId); + if (node is not null) { + yield return node; + } + } + } + + public async IAsyncEnumerable GetNodeAssignments(Guid taskId) { + + await foreach (var entry in QueryAsync(Query.RowKey(taskId.ToString()))) { + var node = await _context.NodeOperations.GetByMachineId(entry.MachineId); + if (node is not null) { + var nodeAssignment = new NodeAssignment(node.MachineId, node.ScalesetId, entry.State); + yield return nodeAssignment; + } + } + } + + public IAsyncEnumerable GetByMachineId(Guid machineId) { + return QueryAsync(Query.PartitionKey(machineId.ToString())); + } + + public IAsyncEnumerable GetByTaskId(Guid taskId) { + return QueryAsync(Query.RowKey(taskId.ToString())); + } + + public async Async.Task ClearByMachineId(Guid machineId) { + _logTracer.Info($"clearing tasks for node {machineId}"); + await foreach (var entry in GetByMachineId(machineId)) { + var res = await Delete(entry); + if (!res.IsOk) { + _logTracer.WithHttpStatus(res.ErrorV).Error($"failed to delete node task entry for machine_id: {entry.MachineId}"); + } + } + } + + public Task Init(NodeTasks nodeTasks) { + // nothing to do + return Async.Task.FromResult(nodeTasks); + } + + public Task SettingUp(NodeTasks nodeTasks) { + // nothing to do + return Async.Task.FromResult(nodeTasks); + } + + public Task Running(NodeTasks nodeTasks) { + // nothing to do + return Async.Task.FromResult(nodeTasks); + } +} diff --git a/src/ApiService/ApiService/onefuzzlib/PoolOperations.cs b/src/ApiService/ApiService/onefuzzlib/PoolOperations.cs index 47b54c2dc..d677ee60b 100644 --- a/src/ApiService/ApiService/onefuzzlib/PoolOperations.cs +++ b/src/ApiService/ApiService/onefuzzlib/PoolOperations.cs @@ -14,10 +14,14 @@ public interface IPoolOperations : IStatefulOrm { IAsyncEnumerable SearchStates(IEnumerable states); Async.Task SetShutdown(Pool pool, bool Now); - Async.Task Init(Pool pool); - Async.Task Create(PoolName name, Os os, Architecture architecture, bool managed, Guid? clientId = null); new Async.Task Delete(Pool pool); + + // state transitions: + Async.Task Init(Pool pool); + Async.Task Running(Pool pool); + Async.Task Shutdown(Pool pool); + Async.Task Halt(Pool pool); } public class PoolOperations : StatefulOrm, IPoolOperations { @@ -226,4 +230,9 @@ public class PoolOperations : StatefulOrm, IPoo return pool; } + + public Task Running(Pool pool) { + // nothing to do + return Async.Task.FromResult(pool); + } } diff --git a/src/ApiService/ApiService/onefuzzlib/ProxyOperations.cs b/src/ApiService/ApiService/onefuzzlib/ProxyOperations.cs index 4754e509b..996341db0 100644 --- a/src/ApiService/ApiService/onefuzzlib/ProxyOperations.cs +++ b/src/ApiService/ApiService/onefuzzlib/ProxyOperations.cs @@ -15,8 +15,16 @@ public interface IProxyOperations : IStatefulOrm { Async.Task SaveProxyConfig(Proxy proxy); bool IsOutdated(Proxy proxy); Async.Task GetOrCreate(string region); - Task IsUsed(Proxy proxy); + + // state transitions: + Async.Task Init(Proxy proxy); + Async.Task ExtensionsLaunch(Proxy proxy); + Async.Task ExtensionsFailed(Proxy proxy); + Async.Task VmAllocationFailed(Proxy proxy); + Async.Task Running(Proxy proxy); + Async.Task Stopping(Proxy proxy); + Async.Task Stopped(Proxy proxy); } public class ProxyOperations : StatefulOrm, IProxyOperations { @@ -285,11 +293,26 @@ public class ProxyOperations : StatefulOrm, IPr return await Stopped(proxy); } - private async Task Stopped(Proxy proxy) { + public async Task Stopped(Proxy proxy) { var stoppedVm = await SetState(proxy, VmState.Stopped); _logTracer.Info($"removing proxy: {stoppedVm.Region}"); await _context.Events.SendEvent(new EventProxyDeleted(stoppedVm.Region, stoppedVm.ProxyId)); await Delete(stoppedVm); return stoppedVm; } + + public Task ExtensionsFailed(Proxy proxy) { + // nothing to do + return Async.Task.FromResult(proxy); + } + + public Task VmAllocationFailed(Proxy proxy) { + // nothing to do + return Async.Task.FromResult(proxy); + } + + public Task Running(Proxy proxy) { + // nothing to do + return Async.Task.FromResult(proxy); + } } diff --git a/src/ApiService/ApiService/onefuzzlib/ReproOperations.cs b/src/ApiService/ApiService/onefuzzlib/ReproOperations.cs index f972556e4..3365294f6 100644 --- a/src/ApiService/ApiService/onefuzzlib/ReproOperations.cs +++ b/src/ApiService/ApiService/onefuzzlib/ReproOperations.cs @@ -8,16 +8,8 @@ namespace Microsoft.OneFuzz.Service; public interface IReproOperations : IStatefulOrm { public IAsyncEnumerable SearchExpired(); - public Async.Task Stopping(Repro repro); - public IAsyncEnumerable SearchStates(IEnumerable? states); - - public Async.Task Init(Repro repro); - public Async.Task ExtensionsLaunch(Repro repro); - - public Async.Task Stopped(Repro repro); - public Async.Task SetFailed(Repro repro, VirtualMachineData vmData); public Async.Task SetError(Repro repro, Error result); @@ -26,6 +18,15 @@ public interface IReproOperations : IStatefulOrm { public Async.Task GetSetupContainer(Repro repro); Task> Create(ReproConfig config, UserInfo userInfo); + + // state transitions: + Task Init(Repro repro); + Task ExtensionsLaunch(Repro repro); + Task ExtensionsFailed(Repro repro); + Task VmAllocationFailed(Repro repro); + Task Running(Repro repro); + Task Stopping(Repro repro); + Task Stopped(Repro repro); } public class ReproOperations : StatefulOrm, IReproOperations { @@ -316,4 +317,19 @@ public class ReproOperations : StatefulOrm, IRe return OneFuzzResult.Error(ErrorCode.UNABLE_TO_FIND, "unable to find report"); } } + + public Task ExtensionsFailed(Repro repro) { + // nothing to do + return Async.Task.FromResult(repro); + } + + public Task VmAllocationFailed(Repro repro) { + // nothing to do + return Async.Task.FromResult(repro); + } + + public Task Running(Repro repro) { + // nothing to do + return Async.Task.FromResult(repro); + } } diff --git a/src/ApiService/ApiService/onefuzzlib/ScalesetOperations.cs b/src/ApiService/ApiService/onefuzzlib/ScalesetOperations.cs index 21cb616d1..7d978ee51 100644 --- a/src/ApiService/ApiService/onefuzzlib/ScalesetOperations.cs +++ b/src/ApiService/ApiService/onefuzzlib/ScalesetOperations.cs @@ -18,8 +18,6 @@ public interface IScalesetOperations : IStatefulOrm { Async.Task CleanupNodes(Scaleset scaleSet); - Async.Task SetSize(Scaleset scaleset, int size); - Async.Task SyncScalesetSize(Scaleset scaleset); Async.Task SetState(Scaleset scaleset, ScalesetState state); @@ -27,6 +25,15 @@ public interface IScalesetOperations : IStatefulOrm { IAsyncEnumerable SearchStates(IEnumerable states); Async.Task SetShutdown(Scaleset scaleset, bool now); Async.Task SetSize(Scaleset scaleset, long size); + + // state transitions: + Async.Task Init(Scaleset scaleset); + Async.Task Setup(Scaleset scaleset); + Async.Task Resize(Scaleset scaleset); + Async.Task Running(Scaleset scaleset); + Async.Task Shutdown(Scaleset scaleset); + Async.Task Halt(Scaleset scaleset); + Async.Task CreationFailed(Scaleset scaleset); } public class ScalesetOperations : StatefulOrm, IScalesetOperations { @@ -77,14 +84,13 @@ public class ScalesetOperations : StatefulOrm Resize(Scaleset scaleset) { - _log.Info($"{SCALESET_LOG_PREFIX} scaleset resize: scaleset_id:{scaleset.ScalesetId} size:{size}"); + if (scaleset.State != ScalesetState.Resize) { + return scaleset; + } + + _log.Info($"{SCALESET_LOG_PREFIX} scaleset resize: scaleset_id:{scaleset.ScalesetId} size:{scaleset.Size}"); var shrinkQueue = new ShrinkQueue(scaleset.ScalesetId, _context.Queue, _log); // # reset the node delete queue @@ -101,14 +107,13 @@ public class ScalesetOperations : StatefulOrm vmssSize) { - await ResizeGrow(scaleset); + return await ResizeGrow(scaleset); } else { - await ResizeShrink(scaleset, vmssSize - scaleset.Size); + return await ResizeShrink(scaleset, vmssSize - scaleset.Size); } } @@ -631,32 +636,33 @@ public class ScalesetOperations : StatefulOrm ResizeEqual(Scaleset scaleset) { //# NOTE: this is the only place we reset to the 'running' state. //# This ensures that our idea of scaleset size agrees with Azure var nodeCount = await _context.NodeOperations.SearchStates(scalesetId: scaleset.ScalesetId).CountAsync(); if (nodeCount == scaleset.Size) { _log.Info($"{SCALESET_LOG_PREFIX} resize finished: {scaleset.ScalesetId}"); - await SetState(scaleset, ScalesetState.Running); + return await SetState(scaleset, ScalesetState.Running); } else { _log.Info($"{SCALESET_LOG_PREFIX} resize finished, waiting for nodes to check in. scaleset_id: {scaleset.ScalesetId} ({nodeCount} of {scaleset.Size} checked in)"); + return scaleset; } } - private async Async.Task ResizeGrow(Scaleset scaleset) { - + private async Async.Task ResizeGrow(Scaleset scaleset) { var resizeResult = await _context.VmssOperations.ResizeVmss(scaleset.ScalesetId, scaleset.Size); if (resizeResult.IsOk == false) { _log.Info($"{SCALESET_LOG_PREFIX} scaleset is mid-operation already scaleset_id: {scaleset.ScalesetId} message: {resizeResult.ErrorV}"); } + return scaleset; } - private async Async.Task ResizeShrink(Scaleset scaleset, long? toRemove) { + private async Async.Task ResizeShrink(Scaleset scaleset, long? toRemove) { _log.Info($"{SCALESET_LOG_PREFIX} shrinking scaleset. scaleset_id: {scaleset.ScalesetId} to remove {toRemove}"); if (!toRemove.HasValue) { - return; + return scaleset; } else { var queue = new ShrinkQueue(scaleset.ScalesetId, _context.Queue, _log); await queue.SetSize(toRemove.Value); @@ -664,6 +670,7 @@ public class ScalesetOperations : StatefulOrm Running(Scaleset scaleset) { + // nothing to do + return Async.Task.FromResult(scaleset); + } + + public Task CreationFailed(Scaleset scaleset) { + // nothing to do + return Async.Task.FromResult(scaleset); + } } diff --git a/src/ApiService/ApiService/onefuzzlib/TaskOperations.cs b/src/ApiService/ApiService/onefuzzlib/TaskOperations.cs index 8dd165881..2c0f22c79 100644 --- a/src/ApiService/ApiService/onefuzzlib/TaskOperations.cs +++ b/src/ApiService/ApiService/onefuzzlib/TaskOperations.cs @@ -26,6 +26,16 @@ public interface ITaskOperations : IStatefulOrm { Async.Task GetPool(Task task); Async.Task SetState(Task task, TaskState state); Async.Task> Create(TaskConfig config, Guid jobId, UserInfo userInfo); + + // state transitions: + Async.Task Init(Task task); + Async.Task Waiting(Task task); + Async.Task Scheduled(Task task); + Async.Task SettingUp(Task task); + Async.Task Running(Task task); + Async.Task Stopping(Task task); + Async.Task Stopped(Task task); + Async.Task WaitJob(Task task); } public class TaskOperations : StatefulOrm, ITaskOperations { @@ -305,8 +315,8 @@ public class TaskOperations : StatefulOrm, ITas return task; } - private async Async.Task Stopped(Task inputTask) { - var task = await SetState(inputTask, TaskState.Stopped); + public async Async.Task Stopped(Task task) { + task = await SetState(task, TaskState.Stopped); await _context.Queue.DeleteQueue($"{task.TaskId}", StorageType.Corpus); // # TODO: we need to 'unschedule' this task from the existing pools @@ -317,4 +327,29 @@ public class TaskOperations : StatefulOrm, ITas return task; } + + public Task Waiting(Task task) { + // nothing to do + return Async.Task.FromResult(task); + } + + public Task Scheduled(Task task) { + // nothing to do + return Async.Task.FromResult(task); + } + + public Task SettingUp(Task task) { + // nothing to do + return Async.Task.FromResult(task); + } + + public Task Running(Task task) { + // nothing to do + return Async.Task.FromResult(task); + } + + public Task WaitJob(Task task) { + // nothing to do + return Async.Task.FromResult(task); + } } diff --git a/src/ApiService/ApiService/onefuzzlib/orm/Orm.cs b/src/ApiService/ApiService/onefuzzlib/orm/Orm.cs index 33bfa15de..3fe1e8454 100644 --- a/src/ApiService/ApiService/onefuzzlib/orm/Orm.cs +++ b/src/ApiService/ApiService/onefuzzlib/orm/Orm.cs @@ -160,9 +160,11 @@ namespace ApiService.OneFuzzLib.Orm { var delegateType = typeof(StateTransition); MethodInfo delegateSignature = delegateType.GetMethod("Invoke")!; + var missing = new List(); foreach (var state in states) { - var methodInfo = thisType?.GetMethod(state.ToString()); + var methodInfo = thisType.GetMethod(state.ToString()); if (methodInfo == null) { + missing.Add(state); continue; } @@ -176,9 +178,12 @@ namespace ApiService.OneFuzzLib.Orm { continue; } - throw new Exception($"State transition method '{state}' in '{thisType?.Name}' does not have the correct signature. Expected '{delegateSignature}' actual '{methodInfo}' "); + throw new InvalidOperationException($"State transition method '{state}' in '{thisType.Name}' does not have the correct signature. Expected '{delegateSignature}' actual '{methodInfo}' "); }; + if (missing.Any()) { + throw new InvalidOperationException($"State transitions are missing for '{thisType.Name}': {string.Join(", ", missing)}"); + } _partitionKeyGetter = typeof(T).GetProperties().FirstOrDefault(p => p.GetCustomAttributes(true).OfType().Any())?.GetMethod switch { diff --git a/src/ApiService/FunctionalTests/1f-api/Helpers.cs b/src/ApiService/FunctionalTests/1f-api/Helpers.cs index 42e21a5e7..a899c13cd 100644 --- a/src/ApiService/FunctionalTests/1f-api/Helpers.cs +++ b/src/ApiService/FunctionalTests/1f-api/Helpers.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit; +using Xunit; namespace FunctionalTests { public class Helpers {