diff --git a/QRBee.Load.Generator/AnomalyBase.cs b/QRBee.Load.Generator/AnomalyBase.cs new file mode 100644 index 0000000..8a2690f --- /dev/null +++ b/QRBee.Load.Generator/AnomalyBase.cs @@ -0,0 +1,71 @@ +using Microsoft.Extensions.Logging; + +namespace QRBee.Load.Generator; + +internal class AnomalyBase +{ + private readonly ILogger _logger; + private readonly IAnomalyReporter _anomalyReporter; + protected ThreadSafeRandom _rng = new(); + private bool _anomalyActive; + private DateTime _anomalyStart = DateTime.MinValue; + private DateTime _anomalyEnd = DateTime.MinValue; + private double _anomalyProbability; + private TimeSpan _anomalyDuration; + private readonly object _lock = new(); + + public string Name { get; } + + public AnomalyBase(string name, Anomaly settings, ILogger logger, IAnomalyReporter anomalyReporter) + { + Name = name; + _logger = logger; + _anomalyReporter = anomalyReporter; + + _anomalyProbability = settings.Probability; + _anomalyDuration = settings.Duration; + if (IsEnabled) + _logger.LogDebug($"{Name} configured: Probability={_anomalyProbability}, Duration={_anomalyDuration}"); + } + + public bool IsEnabled => _anomalyProbability > 0.0; + + protected bool IsActive() + { + if (DateTime.UtcNow < _anomalyEnd) + { + return true; + } + else if (_anomalyActive) + { + // double locking + lock (_lock) + { + if (_anomalyActive) + { + _anomalyActive = false; + _anomalyReporter.Report(_anomalyStart, _anomalyEnd, "Unconfirmed transaction"); + _logger.LogWarning($"Anomaly:{Name} ended"); + } + } + } + + var dice = _rng.NextDouble(); + if (dice < _anomalyProbability) + { + lock (_lock) + { + if (!_anomalyActive) + { + _anomalyStart = DateTime.Now; + _anomalyEnd = _anomalyStart + _anomalyDuration; + _anomalyActive = true; + _logger.LogWarning($"Anomaly: {Name}. Dice={dice} Ends=\"{_anomalyEnd.ToLocalTime():s}\""); + } + } + return true; + } + + return false; + } +} diff --git a/QRBee.Load.Generator/AnomalyReporter.cs b/QRBee.Load.Generator/AnomalyReporter.cs new file mode 100644 index 0000000..52b7956 --- /dev/null +++ b/QRBee.Load.Generator/AnomalyReporter.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QRBee.Load.Generator +{ + internal class AnomalyReporter : IDisposable, IAnomalyReporter + { + private readonly StreamWriter _writer; + private bool disposedValue; + private readonly object _lock = new(); + + public AnomalyReporter() + { + const string fileName = "anomalies.csv"; + var writeHeader = !File.Exists(fileName); + + var file = new FileStream(fileName, writeHeader ? FileMode.Create : FileMode.Append, FileAccess.Write); + _writer = new StreamWriter(file); + + if (writeHeader) + { + _writer.WriteLine("Start,End,Label"); + } + } + + public void Report(DateTime start, DateTime end, string description) + { + lock (_lock) + { + _writer.WriteLine($"\"{start:O}\",\"{end:O}\",\"{description}\""); + _writer.Flush(); + } + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + _writer.Dispose(); + } + + disposedValue = true; + } + } + + ~AnomalyReporter() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/QRBee.Load.Generator/GeneratorSettings.cs b/QRBee.Load.Generator/GeneratorSettings.cs index 0e9201a..db30ce5 100644 --- a/QRBee.Load.Generator/GeneratorSettings.cs +++ b/QRBee.Load.Generator/GeneratorSettings.cs @@ -3,6 +3,7 @@ internal class Anomaly { public double Probability { get; set; } + public TimeSpan Duration { get; set; } public Dictionary Parameters { get; set; } = new(); } diff --git a/QRBee.Load.Generator/IAnomalyReporter.cs b/QRBee.Load.Generator/IAnomalyReporter.cs new file mode 100644 index 0000000..358a2f9 --- /dev/null +++ b/QRBee.Load.Generator/IAnomalyReporter.cs @@ -0,0 +1,7 @@ +namespace QRBee.Load.Generator +{ + internal interface IAnomalyReporter + { + void Report(DateTime start, DateTime end, string description); + } +} \ No newline at end of file diff --git a/QRBee.Load.Generator/LargeAmount.cs b/QRBee.Load.Generator/LargeAmount.cs new file mode 100644 index 0000000..f091978 --- /dev/null +++ b/QRBee.Load.Generator/LargeAmount.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace QRBee.Load.Generator; + +internal class LargeAmount : AnomalyBase +{ + private double _minAmount = 1; + private double _maxAmount = 100; + private decimal _largeAmountValue = 1000; + + public LargeAmount(IOptions settings, ILogger logger, IAnomalyReporter anomalyReporter) + : base("Large amount", settings.Value.LargeAmount, logger, anomalyReporter) + { + _minAmount = settings.Value.MinAmount; + _maxAmount = settings.Value.MaxAmount; + if (settings.Value.LargeAmount.Parameters.TryGetValue("Value", out var s)) + _largeAmountValue = decimal.Parse(s); + } + + public decimal GetAmount() + { + if (IsActive()) + return _largeAmountValue; + return Convert.ToDecimal(_rng.NextDoubleInRange(_minAmount, _maxAmount)); + } +} diff --git a/QRBee.Load.Generator/LoadSpike.cs b/QRBee.Load.Generator/LoadSpike.cs index 6cd5940..e109760 100644 --- a/QRBee.Load.Generator/LoadSpike.cs +++ b/QRBee.Load.Generator/LoadSpike.cs @@ -1,91 +1,42 @@ -using log4net.Core; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace QRBee.Load.Generator +namespace QRBee.Load.Generator; + +internal class LoadSpike : AnomalyBase { - internal class LoadSpike + private TimeSpan _spikeDelay; + private int _delayBetweenMessagesMSec; + private int _delayJitterMSec; + + public LoadSpike(IOptions settings, ILogger logger, IAnomalyReporter anomalyReporter) + : base("Load spike", settings.Value.LoadSpike, logger, anomalyReporter) { - private readonly IOptions _settings; - private readonly ILogger _logger; + var loadSpike = settings.Value.LoadSpike; + _spikeDelay = TimeSpan.FromSeconds(15); - private TimeSpan _spikeDuration; - private TimeSpan _spikeDelay; - private double _spikeProbability; - private bool _spikeActive; + _delayBetweenMessagesMSec = settings.Value.DelayBetweenMessagesMSec; + _delayJitterMSec = settings.Value.DelayJitterMSec; - private ThreadSafeRandom _rng = new(); - private DateTime _spikeEnd = DateTime.MinValue; - - public LoadSpike( IOptions settings, ILogger logger ) + if (IsEnabled) { - _settings = settings; - _logger = logger; - - var loadSpike = settings.Value.LoadSpike; - _spikeDuration = TimeSpan.Zero; - _spikeDelay = TimeSpan.Zero; - _spikeProbability = loadSpike?.Probability ?? 0.0; - - if (loadSpike != null && loadSpike.Probability > 0.0) + if (loadSpike.Parameters.TryGetValue("Delay", out var duration) + && TimeSpan.TryParse(duration, out _spikeDelay)) { - if (!loadSpike.Parameters.TryGetValue("Duration", out var duration) - || !TimeSpan.TryParse(duration, out _spikeDuration)) - { - _spikeProbability = 0.0; - } - else - { - if (loadSpike.Parameters.TryGetValue("Delay", out duration) - && TimeSpan.TryParse(duration, out _spikeDelay)) - { - _spikeDelay = TimeSpan.FromMilliseconds(10); - _logger.LogDebug($"Load spike configured. Probablility={_spikeProbability} Duration=\"{_spikeDuration:g}\" Delay=\"{_spikeDelay:g}\""); - } - } - } - - } - - public async Task Delay() - { - if (DateTime.Now > _spikeEnd) - { - if (_spikeActive) - { - _spikeActive = false; - _logger.LogWarning($"Anomaly: Load spike ended"); - } - - var dice = _rng.NextDouble(); - if (dice < _spikeProbability) - { - // start load spike - _spikeEnd = DateTime.Now + _spikeDuration; - _spikeActive = true; - - _logger.LogWarning($"Anomaly: Load spike until {_spikeEnd} Dice={dice}"); - - await Task.Delay(_spikeDelay); - } - else - { - await Task.Delay(_rng.NextInRange( - _settings.Value.DelayBetweenMessagesMSec, - _settings.Value.DelayBetweenMessagesMSec + _settings.Value.DelayJitterMSec - )); - } - } - else - { - await Task.Delay(_spikeDelay); + _spikeDelay = TimeSpan.FromMilliseconds(10); } } + } + + public async Task Delay() + { + if (IsActive()) + await Task.Delay(_spikeDelay); + else + await Task.Delay(_rng.NextInRange( + _delayBetweenMessagesMSec, + _delayBetweenMessagesMSec + _delayJitterMSec + )); } } diff --git a/QRBee.Load.Generator/PaymentRequestGenerator.cs b/QRBee.Load.Generator/PaymentRequestGenerator.cs index 2f48f83..b740a06 100644 --- a/QRBee.Load.Generator/PaymentRequestGenerator.cs +++ b/QRBee.Load.Generator/PaymentRequestGenerator.cs @@ -9,31 +9,13 @@ internal class PaymentRequestGenerator { private readonly ClientPool _clientPool; private readonly ILogger _logger; - private ThreadSafeRandom _rng = new ThreadSafeRandom(); - private double _minAmount = 1; - private double _maxAmount = 100; - private double _largeAmountProbability; - private double _largeAmountValue; + private readonly LargeAmount _largeAmount; - public PaymentRequestGenerator(ClientPool clientPool, IOptions settings, ILogger logger) + public PaymentRequestGenerator(ClientPool clientPool, IOptions settings, ILogger logger, LargeAmount largeAmount) { _clientPool = clientPool; _logger = logger; - _minAmount = settings.Value.MinAmount; - _maxAmount = settings.Value.MaxAmount; - - var largeAmount = settings.Value.LargeAmount; - _largeAmountProbability = largeAmount.Probability; - if (_largeAmountProbability > 0) - { - if ( largeAmount.Parameters.TryGetValue("Value", out var s)) - _largeAmountValue = Double.Parse(s); - } - - if (_largeAmountValue <= 0.0) - _largeAmountProbability = 0.0; - else - _logger.LogDebug($"Large amount spike configured: Probability={_largeAmountProbability} Value=\"{_largeAmountValue}\""); + _largeAmount = largeAmount; } public async Task GeneratePaymentRequest(int clientId, int merchantId) @@ -79,16 +61,7 @@ internal class PaymentRequestGenerator return response; } - private decimal GetAmount() - { - var dice = _rng.NextDouble(); - if (dice < _largeAmountProbability) - { - _logger.LogWarning($"Anomaly: Large amount Dice={dice}"); - return Convert.ToDecimal(_rng.NextDoubleInRange(_largeAmountValue, _largeAmountValue* 1.10)); - } - return Convert.ToDecimal(_rng.NextDoubleInRange(_minAmount, _maxAmount)); - } + private decimal GetAmount() => _largeAmount.GetAmount(); private Task GetMerchant(int id) => _clientPool.GetMerchant(id); diff --git a/QRBee.Load.Generator/Program.cs b/QRBee.Load.Generator/Program.cs index c98bdd0..f9a76d9 100644 --- a/QRBee.Load.Generator/Program.cs +++ b/QRBee.Load.Generator/Program.cs @@ -39,15 +39,18 @@ builder.ConfigureServices((context, services) => .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton(x => no => new PrivateKeyHandler(x.GetRequiredService>(), x.GetRequiredService(), no)) .AddSingleton(x => no => new AndroidSecurityService(x.GetRequiredService()(no))) .AddHostedService() ; }); -ServicePointManager.DefaultConnectionLimit = 10; +// ServicePointManager.DefaultConnectionLimit = 500; ServicePointManager.ReusePort = true; ServicePointManager.CheckCertificateRevocationList = false; + var host = builder.Build(); host.Run(); diff --git a/QRBee.Load.Generator/TransactionDefiler.cs b/QRBee.Load.Generator/TransactionDefiler.cs index 00adb8b..b44a6df 100644 --- a/QRBee.Load.Generator/TransactionDefiler.cs +++ b/QRBee.Load.Generator/TransactionDefiler.cs @@ -4,57 +4,23 @@ using QRBee.Core.Data; namespace QRBee.Load.Generator { - internal class TransactionDefiler + internal class TransactionDefiler : AnomalyBase { - private readonly ILogger _logger; - - private ThreadSafeRandom _rng = new(); - - private double _corruptionProbability; - private int _sequenceLengthMin; - private int _sequenceLengthMax; - private int _corruptionCounter; - - public TransactionDefiler(IOptions settings, ILogger logger) + public TransactionDefiler(IOptions settings, ILogger logger, IAnomalyReporter anomalyReporter) + : base("Corrupted transaction", settings.Value.TransactionCorruption, logger, anomalyReporter) { - _logger = logger; - - var transactionCorruption = settings.Value.TransactionCorruption; - _corruptionProbability = transactionCorruption.Probability; - if (_corruptionProbability > 0) - { - if (transactionCorruption.Parameters.TryGetValue("SequenceLengthMin", out var s)) - _sequenceLengthMin = int.Parse(s); - if (transactionCorruption.Parameters.TryGetValue("SequenceLengthMax", out s)) - _sequenceLengthMax = int.Parse(s); - _logger.LogDebug($"Transaction corruption configured: Probability={_corruptionProbability}, SequenceLengthMin={_sequenceLengthMin}, SequenceLengthMax={_sequenceLengthMax}"); - } } public void CorruptPaymentRequest(PaymentRequest paymentRequest) { - if(Interlocked.Decrement(ref _corruptionCounter) > 0 ) - { + if ( IsActive() ) paymentRequest.ClientResponse.MerchantRequest.Amount += 0.01M; - return; - } - - var dice = _rng.NextDouble(); - if (dice < _corruptionProbability) - { - paymentRequest.ClientResponse.MerchantRequest.Amount += 10M; - var cc = _rng.NextInRange(_sequenceLengthMin, _sequenceLengthMax); - _corruptionCounter = cc; - _logger.LogWarning($"Anomaly: Corrupted transaction. Dice={dice} SequenceLength={cc}"); - } } public void CorruptPaymentConfirmation(PaymentConfirmation paymentConfirmation) { - if (Interlocked.Decrement(ref _corruptionCounter) > 0) - { + if (IsActive()) paymentConfirmation.GatewayTransactionId = "BadGatewayTransactionId"; - } } } } diff --git a/QRBee.Load.Generator/UnconfirmedTransactions.cs b/QRBee.Load.Generator/UnconfirmedTransactions.cs index 56c84a9..f475fc5 100644 --- a/QRBee.Load.Generator/UnconfirmedTransactions.cs +++ b/QRBee.Load.Generator/UnconfirmedTransactions.cs @@ -3,50 +3,12 @@ using Microsoft.Extensions.Options; namespace QRBee.Load.Generator; -internal class UnconfirmedTransactions +internal class UnconfirmedTransactions : AnomalyBase { - private readonly ILogger _logger; - - private double _unconfirmedProbability; - private TimeSpan _unconfirmedDuration; - private DateTime _anomalyEnd = DateTime.MinValue; - private ThreadSafeRandom _rng = new(); - - public UnconfirmedTransactions(IOptions settings, ILogger logger) + public UnconfirmedTransactions(IOptions settings, ILogger logger, IAnomalyReporter anomalyReporter) + : base("Unconfirmed transaction", settings.Value.UnconfirmedTransaction, logger, anomalyReporter) { - _logger = logger; - _unconfirmedProbability = settings.Value.UnconfirmedTransaction.Probability; - _unconfirmedDuration = TimeSpan.Zero; - - if (_unconfirmedProbability > 0.0) - { - if (settings.Value.UnconfirmedTransaction.Parameters.TryGetValue("Duration", out var duration) - || TimeSpan.TryParse(duration, out _unconfirmedDuration)) - { - _logger.LogInformation($"Unconfirmed transactions configured. Probablility={_unconfirmedProbability}"); - } - else - { - _unconfirmedProbability = 0.0; - } - } } - public bool ShouldConfirm() - { - if ( DateTime.UtcNow < _anomalyEnd ) - { - return false; - } - - var dice = _rng.NextDouble(); - if (dice < _unconfirmedProbability) - { - _anomalyEnd = DateTime.UtcNow + _unconfirmedDuration; - _logger.LogWarning($"Anomaly: Unconfirmed transaction. Dice={dice} Ends=\"{_anomalyEnd.ToLocalTime():s}\""); - return false; - } - - return true; - } + public bool ShouldConfirm() => IsActive(); } diff --git a/QRBee.Load.Generator/appsettings.json b/QRBee.Load.Generator/appsettings.json index 050eec9..93742a2 100644 --- a/QRBee.Load.Generator/appsettings.json +++ b/QRBee.Load.Generator/appsettings.json @@ -15,7 +15,7 @@ "QRBeeURL": "http://localhost:5000/", "NumberOfClients": 100, "NumberOfMerchants": 10, - "NumberOfThreads": 10, + "NumberOfThreads": 6, "DelayBetweenMessagesMSec": 500, "DelayJitterMSec": 50, "MinAmount": 10, @@ -24,8 +24,8 @@ //0.004 "LoadSpike": { "Probability": 0.002, + "Duration": "00:00:15", "Parameters": { - "Duration": "00:00:15", "Delay": "00:00:00.0100000" } }, @@ -33,15 +33,15 @@ //0.002 "TransactionCorruption": { "Probability": 0, + "Duration": "00:00:15", "Parameters": { - "SequenceLengthMin": 2, - "SequenceLengthMax": 10 } }, //0.003 "LargeAmount": { "Probability": 0, + "Duration": "00:00:15", "Parameters": { "Value": "1000" } @@ -50,6 +50,7 @@ //0.003 "UnconfirmedTransaction": { "Probability": 0, + "Duration": "00:00:15", "Parameters": { } }