Neue Features, Optimierungen und Bugfixes
- Neues Projekt `EDMI.API` zur Solution hinzugefügt. - `App.config`: Abhängigkeiten und `bindingRedirect` aktualisiert. - Verbesserte Thread-Sicherheit mit `ReaderWriterLockSlim`. - SQL-Abfragen mit `WITH (NOLOCK)` für bessere Parallelität. - Robuste Verarbeitung von `DataRow`-Werten mit `IIf(IsDBNull(...))`. - Neue Sicherheitsprüfung für `JobListener` initialisiert. - `SaveDataTables` optimiert, Performance-Warnungen hinzugefügt. - Versionsnummer auf `2.5.0.0` aktualisiert, Copyright auf 2026. - Paket `NLog` aus `packages.config` entfernt.
This commit is contained in:
@@ -29,17 +29,17 @@ Public Class DatatableJob
|
||||
|
||||
Try
|
||||
Dim oMSSQL As MSSQLServer = oJobData.Item("MSSQL")
|
||||
Dim oCronSQL As String = $"SELECT * FROM TBAPPSERV_CRON_DETAIL WHERE CRON_ID = {oCronJobId}"
|
||||
Dim oCronSQL As String = $"SELECT * FROM TBAPPSERV_CRON_DETAIL WITH (NOLOCK) WHERE CRON_ID = {oCronJobId}"
|
||||
Dim oCronDetails As DataTable = oMSSQL.GetDatatable(oCronSQL)
|
||||
|
||||
For Each oRow As DataRow In oCronDetails.Rows
|
||||
Dim oDatatableName As String = NotNull(oRow.Item("DT_NAME"), String.Empty)
|
||||
Dim oParentColumn As String = NotNull(oRow.Item("DT_COLUMN"), String.Empty)
|
||||
Dim oChildColumn As String = NotNull(oRow.Item("CHILD_DT_COLUMN"), String.Empty)
|
||||
Dim oConnectionId As Integer = NotNull(oRow.Item("CON_ID"), String.Empty)
|
||||
Dim oTitle As String = NotNull(oRow.Item("TITLE"), String.Empty)
|
||||
Dim oSQL As String = NotNull(oRow.Item("COMMAND"), String.Empty)
|
||||
Dim oIndexColumns As String = NotNull(oRow.Item("INDEX_COLUMNS"), String.Empty)
|
||||
Dim oDatatableName As String = IIf(IsDBNull(oRow.Item("DT_NAME")), String.Empty, oRow.Item("DT_NAME"))
|
||||
Dim oParentColumn As String = IIf(IsDBNull(oRow.Item("DT_COLUMN")), String.Empty, oRow.Item("DT_COLUMN"))
|
||||
Dim oChildColumn As String = IIf(IsDBNull(oRow.Item("CHILD_DT_COLUMN")), String.Empty, oRow.Item("CHILD_DT_COLUMN"))
|
||||
Dim oConnectionId As Integer = IIf(IsDBNull(oRow.Item("CON_ID")), 0, oRow.Item("CON_ID"))
|
||||
Dim oTitle As String = IIf(IsDBNull(oRow.Item("TITLE")), String.Empty, oRow.Item("TITLE"))
|
||||
Dim oSQL As String = IIf(IsDBNull(oRow.Item("COMMAND")), String.Empty, oRow.Item("COMMAND"))
|
||||
Dim oIndexColumns As String = IIf(IsDBNull(oRow.Item("INDEX_COLUMNS")), String.Empty, oRow.Item("INDEX_COLUMNS"))
|
||||
|
||||
Try
|
||||
oLogger.Debug("Running Command-Job [{0}]", oTitle)
|
||||
@@ -80,7 +80,7 @@ Public Class DatatableJob
|
||||
.DetailRow = oRow
|
||||
}
|
||||
|
||||
Dim oChildTableNAme As String = NotNull(oRow.Item("CHILD_DT_NAME"), String.Empty)
|
||||
Dim oChildTableNAme As String = IIf(IsDBNull(oRow.Item("CHILD_DT_NAME")), String.Empty, oRow.Item("CHILD_DT_NAME"))
|
||||
|
||||
If oChildTableNAme <> String.Empty Then
|
||||
oLogger.Debug("Child Datatable [{0}] defined, Relation: Parent [{1}] -> Child [{2}]", oChildTableNAme, oParentColumn, oChildColumn)
|
||||
|
||||
@@ -11,7 +11,10 @@ Public Class JobListener
|
||||
Inherits JobListenerSupport
|
||||
|
||||
Public Overrides ReadOnly Property Name As String = "JobListener"
|
||||
Public Property Dataset As DataSet
|
||||
|
||||
' ✅ Verwaltung mit ReaderWriterLockSlim statt Shadow-Copy
|
||||
Private ReadOnly _Dataset As DataSet
|
||||
Private Shared ReadOnly _DatasetLock As New ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion)
|
||||
|
||||
Private ReadOnly _Logger As Logger
|
||||
Private ReadOnly _LogConfig As LogConfig
|
||||
@@ -23,9 +26,44 @@ Public Class JobListener
|
||||
_LogConfig = LogConfig
|
||||
_Logger = LogConfig.GetLogger()
|
||||
_MSSQL = MSSQL
|
||||
Dataset = ResultDataSet
|
||||
_Dataset = ResultDataSet
|
||||
|
||||
_Logger.Debug("JobListener initialized with ReaderWriterLockSlim for thread-safe access")
|
||||
End Sub
|
||||
|
||||
' ✅ Thread-sichere Methode zum Abrufen einer Tabelle
|
||||
Public Function GetTableSafely(tableName As String) As DataTable
|
||||
Try
|
||||
_DatasetLock.EnterReadLock()
|
||||
|
||||
If _Dataset.Tables.Contains(tableName) Then
|
||||
' Kopie nur von der einzelnen Tabelle, nicht vom ganzen Dataset!
|
||||
Return _Dataset.Tables(tableName).Copy()
|
||||
Else
|
||||
Return Nothing
|
||||
End If
|
||||
|
||||
Finally
|
||||
_DatasetLock.ExitReadLock()
|
||||
End Try
|
||||
End Function
|
||||
|
||||
' ✅ Thread-sichere Methode zum Prüfen ob Tabelle existiert
|
||||
Public Function TableExists(tableName As String) As Boolean
|
||||
Try
|
||||
_DatasetLock.EnterReadLock()
|
||||
Return _Dataset.Tables.Contains(tableName)
|
||||
Finally
|
||||
_DatasetLock.ExitReadLock()
|
||||
End Try
|
||||
End Function
|
||||
|
||||
' ✅ Thread-sicherer Zugriff auf Dataset (nur für GetCachedTables)
|
||||
Public Function GetCurrentDataset() As DataSet
|
||||
' Keine Kopie - nur für Metadaten-Zugriff (Tabellennamen)
|
||||
Return _Dataset
|
||||
End Function
|
||||
|
||||
Public Overrides Function JobWasExecuted(context As IJobExecutionContext, jobException As JobExecutionException, Optional cancellationToken As CancellationToken = Nothing) As Task
|
||||
_Logger.Info("Job [{0}] was executed successfully. Saving Data.", context.JobDetail.Key)
|
||||
|
||||
@@ -33,10 +71,11 @@ Public Class JobListener
|
||||
|
||||
For Each oTableResult As JobResult.ResultTable In oResult.Tables
|
||||
Try
|
||||
_Logger.Debug("Saving Datatable [{0}]", oTableResult.Table)
|
||||
_Logger.Debug("Saving Datatable [{0}]", oTableResult.Table.TableName)
|
||||
SaveDataTables(oTableResult)
|
||||
|
||||
Catch ex As Exception
|
||||
_Logger.Error(ex)
|
||||
_Logger.Error("Error while executing SaveDataTables for {0}", oTableResult.Table.TableName)
|
||||
End Try
|
||||
Next
|
||||
@@ -44,6 +83,86 @@ Public Class JobListener
|
||||
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()
|
||||
|
||||
Try
|
||||
Dim oTable As DataTable = Result.Table
|
||||
Dim oName As String = Result.DetailRow.Item("DT_NAME")
|
||||
Dim oDetailId As Integer = Result.DetailRow.Item("GUID")
|
||||
|
||||
_Logger.Debug("Starting thread-safe update for table [{0}]", oName)
|
||||
|
||||
' ✅ SCHRITT 1: Write-Lock anfordern (blockiert Leser während Update)
|
||||
oStopWatch.Start()
|
||||
_DatasetLock.EnterWriteLock()
|
||||
_Logger.Debug("[Lock] Write-Lock acquired for table [{0}]", oName)
|
||||
|
||||
Try
|
||||
' ✅ SCHRITT 2: Direkte Änderungen im Dataset
|
||||
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
|
||||
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)
|
||||
Else
|
||||
_Logger.Debug("[Performance] Table update completed in {0}ms", oStopWatch.ElapsedMilliseconds)
|
||||
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))
|
||||
|
||||
_MSSQL.ExecuteNonQuery($"INSERT INTO TBAPPSERV_CRON_DETAIL_HISTORY (DETAIL_ID) VALUES ({oDetailId})")
|
||||
|
||||
Catch ex As Exception
|
||||
_Logger.Warn("Unexpected error in SaveDataTables: {0}", ex.Message)
|
||||
_Logger.Error(ex)
|
||||
End Try
|
||||
End Sub
|
||||
Private Shared Function FormatBytes(bytes As Long) As String
|
||||
Const KB As Long = 1024
|
||||
Const MB As Long = KB * 1024
|
||||
Const GB As Long = MB * 1024
|
||||
|
||||
Select Case bytes
|
||||
Case >= GB
|
||||
Return $"{bytes / GB:N2} GB"
|
||||
Case >= MB
|
||||
Return $"{bytes / MB:N2} MB"
|
||||
Case >= KB
|
||||
Return $"{bytes / KB:N2} KB"
|
||||
Case Else
|
||||
Return $"{bytes} Bytes"
|
||||
End Select
|
||||
End Function
|
||||
|
||||
Public Sub ReplaceExistingTable(Name As String, Table As DataTable, DataSet As DataSet, Optional ChildTable As DataTable = Nothing)
|
||||
Try
|
||||
Dim oDatatableNameTemp As String = Name & "-TEMP"
|
||||
@@ -103,13 +222,12 @@ Public Class JobListener
|
||||
DataSet.Tables.Item(oDatatableNameTemp).TableName = Name
|
||||
Catch ex As Exception
|
||||
_Logger.Error(ex)
|
||||
Throw ex
|
||||
Throw
|
||||
End Try
|
||||
End Sub
|
||||
|
||||
Public Sub AddNewTable(Name As String, Table As DataTable, DataSet As DataSet)
|
||||
_Logger.Debug("DataTable [{0}] does not exist, adding to DataSet", Name)
|
||||
|
||||
DataSet.Tables.Add(Table)
|
||||
End Sub
|
||||
|
||||
@@ -117,9 +235,9 @@ Public Class JobListener
|
||||
Return $"{ParentTableName}-{ChildTableName}"
|
||||
End Function
|
||||
|
||||
Public Sub AddRelation(ParentTableName As String, ParentColumnName As String, ChildTableName As String, ChildColumnName As String)
|
||||
Dim oChild As DataTable = Dataset.Tables.Item(ChildTableName)
|
||||
Dim oParent As DataTable = Dataset.Tables.Item(ParentTableName)
|
||||
Public Sub AddRelation(ParentTableName As String, ParentColumnName As String, ChildTableName As String, ChildColumnName As String, TargetDataSet As DataSet)
|
||||
Dim oChild As DataTable = TargetDataSet.Tables.Item(ChildTableName)
|
||||
Dim oParent As DataTable = TargetDataSet.Tables.Item(ParentTableName)
|
||||
|
||||
Dim oRelationName As String = GetRelationName(ParentTableName, ChildTableName)
|
||||
Dim oParentColumn As DataColumn = oParent.Columns.Item(ParentColumnName)
|
||||
@@ -128,12 +246,13 @@ Public Class JobListener
|
||||
_Logger.Debug("New Relation: {0}/{1} => {2}/{3}", ParentTableName, ParentColumnName, ChildTableName, ChildColumnName)
|
||||
Try
|
||||
_Logger.Debug("Creating Relation [{0}]", oRelationName)
|
||||
_Logger.Debug("ParentColumn exists: {0}", IsNothing(oParentColumn))
|
||||
_Logger.Debug("ChildColumn exists: {0}", IsNothing(oChildColumn))
|
||||
_Logger.Debug("ParentColumn exists: {0}", Not IsNothing(oParentColumn))
|
||||
_Logger.Debug("ChildColumn exists: {0}", Not IsNothing(oChildColumn))
|
||||
|
||||
Dim oRelation As New DataRelation(oRelationName, oParentColumn, oChildColumn)
|
||||
|
||||
_Logger.Debug("Adding Relation [{0}]", oRelationName)
|
||||
Dataset.Relations.Add(oRelation)
|
||||
TargetDataSet.Relations.Add(oRelation)
|
||||
|
||||
Catch ex As ArgumentNullException
|
||||
_Logger.Error(ex)
|
||||
@@ -145,7 +264,7 @@ Public Class JobListener
|
||||
|
||||
Catch ex As InvalidConstraintException
|
||||
_Logger.Error(ex)
|
||||
_Logger.Warn("DuplicateNameException in AddRelation: {0}", ex.Message)
|
||||
_Logger.Warn("InvalidConstraintException in AddRelation: {0}", ex.Message)
|
||||
|
||||
Catch ex As ArgumentException
|
||||
_Logger.Error(ex)
|
||||
@@ -158,55 +277,6 @@ Public Class JobListener
|
||||
End Try
|
||||
End Sub
|
||||
|
||||
Public Sub SaveDataTables(Result As JobResult.ResultTable)
|
||||
Try
|
||||
Dim oTable As DataTable = Result.Table
|
||||
|
||||
Dim oName As String = Result.DetailRow.Item("DT_NAME")
|
||||
Dim oDetailId As Integer = Result.DetailRow.Item("GUID")
|
||||
|
||||
' Used for debugging relations and constraints
|
||||
'_Logger.Debug("Dataset BEFORE saving datatables")
|
||||
'ListTables(Dataset)
|
||||
|
||||
If Dataset.Tables.Contains(oName) Then
|
||||
_Logger.Debug("Datatable [{0}] already exists, replacing.", oName)
|
||||
|
||||
' Replace existing table
|
||||
If Result.ChildTable IsNot Nothing Then
|
||||
ReplaceExistingTable(oName, oTable, Dataset, Result.ChildTable)
|
||||
AddRelation(oName, Result.TableRelationColumn, Result.ChildTable.TableName, Result.ChildRelationColumn)
|
||||
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)
|
||||
End If
|
||||
End If
|
||||
|
||||
' Used for debugging relations and constraints
|
||||
'_Logger.Debug("Dataset AFTER saving datatables")
|
||||
'ListTables(Dataset)
|
||||
|
||||
Dim oMemoryUsage = GC.GetTotalMemory(True)
|
||||
Dim oUsageString = Utils.BytesToString(oMemoryUsage)
|
||||
|
||||
_Logger.Debug("Estimated memory usage after Job is [{0}]", oUsageString)
|
||||
|
||||
_MSSQL.ExecuteNonQuery($"INSERT INTO TBAPPSERV_CRON_DETAIL_HISTORY (DETAIL_ID) VALUES ({oDetailId})")
|
||||
Catch ex As Exception
|
||||
_Logger.Warn("Unexpected error in SaveDataTables: {0}", ex.Message)
|
||||
_Logger.Error(ex)
|
||||
End Try
|
||||
End Sub
|
||||
|
||||
Private Sub ListTables(dataset As DataSet)
|
||||
Dim oIndex As Integer = 1
|
||||
|
||||
@@ -226,4 +296,10 @@ Public Class JobListener
|
||||
oIndex += 1
|
||||
Next
|
||||
End Sub
|
||||
End Class
|
||||
|
||||
' ✅ Cleanup beim Dispose
|
||||
Protected Overrides Sub Finalize()
|
||||
_DatasetLock?.Dispose()
|
||||
MyBase.Finalize()
|
||||
End Sub
|
||||
End Class
|
||||
@@ -13,20 +13,25 @@ Public Class Scheduler
|
||||
Private ReadOnly _MSSQL As MSSQLServer
|
||||
Private _Scheduler As IScheduler
|
||||
Private ReadOnly _JobListener As JobListener
|
||||
Private ReadOnly _GlobalState As GlobalState ' ✅ NEU
|
||||
|
||||
Private ReadOnly _Props = New NameValueCollection From {
|
||||
{"quartz.serializer.type", "binary"},
|
||||
{"quartz.threadPool.maxConcurrency", 1}
|
||||
{"quartz.threadPool.maxConcurrency", 3}
|
||||
}
|
||||
|
||||
Private Const JOB_GROUP As String = "DatatableJobs"
|
||||
|
||||
Public Sub New(pLogConfig As LogConfig, pMSSQL_Config As MSSQLServer, pTableStore As DataSet)
|
||||
Public Sub New(pLogConfig As LogConfig, pMSSQL_Config As MSSQLServer, pTableStore As DataSet, pGlobalState As GlobalState)
|
||||
MyBase.New(pLogConfig)
|
||||
|
||||
_Factory = New StdSchedulerFactory(_Props)
|
||||
_MSSQL = pMSSQL_Config
|
||||
_JobListener = New JobListener(pLogConfig, _MSSQL, pTableStore)
|
||||
_GlobalState = pGlobalState
|
||||
|
||||
_GlobalState.JobListener = _JobListener
|
||||
Logger.Debug("JobListener registered in GlobalState")
|
||||
|
||||
Logging.LogProvider.SetCurrentLogProvider(New LogProvider(Logger))
|
||||
End Sub
|
||||
@@ -108,7 +113,7 @@ Public Class Scheduler
|
||||
End Sub
|
||||
Public Async Function GetCronJobs() As Task(Of DataTable)
|
||||
Try
|
||||
Dim oSQL As String = "SELECT * FROM TBAPPSERV_CRON_JOB WHERE ACTIVE = 1"
|
||||
Dim oSQL As String = "SELECT * FROM TBAPPSERV_CRON_JOB WITH (NOLOCK) WHERE ACTIVE = 1"
|
||||
Dim oDatatable As DataTable = Await _MSSQL.GetDatatableAsync(oSQL)
|
||||
|
||||
Return oDatatable
|
||||
@@ -119,7 +124,7 @@ Public Class Scheduler
|
||||
|
||||
Public Async Function GetCronJobDetails(CronJobId As Integer) As Task(Of DataTable)
|
||||
Try
|
||||
Dim oSQL As String = $"SELECT * FROM TBAPPSERV_CRON_DETAIL WHERE CRON_ID = {CronJobId}"
|
||||
Dim oSQL As String = $"SELECT * FROM TBAPPSERV_CRON_DETAIL WITH (NOLOCK) WHERE CRON_ID = {CronJobId}"
|
||||
Dim oDatatable As DataTable = Await _MSSQL.GetDatatableAsync(oSQL)
|
||||
|
||||
Return oDatatable
|
||||
|
||||
Reference in New Issue
Block a user