diff --git a/EDMIAPI/Client.vb b/EDMIAPI/Client.vb index a6cd4227..ee2e3bf6 100644 --- a/EDMIAPI/Client.vb +++ b/EDMIAPI/Client.vb @@ -7,6 +7,7 @@ Imports DigitalData.Modules.EDMI.API.Rights Imports DigitalData.Modules.Logging Public Class Client + Implements IDisposable ' Constants Private Const UPDATE_INTERVAL_IN_MINUTES As Integer = 1 @@ -23,9 +24,53 @@ Public Class Client Private _CachedTables As New List(Of String) Private _IsOnline As Boolean Private _Channel As IEDMIServiceChannel - + Private _ClientName As String + ' Static Registry + Private Shared ReadOnly _ActiveClients As New Collections.Concurrent.ConcurrentDictionary(Of String, ClientStatus) + Private ReadOnly _ClientId As String ' ← Neu: eindeutiger interner Key + Public Shared ReadOnly Property ActiveClients As IReadOnlyDictionary(Of String, ClientStatus) + Get + Return _ActiveClients + End Get + End Property + ''' + ''' Returns a snapshot of all currently registered client instances + ''' + Public Shared Function GetAllClientStatus() As List(Of ClientStatus) + Return _ActiveClients.Values.ToList() + End Function ' Update Timer Private WithEvents UpdateTimer As New Timers.Timer + Private _Disposed As Boolean = False + + ''' + ''' Closes the channel and releases all resources + ''' + Public Sub Dispose() Implements IDisposable.Dispose + Dispose(True) + GC.SuppressFinalize(Me) + End Sub + Protected Overridable Sub Dispose(disposing As Boolean) + If _Disposed Then Return + + If disposing Then + UpdateTimer.Stop() + UpdateTimer.Dispose() + + If _Channel IsNot Nothing Then + Channel.CloseChannel(_Channel) + _Channel = Nothing + End If + + ' Aus Registry austragen + Dim oRemoved As ClientStatus = Nothing + _ActiveClients.TryRemove(_ClientId, oRemoved) + Logger.Debug("Client [{0}] unregistered. Total active clients: [{1}]", _ClientName, _ActiveClients.Count) + Logger.Debug("Client disposed. Channel closed.") + End If + + _Disposed = True + End Sub ' Public Variables Public ReadOnly Property CachedTables @@ -72,9 +117,12 @@ Public Class Client ''' ''' LogConfig object ''' The IP address/hostname and port, separated by semicolon or colon, ex. 1.2.3.4:9000 - Public Sub New(pLogConfig As LogConfig, pServiceAdress As String) + ''' Optional identifier for this client instance (e.g. application name or hostname) + Public Sub New(pLogConfig As LogConfig, pServiceAdress As String, Optional pClientName As String = "") LogConfig = pLogConfig Logger = pLogConfig.GetLogger() + _ClientName = If(String.IsNullOrWhiteSpace(pClientName), Environment.MachineName, pClientName) + _ClientId = $"{_ClientName}_{Guid.NewGuid().ToString("N").Substring(0, 8)}" ' ← eindeutig UpdateTimer.Interval = 60 * 1000 * UPDATE_INTERVAL_IN_MINUTES UpdateTimer.Start() @@ -85,8 +133,8 @@ Public Class Client ChannelManager = New Channel(pLogConfig, _ServerAddress) AddHandler ChannelManager.Reconnect, AddressOf Reconnect - Logger.Debug("Ready for connection to Service at: [{0}:{1}]", _ServerAddress.Host, _ServerAddress.Port) - + Logger.Debug("Ready for connection to Service at: [{0}:{1}] | ClientName=[{2}] | ClientId=[{3}]", + _ServerAddress.Host, _ServerAddress.Port, _ClientName, _ClientId) Catch ex As Exception Logger.Error(ex) End Try @@ -98,9 +146,12 @@ Public Class Client ''' LogConfig object ''' The IP address to connect to ''' The Port number to use for the connection - Public Sub New(pLogConfig As LogConfig, pIPAddress As String, pPortNumber As Integer) + ''' Optional identifier for this client instance (e.g. application name or hostname) + Public Sub New(pLogConfig As LogConfig, pIPAddress As String, pPortNumber As Integer, Optional pClientName As String = "") LogConfig = pLogConfig Logger = pLogConfig.GetLogger() + _ClientName = If(String.IsNullOrWhiteSpace(pClientName), Environment.MachineName, pClientName) + _ClientId = $"{_ClientName}_{Guid.NewGuid().ToString("N").Substring(0, 8)}" ' ← eindeutig UpdateTimer.Interval = 60 * 1000 * UPDATE_INTERVAL_IN_MINUTES UpdateTimer.Start() @@ -111,13 +162,12 @@ Public Class Client ChannelManager = New Channel(pLogConfig, _ServerAddress) AddHandler ChannelManager.Reconnect, AddressOf Reconnect - Logger.Debug("Ready for connection to Service at: [{0}:{1}]", _ServerAddress.Host, _ServerAddress.Port) - + Logger.Debug("Ready for connection to Service at: [{0}:{1}] | ClientName=[{2}] | ClientId=[{3}]", + _ServerAddress.Host, _ServerAddress.Port, _ClientName, _ClientId) Catch ex As Exception Logger.Error(ex) End Try End Sub - ''' ''' Connect to the service ''' @@ -133,13 +183,26 @@ Public Class Client If oResponse.OK Then _ClientConfig = oResponse.ClientConfig Else - Logger.Warn("Client Configuration could not be loaded: [{0}]", oResponse.ErrorMessage) + Logger.Warn("⚠Client Configuration could not be loaded: [{0}]", oResponse.ErrorMessage) End If - Logger.Info($"Connection to AppService [{ServerAddress}] successfully established!") - + Logger.Info("✔✔Connection to AppService [{0}] successfully established! | Client=[{1}] | Id=[{2}]", + ServerAddress, _ClientName, _ClientId) _IsOnline = True + ' CachedTables sofort befüllen – nicht auf den ersten Timer-Tick warten + Try + Dim oTables As String() = _Channel.GetCachedTables() + _CachedTables = oTables.Select(Function(t) t.ToUpper).ToList() + Logger.Debug("✔ CachedTables loaded on connect: [{0}]", _CachedTables.Count) + Catch ex As Exception + Logger.Warn("⚠ CachedTables could not be loaded on connect: {0}", ex.Message) + _CachedTables = New List(Of String) + End Try + ' In Registry eintragen + _ActiveClients(_ClientId) = GetStatus() + Logger.Info("Client [{0}] registered with Id=[{1}]. Total active clients: [{2}]", _ClientName, _ClientId, _ActiveClients.Count) + Return True Catch ex As Exception _IsOnline = False @@ -152,32 +215,83 @@ Public Class Client ''' ''' Aborts the channel and creates a new connection ''' - Public Sub Reconnect() - Logger.Warn("Connection faulted. Trying to reconnect..") + Public Async Sub Reconnect() + Logger.Warn("⚠ Connection faulted. Trying to reconnect..") - Try - _Channel.Abort() - _Channel = ChannelManager.GetChannel() - _Channel.Open() + Dim oRetryDelays = {2, 5, 15, 30} - _IsOnline = True - Catch ex As Exception - Logger.Error(ex) - End Try + For Each oDelay In oRetryDelays + Try + Await Task.Delay(TimeSpan.FromSeconds(oDelay)) + Logger.Debug($"Reconnect attempt after {oDelay}s delay...") + + Channel.CloseChannel(_Channel) ' Sauber schließen statt Abort + _Channel = ChannelManager.GetChannel() + _Channel.Open() + + _IsOnline = True + Logger.Info("Reconnect successful.") + Return + Catch ex As Exception + Logger.Warn($"⚠ Reconnect failed: {ex.Message}") + _IsOnline = False + End Try + Next + + Logger.Error("All reconnect attempts failed. Service unavailable.") End Sub - Private Async Function UpdateTimer_Elapsed(sender As Object, e As Timers.ElapsedEventArgs) As Task Handles UpdateTimer.Elapsed + Private Async Sub UpdateTimer_Elapsed(sender As Object, e As Timers.ElapsedEventArgs) Handles UpdateTimer.Elapsed Try + If _Channel Is Nothing OrElse _Channel.State <> CommunicationState.Opened Then + Logger.Warn("⚠ UpdateTimer skipped: Channel not in Opened state [{0}]", If(_Channel IsNot Nothing, _Channel.State.ToString(), "null")) + _IsOnline = False + Reconnect() + Return + End If + + If Not _IsOnline Then + Logger.Debug("UpdateTimer skipped: Client is offline.") + Return + End If + Dim oTables As String() = Await _Channel.GetCachedTablesAsync() + ' ◄ NullReferenceException-Schutz: Service kann Nothing zurückgeben + If oTables Is Nothing Then + Logger.Warn("⚠ UpdateTimer: GetCachedTablesAsync returned Nothing.") + _CachedTables = New List(Of String) + Return + End If _CachedTables = oTables. - Select(Function(table) table.ToUpper). - ToList() + Select(Function(table) table.ToUpper). + ToList() + + Logger.Info("✔ Channel state: [{0}] | Active connections: [{1}] | CachedTables: [{2}]", + _Channel.State, + Channel.ActiveConnections, + _CachedTables.Count) + + If _ActiveClients.ContainsKey(_ClientId) Then + _ActiveClients(_ClientId) = GetStatus() + End If + + Catch ex As TimeoutException + ' Stale TCP connection (z.B. Firewall/Citrix Idle-Timeout) + Logger.Warn("⚠ UpdateTimer: TCP connection lost (idle timeout / firewall). Triggering reconnect...") + _IsOnline = False + Reconnect() + + Catch ex As CommunicationException + Logger.Warn("⚠ UpdateTimer: Channel faulted. Triggering reconnect...") + _IsOnline = False + Reconnect() + Catch ex As Exception - Logger.Warn("Update of CachedTable was not successful!") + Logger.Warn("⚠ Update of CachedTable was not successful!") Logger.Error(ex) _CachedTables = New List(Of String) End Try - End Function + End Sub ''' ''' Imports a file from a filepath, creating a IDB ObjectId and Filesystem Object @@ -460,7 +574,7 @@ Public Class Client Dim oResult = _Channel.ExecuteNonQuery_MSSQL_IDB(oSql) If oResult.OK = False Then - Logger.Warn("Error while deleting Term object") + Logger.Warn("⚠ Error while IDBNewObjectData") Logger.Error(oResult.ErrorMessage) End If @@ -477,7 +591,7 @@ Public Class Client Dim oResult = _Channel.ExecuteNonQuery_MSSQL_IDB(oDELSQL) If oResult.OK = False Then - Logger.Warn("Error while deleting Term object") + Logger.Warn("⚠ Error while deleting Term object") Logger.Error(oResult.ErrorMessage) End If @@ -490,268 +604,351 @@ Public Class Client #Region "GetDatatable" Public Function GetDatatableFromIDB(pSQL As String) As GetDatatableResponse + EnsureAvailable() Try - Dim oResponse = _Channel.ReturnDatatable(New GetDatatableRequest() With { + Return _Channel.ReturnDatatable(New GetDatatableRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.IDB }) - Return oResponse + Catch ex As EndpointNotFoundException + HandleCommunicationError(ex, NameOf(GetDatatableFromIDB)) + Catch ex As CommunicationException + HandleCommunicationError(ex, NameOf(GetDatatableFromIDB)) Catch ex As Exception Logger.Error(ex) - Throw ex + Throw End Try End Function Public Function GetDatatableFromECM(pSQL As String) As GetDatatableResponse + EnsureAvailable() Try - Dim oResponse = _Channel.ReturnDatatable(New GetDatatableRequest() With { + Return _Channel.ReturnDatatable(New GetDatatableRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.ECM }) - Return oResponse + Catch ex As EndpointNotFoundException + HandleCommunicationError(ex, NameOf(GetDatatableFromECM)) + Catch ex As CommunicationException + HandleCommunicationError(ex, NameOf(GetDatatableFromECM)) Catch ex As Exception Logger.Error(ex) - Throw ex + Throw End Try End Function Public Function GetDatatableFromConnection(pSQL As String, pConnectionId As Integer) As GetDatatableResponse + EnsureAvailable() Try - Dim oResponse = _Channel.ReturnDatatable(New GetDatatableRequest() With { + Return _Channel.ReturnDatatable(New GetDatatableRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.None, .ConnectionId = pConnectionId }) - Return oResponse + Catch ex As EndpointNotFoundException + HandleCommunicationError(ex, NameOf(GetDatatableFromConnection)) + Catch ex As CommunicationException + HandleCommunicationError(ex, NameOf(GetDatatableFromConnection)) Catch ex As Exception Logger.Error(ex) - Throw ex + Throw End Try End Function Public Async Function GetDatatableFromIDBAsync(pSQL As String) As Task(Of GetDatatableResponse) + EnsureAvailable() Try - Dim oResponse = Await _Channel.ReturnDatatableAsync(New GetDatatableRequest() With { + Return Await _Channel.ReturnDatatableAsync(New GetDatatableRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.IDB }) - Return oResponse + Catch ex As EndpointNotFoundException + HandleCommunicationError(ex, NameOf(GetDatatableFromIDBAsync)) + Catch ex As CommunicationException + HandleCommunicationError(ex, NameOf(GetDatatableFromIDBAsync)) Catch ex As Exception Logger.Error(ex) - Throw ex + Throw End Try End Function Public Async Function GetDatatableFromECMAsync(pSQL As String) As Task(Of GetDatatableResponse) + EnsureAvailable() Try - Dim oResponse = Await _Channel.ReturnDatatableAsync(New GetDatatableRequest() With { + Logger.Debug("GetDatatableFromECMAsync will be called with SQL [{0}]", pSQL) + Return Await _Channel.ReturnDatatableAsync(New GetDatatableRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.ECM }) - Return oResponse + Catch ex As EndpointNotFoundException + HandleCommunicationError(ex, NameOf(GetDatatableFromECMAsync)) + Catch ex As CommunicationException + HandleCommunicationError(ex, NameOf(GetDatatableFromECMAsync)) Catch ex As Exception Logger.Error(ex) - Throw ex + Throw End Try End Function Public Async Function GetDatatableFromConnectionAsync(pSQL As String, Optional pConnectionId As Integer = 0) As Task(Of GetDatatableResponse) + EnsureAvailable() Try - Dim oResponse = Await _Channel.ReturnDatatableAsync(New GetDatatableRequest() With { + Return Await _Channel.ReturnDatatableAsync(New GetDatatableRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.None, .ConnectionId = pConnectionId }) - Return oResponse + Catch ex As EndpointNotFoundException + HandleCommunicationError(ex, NameOf(GetDatatableFromConnectionAsync)) + Catch ex As CommunicationException + HandleCommunicationError(ex, NameOf(GetDatatableFromConnectionAsync)) Catch ex As Exception Logger.Error(ex) - Throw ex + Throw End Try End Function #End Region #Region "GetScalarValue" Public Function GetScalarValueFromIDB(pSQL As String) As GetScalarValueResponse + EnsureAvailable() Try - Dim oResponse = _Channel.ReturnScalarValue(New GetScalarValueRequest() With { + Return _Channel.ReturnScalarValue(New GetScalarValueRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.IDB }) - Return oResponse + Catch ex As EndpointNotFoundException + HandleCommunicationError(ex, NameOf(GetScalarValueFromIDB)) + Catch ex As CommunicationException + HandleCommunicationError(ex, NameOf(GetScalarValueFromIDB)) Catch ex As Exception Logger.Error(ex) - Throw ex + Throw End Try End Function Public Function GetScalarValueFromECM(pSQL As String) As GetScalarValueResponse + EnsureAvailable() Try - Dim oResponse = _Channel.ReturnScalarValue(New GetScalarValueRequest() With { + Return _Channel.ReturnScalarValue(New GetScalarValueRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.ECM }) - Return oResponse + Catch ex As EndpointNotFoundException + HandleCommunicationError(ex, NameOf(GetScalarValueFromECM)) + Catch ex As CommunicationException + HandleCommunicationError(ex, NameOf(GetScalarValueFromECM)) Catch ex As Exception Logger.Error(ex) - Throw ex + Throw End Try End Function Public Function GetScalarValueFromConnection(pSQL As String, pConnectionId As Integer) As GetScalarValueResponse + EnsureAvailable() Try - Dim oResponse = _Channel.ReturnScalarValue(New GetScalarValueRequest() With { + Return _Channel.ReturnScalarValue(New GetScalarValueRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.None, .ConnectionId = pConnectionId }) - Return oResponse + Catch ex As EndpointNotFoundException + HandleCommunicationError(ex, NameOf(GetScalarValueFromConnection)) + Catch ex As CommunicationException + HandleCommunicationError(ex, NameOf(GetScalarValueFromConnection)) Catch ex As Exception Logger.Error(ex) - Throw ex + Throw End Try End Function Public Async Function GetScalarValueFromIDBAsync(pSQL As String) As Task(Of GetScalarValueResponse) + EnsureAvailable() Try - Dim oResponse = Await _Channel.ReturnScalarValueAsync(New GetScalarValueRequest() With { + Return Await _Channel.ReturnScalarValueAsync(New GetScalarValueRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.IDB }) - Return oResponse + Catch ex As EndpointNotFoundException + HandleCommunicationError(ex, NameOf(GetScalarValueFromIDBAsync)) + Catch ex As CommunicationException + HandleCommunicationError(ex, NameOf(GetScalarValueFromIDBAsync)) Catch ex As Exception Logger.Error(ex) - Throw ex + Throw End Try End Function Public Async Function GetScalarValueFromECMAsync(pSQL As String) As Task(Of GetScalarValueResponse) + EnsureAvailable() Try - Dim oResponse = Await _Channel.ReturnScalarValueAsync(New GetScalarValueRequest() With { + Return Await _Channel.ReturnScalarValueAsync(New GetScalarValueRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.ECM }) - Return oResponse + Catch ex As EndpointNotFoundException + HandleCommunicationError(ex, NameOf(GetScalarValueFromECMAsync)) + Catch ex As CommunicationException + HandleCommunicationError(ex, NameOf(GetScalarValueFromECMAsync)) Catch ex As Exception Logger.Error(ex) - Throw ex + Throw End Try End Function Public Async Function GetScalarValueFromConnectionAsync(pSQL As String, pConnectionId As Integer) As Task(Of GetScalarValueResponse) + EnsureAvailable() Try - Dim oResponse = Await _Channel.ReturnScalarValueAsync(New GetScalarValueRequest() With { + Return Await _Channel.ReturnScalarValueAsync(New GetScalarValueRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.None, .ConnectionId = pConnectionId }) - Return oResponse + Catch ex As EndpointNotFoundException + HandleCommunicationError(ex, NameOf(GetScalarValueFromConnectionAsync)) + Catch ex As CommunicationException + HandleCommunicationError(ex, NameOf(GetScalarValueFromConnectionAsync)) Catch ex As Exception Logger.Error(ex) - Throw ex + Throw End Try End Function #End Region #Region "ExecuteNonQuery" Public Function ExecuteNonQueryFromIDB(pSQL As String) As ExecuteNonQueryResponse + EnsureAvailable() Try - Dim oResponse = _Channel.ExecuteNonQuery(New ExecuteNonQueryRequest() With { + Return _Channel.ExecuteNonQuery(New ExecuteNonQueryRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.IDB }) - Return oResponse + Catch ex As EndpointNotFoundException + HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromIDB)) + Throw + Catch ex As CommunicationException + HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromIDB)) + Throw Catch ex As Exception Logger.Error(ex) - Throw ex + Throw End Try End Function Public Function ExecuteNonQueryFromECM(pSQL As String) As ExecuteNonQueryResponse + EnsureAvailable() Try - Dim oResponse = _Channel.ExecuteNonQuery(New ExecuteNonQueryRequest() With { + Return _Channel.ExecuteNonQuery(New ExecuteNonQueryRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.ECM }) - Return oResponse + Catch ex As EndpointNotFoundException + HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromECM)) + Catch ex As CommunicationException + HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromECM)) Catch ex As Exception Logger.Error(ex) - Throw ex + Throw End Try End Function Public Function ExecuteNonQueryFromConnection(pSQL As String, pConnectionId As Integer) As ExecuteNonQueryResponse + EnsureAvailable() Try - Dim oResponse = _Channel.ExecuteNonQuery(New ExecuteNonQueryRequest() With { + Return _Channel.ExecuteNonQuery(New ExecuteNonQueryRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.None, .ConnectionId = pConnectionId }) - Return oResponse + Catch ex As EndpointNotFoundException + HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromConnection)) + Catch ex As CommunicationException + HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromConnection)) Catch ex As Exception Logger.Error(ex) - Throw ex + Throw End Try End Function Public Async Function ExecuteNonQueryFromIDBAsync(pSQL As String) As Task(Of ExecuteNonQueryResponse) + EnsureAvailable() Try - Dim oResponse = Await _Channel.ExecuteNonQueryAsync(New ExecuteNonQueryRequest() With { + Return Await _Channel.ExecuteNonQueryAsync(New ExecuteNonQueryRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.IDB }) - Return oResponse + Catch ex As EndpointNotFoundException + HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromIDBAsync)) + Catch ex As CommunicationException + HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromIDBAsync)) Catch ex As Exception Logger.Error(ex) - Throw ex + Throw End Try End Function Public Async Function ExecuteNonQueryFromECMAsync(pSQL As String) As Task(Of ExecuteNonQueryResponse) + EnsureAvailable() Try - Dim oResponse = Await _Channel.ExecuteNonQueryAsync(New ExecuteNonQueryRequest() With { + Return Await _Channel.ExecuteNonQueryAsync(New ExecuteNonQueryRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.ECM }) - Return oResponse + Catch ex As EndpointNotFoundException + HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromECMAsync)) + Catch ex As CommunicationException + HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromECMAsync)) Catch ex As Exception Logger.Error(ex) - Throw ex + Throw End Try End Function Public Async Function ExecuteNonQueryFromConnectionAsync(pSQL As String, pConnectionId As Integer) As Task(Of ExecuteNonQueryResponse) + EnsureAvailable() Try - Dim oResponse = Await _Channel.ExecuteNonQueryAsync(New ExecuteNonQueryRequest() With { + Return Await _Channel.ExecuteNonQueryAsync(New ExecuteNonQueryRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.None, .ConnectionId = pConnectionId }) - Return oResponse + Catch ex As EndpointNotFoundException + HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromConnectionAsync)) + Catch ex As CommunicationException + HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromConnectionAsync)) Catch ex As Exception Logger.Error(ex) - Throw ex + Throw End Try End Function #End Region Public Function GetDatatableByName(DatatableName As String, Optional FilterExpression As String = "", Optional SortByColumn As String = "") As TableResult + EnsureAvailable() Try - Dim oResponse = _Channel.ReturnDatatableFromCache(DatatableName, FilterExpression, SortByColumn) - Return oResponse + Return _Channel.ReturnDatatableFromCache(DatatableName, FilterExpression, SortByColumn) + Catch ex As EndpointNotFoundException + HandleCommunicationError(ex, NameOf(GetDatatableByName)) + Catch ex As CommunicationException + HandleCommunicationError(ex, NameOf(GetDatatableByName)) Catch ex As Exception Logger.Error(ex) - Throw ex + Throw End Try End Function Public Async Function GetDatatableByNameAsync(DatatableName As String, Optional FilterExpression As String = "", Optional SortByColumn As String = "") As Task(Of TableResult) + EnsureAvailable() Try - Dim oResponse = Await _Channel.ReturnDatatableFromCacheAsync(DatatableName, FilterExpression, SortByColumn) - Return oResponse + Return Await _Channel.ReturnDatatableFromCacheAsync(DatatableName, FilterExpression, SortByColumn) + Catch ex As EndpointNotFoundException + HandleCommunicationError(ex, NameOf(GetDatatableByNameAsync)) + Catch ex As CommunicationException + HandleCommunicationError(ex, NameOf(GetDatatableByNameAsync)) Catch ex As Exception Logger.Error(ex) - Throw ex + Throw End Try End Function @@ -819,6 +1016,40 @@ Public Class Client Private Function GetUserName(pOverrideName) As String Return ObjectEx.NotNull(pOverrideName, Environment.UserName) End Function + ''' + ''' Throws InvalidOperationException if the channel is not ready for calls + ''' + Private Sub EnsureAvailable() + If _Channel Is Nothing Then + Throw New InvalidOperationException("Channel is not initialized. Call Connect() first.") + End If + + If _Channel.State <> CommunicationState.Opened Then + Throw New InvalidOperationException($"Channel is not available. State=[{_Channel.State}]") + End If + + If Not _IsOnline Then + Throw New InvalidOperationException("Client is offline.") + End If + End Sub + ''' + ''' Logs the communication error, sets offline state, triggers reconnect and rethrows + ''' + Private Sub HandleCommunicationError(ex As Exception, callerName As String) + Select Case True + Case TypeOf ex Is EndpointNotFoundException + Logger.Warn("⚠ Service not reachable (EndpointNotFoundException in {0}). Triggering reconnect...", callerName) + Case TypeOf ex Is CommunicationException + Logger.Warn("⚠ in {0} during call. Triggering reconnect...", callerName) + Case Else + Logger.Warn("⚠ Unexpected exception in {0} during call. Triggering reconnect...", callerName) + Logger.Error(ex) + End Select + + _IsOnline = False + Reconnect() + Throw ex + End Sub #End Region #Region "Response Classes" @@ -887,34 +1118,74 @@ Public Class Client Select Case Type Case AttributeTypeName.VECTOR_INTEGER Return ValueBigInt - Case AttributeTypeName.BIG_INTEGER Return ValueBigInt - Case AttributeTypeName.VECTOR_STRING Return ValueText - Case AttributeTypeName.VARCHAR Return ValueText - Case AttributeTypeName.BIT Return IIf(ValueBigInt = 1, True, False) - Case AttributeTypeName.DATE Return ValueDate - Case AttributeTypeName.DATETIME Return ValueDate - Case AttributeTypeName.DECIMAL Return ValueDecimal - Case AttributeTypeName.FLOAT Return ValueDecimal - Case Else Return Nothing End Select End Function + End Class ' ← ObjectAttribute endet hier + ''' + ''' Returns a detailed status snapshot of the current client state + ''' + Public Function GetStatus() As ClientStatus + Return New ClientStatus() With { + .IsOnline = _IsOnline, + .ClientId = _ClientId, ' ← Neu + .ClientName = _ClientName, + .ServerAddress = Me.ServerAddress, + .ChannelState = If(_Channel IsNot Nothing, _Channel.State.ToString(), "null"), + .ActiveConnections = Channel.ActiveConnections, + .CachedTableCount = _CachedTables.Count, + .CachedTables = _CachedTables.ToList(), + .HasClientConfig = _ClientConfig IsNot Nothing, + .IsDisposed = _Disposed, + .TimerEnabled = UpdateTimer.Enabled, + .TimerIntervalSeconds = CInt(UpdateTimer.Interval / 1000), + .Timestamp = DateTime.Now + } + End Function + + Public Class ClientStatus + Public Property IsOnline As Boolean + Public Property ClientId As String ' ← Neu + Public Property ClientName As String + Public Property ServerAddress As String + Public Property ChannelState As String + Public Property ActiveConnections As Integer + Public Property CachedTableCount As Integer + Public Property CachedTables As List(Of String) + Public Property HasClientConfig As Boolean + Public Property IsDisposed As Boolean + Public Property TimerEnabled As Boolean + Public Property TimerIntervalSeconds As Integer + Public Property Timestamp As DateTime + + Public Overrides Function ToString() As String + Return String.Format( + "[ClientStatus] ClientName=[{0}] Id=[{1}] Online=[{2}] Channel=[{3}]", + ClientName, ClientId, IsOnline, ChannelState) + End Function + + Public Function Client_Who_ToString() As String + Return String.Format( + "[ClientStatus] Client=[{0}] Id=[{1}] Online=[{2}] Server=[{3}] Channel=[{4}]", + ClientName, ClientId, IsOnline, ServerAddress, ChannelState) + End Function End Class + End Class diff --git a/EDMIAPI/Client/Channel.vb b/EDMIAPI/Client/Channel.vb index 8a8f1d41..53f75c26 100644 --- a/EDMIAPI/Client/Channel.vb +++ b/EDMIAPI/Client/Channel.vb @@ -1,4 +1,5 @@ Imports System.ServiceModel +Imports System.Threading Imports System.Xml Imports DigitalData.Modules.Base Imports DigitalData.Modules.EDMI.API.EDMIServiceReference @@ -10,31 +11,65 @@ Public Class Channel Private ReadOnly ChannelFactory As ChannelFactory(Of IEDMIServiceChannel) Public Event Reconnect As EventHandler + ' Verbindungszähler + Private Shared _ActiveConnections As Integer = 0 + Public Shared ReadOnly Property ActiveConnections As Integer + Get + Return _ActiveConnections + End Get + End Property Public Shared Function GetBinding(Optional AuthenticationMode As TcpClientCredentialType = TcpClientCredentialType.Windows) As NetTcpBinding Return New NetTcpBinding() With { - .MaxReceivedMessageSize = Constants.ChannelSettings.MAX_RECEIVED_MESSAGE_SIZE, - .MaxBufferSize = Constants.ChannelSettings.MAX_BUFFER_SIZE, - .MaxBufferPoolSize = Constants.ChannelSettings.MAX_BUFFER_POOL_SIZE, - .TransferMode = TransferMode.Streamed, - .Security = New NetTcpSecurity() With { - .Mode = SecurityMode.Transport, - .Transport = New TcpTransportSecurity() With { - .ClientCredentialType = AuthenticationMode - } - }, - .ReaderQuotas = New XmlDictionaryReaderQuotas() With { - .MaxArrayLength = Constants.ChannelSettings.MAX_ARRAY_LENGTH, - .MaxStringContentLength = Constants.ChannelSettings.MAX_STRING_CONTENT_LENGTH + .MaxReceivedMessageSize = Constants.ChannelSettings.MAX_RECEIVED_MESSAGE_SIZE, + .MaxBufferSize = Constants.ChannelSettings.MAX_BUFFER_SIZE, + .MaxBufferPoolSize = Constants.ChannelSettings.MAX_BUFFER_POOL_SIZE, + .ListenBacklog = Constants.ChannelSettings.MAX_CONNECTIONS, + .MaxConnections = Constants.ChannelSettings.MAX_CONNECTIONS, + .TransferMode = TransferMode.Streamed, + .SendTimeout = TimeSpan.FromSeconds(30), ' ← War: 5min → Schnellere Fehlererkennung + .ReceiveTimeout = TimeSpan.FromMinutes(10), ' Keepalive < Firewall-Timeout (3600s) ✓ + .OpenTimeout = TimeSpan.FromSeconds(30), ' ← Neu: Verbindungsaufbau begrenzen + .CloseTimeout = TimeSpan.FromSeconds(30), + .Security = New NetTcpSecurity() With { + .Mode = SecurityMode.Transport, + .Transport = New TcpTransportSecurity() With { + .ClientCredentialType = AuthenticationMode } + }, + .ReaderQuotas = New XmlDictionaryReaderQuotas() With { + .MaxArrayLength = Constants.ChannelSettings.MAX_ARRAY_LENGTH, + .MaxStringContentLength = Constants.ChannelSettings.MAX_STRING_CONTENT_LENGTH } + } End Function Public Sub New(pLogConfig As LogConfig, pServerAddress As ServerAddressStruct) MyBase.New(pLogConfig) ChannelFactory = GetChannelFactory(pServerAddress) End Sub + ''' + ''' Closes a channel gracefully, falls back to Abort on error + ''' + Public Shared Sub CloseChannel(pChannel As IEDMIServiceChannel) + If pChannel Is Nothing Then Return + Try + If pChannel.State = CommunicationState.Opened Then + pChannel.Close() + ElseIf pChannel.State = CommunicationState.Faulted OrElse + pChannel.State = CommunicationState.Closing Then + pChannel.Abort() + End If + ' CommunicationState.Created / Closed → nichts tun, Zähler nicht betroffen + Catch ex As Exception + Try + pChannel.Abort() + Catch + ' Ignore + End Try + End Try + End Sub ''' ''' Creates a channel and adds a Faulted-Handler ''' @@ -44,21 +79,45 @@ Public Class Channel Logger.Debug("Creating channel.") Dim oChannel = ChannelFactory.CreateChannel() - AddHandler oChannel.Faulted, Sub() RaiseEvent Reconnect(Me, Nothing) + ' Verbindung zählen beim Öffnen + AddHandler oChannel.Opened, Sub() + Dim oCount = Interlocked.Increment(_ActiveConnections) + Logger.Debug("Channel opened. Active connections: [{0}]", oCount) + End Sub + ' Verbindung abziehen beim Schließen oder Fehler + AddHandler oChannel.Closed, Sub() + Dim oCount = Interlocked.Decrement(_ActiveConnections) + Logger.Debug("Channel closed. Active connections: [{0}]", oCount) + End Sub + + AddHandler oChannel.Faulted, Sub() + Dim oCount = Interlocked.Decrement(_ActiveConnections) + Logger.Warn("Channel faulted. Active connections: [{0}]", oCount) + RaiseEvent Reconnect(Me, Nothing) + End Sub + + Logger.Debug("Channel created. State=[{0}]", oChannel.State) Return oChannel Catch ex As Exception Logger.Error(ex) - Throw ex + Throw End Try End Function Private Function GetChannelFactory(pServerAddress As ServerAddressStruct) As ChannelFactory(Of IEDMIServiceChannel) Dim oBinding = GetBinding() Dim oAddress = New EndpointAddress($"net.tcp://{pServerAddress.Host}:{pServerAddress.Port}/DigitalData/Services/Main") + Logger.Debug("Creating ChannelFactory with Binding: SendTimeout=[{0}], ReceiveTimeout=[{1}], MaxConnections=[{2}], MaxBufferSize=[{3}], TransferMode=[{4}]", + oBinding.SendTimeout, + oBinding.ReceiveTimeout, + oBinding.MaxConnections, + oBinding.MaxBufferSize, + oBinding.TransferMode) Dim oFactory = New ChannelFactory(Of IEDMIServiceChannel)(oBinding, oAddress) Return oFactory End Function + End Class diff --git a/EDMIAPI/DatabaseWithFallback.vb b/EDMIAPI/DatabaseWithFallback.vb index 552b8da8..80f35f53 100644 --- a/EDMIAPI/DatabaseWithFallback.vb +++ b/EDMIAPI/DatabaseWithFallback.vb @@ -1,4 +1,5 @@ -Imports DigitalData.Modules.Database +Imports System.Data.SqlClient +Imports DigitalData.Modules.Database Imports DigitalData.Modules.EDMI.API Imports DigitalData.Modules.EDMI.API.Constants Imports DigitalData.Modules.EDMI.API.EDMIServiceReference @@ -78,7 +79,7 @@ Public Class DatabaseWithFallback End Function Public Async Function GetDatatableECMAsync(pSQL As String) As Task(Of DataTable) - Return Await Task.Run(Function() GetDatatableECM(pSQL)) + Return Await GetDatatableFromServiceAsync(pSQL, Constants.DatabaseType.ECM, 0) End Function Public Function GetDatatableIDB(pSQL As String) As DataTable @@ -86,7 +87,7 @@ Public Class DatabaseWithFallback End Function Public Async Function GetDatatableIDBAsync(pSQL As String) As Task(Of DataTable) - Return Await Task.Run(Function() GetDatatableIDB(pSQL)) + Return Await GetDatatableFromServiceAsync(pSQL, Constants.DatabaseType.IDB, 0) End Function Public Function GetDatatableWithConnection(pSQL As String, pConnectionId As Integer) As DataTable @@ -94,7 +95,7 @@ Public Class DatabaseWithFallback End Function Public Async Function GetDatatableWithConnectionAsync(pSQL As String, pConnectionId As Integer) As Task(Of DataTable) - Return Await Task.Run(Function() GetDatatableWithConnection(pSQL, pConnectionId)) + Return Await GetDatatableFromServiceAsync(pSQL, Constants.DatabaseType.None, pConnectionId) End Function #End Region @@ -104,7 +105,7 @@ Public Class DatabaseWithFallback End Function Public Async Function GetScalarValueECMAsync(pSQL As String) As Task(Of Object) - Return Await Task.Run(Function() GetScalarValueECM(pSQL)) + Return Await GetScalarValueFromServiceAsync(pSQL, Constants.DatabaseType.ECM, 0) End Function Public Function GetScalarValueIDB(pSQL As String) As Object @@ -112,7 +113,7 @@ Public Class DatabaseWithFallback End Function Public Async Function GetScalarValueIDBAsync(pSQL As String) As Task(Of Object) - Return Await Task.Run(Function() GetScalarValueIDB(pSQL)) + Return Await GetScalarValueFromServiceAsync(pSQL, Constants.DatabaseType.IDB, 0) End Function Public Function GetScalarValueWithConnection(pSQL As String, pConnectionId As Integer) As Object @@ -120,7 +121,7 @@ Public Class DatabaseWithFallback End Function Public Async Function GetScalarValueWithConnectionAsync(pSQL As String, pConnectionId As Integer) As Task(Of Object) - Return Await Task.Run(Function() GetScalarValueWithConnection(pSQL, pConnectionId)) + Return Await GetScalarValueFromServiceAsync(pSQL, Constants.DatabaseType.None, pConnectionId) End Function #End Region @@ -130,7 +131,7 @@ Public Class DatabaseWithFallback End Function Public Async Function ExecuteNonQueryECMAsync(pSQL As String) As Task(Of Boolean) - Return Await Task.Run(Function() ExecuteNonQueryECM(pSQL)) + Return Await ExecuteNonQueryFromServiceAsync(pSQL, Constants.DatabaseType.ECM, 0) End Function Public Function ExecuteNonQueryIDB(pSQL As String) As Boolean @@ -138,7 +139,7 @@ Public Class DatabaseWithFallback End Function Public Async Function ExecuteNonQueryIDBAsync(pSQL As String) As Task(Of Boolean) - Return Await Task.Run(Function() ExecuteNonQueryIDB(pSQL)) + Return Await ExecuteNonQueryFromServiceAsync(pSQL, Constants.DatabaseType.IDB, 0) End Function Public Function ExecuteNonQueryWithConnection(pSQL As String, pConnectionId As Integer) As Boolean @@ -146,7 +147,7 @@ Public Class DatabaseWithFallback End Function Public Async Function ExecuteNonQueryWithConnectionAsync(pSQL As String, pConnectionId As Integer) As Task(Of Boolean) - Return Await Task.Run(Function() ExecuteNonQueryWithConnection(pSQL, pConnectionId)) + Return Await ExecuteNonQueryFromServiceAsync(pSQL, Constants.DatabaseType.None, pConnectionId) End Function #End Region @@ -187,33 +188,34 @@ Public Class DatabaseWithFallback Try Dim oResult As DataTable = Nothing Dim oTableResult As TableResult = Nothing - + _Logger.Debug("GetDatatable: Fetching datatable [{0}] , Filter Expression [{1}], Sort By Column [{2}], Force Fallback [{3}]", + pDataTableName, pFilterExpression, pSortByColumn, pForceFallback) ' If there is no client, we assume there is no service (configured) If _Client Is Nothing Then - _Logger.Debug("Client is empty, falling back to direct database access.") + _Logger.Debug("GetDatatable: Client is empty, falling back to direct database access.") Return GetDatatableFromDatabase(pFallbackSQL, pFallbackType, pConnectionId) End If ' If ForceFallback flag is set, we go to database immediately If pForceFallback Or _ClientConfig.ForceDirectDatabaseAccess Then - _Logger.Debug("ForceFallback is True, falling back to direct database access.") + _Logger.Debug("GetDatatable: ForceFallback is True, falling back to direct database access.") Return GetDatatableFromDatabase(pFallbackSQL, pFallbackType, pConnectionId) End If ' If the table is not cached, we try going through the service If Not IsTableCached(pDataTableName) Then - _Logger.Debug("Datatable is not chached, fetching data from service.") + _Logger.Debug("GetDatatable: Datatable [{0}] is not chached, fetching data from service.", pDataTableName) Return GetDatatableFromService(pFallbackSQL, pFallbackType, pConnectionId) End If ' If there is a proper ConnectionId, we try going through the service If pConnectionId > 0 Then - _Logger.Debug("ConnectionId is set, fetching data from service.") + _Logger.Debug("GetDatatable: ConnectionId is set, fetching data from service.") Return GetDatatableFromService(pFallbackSQL, pFallbackType, pConnectionId) End If Try - _Logger.Debug("Datatable is chached, fetching data from cache.") + _Logger.Debug("GetDatatable: Datatable is chached, fetching data from cache.") oTableResult = _Client.GetDatatableByName(pDataTableName, pFilterExpression, pSortByColumn) Catch ex As Exception _Logger.Error(ex) @@ -221,9 +223,10 @@ Public Class DatabaseWithFallback End Try If oTableResult Is Nothing OrElse oTableResult.OK = False Then - _Logger.Warn("Datatable [{0}] could not be fetched from AppServer Cache. Falling back to direct Database Access.", pDataTableName) + _Logger.Warn("⚠ GetDatatable: Datatable [{0}] could not be fetched from AppServer Cache. Falling back to direct Database Access.", pDataTableName) Return GetDatatableFromDatabase(pFallbackSQL, pFallbackType, pConnectionId) Else + _Logger.Debug("✔ GetDatatable: Datatable [{0}] fetched from AppServer Cache.", pDataTableName) Return oTableResult.Table End If @@ -276,11 +279,13 @@ Public Class DatabaseWithFallback ' If there is no client, we assume there is no service (configured) If _Client Is Nothing Then + _Logger.Debug("ExecuteNonQuery - Client is empty, falling back to direct database access.") Return ExecuteNonQueryFromDatabase(pSQL, pDatabaseType, pConnectionId) End If ' If ForceFallback flag is set, we go to database immediately If pForceFallback Or _ClientConfig.ForceDirectDatabaseAccess Then + _Logger.Debug("ExecuteNonQuery - ForceFallback is set, falling back to direct database access.") Return ExecuteNonQueryFromDatabase(pSQL, pDatabaseType, pConnectionId) End If @@ -294,7 +299,9 @@ Public Class DatabaseWithFallback End Function Private Function IsTableCached(pName As String) As Boolean + _Logger.Debug("IsTableCached: Checking if table [{0}] is cached.", pName) If _Client Is Nothing Then + _Logger.Warn("⚠ IsTableCached: Client is nothing, cannot check if table is cached.") Return False End If @@ -303,42 +310,74 @@ Public Class DatabaseWithFallback Private Function GetDatatableFromService(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As DataTable Try + _Logger.Debug("GetDatatableFromService: Fetching data from service [{0}] with Connection Id [{1}] and Command [{2}]", DatabaseType.ToString, pConnectionId, pSQLCommand) Dim oResult As GetDatatableResponse = Nothing Select Case DatabaseType Case Constants.DatabaseType.ECM oResult = _Client.GetDatatableFromECM(pSQLCommand) - Case Constants.DatabaseType.IDB oResult = _Client.GetDatatableFromIDB(pSQLCommand) - Case Else Return GetDatatableFromDatabase(pSQLCommand, DatabaseType, pConnectionId) - End Select - If oResult Is Nothing Then - Throw New ApplicationException("Unexpected server error ocurred!") - End If - - If oResult.OK = False Then - Throw New ApplicationException(oResult.ErrorMessage) + If oResult Is Nothing OrElse oResult.OK = False Then + _Logger.Warn("⚠ GetDatatableFromService: Service returned invalid response [{0}]. Falling back to direct database access.", If(oResult Is Nothing, "null", oResult.ErrorMessage)) + Return GetDatatableFromDatabase(pSQLCommand, DatabaseType, pConnectionId) End If Return oResult.Table + Catch ex As InvalidOperationException + _Logger.Warn("⚠ GetDatatableFromService skipped: [{0}] Falling back to direct database access.", ex.Message) + Return GetDatatableFromDatabase(pSQLCommand, DatabaseType, pConnectionId) + Catch ex As Exception - _Logger.Warn("GetDatatableFromService failed. Falling back to direct database access.") + _Logger.Warn("⚠ GetDatatableFromService failed. Falling back to direct database access.") _Logger.Error(ex) Return GetDatatableFromDatabase(pSQLCommand, DatabaseType, pConnectionId) End Try End Function + Private Async Function GetDatatableFromServiceAsync(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As Task(Of DataTable) + Dim oFallback As Boolean = False + _Logger.Debug("GetDatatableFromServiceAsync: GetDatatableFromServiceAsync called with SQL [{0}], DatabaseType [{1}], ConnectionId [{2}]", pSQLCommand, DatabaseType.ToString, pConnectionId) + If _Client Is Nothing OrElse _ClientConfig?.ForceDirectDatabaseAccess Then + Return Await Task.Run(Function() GetDatatableFromDatabase(pSQLCommand, DatabaseType, pConnectionId)) + End If + Dim oResult As GetDatatableResponse = Nothing + Try + Select Case DatabaseType + Case Constants.DatabaseType.ECM + oResult = Await _Client.GetDatatableFromECMAsync(pSQLCommand) + Case Constants.DatabaseType.IDB + oResult = Await _Client.GetDatatableFromIDBAsync(pSQLCommand) + Case Else + oResult = Await _Client.GetDatatableFromConnectionAsync(pSQLCommand, pConnectionId) + End Select + + If oResult Is Nothing OrElse oResult.OK = False Then + _Logger.Warn("⚠ GetDatatableFromServiceAsync failed. Falling back to direct database access.") + oFallback = True + End If + Catch ex As Exception + _Logger.Warn("⚠ GetDatatableFromServiceAsync failed. Falling back to direct database access.") + _Logger.Error(ex) + oFallback = True + End Try + + If oFallback Then + Return Await Task.Run(Function() GetDatatableFromDatabase(pSQLCommand, DatabaseType, pConnectionId)) + End If + + Return oResult.Table + End Function Private Function GetDatatableFromDatabase(pSQLCommand As String, pDatabaseType As Constants.DatabaseType, pConnectionId As Integer) As DataTable Try - _Logger.Debug("Fetching data from database [{0}] with Connection Id [{0}]", pDatabaseType.ToString, pConnectionId) + _Logger.Debug("GetDatatableFromDatabase: Fetching data from database [{0}] with Connection Id [{1}] and Command [{2}]", pDatabaseType.ToString, pConnectionId, pSQLCommand) Select Case pDatabaseType Case Constants.DatabaseType.IDB @@ -361,35 +400,68 @@ Public Class DatabaseWithFallback End Try End Function + Private Async Function GetScalarValueFromServiceAsync(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As Task(Of Object) + Dim oFallback As Boolean = False + _Logger.Debug("GetScalarValueFromServiceAsync called with SQL [{0}], DatabaseType [{1}], ConnectionId [{2}]", pSQLCommand, DatabaseType.ToString, pConnectionId) + + If _Client Is Nothing OrElse _ClientConfig?.ForceDirectDatabaseAccess Then + Return Await Task.Run(Function() GetScalarValueFromDatabase(pSQLCommand, DatabaseType, pConnectionId)) + End If + + Dim oResult As GetScalarValueResponse = Nothing + Try + Select Case DatabaseType + Case Constants.DatabaseType.ECM + oResult = Await _Client.GetScalarValueFromECMAsync(pSQLCommand) + Case Constants.DatabaseType.IDB + oResult = Await _Client.GetScalarValueFromIDBAsync(pSQLCommand) + Case Else + oResult = Await _Client.GetScalarValueFromConnectionAsync(pSQLCommand, pConnectionId) + End Select + + If oResult Is Nothing OrElse oResult.OK = False Then + _Logger.Warn("⚠ GetScalarValueFromServiceAsync failed. Falling back to direct database access.") + oFallback = True + End If + Catch ex As Exception + _Logger.Warn("⚠ GetScalarValueFromServiceAsync failed. Falling back to direct database access.") + _Logger.Error(ex) + oFallback = True + End Try + + If oFallback Then + Return Await Task.Run(Function() GetScalarValueFromDatabase(pSQLCommand, DatabaseType, pConnectionId)) + End If + + Return oResult.Scalar + End Function Private Function GetScalarValueFromService(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As Object Try Dim oResult As GetScalarValueResponse = Nothing - + _Logger.Debug("GetScalarValueFromService [{0}] with Connection Id [{1}] and Command [{2}]", DatabaseType.ToString, pConnectionId, pSQLCommand) Select Case DatabaseType Case Constants.DatabaseType.ECM oResult = _Client.GetScalarValueFromECM(pSQLCommand) - Case Constants.DatabaseType.IDB oResult = _Client.GetScalarValueFromIDB(pSQLCommand) - Case Else Return GetScalarValueFromDatabase(pSQLCommand, DatabaseType, pConnectionId) - End Select - If oResult Is Nothing Then - Throw New ApplicationException("Unexpected server error ocurred!") - End If - - If oResult.OK = False Then - Throw New ApplicationException(oResult.ErrorMessage) + If oResult Is Nothing OrElse oResult.OK = False Then + _Logger.Warn("⚠ GetScalarValueFromService: Service returned invalid response [{0}]. Falling back to direct database access.", If(oResult Is Nothing, "null", oResult.ErrorMessage)) + Return GetScalarValueFromDatabase(pSQLCommand, DatabaseType, pConnectionId) End If Return oResult.Scalar + Catch ex As InvalidOperationException + _Logger.Warn("⚠ GetScalarValueFromService skipped: [{0}] Falling back to direct database access.", ex.Message) + Return GetScalarValueFromDatabase(pSQLCommand, DatabaseType, pConnectionId) + Catch ex As Exception - _Logger.Warn("GetScalarValueFromService failed. Falling back to direct database access.") + _Logger.Warn("⚠ GetScalarValueFromService failed. Falling back to direct database access.") _Logger.Error(ex) Return GetScalarValueFromDatabase(pSQLCommand, DatabaseType, pConnectionId) @@ -399,6 +471,7 @@ Public Class DatabaseWithFallback Private Function GetScalarValueFromDatabase(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As Object Try + _Logger.Debug("DBwFF GetScalarValueFromDatabase [{0}] with Connection Id [{1}] and Command [{2}]", DatabaseType.ToString, pConnectionId, pSQLCommand) Select Case DatabaseType Case Constants.DatabaseType.IDB Return _DatabaseIDB.GetScalarValue(pSQLCommand) @@ -419,35 +492,68 @@ Public Class DatabaseWithFallback End Try End Function + Private Async Function ExecuteNonQueryFromServiceAsync(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As Task(Of Boolean) + Dim oFallback As Boolean = False + _Logger.Debug("ExecuteNonQueryFromServiceAsync called with SQL [{0}], DatabaseType [{1}], ConnectionId [{2}]", pSQLCommand, DatabaseType.ToString, pConnectionId) + + If _Client Is Nothing OrElse _ClientConfig?.ForceDirectDatabaseAccess Then + Return Await Task.Run(Function() ExecuteNonQueryFromDatabase(pSQLCommand, DatabaseType, pConnectionId)) + End If + + Dim oResult As ExecuteNonQueryResponse = Nothing + Try + Select Case DatabaseType + Case Constants.DatabaseType.ECM + oResult = Await _Client.ExecuteNonQueryFromECMAsync(pSQLCommand) + Case Constants.DatabaseType.IDB + oResult = Await _Client.ExecuteNonQueryFromIDBAsync(pSQLCommand) + Case Else + oResult = Await _Client.ExecuteNonQueryFromConnectionAsync(pSQLCommand, pConnectionId) + End Select + + If oResult Is Nothing OrElse oResult.OK = False Then + _Logger.Warn("⚠ ExecuteNonQueryFromServiceAsync failed. Falling back to direct database access.") + oFallback = True + End If + Catch ex As Exception + _Logger.Warn("⚠ ExecuteNonQueryFromServiceAsync failed. Falling back to direct database access.") + _Logger.Error(ex) + oFallback = True + End Try + + If oFallback Then + Return Await Task.Run(Function() ExecuteNonQueryFromDatabase(pSQLCommand, DatabaseType, pConnectionId)) + End If + + Return oResult.Result + End Function Private Function ExecuteNonQueryFromService(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As Boolean Try Dim oResult As ExecuteNonQueryResponse = Nothing - + _Logger.Debug("ExecuteNonQueryFromService [{0}] with Connection Id [{1}] and Command [{2}]", DatabaseType.ToString, pConnectionId, pSQLCommand) Select Case DatabaseType Case Constants.DatabaseType.ECM oResult = _Client.ExecuteNonQueryFromECM(pSQLCommand) - Case Constants.DatabaseType.IDB oResult = _Client.ExecuteNonQueryFromIDB(pSQLCommand) - Case Else Return ExecuteNonQueryFromDatabase(pSQLCommand, DatabaseType, pConnectionId) - End Select - If oResult Is Nothing Then - Throw New ApplicationException("Unexpected server error ocurred!") - End If - - If oResult.OK = False Then - Throw New ApplicationException(oResult.ErrorMessage) + If oResult Is Nothing OrElse oResult.OK = False Then + _Logger.Warn("⚠ ExecuteNonQueryFromService: Service returned invalid response [{0}]. Falling back to direct database access.", If(oResult Is Nothing, "null", oResult.ErrorMessage)) + Return ExecuteNonQueryFromDatabase(pSQLCommand, DatabaseType, pConnectionId) End If Return oResult.Result + Catch ex As InvalidOperationException + _Logger.Warn("⚠ ExecuteNonQueryFromService skipped: [{0}] Falling back to direct database access.", ex.Message) + Return ExecuteNonQueryFromDatabase(pSQLCommand, DatabaseType, pConnectionId) + Catch ex As Exception - _Logger.Warn("ExecuteNonQueryFromService failed. Falling back to direct database access.") + _Logger.Warn("⚠ ExecuteNonQueryFromService failed. Falling back to direct database access.") _Logger.Error(ex) Return ExecuteNonQueryFromDatabase(pSQLCommand, DatabaseType, pConnectionId) @@ -456,6 +562,8 @@ Public Class DatabaseWithFallback Private Function ExecuteNonQueryFromDatabase(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As Boolean Try + _Logger.Debug("ExecuteNonQueryFromDatabase [{0}] with Connection Id [{1}] and Command [{2}]", DatabaseType.ToString, pConnectionId, pSQLCommand) + Select Case DatabaseType Case Constants.DatabaseType.IDB Return _DatabaseIDB.ExecuteNonQuery(pSQLCommand) diff --git a/EDMIAPI/Modules/IDB/SetAttributeValue.vb b/EDMIAPI/Modules/IDB/SetAttributeValue.vb index 2504720e..c9140f19 100644 --- a/EDMIAPI/Modules/IDB/SetAttributeValue.vb +++ b/EDMIAPI/Modules/IDB/SetAttributeValue.vb @@ -25,7 +25,7 @@ Namespace Modules.IDB Return True End If - Logger.Warn("Error while setting Attribute Value: [{0}]", oResponse.ErrorMessage) + Logger.Warn("⚠ Error while setting Attribute Value: [{0}]", oResponse.ErrorMessage) Return False Catch ex As Exception diff --git a/EDMIAPI/My Project/AssemblyInfo.vb b/EDMIAPI/My Project/AssemblyInfo.vb index 553314f0..1ef4ff0e 100644 --- a/EDMIAPI/My Project/AssemblyInfo.vb +++ b/EDMIAPI/My Project/AssemblyInfo.vb @@ -13,7 +13,7 @@ Imports System.Runtime.InteropServices - + @@ -31,5 +31,5 @@ Imports System.Runtime.InteropServices ' übernehmen, indem Sie "*" eingeben: ' - +