feat: initialize HostBooking and ConveyorDispo code structure and document project processes and database schema

This commit is contained in:
2026-05-06 11:51:58 +02:00
parent 5d856971cd
commit 95a5ea9b8b
35 changed files with 1878 additions and 2 deletions

View File

@@ -0,0 +1,14 @@
<?xml version="1.0"?>
<configuration>
<appSettings>
<add key="Conn1" value="OrderManager" />
<add key="Conn2" value="ToEmptyLeBuffer" />
<add key="Conn3" value="LoopOverloadDistribution" />
<add key="Conn4" value="StartInitialOrdersHost" />
<add key="Intervall_OrderManager" value="345" />
<add key="Intervall_ToEmptyLeBuffer" value="345" />
<add key="Intervall_LoopOverloadDistribution" value="345" />
<add key="Intervall_StartInitialOrdersHost" value="345" />
</appSettings>
</configuration>

View File

@@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using Gebhardt.Shared;
using Gebhardt.Shared.Process;
using Gebhardt.StoreWare.Wcs.Common.Unity;
using Unity;
using Unity.Resolution;
namespace Gebhardt.StoreWare.Wcs.ConveyorDispo
{
internal class Haupt
{
[STAThread]
private static void Main()
{
try
{
if (AppConfigVerifier.CheckAndWriteToLog())
{
ProcessManager manager = new(Convert.ToInt32(ConfigurationManager.AppSettings["ctrlTimer"]), true, ProcessClass.None, null);
ProcessParameter parameter = new();
var connections = new List<string> {parameter.Conn1, parameter.Conn2, parameter.Conn3, parameter.Conn4, parameter.Conn5, parameter.Conn6};
string[] usedConnections = connections.FindAll(x => x != "leer").ToArray();
IUnityContainer unityContainer = WcsContainerFactory.GetInstance();
foreach (string connection in usedConnections)
{
string[] parts = connection.Split(':');
string className = parts[0];
switch (className)
{
case nameof(ToEmptyLeBuffer):
manager.RegisterWorker(unityContainer.Resolve<ToEmptyLeBuffer>(new ParameterOverride("workInterval", Convert.ToInt32(ConfigurationManager.AppSettings["Intervall_ToEmptyLeBuffer"]))));
break;
case nameof(LoopOverloadDistribution):
// Do not use LoopOverloadDistribution for ETRA
break;
manager.RegisterWorker(unityContainer.Resolve<LoopOverloadDistribution>(new ParameterOverride("workInterval", Convert.ToInt32(ConfigurationManager.AppSettings["Intervall_LoopOverloadDistribution"]))));
break;
case nameof(OrderManager):
manager.RegisterWorker(unityContainer.Resolve<OrderManager>(new ParameterOverride("workInterval", Convert.ToInt32(ConfigurationManager.AppSettings["Intervall_OrderManager"]))));
break;
case nameof(StartInitialOrdersHost):
manager.RegisterWorker(unityContainer.Resolve<StartInitialOrdersHost>(new ParameterOverride("workInterval", Convert.ToInt32(ConfigurationManager.AppSettings["Intervall_StartInitialOrdersHost"]))));
break;
default:
throw new NotImplementedException(className);
}
}
manager.RunWorkers();
}
}
catch (Exception exception)
{
Log.WriteException(exception);
}
}
}
}

View File

@@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Model;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Model.Enums;
namespace Gebhardt.StoreWare.Wcs.ConveyorDispo
{
internal record OrderListItem(int OrdersHostId, TransportOrderStatus Status, Le Le, string Destination, int Priority, int? IdOrderWmsHead, DateTime Created, string HostDestination);
internal class OrderList : List<OrderListItem>
{
public OrderList(List<OrderListItem> items) : base(items)
{
}
/// <summary>
/// Removes all order list items with the same LeNo and a higher list index.
/// </summary>
/// <param name="item"></param>
public void RemoveSubsequentWithEqualLeNo(OrderListItem item)
{
if (item is {Le: { }})
{
RemoveAll(i => IndexOf(i) > IndexOf(item) && i.Le.LeNo == item.Le.LeNo);
}
}
/// <summary>
/// Removes all order list items with the same destination and a higher list index.
/// </summary>
/// <param name="item"></param>
public void RemoveSubsequentWithEqualDestination(OrderListItem item)
{
if (item != null)
{
RemoveAll(i => IndexOf(i) > IndexOf(item) && i.Destination == item.Destination);
}
}
/// <summary>
/// Removes all order list items with the same aisle name / storage area and a higher list index.
/// </summary>
/// <param name="item"></param>
public void RemoveSubsequentWithEqualAisle(OrderListItem item)
{
if (item?.Le?.StorageArea != null && item?.Le?.AisleName != null)
{
RemoveAll(i => IndexOf(i) > IndexOf(item) && i.Le.StorageArea == item.Le.StorageArea && i.Le.AisleName == item.Le.AisleName);
}
}
/// <summary>
/// Removes all order list items with a higher list index that have the same LeNo but lower priority.
/// </summary>
/// <param name="item"></param>
public void RemoveSubsequentWithEqualLeNoButLowerPriority(OrderListItem item)
{
if (item is {Le: { }})
{
RemoveAll(i => IndexOf(i) > IndexOf(item) && i.Le.LeNo == item.Le.LeNo && i.Priority < item.Priority);
}
}
}
}

View File

@@ -0,0 +1,237 @@
using Gebhardt.Shared;
using Gebhardt.Shared.Process;
using Gebhardt.StoreWare.Wcs.Common;
using Gebhardt.StoreWare.Wcs.Common.Application.LeHandling.Interfaces;
using Gebhardt.StoreWare.Wcs.Common.Application.StorageHandling.Interfaces;
using Gebhardt.StoreWare.Wcs.Common.Application.TransportHandling.Interfaces;
using Gebhardt.StoreWare.Wcs.Common.Dao;
using Gebhardt.StoreWare.Wcs.Common.DbAccess;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Model;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Model.Enums;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Queries;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using static Gebhardt.StoreWare.Wcs.Common.Constants;
namespace Gebhardt.StoreWare.Wcs.ConveyorDispo;
public class OrderManager : ProcessWorker
{
private readonly IAisleService _aisleService;
private readonly IWcsDbContextFactory _dbContextFactory;
private readonly IDestinationService _destinationService;
private readonly ILeService _leService;
private readonly ITransportOrderService _transportOrderService;
public OrderManager(IDestinationService destinationService, ITransportOrderService transportOrderService, ILeService leService, IAisleService aisleService, IWcsDbContextFactory dbContextFactory, int workInterval)
: base(nameof(OrderManager), workInterval, true)
{
_destinationService = destinationService;
_transportOrderService = transportOrderService;
_leService = leService;
_aisleService = aisleService;
_dbContextFactory = dbContextFactory;
}
public override bool DoWork()
{
bool workDone = false;
try
{
using IWcsDbContext db = _dbContextFactory.GetDbContext();
IQueryable<OrdersHost> pendingOrders = db.OrdersHost.ByStatus(TransportOrderStatus.Pending);
OrderList onConveyor = new(pendingOrders
.Where(o => o.Le.LocationId == null
&& (!o.Le.IsEmpty || o.Type == TransportOrderType.TransportHost)
&& (o.Le.Status == LeStatus.OnConveyor || o.Le.Status == LeStatus.InDestinationZone))
.OrderBy(o => o.StartTime)
.Select(o => new OrderListItem(o.Id, o.Status, o.Le, o.Destination, o.Priority, o.IdOrderWmsHead, o.Created, o.HostDestination))
.AsNoTracking()
.ToList());
workDone |= StartNextOrders(onConveyor);
var pendingOrdersWithDemand = pendingOrders
.Where(x => x.Le.Status != LeStatus.Created)
//Do not start pending orders for LEs inside a sequencer
.ExcludeOrdersInSequencer()
//Consider all destinations that are commissioning workstations or a storage area
.ByDestination(_destinationService.GetCommissioningWorkstations()
.Union(_destinationService.Where(d => d.IsStorageArea).Select(d => d.Name))
.Union(_destinationService.Where(d => d.IsSequencer).Select(d => d.Name)));
//// Always prioritize cancelled sequencer orders
//var cancelledSeqOrders = pendingOrdersWithDemand.ByCancelledSequencerOrder();
//OrderList forResourcesWithDemand = new(cancelledSeqOrders
// .Select(o => new OrderListItem(o.Id, o.Status, o.Le, o.Destination, o.Priority, o.IdOrderWmsHead, o.Created, o.HostDestination))
// .AsNoTracking()
// .ToList());
//workDone |= StartNextOrders(forResourcesWithDemand);
//Then do the normal orders with demand.
pendingOrdersWithDemand = pendingOrdersWithDemand.ApplyWmsOrderingSequencerRetrievalTime(db);
OrderList forResourcesWithDemand = new(pendingOrdersWithDemand
.Select(o => new OrderListItem(o.Id, o.Status, o.Le, o.Destination, o.Priority, o.IdOrderWmsHead, o.Created, o.HostDestination))
.AsNoTracking()
.ToList());
workDone |= StartNextOrders(forResourcesWithDemand);
OrderList forOtherDestinations = new(pendingOrders
.Where(x => x.Le.Status != LeStatus.Created)
.ExcludeDestination(db.ResourceSetting.Select(r => r.Name).ToList())
.ApplyWmsOrderingSequencerRetrievalTime(db)
.Select(o => new OrderListItem(o.Id, o.Status, o.Le, o.Destination, o.Priority, o.IdOrderWmsHead, o.Created, o.HostDestination))
.AsNoTracking()
.ToList());
workDone |= StartNextOrders(forOtherDestinations);
}
catch (Exception ex)
{
Log.WriteException(ex);
}
return workDone;
}
private bool StartNextOrders(OrderList orders)
{
bool workDone = false;
using IWcsDbContext db = _dbContextFactory.GetDbContext();
List<Le> les = db.Le.Where(l => orders.Select(o => o.Le.LeNo).Contains(l.LeNo)).Distinct().ToList();
for (int i = 0; i < orders.Count; i++)
{
OrderListItem order = orders[i];
try
{
Le le = les.Single(l => l.LeNo == order.Le.LeNo);
if (LeIsExcludedAsAisleNotReady(orders, le, order)
|| LeIsExcludedAsOrderMiniloadIsActive(orders, db, le, order)
|| LeIsExcludedAsLeIsOnItsWayToNOK(orders, db, order))
{
workDone = true;
continue;
}
// Special case for sequencer orders: reserve half the space for each workstation
if (order.Destination.StartsWith("SEQ"))
{
Resource sequencerResource = db.ResourceSetting.GetResourceByName(order.Destination);
if (sequencerResource != null)
{
SettingsManager.GetParsedValue(ConstantsCommon.SettingNames.MaximumUsableCapacityPercentPerWorkstation, out int maximumUsableCapacityPercentPerWorkstation, 50, true);
maximumUsableCapacityPercentPerWorkstation = maximumUsableCapacityPercentPerWorkstation > 100 ? 100 : maximumUsableCapacityPercentPerWorkstation;
maximumUsableCapacityPercentPerWorkstation = maximumUsableCapacityPercentPerWorkstation < 50 ? 50 : maximumUsableCapacityPercentPerWorkstation;
int maximumUsableCapacityPerWorkstation = (sequencerResource.Capacity + sequencerResource.Overload) * maximumUsableCapacityPercentPerWorkstation / 100;
// Count active orders for this specific HostDestination going to the same sequencer
int activeOrdersForHostDestination = db.OrdersHost
.Count(o => o.HostDestination == order.HostDestination
&& o.Destination == order.Destination
&& (o.Status == TransportOrderStatus.InProgress
|| o.Status == TransportOrderStatus.InDestinationZone
|| o.Status == TransportOrderStatus.InSequencer));
if (activeOrdersForHostDestination >= maximumUsableCapacityPerWorkstation)
{
Log.Write(LogLevel.Info, 30, $"Sequencer capacity limit reached for {order.HostDestination} at {order.Destination}. Active: {activeOrdersForHostDestination}, Reserved: {maximumUsableCapacityPerWorkstation}");
continue;
}
}
}
Resource resource = db.ResourceSetting.GetResourceByName(order.Destination);
if (resource is null or { Demand: > 0 })
{
// Only cancel active transports not transportHost. Only WMS is allowed to finish this orders
var activeOrdersNotOnTheWayToWorkstation = db.OrdersHost
.ByLeNo(order.Le.LeNo)
.Active()
.Where(o => o.Type != TransportOrderType.TransportHost);
if (activeOrdersNotOnTheWayToWorkstation.Any())
{
_leService.CancelActiveTransports(order.Le.LeNo, $"Another active order to a commissioning area exists");
} else if (!db.OrdersHost.ByLeNo(order.Le.LeNo).Active().Any())
{
_transportOrderService.StartNextTransport(order.OrdersHostId);
}
orders.RemoveSubsequentWithEqualLeNo(order);
workDone = true;
}
else if (resource is { Demand: <= 0 })
{
if (!le.IsInStorage())
{
List<string> destinationAisles = _destinationService.Where(d => d.IsStorage).Select(d => d.Name).ToList();
if (!db.OrdersHost.OpenByLeNo(order.Le.LeNo).ByDestination(destinationAisles).Any())
{
_transportOrderService.PostponeOrdersHost(order.OrdersHostId, $"Destination {order.Destination} has no demand.");
orders.RemoveSubsequentWithEqualLeNo(order);
}
continue;
}
orders.RemoveSubsequentWithEqualDestination(order);
}
}
catch (Exception ex)
{
Log.Write(LogLevel.Error, $"Can not start OrdersHost: {order.OrdersHostId} for LE: {order.Le.LeNo}");
Log.WriteException(ex);
}
}
return workDone;
}
private static bool LeIsExcludedAsOrderMiniloadIsActive(OrderList orders, IWcsDbContext db, Le le, OrderListItem order)
{
if (db.OrdersMiniload.Underway().ByLeNo(le.LeNo).Any())
{
orders.RemoveSubsequentWithEqualLeNo(order);
Log.Write(LogLevel.Info, $"{nameof(OrdersHost)} with Id '{order.OrdersHostId}' for LE {le.LeNo} cannot be started due to an open OrdersMiniload.");
return true;
}
return false;
}
private bool LeIsExcludedAsAisleNotReady(OrderList orders, Le le, OrderListItem order)
{
if (le.IsInStorage())
{
if (!_aisleService.IsAisleReadyForRetrievalOrder(order.Le.StorageArea, order.Le.AisleName))
{
orders.RemoveSubsequentWithEqualAisle(order);
Log.Write(LogLevel.Info, $"{nameof(OrdersHost)} with Id '{order.OrdersHostId}' for LE {le.LeNo} cannot be started as not all participating aisles / devices are available for {le.AisleName}/{le.StorageArea}.");
return true;
}
}
return false;
}
private bool LeIsExcludedAsLeIsOnItsWayToNOK(OrderList orders, IWcsDbContext db, OrderListItem order)
{
OrdersHost orderForSameLe = db.OrdersHost
.ByLeNo(order.Le.LeNo)
.ByStatus(TransportOrderStatus.Pending, TransportOrderStatus.InProgress, TransportOrderStatus.InDestinationZone)
.OrderBy(o => o.Status != TransportOrderStatus.Pending ? 0 : 1) // get active order (if any), pending otherwise
.FirstOrDefault();
if (orderForSameLe == null)
{
return false;
}
if (orderForSameLe.Le.HasError() && orderForSameLe.Destination.IsInList(MfcAllDestinations.ERR12, MfcAllDestinations.ERR11, MfcAllDestinationsOldSystem.ERR01)
|| order.Le.HasError() && order.Destination.IsInList(MfcAllDestinations.ERR12, MfcAllDestinations.ERR11, MfcAllDestinationsOldSystem.ERR01))
{
orders.RemoveSubsequentWithEqualLeNo(order);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,205 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Gebhardt.Shared;
using Gebhardt.Shared.DbAccess;
using Gebhardt.Shared.Process;
using Gebhardt.StoreWare.Wcs.Common;
using Gebhardt.StoreWare.Wcs.Common.Application.TransportHandling.Interfaces;
using Gebhardt.StoreWare.Wcs.Common.Dao;
using Gebhardt.StoreWare.Wcs.Common.Dao.Interfaces;
using Gebhardt.StoreWare.Wcs.Common.DbAccess;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Model;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Model.Enums;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Queries;
using Microsoft.EntityFrameworkCore;
using static Gebhardt.StoreWare.Wcs.Common.Constants;
namespace Gebhardt.StoreWare.Wcs.ConveyorDispo
{
public class StartInitialOrdersHost : ProcessWorker
{
private readonly IWcsDbContextFactory _dbContextFactory;
private readonly ITransportOrderService _transportOrderService;
public StartInitialOrdersHost(int workInterval, ITransportOrderService transportOrderService, IWcsDbContextFactory dbContextFactory) : base(nameof(StartInitialOrdersHost), workInterval, true)
{
_transportOrderService = transportOrderService;
_dbContextFactory = dbContextFactory;
}
public override bool DoWork()
{
try
{
using IWcsDbContext db = _dbContextFactory.GetDbContext();
OrderList initialOrders = new(db.OrdersHost
.ByStatus(TransportOrderStatus.Initial)
.ExcludeNextEmpty()
.Where(o => o.Le.Status != LeStatus.Created)
.ApplyWmsOrderingSequencerRetrievalTime(db)
.Select(o => new OrderListItem(o.Id, o.Status, o.Le, o.Destination, o.Priority, o.IdOrderWmsHead, o.Created, o.HostDestination))
.ToList());
//This step is needed to only start orders with all LEs available
initialOrders = OnlyFirstOrderPerLeNo(db, initialOrders);
return StartOrders(initialOrders);
}
catch (Exception ex)
{
Log.WriteException(ex);
return false;
}
}
/// <summary>
/// Returns only the first order per LE
/// </summary>
/// <param name="initialOrders"></param>
/// <returns>Filtered List</returns>
private OrderList OnlyFirstOrderPerLeNo(IWcsDbContext db, OrderList initialOrders)
{
List<OrderListItem> distinctOrderList = new ();
List<OrderListItem> lockedOrders = new();
foreach (OrderListItem order in initialOrders)
{
//Only take first order per LE
if (!distinctOrderList.Any(d=>d.Le.LeNo == order.Le.LeNo))
{
distinctOrderList.Add(order);
}
//Others are marked as locked (for info message)
else
{
lockedOrders.Add(order);
Log.Write(LogLevel.Info, 60, $"OrdersHost ID {order.OrdersHostId} for LeNo {order.Le.LeNo} waits for OrdersHost: ID {distinctOrderList.Where(d => d.Le.LeNo == order.Le.LeNo).First().OrdersHostId} which is started first.");
}
}
//Make visible in DB (and to user)
string infoMessage = "Le has transports that are started first.";
var markOrders = db.OrdersHost.Where(o => lockedOrders.Select(l => l.OrdersHostId).Contains(o.Id) && o.Info != infoMessage).ToList();
foreach (var order in markOrders)
{
order.Info = infoMessage;
}
//Rest Info if order can be started
var unMarkOrders = db.OrdersHost.Where(o => distinctOrderList.Select(l => l.OrdersHostId).Contains(o.Id) && o.Info == infoMessage).ToList();
foreach (var order in unMarkOrders)
{
order.Info = null;
}
db.SaveChanges();
return new OrderList(distinctOrderList);
}
private bool StartOrders(OrderList orderList)
{
bool workDone = false;
using IWcsDbContext db = _dbContextFactory.GetDbContext();
for (int i = 0; i < orderList.Count; i++)
{
OrderListItem orderToBeScheduled = orderList[i];
try
{
// If there exists a cancelled sequencer orders for this LE this should always be started first
var cancelledSequencerOrder = db.OrdersHost
.ByLeNo(orderToBeScheduled.Le.LeNo)
.ByStatus(TransportOrderStatus.Initial)
.ByCancelledSequencerOrder()
.Select(o => new OrderListItem(o.Id, o.Status, o.Le, o.Destination, o.Priority, o.IdOrderWmsHead, o.Created, o.HostDestination))
.ToList();
if (cancelledSequencerOrder.FirstOrDefault() != null)
{
var cancelledOrderToBeScheduled = cancelledSequencerOrder.FirstOrDefault();
Log.Write(LogLevel.Debug, $"{nameof(OrdersHost)} with Id {cancelledOrderToBeScheduled.OrdersHostId} will be scheduled since this is a cancelled sequencer order.");
_transportOrderService.ScheduleOrdersHost(cancelledOrderToBeScheduled.OrdersHostId);
//Order has been scheduled. Do not consider orders for the same LE.
orderList.RemoveSubsequentWithEqualLeNo(orderToBeScheduled);
workDone = true;
continue;
}
OrdersHost orderForSameLe = db.OrdersHost
.ByLeNo(orderToBeScheduled.Le.LeNo)
.ByStatus(TransportOrderStatus.Pending, TransportOrderStatus.InProgress, TransportOrderStatus.InDestinationZone, TransportOrderStatus.InSequencer)
.OrderBy(o => o.Status != TransportOrderStatus.Pending ? 0 : 1) // get active order (if any), pending otherwise
.FirstOrDefault();
if (orderForSameLe != null && orderForSameLe.Id != orderToBeScheduled.OrdersHostId)
{
workDone = TryScheduleWithExistingOrder(orderForSameLe, db, orderToBeScheduled);
//Check equal LEs with open order only once
orderList.RemoveSubsequentWithEqualLeNo(orderToBeScheduled);
continue;
}
if (TryScheduleWithoutExistingOrder(orderToBeScheduled, db))
{
//Order has been scheduled. Do not consider orders for the same LE.
orderList.RemoveSubsequentWithEqualLeNo(orderToBeScheduled);
workDone = true;
continue;
}
else
{
Log.Write(LogLevel.Debug, $"Destination: {orderToBeScheduled.Destination} has no ressources, skip all orders to this destination");
orderList.RemoveSubsequentWithEqualDestination(orderToBeScheduled);
}
}
catch(Exception ex)
{
Log.Write(LogLevel.Error, $"Can not set ordersHost {orderToBeScheduled.OrdersHostId} for LE {orderToBeScheduled.Le.LeNo} to pending. Due to Exception:");
Log.WriteException(ex);
}
//We can remove all subsequent orders with same LE, because the list is already ordered in a way that priorities are taken into account.
orderList.RemoveSubsequentWithEqualLeNo(orderToBeScheduled);
}
return workDone;
}
private bool TryScheduleWithoutExistingOrder(OrderListItem orderToBeScheduled, IWcsDbContext db)
{
if (CanScheduleOrder(orderToBeScheduled, db))
{
Log.Write(LogLevel.Debug, $"{nameof(OrdersHost)} with Id {orderToBeScheduled.OrdersHostId} will be scheduled, as there are no other {nameof(OrdersHost)} scheduled or active orders for the same LE.");
_transportOrderService.ScheduleOrdersHost(orderToBeScheduled.OrdersHostId);
return true;
}
return false;
}
private bool CanScheduleOrder(OrderListItem orderToBeScheduled, IWcsDbContext db)
{
// Orders with their associated LE not being in storage can always be scheduled...
//TODO Check for OnConveyor/InDestinationZone? IsInStorage() does not cover OnInputLeLifter/OnLhd/OnOutputLeLifter...
if (!orderToBeScheduled.Le.IsInStorage())
{
return true;
}
//...if LE is in storage, whether the order can be scheduled is determined by the amount of pending orders that may exist towards the destination and the destination's demand.
if (!SettingsManager.GetParsedValue(ConstantsCommon.SettingNames.MaxPendingOrdersPerDestination, out int maxPendingOrdersPerDestination))
{
maxPendingOrdersPerDestination = 10;
}
Resource resourceDestination = db.ResourceSetting.GetResourceByName(orderToBeScheduled.Destination);
// for unmanaged resources, a demand of 1 is used.
int destinationDemand = resourceDestination?.Demand ?? 1;
int countPendingOrdersToDestination = db.OrdersHost.ByStatus(TransportOrderStatus.Pending).ByDestination(orderToBeScheduled.Destination).Count();
return destinationDemand - countPendingOrdersToDestination > 0 || countPendingOrdersToDestination - destinationDemand < maxPendingOrdersPerDestination;
}
private bool TryScheduleWithExistingOrder(OrdersHost orderForSameLe, IWcsDbContext db, OrderListItem orderToBeScheduled)
{
// We not want to overwrite existing orders. We wait until we are back in Storage. (This happened only about 40 times a day.
return false;
}
}
}

View File

@@ -0,0 +1,330 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Gebhardt.Shared;
using Gebhardt.Shared.Process;
using Gebhardt.StoreWare.Wcs.Common;
using Gebhardt.StoreWare.Wcs.Common.Application.LeHandling.Interfaces;
using Gebhardt.StoreWare.Wcs.Common.Application.StorageHandling.Interfaces;
using Gebhardt.StoreWare.Wcs.Common.Application.TransportHandling.Interfaces;
using Gebhardt.StoreWare.Wcs.Common.Dao;
using Gebhardt.StoreWare.Wcs.Common.DbAccess;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Model;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Model.Enums;
using Gebhardt.StoreWare.Wcs.Common.DbAccess.Queries;
using static Gebhardt.StoreWare.Wcs.Common.Constants;
namespace Gebhardt.StoreWare.Wcs.ConveyorDispo
{
public class ToEmptyLeBuffer : ProcessWorker
{
private record AisleUtilization(AisleForLe Aisle, int Utilization);
private readonly IWcsDbContextFactory _dbContextFactory;
private readonly IDestinationService _destinationService;
private readonly ILeService _leService;
private readonly ITransportOrderService _transportOrderService;
public ToEmptyLeBuffer(int workInterval,
IDestinationService destinationService,
ILeService leService,
ITransportOrderService transportOrderService,
IAisleService aisleService,
IWcsDbContextFactory dbContextFactory)
: base(nameof(ToEmptyLeBuffer), workInterval, true)
{
_destinationService = destinationService;
_leService = leService;
_transportOrderService = transportOrderService;
_dbContextFactory = dbContextFactory;
}
public override bool DoWork()
{
try
{
using IWcsDbContext db = _dbContextFactory.GetDbContext();
//Retrieve aisles suited for storage (i.e. lifter/handover aisles are excluded)
List<AisleForLe> availableAisles = db.Aisle.GetAislesWithStorageCompartmentsReadyForRetrieval();
availableAisles = FilterForAllowedAisles(availableAisles);
SupplyBuffer(LeTypeName.MiniloadSmall, Constants.MfcAllDestinations.EMB_S, availableAisles.AsReadOnly(), Constants.MfcAllDestinations.AKL01);
SupplyBuffer(LeTypeName.MiniloadBig, Constants.MfcAllDestinations.EMB_M, availableAisles.AsReadOnly(), Constants.MfcAllDestinations.AKL01);
SupplyBuffer(LeTypeName.MiniloadSmall, Constants.MfcAllDestinationsOldSystem.ETXBU_S, availableAisles.AsReadOnly(), Constants.MfcAllDestinations.AKL02);
SupplyBuffer(LeTypeName.MiniloadBig, Constants.MfcAllDestinationsOldSystem.ETXBU_M, availableAisles.AsReadOnly(), Constants.MfcAllDestinations.AKL02);
return true;
}
catch (Exception exception)
{
Log.WriteException(exception);
return false;
}
}
/// <summary>
/// Refills each buffer with empty LEs depending on its demand.
/// First, it is checked if empty LEs on conveyer can be rerouted.
/// Second, empty LE demand is satisfied by retrieval from storage.
/// Last, empty LEs that have been explicitly requested by the WMS are retrieved.
/// </summary>
/// <param name="leType">the type of le to use</param>
/// <param name="buffers">List of buffers to refill.</param>
/// <param name="availableAisles">
/// List of available aisles (aisle, output place, storage device in aisle (if any) are
/// ready)
/// </param>
private void SupplyBuffer(LeTypeName leType, string buffer, IReadOnlyList<AisleForLe> availableAisles, string preferredStorageArea)
{
if (string.IsNullOrEmpty(buffer))
{
Log.Write(LogLevel.Error, $"No buffers provided for {nameof(SupplyBuffer)}");
return;
}
using IWcsDbContext db = _dbContextFactory.GetDbContext();
var destinationBuffer = db.ResourceSetting.GetResourceByName(buffer);
double destinationBufferDemand = destinationBuffer.Demand;
double destinationBufferCapacity = destinationBuffer.Capacity + destinationBuffer.Overload;
double bufferFreePercentage = destinationBufferDemand / destinationBufferCapacity * 100.0;
//Setting to minimize crane usage
SettingsManager.GetParsedValue(ConstantsCommon.SettingNames.EmptyBoxesDemandThreshold, out int demandThreshold, 40);
if (destinationBufferDemand > 0)
{
RerouteLesOnConveyor(leType, buffer);
}
//If the buffers free space percentage is greater than the threshold settings value we also take boxes from storage
if (bufferFreePercentage > demandThreshold)
{
RefillBuffersFromStorage(leType, buffer, availableAisles, preferredStorageArea);
}
RefillBuffersWithRequestedLes(buffer, availableAisles, preferredStorageArea);
}
/// <summary>
/// Refills buffers with demand by rerouting LEs on conveyor that have a storage area as destination
/// (i.e. aisle selection has not been performed yet.
/// </summary>
private void RerouteLesOnConveyor(LeTypeName leType, string buffers)
{
using IWcsDbContext db = _dbContextFactory.GetDbContext();
List<string> destinationsAllowingForRerouting = _destinationService.Where(d => d.IsStorageArea).Select(d => d.Name).ToList();
List<string> dontAllowforReroutingLastWhere = new List<string>() { MfcAllDestinationsOldSystem.IPT01, MfcAllDestinationsOldSystem.IPT02, MfcAllDestinationsOldSystem.IPT03, MfcAllDestinationsOldSystem.TOPUP, MfcAllDestinationsOldSystem.REP01, MfcAllDestinationsOldSystem.REP02, MfcAllDestinationsOldSystem.REP03, MfcAllDestinationsOldSystem.ETXBU_M, MfcAllDestinationsOldSystem.ETXBU_S };
//Transport orders with a storage area as destination
OrderList orders = new(db.OrdersHost
.ByDestination(destinationsAllowingForRerouting)
.ByType(TransportOrderType.Transport)
.ByStatus(TransportOrderStatus.Pending, TransportOrderStatus.InProgress)
.Where(o => o.Le.IsEmpty
&& (o.Le.Status == LeStatus.OnConveyor || o.Le.Status == LeStatus.InDestinationZone)
&& o.Le.Type == leType)
.AsEnumerable()
.Where(o => !o.Le.HasError() && !dontAllowforReroutingLastWhere.Contains(o.Le.LastWhere))
.Select(o => new OrderListItem(o.Id, o.Status, o.Le, o.Destination, o.Priority, o.IdOrderWmsHead, o.Created, o.HostDestination))
.ToList());
foreach (OrderListItem order in orders)
{
Resource destination = db.ResourceSetting.ByName(buffers).GetResourceWithHighestDemand();
if (destination is { Demand: > 0 })
{
if (db.OrdersMiniload.ByLeNo(order.Le.LeNo).Open().Any())
{
continue;
}
//Reroute LE
Log.Write(LogLevel.Debug, $"LE {order.Le.LeNo} will be rerouted to {destination.Name}.");
_leService.CancelOpenTransports(order.Le.LeNo, $"Rerouted to {destination.Name}");
Log.Write(LogLevel.Debug, $"LE {order.Le.LeNo} will be rerouted to {destination.Name}.");
// This is to mitigate filling a very long string into the source field. Having AKL01 for the old system is not a problem.
string source;
if (order.Le.LastWhere == null || order.Le.LastWhere.StartsWith("["))
{
source = Constants.MfcAllDestinations.AKL01;
}
else
{
source = order.Le.LastWhere;
}
int id = _leService.CreateTransport(order.Le.LeNo, source, destination.Name);
_transportOrderService.StartNextTransport(id, nameof(RerouteLesOnConveyor));
}
else if (order.Status == TransportOrderStatus.Pending)
{
_transportOrderService.StartNextTransport(order.OrdersHostId, info: nameof(RerouteLesOnConveyor));
}
}
}
/// <summary>
/// Refills one or more buffers with empty LEs from the storage.
/// First, Overload for each buffer is checked and updated via the SettingsManager
/// Then, for each buffer with demand, an aisle is selected and a retrieval order is created.
/// </summary>
private void RefillBuffersFromStorage(LeTypeName leType, string buffers, IReadOnlyList<AisleForLe> availableAisles, string preferredStorageArea)
{
using IWcsDbContext db = _dbContextFactory.GetDbContext();
List<Resource> buffersWithDemand = db.ResourceSetting.ByName(buffers).GetResources().WithDemand().OrderByDescending(r => r.Demand).ToList();
foreach (Resource buffer in buffersWithDemand)
{
if (buffer.Demand > 0)
{
AisleForLe aisle = GetAisleForEmptyLeRetrieval(leType, availableAisles, preferredStorageArea);
if (aisle != null)
{
//Refill buffer
//OrdersHost has no storage area, i.e. aisle name must be unique by itself when used as source.
Log.Write(LogLevel.Debug, $"Empty LE of type {leType} will be retrieved from {aisle.AisleName} and sent towards {buffer.Name}.");
int id = _leService.CreateTransport(LeType.GetNextEmptyLeTypeForActual(leType).ToString(), aisle.AisleName, buffer.Name);
_transportOrderService.StartNextTransport(id, nameof(RefillBuffersFromStorage));
}
}
}
}
/// <summary>
/// Refills buffers according to a request received from the WMS.
/// An initial <see cref="OrdersHost" /> has already been created when handling the request.
/// Here, select aisle, start <see cref="OrdersHost" /> and create <see cref="OrdersMiniload" />.
/// </summary>
private void RefillBuffersWithRequestedLes(string buffers, IReadOnlyList<AisleForLe> availableAisles, string preferredStorageArea)
{
using IWcsDbContext db = _dbContextFactory.GetDbContext();
//Initial transport orders for empty LEs (requested by the WMS)
OrderList orders = new(db.OrdersHost
.ByDestination(buffers)
.ByStatus(TransportOrderStatus.Initial)
.ByType(TransportOrderType.Transport)
.OnlyNextEmpty()
.Select(o => new OrderListItem(o.Id, o.Status, o.Le, o.Destination, o.Priority, o.IdOrderWmsHead, o.Created, o.HostDestination))
.ToList());
foreach (OrderListItem order in orders)
{
AisleForLe aisle = GetAisleForEmptyLeRetrieval(LeType.GetActualLeTypeForNextEmpty(order.Le.Type), availableAisles, preferredStorageArea);
if (aisle != null)
{
//OrdersHost has no storage area, i.e. aisle name must be unique by itself when used as source.
Log.Write(LogLevel.Debug, $"Requested empty LE of type {order.Le.Type} will be retrieved from {aisle.AisleName} and sent towards {order.Destination}.");
_transportOrderService.ScheduleOrdersHost(order.OrdersHostId, aisle.AisleName, nameof(RefillBuffersWithRequestedLes));
_transportOrderService.StartNextTransport(order.OrdersHostId, nameof(RefillBuffersWithRequestedLes));
}
}
}
private AisleForLe GetAisleForEmptyLeRetrieval(LeTypeName leTypeName, IReadOnlyList<AisleForLe> availableAisles, string preferredStorageArea)
{
using IWcsDbContext db = _dbContextFactory.GetDbContext();
List<AisleForLe> availableForLeRetrieval = availableAisles.ToList();
//Left outer join empty LEs with open orders miniload, then exclude LEs with open orders.
var emptyLesInStorageWithoutOpenOrders = db.Le
.ByStatus(LeStatus.InStorage)
.Where(l => !l.Location.IsLocked)
.ByType(leTypeName)
.Empty()
.GroupJoin(db.OrdersHost.Open(),
le => le.LeNo,
o => o.LeNo,
(le, ordersHost) => new { Le = le, OrdersHost = ordersHost })
.SelectMany(j => j.OrdersHost.DefaultIfEmpty(),
(l, o) => new { l.Le, OrdersHost = o })
.Where(j => j.OrdersHost == null)
.ToList();
if (emptyLesInStorageWithoutOpenOrders.Count > 0)
{
Log.Write(LogLevel.Info, $"empty Les found: {emptyLesInStorageWithoutOpenOrders.Count}");
}
Dictionary<AisleForLe, int> aislesWithEmptyLes = emptyLesInStorageWithoutOpenOrders.GroupBy(xx => xx.Le.Aisle).ToDictionary(xx => xx.Key, xx => xx.Count());
Dictionary<AisleForLe, int> aislesWithActiveNextEmptyMiniload = db.OrdersMiniload.Open().ByLeNo(LeType.GetNextEmptyLeTypeForActual(leTypeName).ToString()).ToList()
.GroupBy(xx => xx.Aisle).ToDictionary(xx => xx.Key, xx => xx.Count());
//Leave only aisles containing LEs that can be retrieved
availableForLeRetrieval.RemoveAll(a => !aislesWithEmptyLes.ContainsKey(a) || (aislesWithEmptyLes.ContainsKey(a) && aislesWithActiveNextEmptyMiniload.ContainsKey(a) && aislesWithEmptyLes[a] - aislesWithActiveNextEmptyMiniload[a] <= 0));
// See if we can remove all other storage areas. If not, allow other.
if (availableForLeRetrieval.Any(xx => xx.StorageArea != preferredStorageArea)
&& availableAisles.Any(x => x.StorageArea == preferredStorageArea))
{
availableForLeRetrieval.RemoveAll(xx => xx.StorageArea != preferredStorageArea);
}
//Build one structre that has all information
Dictionary<AisleForLe, (int EmptyBoxes, int OrderedEmpties)> aislesWithNumbers = new Dictionary<AisleForLe, (int emptyBoxes, int orders)>();
foreach (var aisle in availableForLeRetrieval.Distinct())
{
int emptyBoxes = 0;
int orderedEmpties = 0;
if (aislesWithEmptyLes.ContainsKey(aisle))
{
emptyBoxes = aislesWithEmptyLes[aisle];
}
if (aislesWithActiveNextEmptyMiniload.ContainsKey(aisle))
{
orderedEmpties = aislesWithEmptyLes[aisle];
}
aislesWithNumbers.Add(aisle, (emptyBoxes, orderedEmpties));
}
AisleUtilization currentMinUtilization = null;
//Check MLS before Crane (as longterm MLS should be used for filled boxes), and we want more space in MLS
foreach (AisleForLe aisle in aislesWithNumbers.OrderBy(ar=>ar.Key.AisleName.StartsWith("C")).ThenByDescending(ar => ar.Value.EmptyBoxes - ar.Value.OrderedEmpties).Select(ar=>ar.Key))
{
AisleForLe inputAisle = db.AisleForLe.GetInputAisle(aisle);
int lesTowardsAisle = db.OrdersConveyor.Active().ByDestination(inputAisle.AisleName).Count();
//TODO include input/output le lifter orders?
//TODO include OrdersHost for orders that have no ordersMiniload yet?
int openOrdersForAisle = db.OrdersMiniload.Open().ByAisle(aisle.AisleName, aisle.StorageArea).Count();
int totalUtilization = openOrdersForAisle + lesTowardsAisle;
//We allow a utilization of 4 orders to be neglected, so we put more relevance on the number of totes
if (currentMinUtilization == null || currentMinUtilization.Utilization > totalUtilization + 4)
{
currentMinUtilization = new AisleUtilization(aisle, totalUtilization);
}
}
//Select aisle with lowest utilization
return currentMinUtilization?.Aisle;
}
/// <summary>
/// Filter depending on OnlyEmptiesToOldStorage setting:
/// If setting is true, get all empty LEs from old storage if available
/// </summary>
/// <param name="availableAisles"></param>
/// <param name="le"></param>
/// <returns></returns>
private List<AisleForLe> FilterForAllowedAisles(List<AisleForLe> availableAisles)
{
SettingsManager.GetParsedValue(ConstantsCommon.SettingNames.OnlyEmptiesToOldStorage, out bool onlyEmptiesToOldStorage, false, true);
if (!onlyEmptiesToOldStorage)
{
return availableAisles;
}
//get all empties from AKL02 if possible
if (availableAisles.Any(a => a.StorageArea == Constants.MfcAllDestinations.AKL02))
{
return availableAisles.Where(a => a.StorageArea == Constants.MfcAllDestinations.AKL02).ToList();
}
//Fallback get from available aisles
else
{
return availableAisles;
}
}
}
}

View File

@@ -0,0 +1,30 @@
<?xml version="1.0"?>
<configuration>
<appSettings>
<!-- BEGINN Prozesseinstellungen -->
<add key="Conn1" value="HostBooking"/>
<!-- Telegrammverbuchung aufteilen nach der letzten Stelle der HU Nummer?-->
<add key="UseLoadBalancing" value="false"/>
<!--Polling Intervall der Producerklasse-->
<add key="Intervall" value="201"/>
<!-- so viele Telegramme bekommt ein Consumer maximal auf einen Schlag-->
<add key="ConsumerQueueLength" value="200"/>
<!-- Überprüfungsintervall -->
<add key="ctrlTimer" value="12000" />
<!-- ENDE Prozesseinstellungen-->
<!-- BEGINN Log -->
<!-- 0=ERROR 3=INFO 5=DEBUG 7=LOWLEVEL -->
<add key="LogLevel" value="7"/>
<!-- Aufteilung der Log-Files per Thread -->
<add key="SplitLogFilesByThreadName" value="true"/>
<add key="MainThreadNames" value="-"/>
<!-- ENDE Log -->
<!-- Datenbankverbindung -->
<add key="Eigentuemer" value="Wcs" />
</appSettings>
</configuration>

View File

@@ -0,0 +1,45 @@
using Gebhardt.StoreWare.Wcs.HostBooking.InterfaceWcsWms;
namespace Gebhardt.StoreWare.Wcs.HostBooking
{
public class LeNoMissingException : FromWmsException
{
public LeNoMissingException(string message) : base(message) { }
}
public class LeNoWrongFormatException : FromWmsException
{
public LeNoWrongFormatException(string message) : base(message) { }
}
public class LeNoTooShortException : FromWmsException
{
public LeNoTooShortException(string message) : base(message) { }
}
public class LeNoTooLongException : FromWmsException
{
public LeNoTooLongException(string message) : base(message) { }
}
public class TransportDestinationInvalidException : FromWmsException
{
public TransportDestinationInvalidException(string message) : base(message) { }
}
public class LeAlreadyInStorageException : FromWmsException
{
public LeAlreadyInStorageException(string message) : base(message) { }
}
public class LeNotReachableException : FromWmsException
{
public LeNotReachableException(string message) : base(message) { }
}
public class LeHasActiveTransportOrderException : FromWmsException
{
public LeHasActiveTransportOrderException(string message) : base(message) { }
}
}

View File

@@ -0,0 +1,132 @@
using Gebhardt.Shared;
using Gebhardt.Shared.Process;
using Gebhardt.StoreWare.Wcs.Common.Unity;
using Gebhardt.StoreWare.Wcs.HostBooking.InterfaceWcsWms;
using System;
using System.Collections.Generic;
using System.Configuration;
using Unity;
namespace Gebhardt.StoreWare.Wcs.HostBooking
{
internal class Haupt
{
[STAThread]
private static void Main()
{
AppDomain.CurrentDomain.UnhandledException += HandleAppDomainException;
try
{
IUnityContainer container = WcsContainerFactory.GetChildInstance();
container.RegisterFromWmsHandlers();
container.RegisterFromWmsServices();
if (AppConfigVerifier.CheckAndWriteToLog())
{
// LifeTimer Intervall ist in app.config ctrlTimer eingestellt
int lifeTimerInterval = Convert.ToInt32(ConfigurationManager.AppSettings["ctrlTimer"]);
string[] usedConnections = GetUsedConnections();
ProcessManager manager = new(lifeTimerInterval, true,
ConfigurationManager.AppSettings["ProcessClass"] == "None" ? ProcessClass.None : ProcessClass.Application,
usedConnections);
//Worker (Producer und Consumer) erstellen und verknüpfen
RegisterWorkers(manager, lifeTimerInterval, container);
// Starten
manager.RunWorkers();
}
}
catch (Exception exception)
{
Log.WriteException(exception);
Console.WriteLine("main thread: e: {0} e: {1}", exception.StackTrace, exception);
}
}
private static void HandleAppDomainException(object sender, UnhandledExceptionEventArgs e)
{
Exception exception = (Exception)e.ExceptionObject;
if (exception == null)
{
throw new ArgumentNullException(nameof(exception));
}
Log.WriteException(exception);
Console.WriteLine($@"main thread: e: {exception.StackTrace} e: {exception}");
}
/// <summary>
/// Ruft die Conn Einträge aus der App.config ab die nicht leer sind
/// </summary>
/// <returns></returns>
private static string[] GetUsedConnections()
{
ProcessParameter parameter = new();
var connections = new List<string> { parameter.Conn1, parameter.Conn2, parameter.Conn3, parameter.Conn4, parameter.Conn5, parameter.Conn6 };
string[] usedConnections = connections.FindAll(x => x != "leer").ToArray();
return usedConnections;
}
/// <summary>
/// Erstellt einen Producer und je nach useLoadBalancing einen oder elf Consumer und weißt diese dem Producer und dem
/// Manager zu.
/// </summary>
/// <param name="manager"></param>
/// <param name="ctrTimerInterval">ctrTimer aus App.config</param>
internal static void RegisterWorkers(ProcessManager manager, int ctrTimerInterval, IUnityContainer unityContainer)
{
//AppSettings auslesen
bool useLoadBalancing;
try
{
useLoadBalancing = Convert.ToBoolean(ConfigurationManager.AppSettings["UseLoadBalancing"]);
}
catch (Exception e)
{
useLoadBalancing = false;
Log.Write(LogLevel.Error, "Parameter 'useLoadBalancing' fehlt in der App.config, setze auf false");
}
int consumerQueueLength;
try
{
consumerQueueLength = Convert.ToInt32(ConfigurationManager.AppSettings["ConsumerQueueLength"]);
}
catch (Exception e)
{
consumerQueueLength = 200;
Log.Write(LogLevel.Error, "Parameter 'consumerQueueLength' fehlt in der App.config, setze auf 200");
}
// Producer anmelden
int pollingInterval = Convert.ToInt32(ConfigurationManager.AppSettings["Intervall"]);
HostBookingProducer producer = new(pollingInterval, useLoadBalancing, consumerQueueLength);
manager.RegisterWorker(producer);
string consumerName;
HostBookingConsumer consumer;
//Wenn useLoadBalancing = true, dann wird für jede Endziffer der HU ein Consumer erstellt und einer für Telegramme ohne HU (Default)
if (useLoadBalancing)
{
for (int i = 0; i < 10; i++)
{
consumerName = $"Consumer_{i}";
consumer = new HostBookingConsumer(consumerName, ctrTimerInterval / 2, consumerQueueLength, unityContainer);
producer.AddConsumer(consumerName, consumer);
manager.RegisterWorker(consumer);
}
consumerName = "Consumer_Default";
consumer = new HostBookingConsumer(consumerName, ctrTimerInterval / 2, consumerQueueLength, unityContainer);
producer.AddConsumer(consumerName, consumer);
manager.RegisterWorker(consumer);
}
//ohne useLoadBalancing gibt es nur einen Consumer der alle Telegramme verarbeitet
else
{
consumerName = "Consumer_All";
consumer = new HostBookingConsumer(consumerName, ctrTimerInterval / 2, consumerQueueLength, unityContainer);
producer.AddConsumer(consumerName, consumer);
manager.RegisterWorker(consumer);
}
}
}
}

View File

@@ -0,0 +1,44 @@
using Gebhardt.Shared;
using Gebhardt.Shared.Process.ProducerConsumer;
using Gebhardt.StoreWare.WcsWms.InterfaceWcsWms.Interfaces;
using System;
using Gebhardt.StoreWare.Wcs.HostBooking.InterfaceWcsWms.Interfaces;
using Unity;
using Gebhardt.StoreWare.Wcs.HostBooking.InterfaceWcsWms;
namespace Gebhardt.StoreWare.Wcs.HostBooking
{
public class HostBookingConsumer : Consumer<IHostMessage>
{
private readonly IUnityContainer _unityContainer;
public HostBookingConsumer(string name, int aliveTime, int queueLength, IUnityContainer unityContainer)
: base(name, aliveTime, true, queueLength)
{
_unityContainer = unityContainer;
}
public override bool DoWork(IHostMessage hostMessage)
{
IHostMessageFromWmsService service = _unityContainer.Resolve<IHostMessageFromWmsService>();
try
{
dynamic handler = _unityContainer.Resolve(typeof(IHandleRecord<>).MakeGenericType(hostMessage.GetType()), hostMessage.RecordType);
Log.Write(LogLevel.Info, $"process message [{hostMessage}]");
handler.Handle((dynamic)hostMessage);
hostMessage.SetFinished();
}
catch (Exception e)
{
if (e.GetType() != typeof(FromWmsException))
{
Log.WriteException(e);
}
hostMessage.SetFailed(e.Message);
}
service.Update(hostMessage);
return true;
}
}
}

View File

@@ -0,0 +1,203 @@
using Gebhardt.Shared;
using Gebhardt.Shared.Process.ProducerConsumer;
using Gebhardt.StoreWare.WcsWms.Constants;
using Gebhardt.StoreWare.WcsWms.InterfaceWcsWms.EntityFramework;
using Gebhardt.StoreWare.WcsWms.InterfaceWcsWms.EntityFramework.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Gebhardt.StoreWare.WcsWms.InterfaceWcsWms.Interfaces;
using Gebhardt.StoreWare.WcsWms.InterfaceWcsWms.Services;
using static Gebhardt.StoreWare.Wcs.Common.ConstantsCommon.WatchdogConstants;
namespace Gebhardt.StoreWare.Wcs.HostBooking
{
internal class HostBookingProducer : Producer<IHostMessage>
{
private bool _firstExecution = true;
private int _consumerQueueLength;
private bool _useLoadBalancing;
private readonly IHostMessageFromWmsService _service = new HostMessageFromWmsService();
/// <summary>
/// Fügt im Dictionary dem angegebenen Consumer das Messagem hinzu
/// </summary>
/// <param name="dataForConsumers"></param>
/// <param name="consumer"></param>
/// <param name="fromWms"></param>
private void AddDataForConsumer(ref Dictionary<string, List<IHostMessage>> dataForConsumers, string consumer, IHostMessage fromWms)
{
if (dataForConsumers.ContainsKey(consumer))
{
dataForConsumers[consumer].Add(fromWms);
}
else
{
dataForConsumers.Add(consumer, new List<IHostMessage> { fromWms });
}
}
/// <summary>
/// Bestimmt den Consumer, der für den Datensatz verantwortlich ist
/// </summary>
/// <param name="consumersWithDemand"></param>
/// <param name="criterion"></param>
/// <returns>Consumer Name, wenn ein passender Consumer in der lsite ist, null, wenn kein passender Consumer in der Liste ist</returns>
private string GetConsumerForBooking(List<string> consumersWithDemand, string criterion)
{
//Gibt es eine Letzte Stelle, sonst default
if (!criterion.IsNullOrEmptyOrDbEmpty())
{
//Wir entscheiden mit der letzten Stelle der LE, welcher LE-Consumer verbucht oder ob der default Consumer das tun muss
string endOfLe = criterion.Substring(criterion.Length - 1);
//Der Name des Consumer endet mit dem gleichen Zeichen
if (consumersWithDemand.Any(c => c.EndsWith(endOfLe)))
{
return consumersWithDemand.First(c => c.EndsWith(endOfLe));
}
//Soll dieses Messagem von einem speziellen Consumer bearbeitet werden? Aber dieser ist beschäftigt
else if (Regex.IsMatch(endOfLe, "[0-9]"))
{
//Messagem auslassen - keinem Consumer zuordnen
return null;
}
//HU endet mit einem Zeichen, dass zu keinem Consumer passt also Default oder auslassen
else
{
//Default Consumer muss mit Default enden!
return consumersWithDemand.FirstOrDefault(c => c.EndsWith("Default"));
}
}
else
{
//Default Consumer muss mit Default enden!
return consumersWithDemand.FirstOrDefault(c => c.EndsWith("Default"));
}
}
protected override Dictionary<string, List<IHostMessage>> GetDataForConsumers(List<string> consumersWithDemand)
{
//TODO: jub evtl. die Messagem Stau Meldung aus KW übernehmen
Dictionary<string, List<IHostMessage>> dataForConsumers = new Dictionary<string, List<IHostMessage>>();
try
{
//using HostEntities db = new HostEntitiesFactory(HostEntities.DefaultConnectionStringName).Create();
if (consumersWithDemand.Count > 0)
{
List<IHostMessage> fromWmsEntries;
if (_firstExecution)
{
//beim ersten mal nach Neustart werden die InProgress Messages nochmal auf Pending zurückgesetzt
//Ref == null damit nur Kopfnachrichten gefunden werden
fromWmsEntries = _service.GetAllEntries(t => t.Status == Status.InProgress).ToList();
using HostEntities db = new HostEntitiesFactory(HostEntities.DefaultConnectionStringName).Create();
fromWmsEntries.ForEach(message =>
{
FromWms fromWms = HostMessageFromWmsService.HostMapper.Map<FromWms>(message);
db.Attach(fromWms);
fromWms.Status = Status.Pending;
});
db.SaveChanges();
Log.Write(LogLevel.Info, $"Erste Produce Schleife nach Neustart, setze {fromWmsEntries.Count} Telegramme zur Sicherheit nochmals von InProgress auf Pending");
_firstExecution = false;
}
//nur Messages, die noch an keinen Consumer gegeben wurden
//es werden maximal so viele Messages abgerufen, wie ein einzelner Consumer annehmen könnte, damit das TryAdd sicher klappt und der Status nicht fälschlich
//gesetzt wird
//Ref == null damit nur Kopfnachrichten gefunden werden
fromWmsEntries = _service.GetAllEntries(a => a.Status == Status.Pending).OrderBy(a => a.Id).Take(_consumerQueueLength).ToList();// db.FromWms.Where(a => a.Ref == null).Where(a => a.Status == Status.Pending).OrderBy(a => a.Id).Take(_consumerQueueLength).ToList();
if (fromWmsEntries.Any())
{
using HostEntities db = new HostEntitiesFactory(HostEntities.DefaultConnectionStringName).Create();
//ohne LoadBalancing erhält der erste Consumer alle Messages zum Verbuchen
if (!_useLoadBalancing)
{
//der Consumer muss also alle Messages verarbeiten und nicht nur loggen
dataForConsumers.Add(consumersWithDemand.FirstOrDefault() ?? string.Empty, fromWmsEntries);
//Status auf InProgress damit jede Message nur einmal abgerufen wird
fromWmsEntries.ForEach(message => {
FromWms fromWms = HostMessageFromWmsService.HostMapper.Map<FromWms>(message);
db.Attach(fromWms);
fromWms.Status = Status.InProgress;
});
Log.Write(LogLevel.Low, $"Kein LoadBalancing aktiv, {fromWmsEntries.Count} neue HostMessages für Consumer {consumersWithDemand.FirstOrDefault()} gefunden");
}
//mit LoadBalancing wird nach der letzen Ziffer der ersten HU der Message auf 10 Consumer verteilt,
//enthält die Message keine HU wird es an Consumer 11 übergeben.
else
{
Log.Write(LogLevel.Low, $"LoadBalancing aktiv, {fromWmsEntries.Count} neue HostMessages gefunden, verteile auf Consumer");
foreach (IHostMessage message in fromWmsEntries)
{
try
{
FromWms fromWms = HostMessageFromWmsService.HostMapper.Map<FromWms>(message);
db.Attach(fromWms);
Log.Write(LogLevel.Low, $"Producerschleife für {fromWms}");
string consumer;
//keine HU in der Message, dann dem Default Consumer zuordnen
if (fromWms.LeNo.IsNullOrEmptyOrDbEmpty())
{
consumer = GetConsumerForBooking(consumersWithDemand, null);
//wenn der Default Consumer nicht in der Liste war, Message auslassen
if (consumer != null)
{
AddDataForConsumer(ref dataForConsumers, consumer, message);
//Status auf InProgress damit die Message nur einmal abgerufen wird
fromWms.Status = Status.InProgress;
}
}
//Messages mit HU
else
{
consumer = GetConsumerForBooking(consumersWithDemand, fromWms.LeNo);
AddDataForConsumer(ref dataForConsumers, consumer, message);
//Status auf InProgress damit die Message nur einmal abgerufen wird
fromWms.Status = Status.InProgress;
}
}
catch (Exception ex)
{
Log.WriteException(ex);
}
}
Log.Write(LogLevel.Low, $"Producerschleife beendet");
}
//Status Updates für alle weitergereichten Messages
db.SaveChanges();
}
else
{
Log.Write(LogLevel.Debug, 60, "Keine Messages in FromWms");
}
}
else
{
Log.Write(LogLevel.Debug, 60, "Kein Consumer hat demand");
}
return dataForConsumers;
}
catch (Exception e)
{
Log.WriteException(e);
return dataForConsumers;
}
}
public HostBookingProducer(int workinterval, bool useLoadBalancing, int consumerQueueLength) : base(typeof(HostBookingProducer).Name, workinterval, true)
{
_consumerQueueLength = consumerQueueLength;
_useLoadBalancing = useLoadBalancing;
}
}
}

View File

@@ -0,0 +1,28 @@
namespace Gebhardt.StoreWare.Wcs.HostBooking.Properties {
// This class allows you to handle specific events on the settings class:
// The SettingChanging event is raised before a setting's value is changed.
// The PropertyChanged event is raised after a setting's value is changed.
// The SettingsLoaded event is raised after the setting values are loaded.
// The SettingsSaving event is raised before the setting values are saved.
internal sealed partial class Settings {
public Settings() {
// // To add event handlers for saving and changing settings, uncomment the lines below:
//
// this.SettingChanging += this.SettingChangingEventHandler;
//
// this.SettingsSaving += this.SettingsSavingEventHandler;
//
}
private void SettingChangingEventHandler(object sender, System.Configuration.SettingChangingEventArgs e) {
// Add code to handle the SettingChangingEvent event here.
}
private void SettingsSavingEventHandler(object sender, System.ComponentModel.CancelEventArgs e) {
// Add code to handle the SettingsSaving event here.
}
}
}