Verbesserung von Performance und Monitoring
Neue Performance-Metriken in `EDMIService` hinzugefügt, inkl. Felder für Job-Dauer, langsame Queries und Timestamps. `ServiceStatusResponse` um diese Metriken erweitert. `DatatableJob` optimiert: `<DisallowConcurrentExecution>` hinzugefügt, Logging für langsame Queries (>10s) und Jobs (>30s) verbessert, sowie Gesamt-Dauer-Messung integriert. `Scheduler` nutzt nun dedizierte MSSQL-Instanz zur Entkopplung von Datenbankoperationen. Thread-Pool-Größe reduziert und gestaffeltes Starten von INIT-Jobs eingeführt. Connection-Strings in `WindowsService` angepasst: `ApplicationName` spezifisch gesetzt und `MaxPoolSize` erhöht. Allgemeine Logging-Verbesserungen für bessere Skalierbarkeit und Stabilität.
This commit is contained in:
@@ -34,6 +34,11 @@ Public Class EDMIService
|
||||
' ► 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)
|
||||
' ── Performance-Metriken (thread-safe) ───────────────────────────────
|
||||
Private Shared _LastJobDurationMs As Long = 0
|
||||
Private Shared _LastJobName As String = String.Empty
|
||||
Private Shared _LastJobTimestamp As DateTime = DateTime.MinValue
|
||||
Private Shared _SlowQueryCount As Long = 0
|
||||
|
||||
Public Shared ReadOnly Property ActiveSessions As Integer
|
||||
Get
|
||||
@@ -46,7 +51,6 @@ Public Class EDMIService
|
||||
Return _ActiveSessions.Values.Distinct().OrderBy(Function(u) u).ToList().AsReadOnly()
|
||||
End Get
|
||||
End Property
|
||||
' ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Private ReadOnly _Logger As Logger
|
||||
Private ReadOnly _Debug As Boolean = False
|
||||
@@ -56,7 +60,19 @@ Public Class EDMIService
|
||||
Private Shared _ConfiguredEndpoint As String = String.Empty
|
||||
Private Shared _ConfiguredMaxCalls As Integer = 0
|
||||
Private Shared _ConfiguredMaxSessions As Integer = 0
|
||||
' ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
''' <summary>
|
||||
''' Wird vom JobListener nach jedem DatatableJob aufgerufen.
|
||||
''' </summary>
|
||||
Public Shared Sub PublishJobMetrics(pJobName As String, pDurationMs As Long)
|
||||
Interlocked.Exchange(_LastJobDurationMs, pDurationMs)
|
||||
Interlocked.Exchange(_LastJobTimestamp, DateTime.Now)
|
||||
_LastJobName = pJobName ' String-Zuweisung ist atomar auf 64-Bit CLR
|
||||
If pDurationMs > 10000 Then
|
||||
Interlocked.Increment(_SlowQueryCount)
|
||||
End If
|
||||
End Sub
|
||||
|
||||
|
||||
|
||||
Public Shared Sub Configure(Config As ServiceConfiguration)
|
||||
@@ -180,13 +196,18 @@ Public Class EDMIService
|
||||
OrderByDescending(Function(p) p.Value).
|
||||
Select(Function(p) $"{p.Key} (zuletzt: {p.Value:HH:mm:ss})").
|
||||
ToList()
|
||||
|
||||
Dim oState = _CachedTablesState
|
||||
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,
|
||||
.RecentUsers = oRecentUserList ' ◄ NEU – im Response-Objekt ergänzen
|
||||
.RecentUsers = oRecentUserList, ' ◄ NEU – im Response-Objekt ergänzen
|
||||
.LastJobDurationMs = Interlocked.Read(_LastJobDurationMs),
|
||||
.LastJobName = _LastJobName,
|
||||
.LastJobTimestamp = _LastJobTimestamp,
|
||||
.SlowQueryCount = CInt(Interlocked.Read(_SlowQueryCount)),
|
||||
.CachedTablesSnapshotAge = CInt((DateTime.Now - oState.Timestamp).TotalSeconds)
|
||||
}
|
||||
End Function
|
||||
#End Region
|
||||
|
||||
@@ -8,5 +8,10 @@ Namespace Messages
|
||||
<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)()
|
||||
<DataMember(Order:=6)> Public Property LastJobDurationMs As Long = 0
|
||||
<DataMember(Order:=7)> Public Property LastJobName As String = String.Empty
|
||||
<DataMember(Order:=8)> Public Property LastJobTimestamp As DateTime = DateTime.MinValue
|
||||
<DataMember(Order:=9)> Public Property SlowQueryCount As Integer = 0 ' Queries > 10s seit Service-Start
|
||||
<DataMember(Order:=10)> Public Property CachedTablesSnapshotAge As Integer = 0 ' Sekunden seit letztem Snapshot
|
||||
End Class
|
||||
End Namespace
|
||||
@@ -13,7 +13,7 @@ Imports System.Runtime.InteropServices
|
||||
<Assembly: AssemblyCompany("Digital Data")>
|
||||
<Assembly: AssemblyProduct("EDMIService")>
|
||||
<Assembly: AssemblyCopyright("2026")>
|
||||
<Assembly: AssemblyTrademark("2.8.0")>
|
||||
<Assembly: AssemblyTrademark("DDEDMIService")>
|
||||
|
||||
<Assembly: ComVisible(False)>
|
||||
|
||||
@@ -31,5 +31,5 @@ Imports System.Runtime.InteropServices
|
||||
' übernehmen, indem Sie "*" eingeben:
|
||||
' <Assembly: AssemblyVersion("1.0.*")>
|
||||
|
||||
<Assembly: AssemblyVersion("2.8.0.0")>
|
||||
<Assembly: AssemblyFileVersion("2.8.0.0")>
|
||||
<Assembly: AssemblyVersion("2.9.0.0")>
|
||||
<Assembly: AssemblyFileVersion("2.9.0.0")>
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
Option Explicit On
|
||||
|
||||
Imports DigitalData.Modules.Database
|
||||
Imports DigitalData.Modules.Language.Utils
|
||||
Imports DigitalData.Modules.Logging
|
||||
|
||||
Imports Quartz
|
||||
|
||||
Public Class DatatableJob
|
||||
<DisallowConcurrentExecution> Public Class DatatableJob
|
||||
Implements IJob
|
||||
|
||||
|
||||
|
||||
Private Const COMMAND_SQL_TIMEOUT = 240
|
||||
|
||||
Private _MSSQL As MSSQLServer
|
||||
|
||||
Public Function Execute(context As IJobExecutionContext) As Task Implements IJob.Execute
|
||||
Dim oJobData = context.MergedJobDataMap
|
||||
Dim oLogConfig As LogConfig = oJobData.Item("LogConfig")
|
||||
@@ -26,7 +19,8 @@ Public Class DatatableJob
|
||||
Dim oResult As New JobResult()
|
||||
|
||||
oLogger.Info("Running Datatable Job [{0}]/[{1}]", context.JobDetail.Key, oCronJobTitle)
|
||||
|
||||
Dim oJobStopWatch As New Stopwatch() ' ← Gesamt-Messung für den gesamten Job
|
||||
oJobStopWatch.Start()
|
||||
Try
|
||||
Dim oMSSQL As MSSQLServer = oJobData.Item("MSSQL")
|
||||
Dim oCronSQL As String = $"SELECT * FROM TBAPPSERV_CRON_DETAIL WITH (NOLOCK) WHERE CRON_ID = {oCronJobId}"
|
||||
@@ -46,16 +40,23 @@ Public Class DatatableJob
|
||||
oLogger.Debug("Datatable Name: {0}", oDatatableName)
|
||||
oLogger.Debug("Connection Id: {0}", oConnectionId)
|
||||
|
||||
oStopWatch.Start()
|
||||
oStopWatch.Restart()
|
||||
|
||||
Dim oConnectionString = oMSSQL.Get_ConnectionStringforID(oConnectionId)
|
||||
Dim oDecryptedConnectionString = MSSQLServer.DecryptConnectionString(oConnectionString)
|
||||
Dim oConnectionString As String = oMSSQL.Get_ConnectionStringforID(oConnectionId)
|
||||
|
||||
Dim oTable = oMSSQL.GetDatatableWithConnection(oSQL, oDecryptedConnectionString, COMMAND_SQL_TIMEOUT)
|
||||
Dim oTable = oMSSQL.GetDatatableWithConnection(oSQL, oConnectionString, COMMAND_SQL_TIMEOUT)
|
||||
oTable.TableName = oDatatableName
|
||||
|
||||
oStopWatch.Stop()
|
||||
oLogger.Debug("[Time] Getting Data From DB: {0}ms", oStopWatch.ElapsedMilliseconds)
|
||||
Dim oQueryMs = oStopWatch.ElapsedMilliseconds
|
||||
|
||||
' ✅ Schwellwert-Warnung: Query dauert länger als 10s
|
||||
If oQueryMs > 10000 Then
|
||||
oLogger.Warn("⚠ SLOW QUERY [{0}] | Datatable=[{1}] | Duration=[{2}ms] | SQL=[{3}]",
|
||||
oTitle, oDatatableName, oQueryMs, oSQL.Substring(0, Math.Min(200, oSQL.Length)))
|
||||
Else
|
||||
oLogger.Debug("[Time] Getting Data From DB: {0}ms", oQueryMs)
|
||||
End If
|
||||
oStopWatch.Restart()
|
||||
|
||||
Dim oView As DataView = Nothing
|
||||
@@ -107,7 +108,15 @@ Public Class DatatableJob
|
||||
|
||||
' Das Ergebnis speichern
|
||||
context.Result = oResult
|
||||
oLogger.Info("✔✔ Fetched [{0}] Datatables ✔✔", oResult.Tables.Count)
|
||||
oJobStopWatch.Stop()
|
||||
' ✅ Gesamt-Performance-Log pro Job-Ausführung
|
||||
Dim oTotalMs = oJobStopWatch.ElapsedMilliseconds
|
||||
If oTotalMs > 30000 Then
|
||||
oLogger.Warn("⚠ SLOW JOB [{0}] | Tables=[{1}] | TotalDuration=[{2}ms]",
|
||||
oCronJobTitle, oResult.Tables.Count, oTotalMs)
|
||||
Else
|
||||
oLogger.Info("✔✔ Fetched [{0}] Datatables in [{1}ms] ✔✔", oResult.Tables.Count, oTotalMs)
|
||||
End If
|
||||
|
||||
Catch ex As Exception
|
||||
oLogger.Error(ex)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Imports System.Collections.Specialized
|
||||
Imports System.Data.SqlClient
|
||||
Imports DigitalData.Modules.Database
|
||||
Imports DigitalData.Modules.Logging
|
||||
Imports Quartz
|
||||
@@ -10,14 +11,14 @@ Public Class Scheduler
|
||||
Inherits BaseClass
|
||||
|
||||
Private ReadOnly _Factory As StdSchedulerFactory
|
||||
Private ReadOnly _MSSQL As MSSQLServer
|
||||
Private ReadOnly _SchedulerMSSQL As MSSQLServer ' ✅ Dedizierte Connection für alle Scheduler-Operationen
|
||||
Private _Scheduler As IScheduler
|
||||
Private ReadOnly _JobListener As JobListener
|
||||
Private ReadOnly _GlobalState As GlobalState ' ✅ NEU
|
||||
Private ReadOnly _GlobalState As GlobalState
|
||||
|
||||
Private ReadOnly _Props = New NameValueCollection From {
|
||||
{"quartz.serializer.type", "binary"},
|
||||
{"quartz.threadPool.maxConcurrency", 3}
|
||||
{"quartz.threadPool.maxConcurrency", 2} ' ← von 3 auf 2
|
||||
}
|
||||
|
||||
Private Const JOB_GROUP As String = "DatatableJobs"
|
||||
@@ -26,8 +27,17 @@ Public Class Scheduler
|
||||
MyBase.New(pLogConfig)
|
||||
|
||||
_Factory = New StdSchedulerFactory(_Props)
|
||||
_MSSQL = pMSSQL_Config
|
||||
_JobListener = New JobListener(pLogConfig, _MSSQL, pTableStore)
|
||||
|
||||
' ✅ Separater Connection-Pool ausschließlich für Scheduler-Jobs
|
||||
' Application Name weicht ab → ADO.NET legt eigenständigen Pool an
|
||||
' → Scheduler-Queries blockieren nie die WCF-Verbindungen des Service
|
||||
Dim oBuilder As New SqlConnectionStringBuilder(pMSSQL_Config.CurrentConnectionString)
|
||||
oBuilder.ApplicationName = "EDMIService-Scheduler"
|
||||
oBuilder.MaxPoolSize = 2 ' ← NEU: begrenzt SQL Server-Last unter Volllast
|
||||
_SchedulerMSSQL = New MSSQLServer(pLogConfig, oBuilder.ToString())
|
||||
Logger.Info("Scheduler: dedicated MSSQLServer instance created | AppName=[{0}]", oBuilder.ApplicationName)
|
||||
|
||||
_JobListener = New JobListener(pLogConfig, _SchedulerMSSQL, pTableStore)
|
||||
_GlobalState = pGlobalState
|
||||
|
||||
_GlobalState.JobListener = _JobListener
|
||||
@@ -37,14 +47,11 @@ Public Class Scheduler
|
||||
End Sub
|
||||
|
||||
Public Async Sub Start()
|
||||
' Get new scheduler
|
||||
_Scheduler = Await _Factory.GetScheduler()
|
||||
|
||||
' configure it
|
||||
_Scheduler.ListenerManager.AddJobListener(_JobListener,
|
||||
GroupMatcher(Of JobKey).GroupEquals(JOB_GROUP))
|
||||
|
||||
' start it
|
||||
Await _Scheduler.Start()
|
||||
|
||||
Dim oCronjobs As DataTable = Await GetCronJobs()
|
||||
@@ -66,10 +73,13 @@ Public Class Scheduler
|
||||
|
||||
Dim oJobData As New JobDataMap From {
|
||||
{"LogConfig", LogConfig},
|
||||
{"MSSQL", _MSSQL},
|
||||
{"MSSQL", _SchedulerMSSQL},
|
||||
{"CronJobId", oGuid},
|
||||
{"CronJobTitle", oTitle}
|
||||
}
|
||||
' INIT-Job: gestaffelt starten – 15s Abstand statt 10s
|
||||
' Bei maxConcurrency=2 laufen max. 2 Jobs gleichzeitig
|
||||
Dim oInitDelaySeconds As Integer = oCronjobs.Rows.IndexOf(oRow) * 15
|
||||
|
||||
Dim oIdentity As String = $"CRON-JOB-{oGuid}"
|
||||
|
||||
@@ -83,15 +93,16 @@ Public Class Scheduler
|
||||
UsingJobData(oJobData).
|
||||
Build()
|
||||
|
||||
Await _Scheduler.ScheduleJob(oJob, oCronTrigger)
|
||||
Dim oNextFireTime As DateTimeOffset? = Await _Scheduler.ScheduleJob(oJob, oCronTrigger)
|
||||
Logger.Info("Cron Job [{0}] -> next scheduled run: {1}", oTitle,
|
||||
If(oNextFireTime.HasValue, oNextFireTime.Value.LocalDateTime.ToString("yyyy-MM-dd HH:mm:ss"), "unknown"))
|
||||
|
||||
' Also create variant of the job that runs immediately, once
|
||||
|
||||
Dim oInitIdentity As String = $"CRON-JOB-INIT-{oGuid}"
|
||||
|
||||
oInitTrigger = TriggerBuilder.Create().
|
||||
WithIdentity(oInitIdentity & "-TRIGGER", JOB_GROUP).
|
||||
StartNow().
|
||||
StartAt(DateTimeOffset.Now.AddSeconds(oInitDelaySeconds)). ' ✅ war: StartNow()
|
||||
Build()
|
||||
|
||||
oInitJob = JobBuilder.Create(Of DatatableJob)().
|
||||
@@ -112,12 +123,11 @@ Public Class Scheduler
|
||||
Logger.Warn("⚠ Unexpected Error while setting up scheduler: " & ex.Message)
|
||||
End Try
|
||||
End Sub
|
||||
|
||||
Public Async Function GetCronJobs() As Task(Of DataTable)
|
||||
Try
|
||||
Dim oSQL As String = "SELECT * FROM TBAPPSERV_CRON_JOB WITH (NOLOCK) WHERE ACTIVE = 1"
|
||||
Dim oDatatable As DataTable = Await _MSSQL.GetDatatableAsync(oSQL)
|
||||
|
||||
Return oDatatable
|
||||
Return Await _SchedulerMSSQL.GetDatatableAsync(oSQL) ' ✅ dedizierte Instanz
|
||||
Catch ex As Exception
|
||||
Return Nothing
|
||||
End Try
|
||||
@@ -126,9 +136,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 WITH (NOLOCK) WHERE CRON_ID = {CronJobId}"
|
||||
Dim oDatatable As DataTable = Await _MSSQL.GetDatatableAsync(oSQL)
|
||||
|
||||
Return oDatatable
|
||||
Return Await _SchedulerMSSQL.GetDatatableAsync(oSQL) ' ✅ dedizierte Instanz
|
||||
Catch ex As Exception
|
||||
Return Nothing
|
||||
End Try
|
||||
@@ -137,4 +145,4 @@ Public Class Scheduler
|
||||
Public Async Sub [Stop]()
|
||||
Await _Scheduler.Shutdown()
|
||||
End Sub
|
||||
End Class
|
||||
End Class
|
||||
@@ -1,11 +1,12 @@
|
||||
Imports System.ServiceModel
|
||||
Imports System.ServiceProcess
|
||||
Imports DigitalData.Modules.Logging
|
||||
Imports DigitalData.Modules.Database
|
||||
Imports DigitalData.Modules
|
||||
Imports System.ServiceModel.Description
|
||||
Imports DigitalData.Modules.Config
|
||||
Imports System.Data.SqlClient
|
||||
Imports System.ServiceModel
|
||||
Imports System.ServiceModel.Channels
|
||||
Imports System.ServiceModel.Description
|
||||
Imports System.ServiceProcess
|
||||
Imports DigitalData.Modules
|
||||
Imports DigitalData.Modules.Config
|
||||
Imports DigitalData.Modules.Database
|
||||
Imports DigitalData.Modules.Logging
|
||||
|
||||
Public Class WindowsService
|
||||
Inherits ServiceBase
|
||||
@@ -187,8 +188,11 @@ Public Class WindowsService
|
||||
Throw New ArgumentNullException("ConnectionString_ECM")
|
||||
End If
|
||||
|
||||
Dim oMSSQL = New MSSQLServer(pLogConfig, Config.ConnectionString_ECM)
|
||||
Logger.Info("✔ Database connection to ECM Database established.")
|
||||
Dim oBuilder As New SqlConnectionStringBuilder(Config.ConnectionString_ECM)
|
||||
oBuilder.ApplicationName = "DD_EDMIAppService"
|
||||
oBuilder.MaxPoolSize = Math.Max(50, oBuilder.MaxPoolSize) ' ← nie kleiner als 50 setzen
|
||||
Dim oMSSQL = New MSSQLServer(pLogConfig, oBuilder.ToString())
|
||||
Logger.Info("✔ Database connection to ECM Database established | MaxPool=[{0}]", oBuilder.MaxPoolSize)
|
||||
Return oMSSQL
|
||||
End Function
|
||||
|
||||
@@ -199,8 +203,11 @@ Public Class WindowsService
|
||||
Throw New ArgumentNullException("ConnectionString_IDB")
|
||||
End If
|
||||
|
||||
Dim oMSSQL = New MSSQLServer(pLogConfig, Config.ConnectionString_IDB)
|
||||
Logger.Info("✔ Database connection to IDB Database established.")
|
||||
Dim oBuilder As New SqlConnectionStringBuilder(Config.ConnectionString_IDB)
|
||||
oBuilder.ApplicationName = "DD_EDMIAppService-IDB"
|
||||
oBuilder.MaxPoolSize = Math.Max(20, oBuilder.MaxPoolSize) ' ← nie kleiner als 20 setzen
|
||||
Dim oMSSQL = New MSSQLServer(pLogConfig, oBuilder.ToString())
|
||||
Logger.Info("✔ Database connection to IDB Database established | MaxPool=[{0}]", oBuilder.MaxPoolSize)
|
||||
Return oMSSQL
|
||||
End Function
|
||||
|
||||
|
||||
Reference in New Issue
Block a user