Skip to content

Commit

Permalink
Merge pull request #138 from ordercloud-api/nprog
Browse files Browse the repository at this point in the history
adding NProg tracker class
  • Loading branch information
erincdustin authored Jan 10, 2025
2 parents 48e29b6 + a68eaf8 commit fabaf13
Show file tree
Hide file tree
Showing 16 changed files with 266 additions and 14 deletions.
225 changes: 225 additions & 0 deletions OrderCloud.Catalyst/Core/NProg.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace OrderCloud.Catalyst
{
public class Tracker
{
private long _startTime = DateTime.UtcNow.Ticks;
private long _endTime;
private int _total;
private int _started;
private int _succeeded;
private int _failed;

private readonly List<ProgressAction> _actions = new List<ProgressAction>();
private readonly List<Task> _tasks = new List<Task>();
private readonly List<TimerAction> _timers = new List<TimerAction>();

public Tracker() { }
public Tracker(int itemCount) => _total = itemCount;

public void Start()
{
_startTime = DateTime.UtcNow.Ticks;
_timers.ForEach(t => t.Start(this));
}

public void Stop()
{
_endTime = DateTime.UtcNow.Ticks;
_timers.ForEach(t => t.Stop());
}

public void Every(Trigger trigger, Action<Progress> action) => _actions.Add(new ProgressAction { Trigger = trigger, Action = action, Recurring = true });
public void On(Trigger trigger, Action<Progress> action) => _actions.Add(new ProgressAction { Trigger = trigger, Action = action, Recurring = false });
public void Every(TimeSpan interval, Action<Progress> action) => _timers.Add(new TimerAction { Interval = interval, Action = action });
public void OnComplete(Action<Progress> action) => On(_total.ItemsDone(), action);
public void Now(Action<Progress> action) => action(GetProgress());

public void Every(Trigger trigger, Func<Progress, Task> action) => _actions.Add(new ProgressAction { Trigger = trigger, AsyncAction = action, Recurring = true });
public void On(Trigger trigger, Func<Progress, Task> action) => _actions.Add(new ProgressAction { Trigger = trigger, AsyncAction = action, Recurring = false });
public void Every(TimeSpan interval, Func<Progress, Task> action) => _timers.Add(new TimerAction { Interval = interval, AsyncAction = action });
public void OnComplete(Func<Progress, Task> action) => On(_total.ItemsDone(), action);
public void Now(Func<Progress, Task> action) => action(GetProgress());

private readonly object _lock = new object();

public void ItemStarted() => ProcessTriggers(ref _started);
public void ItemSucceeded() => ProcessTriggers(ref _succeeded);
public void ItemFailed() => ProcessTriggers(ref _failed);

public void ItemsDiscovered(int count)
{
Interlocked.Add(ref _total, count);
}

/// <summary>
/// Returns a completion task that should be awaited if any async actions were triggered.
/// Async actions are NOT awaited inline.
/// </summary>
public Task CompleteAsync() => Task.WhenAll(_tasks);

public Progress GetProgress() => new Progress(_startTime, _endTime, _total, _started, _succeeded, _failed);

private void ProcessTriggers(ref int fieldToIncrement)
{
var fired = new List<ProgressAction>();
Progress prog;

lock (_lock)
{
fieldToIncrement++;
prog = GetProgress();
fired = _actions.Where(act => act.Trigger.IsFired(prog)).ToList();

foreach (var act in fired)
{
if (!act.Recurring)
_actions.Remove(act);
}

_tasks.RemoveAll(t => t.IsCompleted);
}

fired.ForEach(act => act.Invoke(prog, _tasks));
}

private abstract class ActionBase
{
public Action<Progress> Action { get; set; }
public Func<Progress, Task> AsyncAction { get; set; }

public void Invoke(Progress prog, IList<Task> tasks)
{
var task = AsyncAction?.Invoke(prog);
Action?.Invoke(prog);
if (task?.IsCompleted == false)
tasks.Add(task);
}
}

private class ProgressAction : ActionBase
{
public Trigger Trigger { get; set; }
public bool Recurring { get; set; }
}

private class TimerAction : ActionBase
{
private Timer _timer;

public TimeSpan Interval { get; set; }

public void Start(Tracker tracker)
{
_timer = new Timer(_ => Invoke(tracker.GetProgress(), tracker._tasks), null, Interval, Interval);
}

public void Stop()
{
_timer.Dispose();
}
}
}

public class Trigger
{
private readonly int _targetNumber;
private readonly Func<Progress, int> _getCurrentNumber;
private int _nextNumber;

public Trigger(int targetNumber, Func<Progress, int> getCurrentNumber)
{
_nextNumber = _targetNumber = targetNumber;
_getCurrentNumber = getCurrentNumber;
}

public bool IsFired(Progress prog)
{
if (_getCurrentNumber(prog) >= _nextNumber)
{
_nextNumber += _targetNumber;
return true;
}
return false;
}
}

public static class TriggerBuilderExtensions
{
public static Trigger ItemsStarted(this int i) => new Trigger(i, p => p.ItemsStarted);
public static Trigger ItemsDone(this int i) => new Trigger(i, p => p.ItemsDone);
public static Trigger ItemsSucceeded(this int i) => new Trigger(i, p => p.ItemsSucceeded);
public static Trigger ItemsFailed(this int i) => new Trigger(i, p => p.ItemsFailed);

public static Trigger PercentStarted(this int i) => new Trigger(i, p => p.PercentStarted);
public static Trigger PercentDone(this int i) => new Trigger(i, p => p.PercentDone);
public static Trigger PercentSucceeded(this int i) => new Trigger(i, p => p.PercentSucceeded);
public static Trigger PercentFailed(this int i) => new Trigger(i, p => p.PercentFailed);

public static TimeSpan Seconds(this int i) => TimeSpan.FromSeconds(i);
public static TimeSpan Minutes(this int i) => TimeSpan.FromMinutes(i);
public static TimeSpan Hours(this int i) => TimeSpan.FromHours(i);
}

public class Progress
{
private readonly long _startTime;
private readonly long _endTime;

public int TotalItems { get; }
public int ItemsStarted { get; }
public int ItemsSucceeded { get; }
public int ItemsFailed { get; }

public int ItemsDone => ItemsSucceeded + ItemsFailed;
public int ItemsInProgress => ItemsStarted - ItemsDone;
public int ItemsRemaining => TotalItems - ItemsStarted;

public int PercentStarted => SafeDivide(100 * ItemsStarted, TotalItems);
public int PercentDone => SafeDivide(100 * ItemsDone, TotalItems);
public int PercentSucceeded => SafeDivide(100 * ItemsSucceeded, TotalItems);
public int PercentFailed => SafeDivide(100 * ItemsFailed, TotalItems);
public int PercentInProgress => SafeDivide(100 * ItemsInProgress, TotalItems);
public int PercentRemaining => SafeDivide(100 * ItemsRemaining, TotalItems);

public double PercentStartedExact => SafeDivideExact(100 * ItemsStarted, TotalItems);
public double PercentDoneExact => SafeDivideExact(100 * ItemsDone, TotalItems);
public double PercentSucceededExact => SafeDivideExact(100 * ItemsSucceeded, TotalItems);
public double PercentFailedExact => SafeDivideExact(100 * ItemsFailed, TotalItems);
public double PercentInProgressExact => SafeDivideExact(100 * ItemsInProgress, TotalItems);
public double PercentRemainingExact => SafeDivideExact(100 * ItemsRemaining, TotalItems);

private long ElapsedTicks => (_endTime == 0 ? DateTime.UtcNow.Ticks : _endTime) - _startTime;

public TimeSpan ElapsedTime => TimeSpan.FromTicks(ElapsedTicks);
public int ElapsedSeconds => (int)ElapsedTime.TotalSeconds;
public int ElapsedMinutes => (int)ElapsedTime.TotalMinutes;
public int ElapsedHours => (int)ElapsedTime.TotalHours;

public TimeSpan EstTotalTime => TimeSpan.FromTicks(SafeDivide(ElapsedTicks * TotalItems, ItemsDone));
public TimeSpan EstTimeRemaining => EstTotalTime - ElapsedTime;
public DateTime EstEndTimeUtc => DateTime.UtcNow + EstTimeRemaining;
public DateTime EstEndTimeLocal => DateTime.Now + EstTimeRemaining;

public bool IsDone => ItemsDone == TotalItems;

public Progress(long startTime, long endTime, int total, int started, int succeeded, int failed)
{
_startTime = startTime;
_endTime = endTime;
TotalItems = total;
ItemsStarted = started;
ItemsSucceeded = succeeded;
ItemsFailed = failed;
}

private int SafeDivide(int x, int y) => y == 0 ? 0 : x / y;
private long SafeDivide(long x, long y) => y == 0 ? 0 : x / y;
private double SafeDivideExact(long x, long y) => y == 0 ? 0 : (double)x / y;
}
}
2 changes: 1 addition & 1 deletion OrderCloud.Catalyst/OrderCloud.Catalyst.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Version>2.7.0</Version>
<Version>2.8.0</Version>
<PackageId>ordercloud-dotnet-catalyst</PackageId>
<Title>OrderCloud SDK Extensions for Azure App Services</Title>
<Authors>OrderCloud Team</Authors>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Version>2.3.0</Version>
<Version>2.4.0</Version>
<PackageId>OrderCloud.Integrations.Messaging.MailChimp</PackageId>
<Title>OrderCloud Email Integration with MailChimp</Title>
<Authors>OrderCloud Team</Authors>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Version>2.3.0</Version>
<Version>2.4.0</Version>
<PackageId>OrderCloud.Integrations.Messaging.SendGrid</PackageId>
<Title>OrderCloud Email Integration with SendGrid</Title>
<Authors>OrderCloud Team</Authors>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Version>2.3.0</Version>
<Version>2.4.0</Version>
<PackageId>OrderCloud.Integrations.Messaging.SendInBlue</PackageId>
<Title>OrderCloud Email Integration with Sendinblue</Title>
<Authors>OrderCloud Team</Authors>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Version>2.5.0</Version>
<Version>2.6.0</Version>
<PackageId>OrderCloud.Integrations.Payment.BlueSnap</PackageId>
<Title>OrderCloud Payment Integration with BlueSnap</Title>
<Authors>OrderCloud Team</Authors>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Version>2.4.0</Version>
<Version>2.7.0</Version>
<PackageId>OrderCloud.Integrations.Payment.CardConnect</PackageId>
<Title>OrderCloud Tax Integration with CardConnect</Title>
<Authors>OrderCloud Team</Authors>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Version>2.7.0</Version>
<Version>2.8.0</Version>
<PackageId>OrderCloud.Integrations.Payment.PayPal</PackageId>
<Title>OrderCloud Payment Integration with PayPal</Title>
<Authors>OrderCloud Team</Authors>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Version>2.4.0</Version>
<Version>2.5.0</Version>
<PackageId>OrderCloud.Integrations.Payment.Stripe</PackageId>
<Title>OrderCloud Tax Integration with Stripe</Title>
<Authors>OrderCloud Team</Authors>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Version>2.3.0</Version>
<Version>2.4.0</Version>
<PackageId>OrderCloud.Integrations.Shipping.EasyPost</PackageId>
<Title>OrderCloud Shipping Integration with EasyPost</Title>
<Authors>OrderCloud Team</Authors>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Version>2.3.0</Version>
<Version>2.4.0</Version>
<PackageId>OrderCloud.Integrations.Shipping.Fedex</PackageId>
<Title>OrderCloud Shipping Integration with Fedex</Title>
<Authors>OrderCloud Team</Authors>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Version>2.3.0</Version>
<Version>2.4.0</Version>
<PackageId>OrderCloud.Integrations.Shipping.UPS</PackageId>
<Title>OrderCloud Shipping Integration with UPS</Title>
<Authors>OrderCloud Team</Authors>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Version>2.3.0</Version>
<Version>2.4.0</Version>
<PackageId>OrderCloud.Integrations.Tax.Avalara</PackageId>
<Title>OrderCloud Tax Integration with Avalara</Title>
<Authors>OrderCloud Team</Authors>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Version>2.3.0</Version>
<Version>2.4.0</Version>
<PackageId>OrderCloud.Integrations.Tax.TaxJar</PackageId>
<Title>OrderCloud Tax Integration with TaxJar</Title>
<Authors>OrderCloud Team</Authors>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Version>2.3.0</Version>
<Version>2.4.0</Version>
<PackageId>OrderCloud.Integrations.Tax.Vertex</PackageId>
<Title>OrderCloud Tax Integration with Vertex</Title>
<Authors>OrderCloud Team</Authors>
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,30 @@ var token = FakeOrderCloudToken.Create(
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
```


### Progress tracker

Track long operations such as a large product upload with a timer function that writes updates to the console or function apps.

```c#
void LogProgress(Progress p) =>
Console.WriteLine($"{p.ElapsedTime:hh\\:mm\\:ss} elapsed. {p.ItemsDone} of {p.TotalItems} complete ({p.PercentDone}%)");

var tracker = new Tracker();
tracker.Every(1.Minutes(), LogProgress);
tracker.OnComplete(LogProgress);
tracker.Start();
tracker.ItemsDiscovered(products.Count);

foreach (var product in products)
{
tracker.ItemStarted();
await _oc.Products.CreateAsync(product);
tracker.ItemSucceeded();
}

tracker.Stop();
tracker.Now(LogProgress);
await tracker.CompleteAsync();
```

0 comments on commit fabaf13

Please sign in to comment.