From 8220852e568126a3f6024e1b84f53cd467423c72 Mon Sep 17 00:00:00 2001 From: Developer01 Date: Tue, 13 Oct 2026 12:04:13 +0200 Subject: [PATCH] =?UTF-8?q?Verbesserung=20von=20Performance=20und=20Monito?= =?UTF-8?q?ring=20Neue=20Performance-Metriken=20in=20`EDMIService`=20hinzu?= =?UTF-8?q?gef=C3=BCgt,=20inkl.=20Felder=20f=C3=BCr=20Job-Dauer,=20langsam?= =?UTF-8?q?e=20Queries=20und=20Timestamps.=20`ServiceStatusResponse`=20um?= =?UTF-8?q?=20diese=20Metriken=20erweitert.=20`DatatableJob`=20optimiert:?= =?UTF-8?q?=20``=20hinzugef=C3=BCgt,=20Loggin?= =?UTF-8?q?g=20f=C3=BCr=20langsame=20Queries=20(>10s)=20und=20Jobs=20(>30s?= =?UTF-8?q?)=20verbessert,=20sowie=20Gesamt-Dauer-Messung=20integriert.=20?= =?UTF-8?q?`Scheduler`=20nutzt=20nun=20dedizierte=20MSSQL-Instanz=20zur=20?= =?UTF-8?q?Entkopplung=20von=20Datenbankoperationen.=20Thread-Pool-Gr?= =?UTF-8?q?=C3=B6=C3=9Fe=20reduziert=20und=20gestaffeltes=20Starten=20von?= =?UTF-8?q?=20INIT-Jobs=20eingef=C3=BChrt.=20Connection-Strings=20in=20`Wi?= =?UTF-8?q?ndowsService`=20angepasst:=20`ApplicationName`=20spezifisch=20g?= =?UTF-8?q?esetzt=20und=20`MaxPoolSize`=20erh=C3=B6ht.=20Allgemeine=20Logg?= =?UTF-8?q?ing-Verbesserungen=20f=C3=BCr=20bessere=20Skalierbarkeit=20und?= =?UTF-8?q?=20Stabilit=C3=A4t.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Service.EDMIService/EDMIService.vb | 29 ++++++++++-- .../Messages/GetServiceStatus.vb | 5 ++ .../My Project/AssemblyInfo.vb | 6 +-- Service.EDMIService/Scheduler/DatatableJob.vb | 39 ++++++++++------ Service.EDMIService/Scheduler/Scheduler.vb | 46 +++++++++++-------- Service.EDMIService/WindowsService.vb | 29 +++++++----- 6 files changed, 102 insertions(+), 52 deletions(-) diff --git a/Service.EDMIService/EDMIService.vb b/Service.EDMIService/EDMIService.vb index 2c36be07..334f0c64 100644 --- a/Service.EDMIService/EDMIService.vb +++ b/Service.EDMIService/EDMIService.vb @@ -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 - ' ───────────────────────────────────────────────────────────────────────── + + ''' + ''' Wird vom JobListener nach jedem DatatableJob aufgerufen. + ''' + 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 diff --git a/Service.EDMIService/Messages/GetServiceStatus.vb b/Service.EDMIService/Messages/GetServiceStatus.vb index 01bed182..ebf12c30 100644 --- a/Service.EDMIService/Messages/GetServiceStatus.vb +++ b/Service.EDMIService/Messages/GetServiceStatus.vb @@ -8,5 +8,10 @@ Namespace Messages Public Property ActiveSessions As Dictionary(Of String, String) Public Property Timestamp As DateTime Public Property RecentUsers As List(Of String) = New List(Of String)() + Public Property LastJobDurationMs As Long = 0 + Public Property LastJobName As String = String.Empty + Public Property LastJobTimestamp As DateTime = DateTime.MinValue + Public Property SlowQueryCount As Integer = 0 ' Queries > 10s seit Service-Start + Public Property CachedTablesSnapshotAge As Integer = 0 ' Sekunden seit letztem Snapshot End Class End Namespace \ No newline at end of file diff --git a/Service.EDMIService/My Project/AssemblyInfo.vb b/Service.EDMIService/My Project/AssemblyInfo.vb index 9dbea938..3df4911a 100644 --- a/Service.EDMIService/My Project/AssemblyInfo.vb +++ b/Service.EDMIService/My Project/AssemblyInfo.vb @@ -13,7 +13,7 @@ Imports System.Runtime.InteropServices - + @@ -31,5 +31,5 @@ Imports System.Runtime.InteropServices ' übernehmen, indem Sie "*" eingeben: ' - - + + diff --git a/Service.EDMIService/Scheduler/DatatableJob.vb b/Service.EDMIService/Scheduler/DatatableJob.vb index b1c9e193..9eda1514 100644 --- a/Service.EDMIService/Scheduler/DatatableJob.vb +++ b/Service.EDMIService/Scheduler/DatatableJob.vb @@ -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 + 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) diff --git a/Service.EDMIService/Scheduler/Scheduler.vb b/Service.EDMIService/Scheduler/Scheduler.vb index a3833e4a..73d1c73b 100644 --- a/Service.EDMIService/Scheduler/Scheduler.vb +++ b/Service.EDMIService/Scheduler/Scheduler.vb @@ -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 \ No newline at end of file diff --git a/Service.EDMIService/WindowsService.vb b/Service.EDMIService/WindowsService.vb index db69c91f..7ffd71e2 100644 --- a/Service.EDMIService/WindowsService.vb +++ b/Service.EDMIService/WindowsService.vb @@ -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