Skip to content

Commit 6a2adee

Browse files
authored
refactor: Network Layer Refactoring and Optimization (#502)
* refactor: completely redo internal network processing Signed-off-by: Rob Walworth <[email protected]> * refactor: formatting Signed-off-by: Rob Walworth <[email protected]> * refactor: final improvements Signed-off-by: Rob Walworth <[email protected]> * refactor: more improvements Signed-off-by: Rob Walworth <[email protected]> * refactor: static code analysis checks Signed-off-by: Rob Walworth <[email protected]> * refactor: static code analysis comments Signed-off-by: Rob Walworth <[email protected]> * refactor: forgot one Signed-off-by: Rob Walworth <[email protected]> * docs: remove warning and add TODO Signed-off-by: Rob Walworth <[email protected]> * chore: fix codeql warning Signed-off-by: Rob Walworth <[email protected]> * fix: disable network updates during integration tests Signed-off-by: Rob Walworth <[email protected]> * fix: disable automatic network updates for tests Signed-off-by: Rob Walworth <[email protected]> --------- Signed-off-by: Rob Walworth <[email protected]>
1 parent 62da63d commit 6a2adee

28 files changed

+3077
-1443
lines changed

Sources/Hiero/Backoff.swift

Lines changed: 0 additions & 115 deletions
This file was deleted.

Sources/Hiero/Client/Backoff.swift

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
import Foundation
4+
5+
// MARK: - Client Backoff Configuration
6+
7+
/// Configuration for retry backoff behavior.
8+
///
9+
/// This structure defines the parameters for exponential backoff retry logic,
10+
/// including timing constraints and maximum attempt limits.
11+
///
12+
/// ## Related Types
13+
/// - `ExponentialBackoff` - The underlying backoff algorithm implementation
14+
/// - `Client` - Uses this configuration for request retries
15+
internal struct Backoff {
16+
// MARK: - Constants
17+
18+
/// Default maximum number of retry attempts before giving up
19+
internal static let defaultMaxAttempts: Int = 10
20+
21+
// MARK: - Initialization
22+
23+
/// Creates a new backoff configuration.
24+
///
25+
/// - Parameters:
26+
/// - maxBackoff: Maximum delay between retries (default: 60 seconds)
27+
/// - initialBackoff: Initial delay for first retry (default: 0.5 seconds)
28+
/// - maxAttempts: Maximum number of retry attempts (default: 10)
29+
/// - requestTimeout: Overall timeout for the entire request (default: nil = no timeout)
30+
/// - grpcTimeout: Timeout for individual GRPC calls (currently unused)
31+
internal init(
32+
maxBackoff: TimeInterval = ExponentialBackoff.defaultMaxInterval,
33+
initialBackoff: TimeInterval = ExponentialBackoff.defaultInitialInterval,
34+
maxAttempts: Int = Self.defaultMaxAttempts,
35+
requestTimeout: TimeInterval? = nil,
36+
grpcTimeout: TimeInterval? = nil
37+
) {
38+
self.maxBackoff = maxBackoff
39+
self.initialBackoff = initialBackoff
40+
self.maxAttempts = maxAttempts
41+
self.requestTimeout = requestTimeout
42+
self.grpcTimeout = grpcTimeout
43+
}
44+
45+
// MARK: - Properties
46+
47+
/// Maximum backoff delay between retries
48+
internal var maxBackoff: TimeInterval
49+
50+
/// Initial backoff delay for first retry
51+
internal var initialBackoff: TimeInterval
52+
53+
/// Maximum number of retry attempts
54+
internal var maxAttempts: Int
55+
56+
/// Overall timeout for the entire request including retries
57+
internal var requestTimeout: TimeInterval?
58+
59+
/// Timeout for individual GRPC calls (currently unused)
60+
internal var grpcTimeout: TimeInterval?
61+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
import GRPC
4+
import NIOCore
5+
import SwiftProtobuf
6+
7+
// MARK: - Channel Balancer
8+
9+
/// Load balancer for GRPC channels using random selection strategy.
10+
///
11+
/// This balancer uses simple random selection for load distribution. While the
12+
/// Power of Two Choices (P2C) algorithm would provide better distribution by
13+
/// selecting the less-loaded of two random channels, implementing it requires
14+
/// reliable metrics on in-flight requests, which the current GRPC API doesn't
15+
/// easily provide.
16+
///
17+
/// ## Design Rationale
18+
/// Random selection is sufficient for most use cases and avoids:
19+
/// - Thundering herd problems of round-robin selection
20+
/// - Complexity of maintaining accurate per-channel metrics
21+
/// - Race conditions in concurrent request tracking
22+
///
23+
/// ## Timeout Handling
24+
/// This balancer does not enforce per-request timeouts. Timeout protection is
25+
/// provided at the execution layer via `ExponentialBackoff.maxElapsedTime` and
26+
/// `Backoff.requestTimeout`, which prevent indefinite hangs during retries.
27+
///
28+
/// - TODO: Implement GRPC-level timeouts using `Backoff.grpcTimeout`.
29+
/// To add this functionality, update `Execute.applyGrpcHeader()` to accept
30+
/// a timeout parameter and configure `CallOptions.timeLimit`. This would provide
31+
/// finer-grained timeout control for individual GRPC calls.
32+
///
33+
/// ## Related Types
34+
/// - `NodeConnection` - Uses ChannelBalancer for node-level load balancing
35+
/// - `MirrorNetwork` - Uses ChannelBalancer for mirror node distribution
36+
/// - `Backoff` - Contains grpcTimeout field for future GRPC-level timeout support
37+
internal final class ChannelBalancer: GRPCChannel {
38+
// MARK: - Properties
39+
40+
/// The event loop for managing channel operations
41+
internal let eventLoop: EventLoop
42+
43+
/// Pool of GRPC channels available for load balancing
44+
private let channels: [any GRPCChannel]
45+
46+
// MARK: - Initialization
47+
48+
/// Creates a new channel balancer with the specified target/security pairs.
49+
///
50+
/// - Parameters:
51+
/// - eventLoop: The event loop for channel operations
52+
/// - targetSecurityPairs: Array of connection targets paired with their security configurations
53+
///
54+
/// - Precondition: GRPC channel pool creation must succeed. If it fails, this is a
55+
/// programming error and the application should crash rather than continue with invalid state.
56+
internal init(
57+
eventLoop: EventLoop,
58+
targetSecurityPairs: [(GRPC.ConnectionTarget, GRPCChannelPool.Configuration.TransportSecurity)]
59+
) {
60+
self.eventLoop = eventLoop
61+
self.channels = targetSecurityPairs.map { (target, security) in
62+
// Crash intentionally if channel creation fails - no recovery possible at initialization
63+
try! GRPCChannelPool.with(target: target, transportSecurity: security, eventLoopGroup: eventLoop)
64+
}
65+
}
66+
67+
// MARK: - GRPCChannel Protocol
68+
69+
/// Creates a call using a randomly selected channel from the pool.
70+
internal func makeCall<Request, Response>(
71+
path: String,
72+
type: GRPC.GRPCCallType,
73+
callOptions: GRPC.CallOptions,
74+
interceptors: [GRPC.ClientInterceptor<Request, Response>]
75+
)
76+
-> GRPC.Call<Request, Response> where Request: GRPC.GRPCPayload, Response: GRPC.GRPCPayload
77+
{
78+
return selectChannel().makeCall(
79+
path: path,
80+
type: type,
81+
callOptions: callOptions,
82+
interceptors: interceptors
83+
)
84+
}
85+
86+
/// Creates a call using a randomly selected channel from the pool.
87+
internal func makeCall<Request, Response>(
88+
path: String,
89+
type: GRPC.GRPCCallType,
90+
callOptions: GRPC.CallOptions,
91+
interceptors: [GRPC.ClientInterceptor<Request, Response>]
92+
) -> GRPC.Call<Request, Response> where Request: SwiftProtobuf.Message, Response: SwiftProtobuf.Message {
93+
return selectChannel().makeCall(
94+
path: path,
95+
type: type,
96+
callOptions: callOptions,
97+
interceptors: interceptors
98+
)
99+
}
100+
101+
/// Closes all channels in the pool.
102+
internal func close() -> NIOCore.EventLoopFuture<Void> {
103+
EventLoopFuture.reduce(
104+
into: (),
105+
channels.map { $0.close() },
106+
on: eventLoop
107+
) { _, _ in }
108+
}
109+
110+
// MARK: - Private Methods
111+
112+
/// Randomly selects a channel from the pool for load balancing.
113+
///
114+
/// - Returns: A randomly selected GRPC channel
115+
private func selectChannel() -> any GRPCChannel {
116+
channels.randomElement()! // Safe: channels is non-empty (created in init)
117+
}
118+
}

0 commit comments

Comments
 (0)