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,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.
}
}
}