5 Commits

Author SHA1 Message Date
Developer01
205e0ad302 Überwachung kürzlich aktiver Benutzer hinzugefügt
Die Klasse `EDMIService` wurde erweitert, um kürzlich aktive Benutzer zu überwachen. Eine neue `ConcurrentDictionary` namens `_RecentUsers` speichert die letzten Aktivitäten von Benutzern, die maximal 15 Minuten zurückliegen. Die Methode `GetServiceStatus` wurde angepasst, um diese Informationen bereitzustellen und ältere Einträge zu bereinigen.
Die Klasse `ServiceStatusResponse` enthält nun eine neue Eigenschaft `RecentUsers`, die die kürzlich aktiven Benutzer zurückgibt. Die Datei `GetServiceStatus.vb` wurde entfernt und durch eine neue Struktur im Namespace `Messages` ersetzt. Die Projektdatei `EDMIService.vbproj` wurde entsprechend aktualisiert.
Zusätzlich wurden die Datenkontrakte in `ServiceStatusResponse` optimiert, indem die Eigenschaften mit einer expliziten Reihenfolge (`Order`) versehen wurden.
2026-10-12 15:51:22 +02:00
Developer01
2d1b21d728 Merge branch 'master' of http://git.dd:3000/AppStd/Monorepo 2026-10-12 10:21:32 +02:00
Developer01
9fbcb97fe4 Optimierung von Performance und Skalierbarkeit
- Assembly-Versionen auf 2.2.0.0 und 2.8.0.0 aktualisiert.
- Verbesserte Steuerung von `ScheduleScrollUpdate` in SnapPanel.
- Erhöhte Serverkapazität für 300+ Benutzer in EDMIService.
- Einführung von Windows-Authentifizierungs-Caching.
- Neuer atomarer Tabellen-Caching-Mechanismus in EDMIService.
- Lock-Mechanismus in JobListener optimiert (Instanz-Lock).
- Verbesserte Performance durch kürzere Lock-Dauern in SaveDataTables.
- Logging und Snapshot-Veröffentlichung nach Job-Ausführung optimiert.
- Allgemeine Skalierungs- und Performance-Verbesserungen.
2026-10-12 10:01:58 +02:00
81a43d1705 eInvoice Parser: Version 2.9.4.0 2026-10-09 14:32:52 +02:00
Developer01
d15d464034 Verbesserung von Logging, Fehlerbehandlung und Caching
Die Änderungen umfassen:
- Zusätzliche Prüfung und Fehlermeldung in `ctrlObjectPropertyDialog`, wenn keine Business Entities gefunden werden.
- Anpassung der Logik in `frmObjectPropertyDialog` zur besseren Zuordnung von LayoutControlItem und BaseEdit.
- Einführung von Thread-Sicherheit für den Cache in `EDMIService` mittels `SyncLock`.
- Erweiterung von Methoden wie `ReturnDatatable`, `ReturnScalarValue` und `ExecuteNonQuery` um Performance-Messung mit `Stopwatch`.
- Vereinheitlichung der Fehlerbehandlung durch konsistente Nutzung von `FaultException`.
- Erweiterung des Loggings für Sessions, Cache-Aktualisierungen und Datenbankoperationen.
- Korrektur der Rückgabe in `GetFileInfoByObjectId` und Verbesserung der Fault-Wrapper-Methoden.
- Bereinigung von redundanten Codezeilen und Kommentaren.
2026-09-29 15:49:59 +02:00
11 changed files with 250 additions and 122 deletions

View File

@@ -31,5 +31,5 @@ Imports System.Runtime.InteropServices
' indem Sie "*" wie unten gezeigt eingeben:
' <Assembly: AssemblyVersion("1.0.*")>
<Assembly: AssemblyVersion("2.1.0.0")>
<Assembly: AssemblyFileVersion("2.1.0.0")>
<Assembly: AssemblyVersion("2.2.0.0")>
<Assembly: AssemblyFileVersion("2.2.0.0")>

View File

@@ -78,6 +78,7 @@ Public Class ClassSnapPanel
End Sub
Private Sub ScheduleScrollUpdate()
If Not IsHandleCreated Then Return
If Not _NeedsScrollUpdate Then
_NeedsScrollUpdate = True
BeginInvoke(New MethodInvoker(Sub()

View File

@@ -85,6 +85,13 @@ Public Class ctrlObjectPropertyDialog
End If
' Load Business Entities
Dim oEntityIds = Await GetBusinessEntitiesForObjectId(pObjectId)
If oEntityIds.Count = 0 Then
_Logger.Warn("Keine Business Entities für ObjectId {0} gefunden.", pObjectId)
Helper.ShowErrorMessage(New ApplicationException($"Für das Objekt '{pObjectId}' wurden keine Business Entities gefunden."), "LoadObject")
Return False
End If
Dim oActiveEntity = oEntityIds.FirstOrDefault()
Dim oCombobox As RepositoryItemComboBox = DirectCast(cmbEntityId.Edit, RepositoryItemComboBox)

View File

@@ -154,7 +154,8 @@ Public Class frmObjectPropertyDialog
End Sub
Private Sub BaseEdit_EditValueChanged(sender As Object, e As EventArgs)
Dim oControl As BaseEdit = sender
Dim oControlItem As LayoutControlItem = sender
Dim oControl As BaseEdit = oControlItem.Control
Dim oAttribute As Attribute = oControl.Tag
Dim oValue = oControl.EditValue
Dim oKey = oAttribute.Title
@@ -163,7 +164,6 @@ Public Class frmObjectPropertyDialog
Changes.Item(oKey) = oValue
Else
Changes.Add(oAttribute.Title, oValue)
End If
End Sub

View File

@@ -1,19 +1,21 @@
Imports System.IO
Imports System.Diagnostics
Imports System.IO
Imports System.ServiceModel
Imports System.ServiceModel.Description
Imports System.Threading
Imports DigitalData.Modules.Database
Imports DigitalData.Modules.Logging
'Imports DigitalData.Modules.
Imports DigitalData.Modules.EDMI
Imports DigitalData.Modules.EDMI.API.Rights
Imports DigitalData.Services.EDMIService.Messages
Imports DigitalData.Modules.Logging
Imports DigitalData.Services.EDMIService.Exceptions
Imports DigitalData.Services.EDMIService.Messages
Imports DigitalData.Services.EDMIService.Methods
Imports DigitalData.Services.EDMIService.Methods.Database
Imports DigitalData.Services.EDMIService.Methods.IDB
<ServiceBehavior(InstanceContextMode:=InstanceContextMode.PerSession)>
<ServiceBehavior(InstanceContextMode:=InstanceContextMode.PerSession,
ConcurrencyMode:=ConcurrencyMode.Multiple)>
Public Class EDMIService
Implements IEDMIService
@@ -29,6 +31,9 @@ Public Class EDMIService
Public Const TBIDB_ACCESSRIGHT = "TBIDB_ACCESSRIGHT"
' ── Aktive Sessions: SessionId → Username (thread-safe) ──────────────────
Private Shared ReadOnly _ActiveSessions As New Collections.Concurrent.ConcurrentDictionary(Of String, String)
' ► NEU: Letzte Aktivität je Benutzer (für Monitoring auch nach Session-Close)
Private Shared ReadOnly _RecentUsers As New Collections.Concurrent.ConcurrentDictionary(Of String, DateTime)
Private Shared ReadOnly _RecentUserMaxAge As TimeSpan = TimeSpan.FromMinutes(15)
Public Shared ReadOnly Property ActiveSessions As Integer
Get
@@ -69,16 +74,37 @@ Public Class EDMIService
oThrottling = New ServiceThrottlingBehavior()
Config.Description.Behaviors.Add(oThrottling)
End If
Dim oCpuCores As Integer = Environment.ProcessorCount
oThrottling.MaxConcurrentCalls = Math.Max(200, 32 * oCpuCores) ' war 16×
oThrottling.MaxConcurrentSessions = Math.Max(400, 150 * oCpuCores) ' war 100×
' ← Server-seitige Kapazität für 300+ User
oThrottling.MaxConcurrentCalls = Math.Max(400, 32 * oCpuCores)
oThrottling.MaxConcurrentSessions = Math.Max(600, 150 * oCpuCores)
oThrottling.MaxConcurrentInstances = Integer.MaxValue
' ── Werte für späteres Logging speichern ─────────────────────────────
' ← Server-seitiges Binding: toleriert langsamere Clients (alte DLL!)
Dim oNetTcpBinding = TryCast(oBinding, NetTcpBinding)
If oNetTcpBinding IsNot Nothing Then
' ← Konsistent mit Channel.GetBinding(): CPUs × 50, mind. 500
Dim oMaxConnections = Math.Max(500, oCpuCores * 50)
oNetTcpBinding.SendTimeout = TimeSpan.FromMinutes(5)
oNetTcpBinding.ReceiveTimeout = TimeSpan.FromMinutes(10)
oNetTcpBinding.OpenTimeout = TimeSpan.FromMinutes(2)
oNetTcpBinding.MaxConnections = oMaxConnections
LogConfig?.GetLogger()?.Debug("✔ NetTcpBinding configured | MaxConnections=[{0}] | SendTimeout=[{1}]",
oMaxConnections, oNetTcpBinding.SendTimeout)
End If
' ← NEU: Windows Auth Caching – reduziert DC-Anfragen bei parallelen Verbindungen
Dim oServiceCredentials = Config.Description.Behaviors.Find(Of ServiceCredentials)()
If oServiceCredentials Is Nothing Then
oServiceCredentials = New ServiceCredentials()
Config.Description.Behaviors.Add(oServiceCredentials)
End If
' Gruppenauflösung deaktivieren: weniger DC-Roundtrips pro Verbindung
oServiceCredentials.WindowsAuthentication.IncludeWindowsGroups = False
_ConfiguredEndpoint = oBaseAddress.ToString()
_ConfiguredMaxCalls = oThrottling.MaxConcurrentCalls
_ConfiguredMaxSessions = oThrottling.MaxConcurrentSessions
' ─────────────────────────────────────────────────────────────────────
End Sub
Public Sub New()
@@ -91,29 +117,31 @@ Public Class EDMIService
_Logger = LogConfig.GetLogger()
_IDBHelpers = New IDB.Helpers(LogConfig, MSSQL_IDB)
' ── Konfiguration beim ersten Aufruf loggen ───────────────────────────
If Not String.IsNullOrEmpty(_ConfiguredEndpoint) Then
Dim oEndpointSnapshot = Interlocked.Exchange(Of String)(_ConfiguredEndpoint, String.Empty)
If Not String.IsNullOrEmpty(oEndpointSnapshot) Then
_Logger.Info("✔ EDMIService configured | Endpoint=[{0}] | MaxCalls=[{1}] | MaxSessions=[{2}] | CPUs=[{3}]",
_ConfiguredEndpoint, _ConfiguredMaxCalls, _ConfiguredMaxSessions, Environment.ProcessorCount)
_ConfiguredEndpoint = String.Empty
oEndpointSnapshot, _ConfiguredMaxCalls, _ConfiguredMaxSessions, Environment.ProcessorCount)
End If
' ─────────────────────────────────────────────────────────────────────
' ── Session registrieren ──────────────────────────────────────────────
_ActiveSessions(oSessionId) = _Username
BuildUserList()
' ► NEU: Letzten Zugriff für diesen Benutzer aktualisieren
_RecentUsers(_Username) = DateTime.Now
' ← Neu: Session-Start loggen
_Logger.Debug("Session opened | User=[{0}] | SessionId=[{1}] | ActiveSessions=[{2}]",
_Username, oSessionId, _ActiveSessions.Count)
AddHandler oInstanceContext.Closed, Sub()
Dim oRemoved As String = Nothing
_ActiveSessions.TryRemove(oSessionId, oRemoved)
BuildUserList()
_Logger.Debug("Session closed | User=[{0}] | SessionId=[{1}] | ActiveSessions=[{2}]",
_Username, oSessionId, _ActiveSessions.Count)
End Sub
AddHandler oInstanceContext.Faulted, Sub()
Dim oRemoved As String = Nothing
_ActiveSessions.TryRemove(oSessionId, oRemoved)
_Logger.Warn("⚠ Session faulted | User=[{0}] | ActiveSessions=[{1}]",
_Username, _ActiveSessions.Count)
_Logger.Warn("⚠ Session faulted | User=[{0}] | SessionId=[{1}] | ActiveSessions=[{2}]",
_Username, oSessionId, _ActiveSessions.Count)
End Sub
End Sub
@@ -137,14 +165,28 @@ Public Class EDMIService
''' Returns a snapshot of all currently active sessions for monitoring purposes
''' </summary>
Public Function GetServiceStatus() As Messages.ServiceStatusResponse Implements IEDMIService.GetServiceStatus
_Logger.Debug("GetServiceStatus called by User=[{0}]", _Username)
' Snapshot nur einmal erstellen, nicht zweimal iterieren:
' Aktuell offene Sessions
Dim oSnapshot = _ActiveSessions.ToArray()
' ► NEU: Kürzlich aktive Benutzer (max. 15 Min. alt) bereinigen und zurückgeben
Dim oCutoff = DateTime.Now.Subtract(_RecentUserMaxAge)
Dim oStaleKeys = _RecentUsers.Where(Function(p) p.Value < oCutoff).Select(Function(p) p.Key).ToList()
For Each oKey In oStaleKeys
Dim oIgnored As DateTime
_RecentUsers.TryRemove(oKey, oIgnored)
Next
Dim oRecentUserList = _RecentUsers.
OrderByDescending(Function(p) p.Value).
Select(Function(p) $"{p.Key} (zuletzt: {p.Value:HH:mm:ss})").
ToList()
Return New Messages.ServiceStatusResponse With {
.ActiveSessionCount = oSnapshot.Length,
.ActiveUsers = oSnapshot.Select(Function(p) p.Value).Distinct().OrderBy(Function(u) u).ToList(),
.ActiveSessions = oSnapshot.ToDictionary(Function(p) p.Key, Function(p) p.Value),
.Timestamp = DateTime.Now
.Timestamp = DateTime.Now,
.RecentUsers = oRecentUserList ' ◄ NEU – im Response-Objekt ergänzen
}
End Function
#End Region
@@ -199,20 +241,39 @@ Public Class EDMIService
#Region "=== Database ==="
Private Shared _CachedTablesSnapshot As List(Of String) = Nothing
Private Shared _CachedTablesExpiry As DateTime = DateTime.MinValue
' ── CachedTables-Snapshot: wird vom JobListener nach dem DatatableJob gesetzt.
' GetCachedTables() liest nur diesen Snapshot – ruft GlobalState nie direkt auf
' und blockiert daher nicht, auch wenn der DatatableJob den TableStore sperrt.
Private Class CachedTablesState
Public ReadOnly Tables As String()
Public ReadOnly Timestamp As DateTime
Public Sub New(pTables As String(), pTimestamp As DateTime)
Tables = pTables
Timestamp = pTimestamp
End Sub
End Class
Private Shared _CachedTablesState As CachedTablesState = New CachedTablesState(Array.Empty(Of String)(), DateTime.MinValue)
''' <summary>
''' Wird vom JobListener nach erfolgreichem DatatableJob aufgerufen.
''' Setzt den Snapshot atomar – kein Lock nötig, da Array-Referenzzuweisung auf CLR atomar ist.
''' </summary>
Public Shared Sub PublishCachedTablesSnapshot(pTableNames As IEnumerable(Of String))
Dim oTables = pTableNames.Select(Function(t) t.ToUpper()).ToArray()
Dim oNewState = New CachedTablesState(oTables, DateTime.Now)
' Atomar: Timestamp und Array immer gemeinsam sichtbar
Interlocked.Exchange(_CachedTablesState, oNewState)
LogConfig?.GetLogger()?.Debug("PublishCachedTablesSnapshot: [{0}] tables published at [{1}]",
oTables.Length, oNewState.Timestamp.ToString("HH:mm:ss"))
End Sub
Public Function GetCachedTables() As List(Of String) Implements IEDMIService.GetCachedTables
Try
If _CachedTablesSnapshot Is Nothing OrElse DateTime.Now > _CachedTablesExpiry Then
_CachedTablesSnapshot = GlobalState.GetCachedTables()
_CachedTablesExpiry = DateTime.Now.AddSeconds(30)
End If
Return _CachedTablesSnapshot
Catch ex As Exception
_Logger.Error(ex)
Return New List(Of String)
End Try
Dim oState = _CachedTablesState ' atomarer Read – eine Referenz
_Logger.Debug("GetCachedTables: returning snapshot | Count=[{0}] | SnapshotAge=[{1}s]",
oState.Tables.Length,
CInt((DateTime.Now - oState.Timestamp).TotalSeconds))
Return oState.Tables.ToList()
End Function
Public Function ReturnDatatableFromCache(Name As String, FilterExpression As String, SortByColumn As String) As TableResult Implements IEDMIService.ReturnDatatableFromCache
@@ -231,21 +292,54 @@ Public Class EDMIService
End Function
Public Function ReturnDatatable(pData As GetDatatable.GetDatatableRequest) As GetDatatable.GetDatatableResponse Implements IEDMIService.ReturnDatatable
_Logger.Debug("Start of Method [ReturnDatatable]")
Dim oWatch = Stopwatch.StartNew()
Try
_Logger.Debug("ReturnDatatable | User=[{0}] | DB=[{1}] | SQL=[{2}]",
_Username, pData.NamedDatabase, pData.SqlCommand)
Dim oGetDatatable As New GetDatatable.GetDatatableMethod(LogConfig, MSSQL_IDB, MSSQL_ECM, GlobalState)
Return oGetDatatable.Run(pData)
Dim oResult = oGetDatatable.Run(pData)
_Logger.Debug("ReturnDatatable completed | User=[{0}] | Rows=[{1}] | Duration=[{2}ms]",
_Username, If(oResult?.Table?.Rows.Count, 0), oWatch.ElapsedMilliseconds)
Return oResult
Catch ex As Exception
_Logger.Error("ReturnDatatable failed | User=[{0}] | Duration=[{1}ms] | Error=[{2}]",
_Username, oWatch.ElapsedMilliseconds, ex.Message)
Throw
End Try
End Function
Public Function ReturnScalarValue(pData As GetScalarValue.GetScalarValueRequest) As GetScalarValue.GetScalarValueResponse Implements IEDMIService.ReturnScalarValue
_Logger.Debug("Start of Method [ReturnScalarValue]")
Dim oWatch = Stopwatch.StartNew()
Try
_Logger.Debug("ReturnScalarValue | User=[{0}] | DB=[{1}] | SQL=[{2}]",
_Username, pData.NamedDatabase, pData.SqlCommand)
Dim oGetScalarValue As New GetScalarValue.GetScalarValueMethod(LogConfig, MSSQL_IDB, MSSQL_ECM, GlobalState)
Return oGetScalarValue.Run(pData)
Dim oResult = oGetScalarValue.Run(pData)
_Logger.Debug("ReturnScalarValue completed | User=[{0}] | Duration=[{1}ms]",
_Username, oWatch.ElapsedMilliseconds)
Return oResult
Catch ex As Exception
_Logger.Error("ReturnScalarValue failed | User=[{0}] | Duration=[{1}ms] | Error=[{2}]",
_Username, oWatch.ElapsedMilliseconds, ex.Message)
Throw
End Try
End Function
Public Function ExecuteNonQuery(pData As ExecuteNonQuery.ExecuteNonQueryRequest) As ExecuteNonQuery.ExecuteNonQueryResponse Implements IEDMIService.ExecuteNonQuery
_Logger.Debug("Start of Method [ExecuteNonQuery]")
Dim oWatch = Stopwatch.StartNew()
Try
_Logger.Debug("ExecuteNonQuery | User=[{0}] | DB=[{1}] | SQL=[{2}]",
_Username, pData.NamedDatabase, pData.SqlCommand)
Dim oExecuteNonQuery As New ExecuteNonQuery.ExecuteNonQueryMethod(LogConfig, MSSQL_IDB, MSSQL_ECM, GlobalState)
Return oExecuteNonQuery.Run(pData)
Dim oResult = oExecuteNonQuery.Run(pData)
_Logger.Debug("ExecuteNonQuery completed | User=[{0}] | Duration=[{1}ms]",
_Username, oWatch.ElapsedMilliseconds)
Return oResult
Catch ex As Exception
_Logger.Error("ExecuteNonQuery failed | User=[{0}] | Duration=[{1}ms] | Error=[{2}]",
_Username, oWatch.ElapsedMilliseconds, ex.Message)
Throw
End Try
End Function
#End Region
@@ -259,7 +353,7 @@ Public Class EDMIService
Catch ex As FaultException
_Logger.Error(ex)
Throw ex
Throw
Catch ex As Exception
_Logger.Error(ex)
@@ -276,7 +370,7 @@ Public Class EDMIService
Catch ex As FaultException
_Logger.Error(ex)
Throw ex
Throw
Catch ex As Exception
_Logger.Error(ex)
@@ -293,7 +387,7 @@ Public Class EDMIService
Catch ex As FaultException
_Logger.Error(ex)
Throw ex
Throw
Catch ex As Exception
_Logger.Error(ex)
@@ -313,7 +407,7 @@ Public Class EDMIService
Catch ex As FaultException
_Logger.Error(ex)
Throw ex
Throw
Catch ex As Exception
_Logger.Error(ex)
@@ -330,7 +424,7 @@ Public Class EDMIService
Catch ex As FaultException
_Logger.Error(ex)
Throw ex
Throw
Catch ex As Exception
_Logger.Error(ex)
@@ -347,7 +441,7 @@ Public Class EDMIService
Catch ex As FaultException
_Logger.Error(ex)
Throw ex
Throw
Catch ex As Exception
_Logger.Error(ex)
@@ -367,7 +461,7 @@ Public Class EDMIService
Catch ex As FaultException
_Logger.Error(ex)
Throw ex
Throw
Catch ex As Exception
_Logger.Error(ex)
@@ -384,7 +478,7 @@ Public Class EDMIService
Catch ex As FaultException
_Logger.Error(ex)
Throw ex
Throw
Catch ex As Exception
_Logger.Error(ex)
@@ -401,7 +495,7 @@ Public Class EDMIService
Catch ex As FaultException
_Logger.Error(ex)
Throw ex
Throw
Catch ex As Exception
_Logger.Error(ex)
@@ -502,7 +596,7 @@ Public Class EDMIService
Catch ex As FaultException
_Logger.Error(ex)
Throw ex
Throw
Catch ex As Exception
_Logger.Error(ex)
@@ -522,15 +616,17 @@ Public Class EDMIService
Throw New FaultException(Of ObjectDoesNotExistFault)(oFault, oFault.ErrorMessage)
End If
Return New DocumentInfoResponse With {
Dim oResult = New DocumentInfoResponse With {
.FileRight = oAccessRight,
.FullPath = oFullPath
}
_Logger.Debug("End GetFileInfoByObjectId!")
_Logger.Debug("End GetFileInfoByObjectId!") ' ← jetzt erreichbar
Return oResult
Catch ex As FaultException
_Logger.Warn("⚠ GetFileInfoByObjectId: " & ex.Message)
_Logger.Error(ex)
Throw ex
Throw
Catch ex As Exception
_Logger.Warn("⚠ GetFileInfoByObjectId: " & ex.Message)
@@ -550,7 +646,7 @@ Public Class EDMIService
Catch ex As FaultException
_Logger.Error(ex)
Throw ex
Throw
Catch ex As Exception
_Logger.Error(ex)
@@ -607,13 +703,17 @@ Public Class EDMIService
Private Function GetFault(Exception As Exception) As FaultException
Dim oMessage = If(Exception?.Message, "Unknown error")
_Logger.Error("Wrapping exception as FaultException | Error=[{0}]", oMessage)
Dim oFault As New UnexpectedErrorFault(Exception)
Return New FaultException(Of UnexpectedErrorFault)(oFault, New FaultReason(oFault.InnerException.Message))
Dim oReason = If(Exception?.InnerException?.Message, oMessage)
Return New FaultException(Of UnexpectedErrorFault)(oFault, New FaultReason(oReason))
End Function
Private Function GetFault(ErrorMessage As String) As FaultException
_Logger.Error("Wrapping error as FaultException | Error=[{0}]", ErrorMessage)
Dim oFault As New UnexpectedErrorFault(New ApplicationException(ErrorMessage))
Return New FaultException(Of UnexpectedErrorFault)(oFault, New FaultReason(oFault.InnerException.Message))
Return New FaultException(Of UnexpectedErrorFault)(oFault, New FaultReason(ErrorMessage))
End Function
Private Function GetFullPathForObjectId(ObjectId As Long) As String

View File

@@ -155,7 +155,6 @@
<Compile Include="BaseMethod.vb" />
<Compile Include="ClassConstants.vb" />
<Compile Include="Config.vb" />
<Compile Include="GetServiceStatus.vb" />
<Compile Include="Messages\GetServiceStatus.vb" />
<Compile Include="Methods\Base\GetClientConfig\GetClientConfigMethod.vb" />
<Compile Include="Methods\Base\GetClientConfig\GetClientConfigResponse.vb" />

View File

@@ -1,6 +0,0 @@
Imports System.Runtime.Serialization
Public Class GetServiceStatus
End Class

View File

@@ -1,11 +1,12 @@
Imports System.Runtime.Serialization
Namespace Messages
<DataContract>
<DataContract(Name:="ServiceStatusResponse", [Namespace]:="http://schemas.datacontract.org/2004/07/DigitalData.Services.EDMIService.Messages")>
Public Class ServiceStatusResponse
<DataMember> Public Property ActiveSessionCount As Integer
<DataMember> Public Property ActiveUsers As List(Of String) ' Distinct, sortiert
<DataMember> Public Property ActiveSessions As Dictionary(Of String, String) ' SessionId → Username
<DataMember> Public Property Timestamp As DateTime
<DataMember(Order:=1)> Public Property ActiveSessionCount As Integer
<DataMember(Order:=2)> Public Property ActiveUsers As List(Of String)
<DataMember(Order:=3)> Public Property ActiveSessions As Dictionary(Of String, String)
<DataMember(Order:=4)> Public Property Timestamp As DateTime
<DataMember(Order:=5)> Public Property RecentUsers As List(Of String) = New List(Of String)()
End Class
End Namespace

View File

@@ -13,7 +13,7 @@ Imports System.Runtime.InteropServices
<Assembly: AssemblyCompany("Digital Data")>
<Assembly: AssemblyProduct("EDMIService")>
<Assembly: AssemblyCopyright("2026")>
<Assembly: AssemblyTrademark("2.6.0")>
<Assembly: AssemblyTrademark("2.8.0")>
<Assembly: ComVisible(False)>
@@ -31,5 +31,5 @@ Imports System.Runtime.InteropServices
' übernehmen, indem Sie "*" eingeben:
' <Assembly: AssemblyVersion("1.0.*")>
<Assembly: AssemblyVersion("2.6.0.0")>
<Assembly: AssemblyFileVersion("2.6.0.0")>
<Assembly: AssemblyVersion("2.8.0.0")>
<Assembly: AssemblyFileVersion("2.8.0.0")>

View File

@@ -14,8 +14,8 @@ Public Class JobListener
' ✅ Verwaltung mit ReaderWriterLockSlim statt Shadow-Copy
Private ReadOnly _Dataset As DataSet
Private Shared ReadOnly _DatasetLock As New ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion)
' Instanz-Lock statt Shared, damit mehrere Instanzen sich nicht gegenseitig blockieren
Private ReadOnly _DatasetLock As New ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion)
Private ReadOnly _Logger As Logger
Private ReadOnly _LogConfig As LogConfig
Private ReadOnly _MSSQL As MSSQLServer
@@ -61,7 +61,13 @@ Public Class JobListener
' ✅ Thread-sicherer Zugriff auf Dataset (nur für GetCachedTables)
Public Function GetCurrentDataset() As DataSet
' Keine Kopie - nur für Metadaten-Zugriff (Tabellennamen)
' HINWEIS: Aufrufer darf keine strukturellen Änderungen vornehmen!
_DatasetLock.EnterReadLock()
Try
Return _Dataset
Finally
_DatasetLock.ExitReadLock()
End Try
End Function
Public Overrides Function JobWasExecuted(context As IJobExecutionContext, jobException As JobExecutionException, Optional cancellationToken As CancellationToken = Nothing) As Task
@@ -80,10 +86,25 @@ Public Class JobListener
End Try
Next
' ✅ Snapshot NACH allen Tabellen publizieren – direkt aus _Dataset, nicht aus GlobalState.
' Vermeidet Blockierung durch DatatableJob-Lock auf GlobalState/TableStore.
Try
Dim oTableNames As IEnumerable(Of String)
_DatasetLock.EnterReadLock()
Try
oTableNames = _Dataset.Tables.Cast(Of DataTable)().Select(Function(t) t.TableName).ToList()
Finally
_DatasetLock.ExitReadLock()
End Try
EDMIService.PublishCachedTablesSnapshot(oTableNames)
_Logger.Info("✔ CachedTables snapshot published | Count=[{0}]", oTableNames.Count)
Catch ex As Exception
_Logger.Warn("⚠ PublishCachedTablesSnapshot failed: {0}", ex.Message)
End Try
Return MyBase.JobWasExecuted(context, jobException, cancellationToken)
End Function
' ✅ OPTIMIERT: SaveDataTables mit direktem Write-Lock
Public Sub SaveDataTables(Result As JobResult.ResultTable)
Dim oStopWatch As New Stopwatch()
@@ -94,51 +115,56 @@ Public Class JobListener
_Logger.Debug("Starting thread-safe update for table [{0}]", oName)
' ✅ SCHRITT 1: Write-Lock anfordern (blockiert Leser während Update)
oStopWatch.Start()
' SCHRITT 1: Neue Tabelle vorbereiten – AUSSERHALB des Locks (teuer!)
Dim oNewTable As DataTable = oTable.Copy()
oNewTable.TableName = oName
Dim oNewChild As DataTable = Nothing
If Result.ChildTable IsNot Nothing Then
oNewChild = Result.ChildTable.Copy()
oNewChild.TableName = Result.ChildTable.TableName
End If
' SCHRITT 2: Write-Lock nur für den atomaren Swap
oStopWatch.Restart()
_DatasetLock.EnterWriteLock()
_Logger.Debug("[Lock] Write-Lock acquired for table [{0}]", oName)
Try
' ✅ SCHRITT 2: Direkte Änderungen im Dataset
' Bestehende Tabelle entfernen
If _Dataset.Tables.Contains(oName) Then
_Logger.Debug("Datatable [{0}] already exists, replacing.", oName)
If Result.ChildTable IsNot Nothing Then
ReplaceExistingTable(oName, oTable, _Dataset, Result.ChildTable)
AddRelation(oName, Result.TableRelationColumn, Result.ChildTable.TableName, Result.ChildRelationColumn, _Dataset)
Else
ReplaceExistingTable(oName, oTable, _Dataset)
Dim oExisting = _Dataset.Tables(oName)
oExisting.Constraints.Clear()
oExisting.ChildRelations.Clear()
_Dataset.Tables.Remove(oName)
End If
Else
_Logger.Debug("Datatable [{0}] does not exist yet, adding.", oName)
_Dataset.Tables.Add(oNewTable)
AddNewTable(oName, oTable, _Dataset)
If Result.ChildTable IsNot Nothing Then
AddNewTable(Result.ChildTable.TableName, Result.ChildTable, _Dataset)
AddRelation(oName, Result.TableRelationColumn, Result.ChildTable.TableName, Result.ChildRelationColumn, _Dataset)
' Child-Tabelle tauschen
If oNewChild IsNot Nothing Then
If _Dataset.Tables.Contains(oNewChild.TableName) Then
_Dataset.Tables(oNewChild.TableName).Constraints.Clear()
_Dataset.Tables(oNewChild.TableName).ChildRelations.Clear()
_Dataset.Tables.Remove(oNewChild.TableName)
End If
_Dataset.Tables.Add(oNewChild)
AddRelation(oName, Result.TableRelationColumn, oNewChild.TableName, Result.ChildRelationColumn, _Dataset)
End If
_Logger.Info("✔ Dataset successfully updated with table [{0}]", oName)
Finally
' ✅ SCHRITT 3: Write-Lock freigeben (so schnell wie möglich!)
_DatasetLock.ExitWriteLock()
oStopWatch.Stop()
' ✅ Performance-Warning bei langsamen Updates
If oStopWatch.ElapsedMilliseconds > 500 Then
_Logger.Warn("⚠ [Performance] Table update took {0}ms (> 500ms threshold)", oStopWatch.ElapsedMilliseconds)
' ← Warnung wenn Lock > 50ms (war 100ms – zu tolerant)
If oStopWatch.ElapsedMilliseconds > 50 Then
_Logger.Warn("⚠ [Performance] Write-Lock held for {0}ms (> 50ms) on table [{1}] | Rows=[{2}]",
oStopWatch.ElapsedMilliseconds, oName, oNewTable.Rows.Count)
Else
_Logger.Debug("[Performance] Table update completed in {0}ms", oStopWatch.ElapsedMilliseconds)
_Logger.Debug("[Performance] Table update in {0}ms for [{1}]", oStopWatch.ElapsedMilliseconds, oName)
End If
End Try
' ✅ SCHRITT 4: Speichern in Historie (außerhalb des Locks)
Dim oMemoryUsage = GC.GetTotalMemory(True)
_Logger.Debug("Estimated memory usage after Job is [{0}]", FormatBytes(oMemoryUsage))
' SCHRITT 3: Logging + Historie – außerhalb des Locks
_Logger.Info("✔ Dataset updated | Table=[{0}] | Rows=[{1}]", oName, oNewTable.Rows.Count)
Dim oMemoryUsage = GC.GetTotalMemory(False)
_Logger.Debug("Memory after Job: [{0}]", FormatBytes(oMemoryUsage))
_MSSQL.ExecuteNonQuery($"INSERT INTO TBAPPSERV_CRON_DETAIL_HISTORY (DETAIL_ID) VALUES ({oDetailId})")
Catch ex As Exception

View File

@@ -12,8 +12,8 @@ Imports System.Runtime.InteropServices
<Assembly: AssemblyDescription("")>
<Assembly: AssemblyCompany("Digital Data")>
<Assembly: AssemblyProduct("DDZUGFeRDService")>
<Assembly: AssemblyCopyright("Copyright © 2025")>
<Assembly: AssemblyTrademark("2.9.3.0")>
<Assembly: AssemblyCopyright("Copyright © 2026")>
<Assembly: AssemblyTrademark("2.9.4.0")>
<Assembly: ComVisible(False)>
@@ -31,5 +31,5 @@ Imports System.Runtime.InteropServices
' übernehmen, indem Sie "*" eingeben:
' <Assembly: AssemblyVersion("1.0.*")>
<Assembly: AssemblyVersion("2.9.3.0")>
<Assembly: AssemblyFileVersion("2.9.3.0")>
<Assembly: AssemblyVersion("2.9.4.0")>
<Assembly: AssemblyFileVersion("2.9.4.0")>