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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user