Compare commits

...

15 Commits

Author SHA1 Message Date
khwezi 6683234642 Merge pull request 'Refactored batch drop logic' (#27) from emailjobs into master
Reviewed-on: #27
2026-05-16 00:05:51 +02:00
Khwezi Mngoma 1471d9e597 Refactored batch drop logic
continuous-integration/drone/pr Build is passing
2026-05-16 00:04:58 +02:00
khwezi 6ddbb9479a Merge pull request 'Added an empty constructor to ProcessEmailNotificationEvent' (#26) from emailjobs into master
Reviewed-on: #26
2026-05-15 23:53:09 +02:00
Khwezi Mngoma e978aa17f8 Added an empty constructor to ProcessEmailNotificationEvent
continuous-integration/drone/pr Build is passing
2026-05-15 23:52:38 +02:00
khwezi 6c7349a0f8 Merge pull request 'Added additional logging and traces' (#25) from emailjobs into master
Reviewed-on: #25
2026-05-15 23:21:52 +02:00
Khwezi Mngoma a31f75c5ef Added additional logging and traces
continuous-integration/drone/pr Build is passing
2026-05-15 23:21:31 +02:00
khwezi e97fd6cd3f Merge pull request 'Added debug logging' (#24) from emailjobs into master
Reviewed-on: #24
2026-05-15 23:09:21 +02:00
Khwezi Mngoma 7f4246ac63 Added debug logging
continuous-integration/drone/pr Build is passing
2026-05-15 23:08:15 +02:00
khwezi 184c7c252a Merge pull request 'Set misfireThreshold to 2min and eased Cluster node checkin limit' (#23) from emailjobs into master
Reviewed-on: #23
2026-05-15 22:29:08 +02:00
Khwezi Mngoma dfc62c8fe1 Set misfireThreshold to 2min and eased Cluster node checkin limit
continuous-integration/drone/pr Build is passing
2026-05-15 22:28:18 +02:00
khwezi bfe8c458d6 Merge pull request 'Optimised quartz' (#22) from emailjobs into master
Reviewed-on: #22
2026-05-15 09:52:06 +02:00
Khwezi Mngoma 0f91f102e5 Optimised quartz
continuous-integration/drone/pr Build is passing
2026-05-15 09:51:26 +02:00
khwezi e6e0475db1 Merge pull request 'emailjobs' (#21) from emailjobs into master
Reviewed-on: #21
2026-05-15 08:39:36 +02:00
Khwezi Mngoma be9c83c8a3 Removed Abstractions listing from Git Tag
continuous-integration/drone/pr Build is passing
2026-05-15 08:39:10 +02:00
Khwezi Mngoma 65687d231e Ensured UTC is used 2026-05-15 08:37:58 +02:00
12 changed files with 55 additions and 31 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ steps:
\"tag_name\": \"$VERSION\",
\"target_commitish\": \"${DRONE_COMMIT_SHA}\",
\"name\": \"Library Suite $VERSION\",
\"body\": \"### Published NuGet Packages\nAll packages versioned as **$VERSION**:\n* LiteCharms.Abstractions\n* LiteCharms.Features\n\n[View in Nexus](https://nexus.khongisa.co.za/repository/nuget-group/)\",
\"body\": \"### Published NuGet Packages\nAll packages versioned as **$VERSION**:\n* LiteCharms.Features\n\n[View in Nexus](https://nexus.khongisa.co.za/repository/nuget-group/)\",
\"draft\": false,
\"prerelease\": false
}"
+8 -4
View File
@@ -34,7 +34,7 @@ public static class Quartz
storage.UseClustering(cluster =>
{
cluster.CheckinInterval = TimeSpan.FromSeconds(30);
cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(2);
cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(90);
});
});
});
@@ -48,6 +48,8 @@ public static class Quartz
services.ConfigureCommon();
services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
services.AddQuartz(config =>
{
config.SchedulerName = schedulerName;
@@ -60,6 +62,8 @@ public static class Quartz
config.UseDefaultThreadPool(options => options.MaxConcurrency = 1);
config.UseTimeZoneConverter();
config.SetProperty("quartz.jobStore.misfireThreshold", TimeSpan.FromMinutes(2).TotalMilliseconds.ToString());
config.UsePersistentStore(storage =>
{
storage.PerformSchemaValidation = false;
@@ -72,7 +76,7 @@ public static class Quartz
storage.UseClustering(cluster =>
{
cluster.CheckinInterval = TimeSpan.FromSeconds(30);
cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(2);
cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(90);
});
});
});
@@ -86,14 +90,14 @@ public static class Quartz
{
options.Scheduling.IgnoreDuplicates = true;
options.Scheduling.OverWriteExistingData = true;
options["quartz.plugin.jobHistory.type"] = "Quartz.Plugin.History.LoggingJobHistoryPlugin, Quartz.Plugins";
options["quartz.plugin.triggerHistory.type"] = "Quartz.Plugin.History.LoggingTriggerHistoryPlugin, Quartz.Plugins";
});
services.AddTransient<RetryJobListener>();
services.AddTransient<IJobOrchestrator, JobOrchestrator>();
services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
return services;
}
}
@@ -1,6 +1,5 @@
using LiteCharms.Features.Abstractions;
using LiteCharms.Features.Quartz.Abstractions;
using static LiteCharms.Features.Extensions.Timezones;
namespace LiteCharms.Features.Quartz;
@@ -47,13 +46,13 @@ public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestra
.StoreDurably()
.Build();
var now = SouthAfricanTimeZone.UtcNow();
var now = DateTime.UtcNow;
var trigger = global::Quartz.TriggerBuilder.Create()
.WithIdentity(triggerKey)
.WithDescription($"Scheduled via Main Job at {now:g}")
.WithCronSchedule(cronExpression, cron => cron.InTimeZone(SouthAfricanTimeZone)
.WithMisfireHandlingInstructionFireAndProceed())
.WithCronSchedule(cronExpression, cron => cron
.WithMisfireHandlingInstructionIgnoreMisfires())
.StartAt(now)
.Build();
+20 -4
View File
@@ -1,4 +1,5 @@
using LiteCharms.Features.Abstractions;
using LiteCharms.Features.Mediator;
namespace LiteCharms.Features.Quartz;
@@ -9,13 +10,28 @@ public class MediatorJob<TNotification>(IMediator mediator) : IJob where TNotifi
{
var data = context.MergedJobDataMap["Payload"] as string;
if (string.IsNullOrWhiteSpace(data)) return;
if (string.IsNullOrWhiteSpace(data))
{
Trace.WriteLine("Job Payload missing, job ended");
return;
}
var notification = JsonSerializer.Deserialize<TNotification>(data);
if(notification is null) return;
if (notification is null)
{
Trace.WriteLine("Notification could not be JSon converted from data string, job ended");
if(notification is TNotification)
await mediator.Publish(notification, context.CancellationToken);
return;
}
using var activity = MediatorTelemetry.Source.StartActivity($"Quartz: {typeof(TNotification).Name}");
activity?.SetTag("event.correlation_id", notification.CorrelationId);
await mediator.Publish(notification, context.CancellationToken);
Trace.WriteLine("Job published");
}
}
@@ -51,7 +51,6 @@ public class PackageService(IDbContextFactory<ShopDbContext> contextFactory)
var newPackage = context.Packages.Add(new Entities.Package
{
UpdatedAt = null,
Name = name,
Summary = summary,
Description = description,
@@ -206,7 +205,7 @@ public class PackageService(IDbContextFactory<ShopDbContext> contextFactory)
package.Summary = summary;
package.Description = description;
package.ImageUrl = ImageUrl;
package.UpdatedAt = SouthAfricanTimeZone.UtcNow();
package.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
@@ -103,6 +103,7 @@ public class LeadService(IDbContextFactory<ShopDbContext> contextFactory)
return Result.Fail(new Error($"Lead with ID {leadId} not found."));
lead.Status = status;
lead.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
@@ -1,7 +1,6 @@
using LiteCharms.Features.Email;
using LiteCharms.Features.Shop.Notifications.Entities;
using LiteCharms.Features.Shop.Notifications.Models;
using LiteCharms.Features.Shop.Postgres;
using static LiteCharms.Features.Extensions.Timezones;
namespace LiteCharms.Features.Shop.Notifications.Events.Handlers;
@@ -14,11 +13,13 @@ public class ProcessEmailNotificationsEventHandler(IDbContextFactory<ShopDbConte
{
try
{
logger.LogInformation("Started");
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var notifications = await context.Notifications
.OrderByDescending(o => o.Priority)
.ThenBy(o => o.CreatedAt)
.OrderByDescending(o => o.CreatedAt)
.ThenBy(o => o.Priority)
.Where(n => n.CorrelationIdType == CorrelationIdTypes.Email)
.Where(n => n.Direction == NotificationDirection.Outgoing)
.Take(message.MaxRecords)
@@ -26,7 +27,7 @@ public class ProcessEmailNotificationsEventHandler(IDbContextFactory<ShopDbConte
foreach (var notification in notifications)
{
if (dropBatch || cancellationToken.IsCancellationRequested) break;
if (dropBatch) break;
var sendResult = await SendEmailAsync(notification,emailService, cancellationToken);
@@ -44,7 +45,7 @@ public class ProcessEmailNotificationsEventHandler(IDbContextFactory<ShopDbConte
}
notification.Processed = true;
notification.UpdatedAt = SouthAfricanTimeZone.UtcNow();
notification.UpdatedAt = DateTime.UtcNow;
}
await context.SaveChangesAsync(cancellationToken);
@@ -53,6 +54,10 @@ public class ProcessEmailNotificationsEventHandler(IDbContextFactory<ShopDbConte
{
logger.LogError(ex, ex.Message);
}
finally
{
logger.LogInformation("Finished");
}
}
private async Task<Result> SendEmailAsync(Notification notification, EmailService service, CancellationToken cancellationToken = default)
@@ -8,6 +8,8 @@ public class ProcessEmailNotificationsEvent : EventBase, IEvent
public int MaxRecords { get; set; }
public ProcessEmailNotificationsEvent() { MaxRecords = 1000; }
private ProcessEmailNotificationsEvent(int maxRecords = 1000) => MaxRecords = maxRecords;
public static ProcessEmailNotificationsEvent Create(int maxRecords = 1000) => new(maxRecords);
@@ -2,7 +2,6 @@
using LiteCharms.Features.Models;
using LiteCharms.Features.Shop.Orders.Models;
using LiteCharms.Features.Shop.Postgres;
using static LiteCharms.Features.Extensions.Timezones;
namespace LiteCharms.Features.Shop.Orders;
@@ -241,7 +240,7 @@ public class OrderService(IDbContextFactory<ShopDbContext> contextFactory)
return Result.Fail(new Error($"Order {request.OrderId} not found"));
order.Status = request.Status;
order.UpdatedAt = SouthAfricanTimeZone.UtcNow();
order.UpdatedAt = DateTime.UtcNow;
if(!string.IsNullOrWhiteSpace(request.InvoiceUrl)) order.InvoiceUrl = request.InvoiceUrl;
@@ -1,7 +1,6 @@
using LiteCharms.Features.Extensions;
using LiteCharms.Features.Shop.Postgres;
using LiteCharms.Features.Shop.Products.Models;
using static LiteCharms.Features.Extensions.Timezones;
namespace LiteCharms.Features.Shop.Products;
@@ -19,7 +18,7 @@ public class ProductService(IDbContextFactory<ShopDbContext> contextFactory)
return Result.Fail($"Could not find product price with ID {productPriceId}");
price.Active = active;
price.UpdatedAt = SouthAfricanTimeZone.UtcNow();
price.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
@@ -200,7 +199,7 @@ public class ProductService(IDbContextFactory<ShopDbContext> contextFactory)
return Result.Fail($"Could not find product price with ID {productPriceId}");
existingPrice.Active = false;
existingPrice.UpdatedAt = SouthAfricanTimeZone.UtcNow();
existingPrice.UpdatedAt = DateTime.UtcNow;
if (!(await context.SaveChangesAsync(cancellationToken) > 0))
return Result.Fail<Guid>($"Failed to deactivate existing price of ID {productPriceId}, try again later");
@@ -212,7 +211,7 @@ public class ProductService(IDbContextFactory<ShopDbContext> contextFactory)
var deactivatedPrice = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken);
existingPrice.Active = true;
existingPrice.UpdatedAt = SouthAfricanTimeZone.UtcNow();
existingPrice.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Fail<Guid>("Reverted to old price, creation of new price failed")
@@ -2,7 +2,6 @@
using LiteCharms.Features.Models;
using LiteCharms.Features.Shop.Postgres;
using LiteCharms.Features.Shop.Quotes.Models;
using static LiteCharms.Features.Extensions.Timezones;
namespace LiteCharms.Features.Shop.Quotes;
@@ -26,6 +25,7 @@ public class QuoteService(IDbContextFactory<ShopDbContext> contextFactory)
return Result.Fail(new Error($"Quote with id {quoteId} is already assigned to order with id {orderId}"));
quote.OrderId = orderId;
quote.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
@@ -52,6 +52,7 @@ public class QuoteService(IDbContextFactory<ShopDbContext> contextFactory)
return Result.Fail(new Error($"Shopping Cart with id {shoppingCartId} not found"));
quote.ShoppingCartId = shoppingCartId;
quote.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
@@ -140,7 +141,7 @@ public class QuoteService(IDbContextFactory<ShopDbContext> contextFactory)
return Result.Fail(new Error("Quote not found."));
quote.Status = status;
quote.UpdatedAt = SouthAfricanTimeZone.UtcNow();
quote.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
@@ -1,7 +1,6 @@
using LiteCharms.Features.Extensions;
using LiteCharms.Features.Shop.Postgres;
using LiteCharms.Features.Shop.ShoppingCarts.Models;
using static LiteCharms.Features.Extensions.Timezones;
namespace LiteCharms.Features.Shop.ShoppingCarts;
@@ -284,7 +283,7 @@ public class ShoppingCartService(IDbContextFactory<ShopDbContext> contextFactory
return Result.Fail($"Shopping cart item could not be found with id {shoppingCartItemId}");
item.Quantity = quantity;
item.UpdatedAt = SouthAfricanTimeZone.UtcNow();
item.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()