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.
This commit is contained in:
Developer01
2026-10-12 10:01:58 +02:00
parent d15d464034
commit 9fbcb97fe4
5 changed files with 124 additions and 64 deletions

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)
Return _Dataset
' 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)
End If
Else
_Logger.Debug("Datatable [{0}] does not exist yet, adding.", oName)
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)
End If
Dim oExisting = _Dataset.Tables(oName)
oExisting.Constraints.Clear()
oExisting.ChildRelations.Clear()
_Dataset.Tables.Remove(oName)
End If
_Dataset.Tables.Add(oNewTable)
_Logger.Info("✔ Dataset successfully updated with table [{0}]", oName)
' 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
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