Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug: Metrics thread safety issue #593

Closed
jessepoulton-mydeal opened this issue May 13, 2024 · 3 comments · Fixed by #594
Closed

Bug: Metrics thread safety issue #593

jessepoulton-mydeal opened this issue May 13, 2024 · 3 comments · Fixed by #594
Labels
area/metrics Core metrics utility bug Unexpected, reproducible and unintended software behaviour status/confirmed The scope is clear, ready for implementation

Comments

@jessepoulton-mydeal
Copy link

jessepoulton-mydeal commented May 13, 2024

Expected Behaviour

I am able to add metrics from multiple threads without issue.

Current Behaviour

Metrics throws an exception:

System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
   at System.Collections.Generic.List`1.Enumerator.MoveNext()
   at System.Linq.Enumerable.TryGetFirst[TSource](IEnumerable`1 source, Func`2 predicate, Boolean& found)
   at System.Linq.Enumerable.FirstOrDefault[TSource](IEnumerable`1 source, Func`2 predicate)
   at AWS.Lambda.Powertools.Metrics.Metrics.AWS.Lambda.Powertools.Metrics.IMetrics.AddMetric(String key, Double value, MetricUnit unit, MetricResolution metricResolution) in C:\Users\jesse.poulton\repos\powertools-lambda-dotnet\libraries\src\AWS.Lambda.Powertools.Metrics\Metrics.cs:line 103
   at AWS.Lambda.Powertools.Metrics.Metrics.AddMetric(String key, Double value, MetricUnit unit, MetricResolution metricResolution) in C:\Users\jesse.poulton\repos\powertools-lambda-dotnet\libraries\src\AWS.Lambda.Powertools.Metrics\Metrics.cs:line 302
   at AWS.Lambda.Powertools.Metrics.Tests.Handlers.FunctionHandler.<>c.<<HandleMultipleThreads>b__2_0>d.MoveNext() in C:\Users\jesse.poulton\repos\powertools-lambda-dotnet\libraries\tests\AWS.Lambda.Powertools.Metrics.Tests\Handlers\FunctionHandler.cs:line 51
--- End of stack trace from previous location ---
   at System.Threading.Tasks.Parallel.<>c__53`1.<<ForEachAsync>b__53_0>d.MoveNext()
--- End of stack trace from previous location ---
   at AWS.Lambda.Powertools.Metrics.Tests.Handlers.FunctionHandler.HandleMultipleThreads(String input) in C:\Users\jesse.poulton\repos\powertools-lambda-dotnet\libraries\tests\AWS.Lambda.Powertools.Metrics.Tests\Handlers\FunctionHandler.cs:line 49
   at AWS.Lambda.Powertools.Common.MethodAspectAttribute.WrapAsync[T](Func`2 target, Object[] args, AspectEventArgs eventArgs) in C:\Users\jesse.poulton\repos\powertools-lambda-dotnet\libraries\src\AWS.Lambda.Powertools.Common\Aspects\MethodAspectAttribute.cs:line 90
   at AWS.Lambda.Powertools.Metrics.MetricsAspectHandler.OnException(AspectEventArgs eventArgs, Exception exception) in C:\Users\jesse.poulton\repos\powertools-lambda-dotnet\libraries\src\AWS.Lambda.Powertools.Metrics\Internal\MetricsAspectHandler.cs:line 133
   at AWS.Lambda.Powertools.Common.MethodAspectAttribute.WrapAsync[T](Func`2 target, Object[] args, AspectEventArgs eventArgs) in C:\Users\jesse.poulton\repos\powertools-lambda-dotnet\libraries\src\AWS.Lambda.Powertools.Common\Aspects\MethodAspectAttribute.cs:line 96
   at Xunit.Record.ExceptionAsync(Func`1 testCode) in /_/src/xunit.core/Record.cs:line 76

Code snippet

To make this test fail reliably you need to set the PowertoolsConfigurations.MaxMetrics to a low number like 1

Add this method to the FunctionHandler.cs in the metrics tests project:

    [Metrics(Namespace = "ns", Service = "svc")]
    public async Task<string> HandleMultipleThreads(string input)
    {
        await Parallel.ForEachAsync(Enumerable.Range(0, Environment.ProcessorCount * 2), async (x, _) =>
        {
            Metrics.AddMetric("MyMetric", 1);
            await Task.Delay(1);
        });

        return input.ToUpper(CultureInfo.InvariantCulture);
    }

and this test to FunctionHandlerTests.cs:

    [Fact]
    public async Task When_Metrics_Add_Metadata_FromMultipleThread_Should_Not_Throw_Exception()
    {
        // Arrange
        Metrics.ResetForTest();
        var handler = new FunctionHandler();

        // Act
        var exception = await Record.ExceptionAsync(() => handler.HandleMultipleThreads("whatever"));
        Assert.Null(exception);
    }

Possible Solution

You could either add additional locks to the Metrics.AddMetric method:

void IMetrics.AddMetric(string key, double value, MetricUnit unit, MetricResolution metricResolution)
{
    if (string.IsNullOrWhiteSpace(key))
        throw new ArgumentNullException(
            nameof(key), "'AddMetric' method requires a valid metrics key. 'Null' or empty values are not allowed.");
    
    if (value < 0) {
        throw new ArgumentException(
            "'AddMetric' method requires a valid metrics value. Value must be >= 0.", nameof(value));
    }

    lock (_lockObj)
    {
        var metrics = _context.GetMetrics();

        if (metrics.Count > 0 &&
            (metrics.Count == PowertoolsConfigurations.MaxMetrics ||
             metrics.FirstOrDefault(x => x.Name == key)
                 ?.Values.Count == PowertoolsConfigurations.MaxMetrics))
        {
            _instance.Flush(true);
        }
    
        _context.AddMetric(key, value, unit, metricResolution);
    }
}

Or you could possibly use concurrent collections under the hood and the Interlocked class to handle incrementing counts and swapping objects when a flush is requried.

Steps to Reproduce

Unit test that shows the issue is in the Code Snippet section. Ensure the MaxMetrics is set to a low value like 1, otherwise the test will inconsistently pass/fail.

Powertools for AWS Lambda (.NET) version

latest

AWS Lambda function runtime

dotnet6

Debugging logs

No response

@jessepoulton-mydeal jessepoulton-mydeal added bug Unexpected, reproducible and unintended software behaviour triage Pending triage from maintainers labels May 13, 2024
Copy link

boring-cyborg bot commented May 13, 2024

Thanks for opening your first issue here! We'll come back to you as soon as we can.
In the meantime, check out the #dotnet channel on our Powertools for AWS Lambda Discord: Invite link

@hjgraca
Copy link
Contributor

hjgraca commented May 15, 2024

Hey @jessepoulton-mydeal thank you for reporting the issue and the possible solution. I will be looking at this today and keep you posted. If you have the time and want to contribute feel free to create a pull request.

@hjgraca hjgraca added area/metrics Core metrics utility status/confirmed The scope is clear, ready for implementation and removed triage Pending triage from maintainers labels May 20, 2024
@hjgraca hjgraca linked a pull request May 20, 2024 that will close this issue
7 tasks
@github-actions github-actions bot added the pending-release Fix or implementation already in dev waiting to be released label May 21, 2024
@hjgraca
Copy link
Contributor

hjgraca commented May 23, 2024

@jessepoulton-mydeal Fix released. Metrics 1.6.2

@hjgraca hjgraca removed the pending-release Fix or implementation already in dev waiting to be released label Jul 9, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/metrics Core metrics utility bug Unexpected, reproducible and unintended software behaviour status/confirmed The scope is clear, ready for implementation
Projects
Status: 🏗 In progress
Development

Successfully merging a pull request may close this issue.

2 participants