Compare commits
5 Commits
ab7e063cbe
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
205e0ad302 | ||
|
|
2d1b21d728 | ||
|
|
9fbcb97fe4 | ||
| 81a43d1705 | |||
|
|
d15d464034 |
@@ -31,5 +31,5 @@ Imports System.Runtime.InteropServices
|
||||
' indem Sie "*" wie unten gezeigt eingeben:
|
||||
' <Assembly: AssemblyVersion("1.0.*")>
|
||||
|
||||
<Assembly: AssemblyVersion("2.1.0.0")>
|
||||
<Assembly: AssemblyFileVersion("2.1.0.0")>
|
||||
<Assembly: AssemblyVersion("2.2.0.0")>
|
||||
<Assembly: AssemblyFileVersion("2.2.0.0")>
|
||||
|
||||
@@ -78,6 +78,7 @@ Public Class ClassSnapPanel
|
||||
End Sub
|
||||
|
||||
Private Sub ScheduleScrollUpdate()
|
||||
If Not IsHandleCreated Then Return
|
||||
If Not _NeedsScrollUpdate Then
|
||||
_NeedsScrollUpdate = True
|
||||
BeginInvoke(New MethodInvoker(Sub()
|
||||
|
||||
@@ -85,6 +85,13 @@ Public Class ctrlObjectPropertyDialog
|
||||
End If
|
||||
' Load Business Entities
|
||||
Dim oEntityIds = Await GetBusinessEntitiesForObjectId(pObjectId)
|
||||
|
||||
If oEntityIds.Count = 0 Then
|
||||
_Logger.Warn("Keine Business Entities für ObjectId {0} gefunden.", pObjectId)
|
||||
Helper.ShowErrorMessage(New ApplicationException($"Für das Objekt '{pObjectId}' wurden keine Business Entities gefunden."), "LoadObject")
|
||||
Return False
|
||||
End If
|
||||
|
||||
Dim oActiveEntity = oEntityIds.FirstOrDefault()
|
||||
|
||||
Dim oCombobox As RepositoryItemComboBox = DirectCast(cmbEntityId.Edit, RepositoryItemComboBox)
|
||||
|
||||
@@ -154,7 +154,8 @@ Public Class frmObjectPropertyDialog
|
||||
End Sub
|
||||
|
||||
Private Sub BaseEdit_EditValueChanged(sender As Object, e As EventArgs)
|
||||
Dim oControl As BaseEdit = sender
|
||||
Dim oControlItem As LayoutControlItem = sender
|
||||
Dim oControl As BaseEdit = oControlItem.Control
|
||||
Dim oAttribute As Attribute = oControl.Tag
|
||||
Dim oValue = oControl.EditValue
|
||||
Dim oKey = oAttribute.Title
|
||||
@@ -163,7 +164,6 @@ Public Class frmObjectPropertyDialog
|
||||
Changes.Item(oKey) = oValue
|
||||
Else
|
||||
Changes.Add(oAttribute.Title, oValue)
|
||||
|
||||
End If
|
||||
End Sub
|
||||
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
Imports System.IO
|
||||
Imports System.Diagnostics
|
||||
Imports System.IO
|
||||
Imports System.ServiceModel
|
||||
Imports System.ServiceModel.Description
|
||||
Imports System.Threading
|
||||
Imports DigitalData.Modules.Database
|
||||
Imports DigitalData.Modules.Logging
|
||||
'Imports DigitalData.Modules.
|
||||
Imports DigitalData.Modules.EDMI
|
||||
Imports DigitalData.Modules.EDMI.API.Rights
|
||||
Imports DigitalData.Services.EDMIService.Messages
|
||||
Imports DigitalData.Modules.Logging
|
||||
Imports DigitalData.Services.EDMIService.Exceptions
|
||||
Imports DigitalData.Services.EDMIService.Messages
|
||||
Imports DigitalData.Services.EDMIService.Methods
|
||||
Imports DigitalData.Services.EDMIService.Methods.Database
|
||||
Imports DigitalData.Services.EDMIService.Methods.IDB
|
||||
|
||||
|
||||
<ServiceBehavior(InstanceContextMode:=InstanceContextMode.PerSession)>
|
||||
<ServiceBehavior(InstanceContextMode:=InstanceContextMode.PerSession,
|
||||
ConcurrencyMode:=ConcurrencyMode.Multiple)>
|
||||
Public Class EDMIService
|
||||
Implements IEDMIService
|
||||
|
||||
@@ -29,6 +31,9 @@ Public Class EDMIService
|
||||
Public Const TBIDB_ACCESSRIGHT = "TBIDB_ACCESSRIGHT"
|
||||
' ── Aktive Sessions: SessionId → Username (thread-safe) ──────────────────
|
||||
Private Shared ReadOnly _ActiveSessions As New Collections.Concurrent.ConcurrentDictionary(Of String, String)
|
||||
' ► 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)
|
||||
|
||||
Public Shared ReadOnly Property ActiveSessions As Integer
|
||||
Get
|
||||
@@ -69,16 +74,37 @@ Public Class EDMIService
|
||||
oThrottling = New ServiceThrottlingBehavior()
|
||||
Config.Description.Behaviors.Add(oThrottling)
|
||||
End If
|
||||
|
||||
Dim oCpuCores As Integer = Environment.ProcessorCount
|
||||
oThrottling.MaxConcurrentCalls = Math.Max(200, 32 * oCpuCores) ' war 16×
|
||||
oThrottling.MaxConcurrentSessions = Math.Max(400, 150 * oCpuCores) ' war 100×
|
||||
|
||||
' ← Server-seitige Kapazität für 300+ User
|
||||
oThrottling.MaxConcurrentCalls = Math.Max(400, 32 * oCpuCores)
|
||||
oThrottling.MaxConcurrentSessions = Math.Max(600, 150 * oCpuCores)
|
||||
oThrottling.MaxConcurrentInstances = Integer.MaxValue
|
||||
|
||||
' ── Werte für späteres Logging speichern ─────────────────────────────
|
||||
' ← Server-seitiges Binding: toleriert langsamere Clients (alte DLL!)
|
||||
Dim oNetTcpBinding = TryCast(oBinding, NetTcpBinding)
|
||||
If oNetTcpBinding IsNot Nothing Then
|
||||
' ← Konsistent mit Channel.GetBinding(): CPUs × 50, mind. 500
|
||||
Dim oMaxConnections = Math.Max(500, oCpuCores * 50)
|
||||
oNetTcpBinding.SendTimeout = TimeSpan.FromMinutes(5)
|
||||
oNetTcpBinding.ReceiveTimeout = TimeSpan.FromMinutes(10)
|
||||
oNetTcpBinding.OpenTimeout = TimeSpan.FromMinutes(2)
|
||||
oNetTcpBinding.MaxConnections = oMaxConnections
|
||||
LogConfig?.GetLogger()?.Debug("✔ NetTcpBinding configured | MaxConnections=[{0}] | SendTimeout=[{1}]",
|
||||
oMaxConnections, oNetTcpBinding.SendTimeout)
|
||||
End If
|
||||
' ← NEU: Windows Auth Caching – reduziert DC-Anfragen bei parallelen Verbindungen
|
||||
Dim oServiceCredentials = Config.Description.Behaviors.Find(Of ServiceCredentials)()
|
||||
If oServiceCredentials Is Nothing Then
|
||||
oServiceCredentials = New ServiceCredentials()
|
||||
Config.Description.Behaviors.Add(oServiceCredentials)
|
||||
End If
|
||||
' Gruppenauflösung deaktivieren: weniger DC-Roundtrips pro Verbindung
|
||||
oServiceCredentials.WindowsAuthentication.IncludeWindowsGroups = False
|
||||
_ConfiguredEndpoint = oBaseAddress.ToString()
|
||||
_ConfiguredMaxCalls = oThrottling.MaxConcurrentCalls
|
||||
_ConfiguredMaxSessions = oThrottling.MaxConcurrentSessions
|
||||
' ─────────────────────────────────────────────────────────────────────
|
||||
End Sub
|
||||
|
||||
Public Sub New()
|
||||
@@ -91,29 +117,31 @@ Public Class EDMIService
|
||||
_Logger = LogConfig.GetLogger()
|
||||
_IDBHelpers = New IDB.Helpers(LogConfig, MSSQL_IDB)
|
||||
|
||||
' ── Konfiguration beim ersten Aufruf loggen ───────────────────────────
|
||||
If Not String.IsNullOrEmpty(_ConfiguredEndpoint) Then
|
||||
Dim oEndpointSnapshot = Interlocked.Exchange(Of String)(_ConfiguredEndpoint, String.Empty)
|
||||
If Not String.IsNullOrEmpty(oEndpointSnapshot) Then
|
||||
_Logger.Info("✔ EDMIService configured | Endpoint=[{0}] | MaxCalls=[{1}] | MaxSessions=[{2}] | CPUs=[{3}]",
|
||||
_ConfiguredEndpoint, _ConfiguredMaxCalls, _ConfiguredMaxSessions, Environment.ProcessorCount)
|
||||
_ConfiguredEndpoint = String.Empty
|
||||
oEndpointSnapshot, _ConfiguredMaxCalls, _ConfiguredMaxSessions, Environment.ProcessorCount)
|
||||
End If
|
||||
' ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
' ── Session registrieren ──────────────────────────────────────────────
|
||||
_ActiveSessions(oSessionId) = _Username
|
||||
BuildUserList()
|
||||
' ► NEU: Letzten Zugriff für diesen Benutzer aktualisieren
|
||||
_RecentUsers(_Username) = DateTime.Now
|
||||
' ← Neu: Session-Start loggen
|
||||
_Logger.Debug("Session opened | User=[{0}] | SessionId=[{1}] | ActiveSessions=[{2}]",
|
||||
_Username, oSessionId, _ActiveSessions.Count)
|
||||
|
||||
AddHandler oInstanceContext.Closed, Sub()
|
||||
Dim oRemoved As String = Nothing
|
||||
_ActiveSessions.TryRemove(oSessionId, oRemoved)
|
||||
BuildUserList()
|
||||
_Logger.Debug("Session closed | User=[{0}] | SessionId=[{1}] | ActiveSessions=[{2}]",
|
||||
_Username, oSessionId, _ActiveSessions.Count)
|
||||
End Sub
|
||||
|
||||
AddHandler oInstanceContext.Faulted, Sub()
|
||||
Dim oRemoved As String = Nothing
|
||||
_ActiveSessions.TryRemove(oSessionId, oRemoved)
|
||||
_Logger.Warn("⚠ Session faulted | User=[{0}] | ActiveSessions=[{1}]",
|
||||
_Username, _ActiveSessions.Count)
|
||||
_Logger.Warn("⚠ Session faulted | User=[{0}] | SessionId=[{1}] | ActiveSessions=[{2}]",
|
||||
_Username, oSessionId, _ActiveSessions.Count)
|
||||
End Sub
|
||||
|
||||
End Sub
|
||||
@@ -137,14 +165,28 @@ Public Class EDMIService
|
||||
''' Returns a snapshot of all currently active sessions for monitoring purposes
|
||||
''' </summary>
|
||||
Public Function GetServiceStatus() As Messages.ServiceStatusResponse Implements IEDMIService.GetServiceStatus
|
||||
_Logger.Debug("GetServiceStatus called by User=[{0}]", _Username)
|
||||
' Snapshot nur einmal erstellen, nicht zweimal iterieren:
|
||||
' Aktuell offene Sessions
|
||||
Dim oSnapshot = _ActiveSessions.ToArray()
|
||||
|
||||
' ► NEU: Kürzlich aktive Benutzer (max. 15 Min. alt) bereinigen und zurückgeben
|
||||
Dim oCutoff = DateTime.Now.Subtract(_RecentUserMaxAge)
|
||||
Dim oStaleKeys = _RecentUsers.Where(Function(p) p.Value < oCutoff).Select(Function(p) p.Key).ToList()
|
||||
For Each oKey In oStaleKeys
|
||||
Dim oIgnored As DateTime
|
||||
_RecentUsers.TryRemove(oKey, oIgnored)
|
||||
Next
|
||||
|
||||
Dim oRecentUserList = _RecentUsers.
|
||||
OrderByDescending(Function(p) p.Value).
|
||||
Select(Function(p) $"{p.Key} (zuletzt: {p.Value:HH:mm:ss})").
|
||||
ToList()
|
||||
|
||||
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
|
||||
.Timestamp = DateTime.Now,
|
||||
.RecentUsers = oRecentUserList ' ◄ NEU – im Response-Objekt ergänzen
|
||||
}
|
||||
End Function
|
||||
#End Region
|
||||
@@ -199,20 +241,39 @@ Public Class EDMIService
|
||||
|
||||
|
||||
#Region "=== Database ==="
|
||||
Private Shared _CachedTablesSnapshot As List(Of String) = Nothing
|
||||
Private Shared _CachedTablesExpiry As DateTime = DateTime.MinValue
|
||||
' ── CachedTables-Snapshot: wird vom JobListener nach dem DatatableJob gesetzt.
|
||||
' GetCachedTables() liest nur diesen Snapshot – ruft GlobalState nie direkt auf
|
||||
' und blockiert daher nicht, auch wenn der DatatableJob den TableStore sperrt.
|
||||
Private Class CachedTablesState
|
||||
Public ReadOnly Tables As String()
|
||||
Public ReadOnly Timestamp As DateTime
|
||||
Public Sub New(pTables As String(), pTimestamp As DateTime)
|
||||
Tables = pTables
|
||||
Timestamp = pTimestamp
|
||||
End Sub
|
||||
End Class
|
||||
|
||||
Private Shared _CachedTablesState As CachedTablesState = New CachedTablesState(Array.Empty(Of String)(), DateTime.MinValue)
|
||||
|
||||
''' <summary>
|
||||
''' Wird vom JobListener nach erfolgreichem DatatableJob aufgerufen.
|
||||
''' Setzt den Snapshot atomar – kein Lock nötig, da Array-Referenzzuweisung auf CLR atomar ist.
|
||||
''' </summary>
|
||||
Public Shared Sub PublishCachedTablesSnapshot(pTableNames As IEnumerable(Of String))
|
||||
Dim oTables = pTableNames.Select(Function(t) t.ToUpper()).ToArray()
|
||||
Dim oNewState = New CachedTablesState(oTables, DateTime.Now)
|
||||
' Atomar: Timestamp und Array immer gemeinsam sichtbar
|
||||
Interlocked.Exchange(_CachedTablesState, oNewState)
|
||||
LogConfig?.GetLogger()?.Debug("PublishCachedTablesSnapshot: [{0}] tables published at [{1}]",
|
||||
oTables.Length, oNewState.Timestamp.ToString("HH:mm:ss"))
|
||||
End Sub
|
||||
|
||||
Public Function GetCachedTables() As List(Of String) Implements IEDMIService.GetCachedTables
|
||||
Try
|
||||
If _CachedTablesSnapshot Is Nothing OrElse DateTime.Now > _CachedTablesExpiry Then
|
||||
_CachedTablesSnapshot = GlobalState.GetCachedTables()
|
||||
_CachedTablesExpiry = DateTime.Now.AddSeconds(30)
|
||||
End If
|
||||
Return _CachedTablesSnapshot
|
||||
Catch ex As Exception
|
||||
_Logger.Error(ex)
|
||||
Return New List(Of String)
|
||||
End Try
|
||||
Dim oState = _CachedTablesState ' atomarer Read – eine Referenz
|
||||
_Logger.Debug("GetCachedTables: returning snapshot | Count=[{0}] | SnapshotAge=[{1}s]",
|
||||
oState.Tables.Length,
|
||||
CInt((DateTime.Now - oState.Timestamp).TotalSeconds))
|
||||
Return oState.Tables.ToList()
|
||||
End Function
|
||||
|
||||
Public Function ReturnDatatableFromCache(Name As String, FilterExpression As String, SortByColumn As String) As TableResult Implements IEDMIService.ReturnDatatableFromCache
|
||||
@@ -231,21 +292,54 @@ Public Class EDMIService
|
||||
End Function
|
||||
|
||||
Public Function ReturnDatatable(pData As GetDatatable.GetDatatableRequest) As GetDatatable.GetDatatableResponse Implements IEDMIService.ReturnDatatable
|
||||
_Logger.Debug("Start of Method [ReturnDatatable]")
|
||||
Dim oWatch = Stopwatch.StartNew()
|
||||
Try
|
||||
_Logger.Debug("ReturnDatatable | User=[{0}] | DB=[{1}] | SQL=[{2}]",
|
||||
_Username, pData.NamedDatabase, pData.SqlCommand)
|
||||
Dim oGetDatatable As New GetDatatable.GetDatatableMethod(LogConfig, MSSQL_IDB, MSSQL_ECM, GlobalState)
|
||||
Return oGetDatatable.Run(pData)
|
||||
Dim oResult = oGetDatatable.Run(pData)
|
||||
_Logger.Debug("ReturnDatatable completed | User=[{0}] | Rows=[{1}] | Duration=[{2}ms]",
|
||||
_Username, If(oResult?.Table?.Rows.Count, 0), oWatch.ElapsedMilliseconds)
|
||||
Return oResult
|
||||
Catch ex As Exception
|
||||
_Logger.Error("ReturnDatatable failed | User=[{0}] | Duration=[{1}ms] | Error=[{2}]",
|
||||
_Username, oWatch.ElapsedMilliseconds, ex.Message)
|
||||
Throw
|
||||
End Try
|
||||
End Function
|
||||
|
||||
Public Function ReturnScalarValue(pData As GetScalarValue.GetScalarValueRequest) As GetScalarValue.GetScalarValueResponse Implements IEDMIService.ReturnScalarValue
|
||||
_Logger.Debug("Start of Method [ReturnScalarValue]")
|
||||
Dim oWatch = Stopwatch.StartNew()
|
||||
Try
|
||||
_Logger.Debug("ReturnScalarValue | User=[{0}] | DB=[{1}] | SQL=[{2}]",
|
||||
_Username, pData.NamedDatabase, pData.SqlCommand)
|
||||
Dim oGetScalarValue As New GetScalarValue.GetScalarValueMethod(LogConfig, MSSQL_IDB, MSSQL_ECM, GlobalState)
|
||||
Return oGetScalarValue.Run(pData)
|
||||
Dim oResult = oGetScalarValue.Run(pData)
|
||||
_Logger.Debug("ReturnScalarValue completed | User=[{0}] | Duration=[{1}ms]",
|
||||
_Username, oWatch.ElapsedMilliseconds)
|
||||
Return oResult
|
||||
Catch ex As Exception
|
||||
_Logger.Error("ReturnScalarValue failed | User=[{0}] | Duration=[{1}ms] | Error=[{2}]",
|
||||
_Username, oWatch.ElapsedMilliseconds, ex.Message)
|
||||
Throw
|
||||
End Try
|
||||
End Function
|
||||
|
||||
Public Function ExecuteNonQuery(pData As ExecuteNonQuery.ExecuteNonQueryRequest) As ExecuteNonQuery.ExecuteNonQueryResponse Implements IEDMIService.ExecuteNonQuery
|
||||
_Logger.Debug("Start of Method [ExecuteNonQuery]")
|
||||
Dim oWatch = Stopwatch.StartNew()
|
||||
Try
|
||||
_Logger.Debug("ExecuteNonQuery | User=[{0}] | DB=[{1}] | SQL=[{2}]",
|
||||
_Username, pData.NamedDatabase, pData.SqlCommand)
|
||||
Dim oExecuteNonQuery As New ExecuteNonQuery.ExecuteNonQueryMethod(LogConfig, MSSQL_IDB, MSSQL_ECM, GlobalState)
|
||||
Return oExecuteNonQuery.Run(pData)
|
||||
Dim oResult = oExecuteNonQuery.Run(pData)
|
||||
_Logger.Debug("ExecuteNonQuery completed | User=[{0}] | Duration=[{1}ms]",
|
||||
_Username, oWatch.ElapsedMilliseconds)
|
||||
Return oResult
|
||||
Catch ex As Exception
|
||||
_Logger.Error("ExecuteNonQuery failed | User=[{0}] | Duration=[{1}ms] | Error=[{2}]",
|
||||
_Username, oWatch.ElapsedMilliseconds, ex.Message)
|
||||
Throw
|
||||
End Try
|
||||
End Function
|
||||
#End Region
|
||||
|
||||
@@ -259,7 +353,7 @@ Public Class EDMIService
|
||||
|
||||
Catch ex As FaultException
|
||||
_Logger.Error(ex)
|
||||
Throw ex
|
||||
Throw
|
||||
|
||||
Catch ex As Exception
|
||||
_Logger.Error(ex)
|
||||
@@ -276,7 +370,7 @@ Public Class EDMIService
|
||||
|
||||
Catch ex As FaultException
|
||||
_Logger.Error(ex)
|
||||
Throw ex
|
||||
Throw
|
||||
|
||||
Catch ex As Exception
|
||||
_Logger.Error(ex)
|
||||
@@ -293,7 +387,7 @@ Public Class EDMIService
|
||||
|
||||
Catch ex As FaultException
|
||||
_Logger.Error(ex)
|
||||
Throw ex
|
||||
Throw
|
||||
|
||||
Catch ex As Exception
|
||||
_Logger.Error(ex)
|
||||
@@ -313,7 +407,7 @@ Public Class EDMIService
|
||||
|
||||
Catch ex As FaultException
|
||||
_Logger.Error(ex)
|
||||
Throw ex
|
||||
Throw
|
||||
|
||||
Catch ex As Exception
|
||||
_Logger.Error(ex)
|
||||
@@ -330,7 +424,7 @@ Public Class EDMIService
|
||||
|
||||
Catch ex As FaultException
|
||||
_Logger.Error(ex)
|
||||
Throw ex
|
||||
Throw
|
||||
|
||||
Catch ex As Exception
|
||||
_Logger.Error(ex)
|
||||
@@ -347,7 +441,7 @@ Public Class EDMIService
|
||||
|
||||
Catch ex As FaultException
|
||||
_Logger.Error(ex)
|
||||
Throw ex
|
||||
Throw
|
||||
|
||||
Catch ex As Exception
|
||||
_Logger.Error(ex)
|
||||
@@ -367,7 +461,7 @@ Public Class EDMIService
|
||||
|
||||
Catch ex As FaultException
|
||||
_Logger.Error(ex)
|
||||
Throw ex
|
||||
Throw
|
||||
|
||||
Catch ex As Exception
|
||||
_Logger.Error(ex)
|
||||
@@ -384,7 +478,7 @@ Public Class EDMIService
|
||||
|
||||
Catch ex As FaultException
|
||||
_Logger.Error(ex)
|
||||
Throw ex
|
||||
Throw
|
||||
|
||||
Catch ex As Exception
|
||||
_Logger.Error(ex)
|
||||
@@ -401,7 +495,7 @@ Public Class EDMIService
|
||||
|
||||
Catch ex As FaultException
|
||||
_Logger.Error(ex)
|
||||
Throw ex
|
||||
Throw
|
||||
|
||||
Catch ex As Exception
|
||||
_Logger.Error(ex)
|
||||
@@ -502,7 +596,7 @@ Public Class EDMIService
|
||||
|
||||
Catch ex As FaultException
|
||||
_Logger.Error(ex)
|
||||
Throw ex
|
||||
Throw
|
||||
|
||||
Catch ex As Exception
|
||||
_Logger.Error(ex)
|
||||
@@ -522,15 +616,17 @@ Public Class EDMIService
|
||||
Throw New FaultException(Of ObjectDoesNotExistFault)(oFault, oFault.ErrorMessage)
|
||||
End If
|
||||
|
||||
Return New DocumentInfoResponse With {
|
||||
Dim oResult = New DocumentInfoResponse With {
|
||||
.FileRight = oAccessRight,
|
||||
.FullPath = oFullPath
|
||||
}
|
||||
_Logger.Debug("End GetFileInfoByObjectId!")
|
||||
_Logger.Debug("End GetFileInfoByObjectId!") ' ← jetzt erreichbar
|
||||
Return oResult
|
||||
|
||||
Catch ex As FaultException
|
||||
_Logger.Warn("⚠ GetFileInfoByObjectId: " & ex.Message)
|
||||
_Logger.Error(ex)
|
||||
Throw ex
|
||||
Throw
|
||||
|
||||
Catch ex As Exception
|
||||
_Logger.Warn("⚠ GetFileInfoByObjectId: " & ex.Message)
|
||||
@@ -550,7 +646,7 @@ Public Class EDMIService
|
||||
|
||||
Catch ex As FaultException
|
||||
_Logger.Error(ex)
|
||||
Throw ex
|
||||
Throw
|
||||
|
||||
Catch ex As Exception
|
||||
_Logger.Error(ex)
|
||||
@@ -607,13 +703,17 @@ Public Class EDMIService
|
||||
|
||||
|
||||
Private Function GetFault(Exception As Exception) As FaultException
|
||||
Dim oMessage = If(Exception?.Message, "Unknown error")
|
||||
_Logger.Error("Wrapping exception as FaultException | Error=[{0}]", oMessage)
|
||||
Dim oFault As New UnexpectedErrorFault(Exception)
|
||||
Return New FaultException(Of UnexpectedErrorFault)(oFault, New FaultReason(oFault.InnerException.Message))
|
||||
Dim oReason = If(Exception?.InnerException?.Message, oMessage)
|
||||
Return New FaultException(Of UnexpectedErrorFault)(oFault, New FaultReason(oReason))
|
||||
End Function
|
||||
|
||||
Private Function GetFault(ErrorMessage As String) As FaultException
|
||||
_Logger.Error("Wrapping error as FaultException | Error=[{0}]", ErrorMessage)
|
||||
Dim oFault As New UnexpectedErrorFault(New ApplicationException(ErrorMessage))
|
||||
Return New FaultException(Of UnexpectedErrorFault)(oFault, New FaultReason(oFault.InnerException.Message))
|
||||
Return New FaultException(Of UnexpectedErrorFault)(oFault, New FaultReason(ErrorMessage))
|
||||
End Function
|
||||
|
||||
Private Function GetFullPathForObjectId(ObjectId As Long) As String
|
||||
|
||||
@@ -155,7 +155,6 @@
|
||||
<Compile Include="BaseMethod.vb" />
|
||||
<Compile Include="ClassConstants.vb" />
|
||||
<Compile Include="Config.vb" />
|
||||
<Compile Include="GetServiceStatus.vb" />
|
||||
<Compile Include="Messages\GetServiceStatus.vb" />
|
||||
<Compile Include="Methods\Base\GetClientConfig\GetClientConfigMethod.vb" />
|
||||
<Compile Include="Methods\Base\GetClientConfig\GetClientConfigResponse.vb" />
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
Imports System.Runtime.Serialization
|
||||
|
||||
Public Class GetServiceStatus
|
||||
|
||||
|
||||
End Class
|
||||
@@ -1,11 +1,12 @@
|
||||
Imports System.Runtime.Serialization
|
||||
|
||||
Namespace Messages
|
||||
<DataContract>
|
||||
<DataContract(Name:="ServiceStatusResponse", [Namespace]:="http://schemas.datacontract.org/2004/07/DigitalData.Services.EDMIService.Messages")>
|
||||
Public Class ServiceStatusResponse
|
||||
<DataMember> Public Property ActiveSessionCount As Integer
|
||||
<DataMember> Public Property ActiveUsers As List(Of String) ' Distinct, sortiert
|
||||
<DataMember> Public Property ActiveSessions As Dictionary(Of String, String) ' SessionId → Username
|
||||
<DataMember> Public Property Timestamp As DateTime
|
||||
<DataMember(Order:=1)> Public Property ActiveSessionCount As Integer
|
||||
<DataMember(Order:=2)> Public Property ActiveUsers As List(Of String)
|
||||
<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)()
|
||||
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.6.0")>
|
||||
<Assembly: AssemblyTrademark("2.8.0")>
|
||||
|
||||
<Assembly: ComVisible(False)>
|
||||
|
||||
@@ -31,5 +31,5 @@ Imports System.Runtime.InteropServices
|
||||
' übernehmen, indem Sie "*" eingeben:
|
||||
' <Assembly: AssemblyVersion("1.0.*")>
|
||||
|
||||
<Assembly: AssemblyVersion("2.6.0.0")>
|
||||
<Assembly: AssemblyFileVersion("2.6.0.0")>
|
||||
<Assembly: AssemblyVersion("2.8.0.0")>
|
||||
<Assembly: AssemblyFileVersion("2.8.0.0")>
|
||||
|
||||
@@ -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)
|
||||
' 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)
|
||||
Dim oExisting = _Dataset.Tables(oName)
|
||||
oExisting.Constraints.Clear()
|
||||
oExisting.ChildRelations.Clear()
|
||||
_Dataset.Tables.Remove(oName)
|
||||
End If
|
||||
Else
|
||||
_Logger.Debug("Datatable [{0}] does not exist yet, adding.", oName)
|
||||
_Dataset.Tables.Add(oNewTable)
|
||||
|
||||
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)
|
||||
' 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
|
||||
|
||||
_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)
|
||||
' ← 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
|
||||
|
||||
@@ -12,8 +12,8 @@ Imports System.Runtime.InteropServices
|
||||
<Assembly: AssemblyDescription("")>
|
||||
<Assembly: AssemblyCompany("Digital Data")>
|
||||
<Assembly: AssemblyProduct("DDZUGFeRDService")>
|
||||
<Assembly: AssemblyCopyright("Copyright © 2025")>
|
||||
<Assembly: AssemblyTrademark("2.9.3.0")>
|
||||
<Assembly: AssemblyCopyright("Copyright © 2026")>
|
||||
<Assembly: AssemblyTrademark("2.9.4.0")>
|
||||
|
||||
<Assembly: ComVisible(False)>
|
||||
|
||||
@@ -31,5 +31,5 @@ Imports System.Runtime.InteropServices
|
||||
' übernehmen, indem Sie "*" eingeben:
|
||||
' <Assembly: AssemblyVersion("1.0.*")>
|
||||
|
||||
<Assembly: AssemblyVersion("2.9.3.0")>
|
||||
<Assembly: AssemblyFileVersion("2.9.3.0")>
|
||||
<Assembly: AssemblyVersion("2.9.4.0")>
|
||||
<Assembly: AssemblyFileVersion("2.9.4.0")>
|
||||
|
||||
Reference in New Issue
Block a user