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