Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Raw Oracle driver error in the schema switch failure dialog. (#3053)
- Oracle health check closing a connection a statement was still running on. (#3053)
- Global saved query inside a folder missing from every other connection. (#3045)
- Saved query and folder drawn nowhere when the folder holding it was gone.
- Keyword accepted for a global saved query while another connection already held it.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Foundation

/// What is known about the last time this connection's channel went away, and whether the
/// connection is finished for good.
///
/// Two closers reach one channel: whoever decided to close it, and the statement that was on the
/// wire when it went. The second one arrives through OracleNIO as `clientClosedConnection` and
/// knows nothing about the first, so it must not be allowed to overwrite what the first recorded.
/// Letting it did exactly that: a user disconnect recorded "the app closed it", the dying
/// statement replaced it with "OracleNIO had already closed the channel", and the replay guard
/// then read a reason that permits a redial.
///
/// `isFinished` is deliberately one-way. The plugin drops its `OracleCoreConnection` when the app
/// disconnects and builds a new one to reconnect, so a connection closed that way is never reached
/// again by anything the app owns, and anything still holding it has to find it finished.
internal struct OracleCloseRecord: Sendable, Equatable {
private(set) var reason: OracleDisconnectReason?
private(set) var isFinished = false

mutating func record(_ reason: OracleDisconnectReason) {
isFinished = isFinished || reason.endsConnection
guard self.reason == nil else { return }
self.reason = reason
}

mutating func clearOnConnect() {
reason = nil
}

/// Whether a statement that only configures the session may be sent again on a replacement
/// connection.
var allowsSessionSetupReplay: Bool {
!isFinished && reason?.allowsReplay == true
}

/// Whether a statement that finds no channel may open one.
var allowsReconnect: Bool {
!isFinished
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,69 @@ public enum OracleConnectErrorClassifier {
}
}

/// Which OracleNIO failures leave the channel unusable.
///
/// It mirrors OracleNIO's own `ConnectionStateMachine.shouldCloseConnection(reason:)`, which is
/// internal and so cannot be called. Disagreeing with it means the app keeps a channel OracleNIO
/// has already torn down, and the next statement on it fails for a reason nobody can act on. The
/// old three-code list did exactly that for a client-side close (#3053).
///
/// `clientClosesConnection` and `clientClosedConnection` are the two OracleNIO refuses to classify
/// at all, because it raises them only from `OracleConnection.close()`: by the time one exists the
/// channel is gone, so they are unambiguously fatal here.
public enum OracleChannelFatalCode {
public static func isChannelFatal(_ codeDescription: String) -> Bool {
public static func isChannelFatal(_ codeDescription: String, serverErrorNumber: Int? = nil) -> Bool {
if codeDescription.hasPrefix("unsupportedVerifierType") {
return true
}
switch codeDescription {
case "connectionError", "messageDecodingFailure", "unexpectedBackendMessage":
case "clientClosesConnection",
"clientClosedConnection",
"failedToAddSSLHandler",
"failedToVerifyTLSCertificates",
"connectionError",
"messageDecodingFailure",
"missingParameter",
"unexpectedBackendMessage",
"serverVersionNotSupported",
"sidNotSupported",
"uncleanShutdown",
"unsupportedDataType",
"advancedNegotiationFailed",
"advancedNegotiationRequired",
"loginHandshakeTimedOut":
return true
case "server":
return serverErrorNumber == 28 || serverErrorNumber == 600
default:
return false
}
}

/// What took the channel away, for a code ``isChannelFatal(_:serverErrorNumber:)`` calls fatal.
///
/// The three read very differently to a user. A lost socket and a close from this side are both
/// "the connection went away, run it again"; only a protocol failure is worth telling anyone
/// the server sent something the driver could not read.
public static func closureKind(_ codeDescription: String) -> OracleChannelClosureKind {
switch codeDescription {
case "clientClosesConnection", "clientClosedConnection":
return .clientClose
case "uncleanShutdown", "connectionError":
return .transportLoss
default:
return .protocolFailure
}
}
}

public enum OracleChannelClosureKind: Sendable, Equatable {
/// This side called `OracleConnection.close()` while the statement was on the wire.
case clientClose
/// The socket went away: the server, a VPN, or the OS closed it.
case transportLoss
/// The driver could not make sense of what came back.
case protocolFailure
}

public enum OracleSSLClassifier {
Expand Down
Loading
Loading