From 56fa5fc7b8178558deb48be347a7c37758dd58a4 Mon Sep 17 00:00:00 2001 From: William Date: Thu, 11 Sep 2025 07:03:22 +0200 Subject: [PATCH 01/10] refactor(SeleniumGridNodeAppInteractor): short methods for SeleniumGridNodeAppInteractor Refactor autoscaler and autodestroyer to have short methods as well. Initialize authHeader as class instance to avoid getting it from payload every time (very long). Then, add meaningful logs. --- .../Package.resolved | 43 +++-- .../Package.swift | 4 +- .../AnyEncodable.swift | 61 ------- .../Controllers/.gitkeep | 0 .../EnvironmentExtensions.swift | 17 -- .../SeleniumGridNodeAppInteractor.swift | 153 +++++++++++++----- .../SeleniumGridNodeAutoDestroyer.swift | 12 +- .../SeleniumGridNodeAutoscaler.swift | 16 +- 8 files changed, 157 insertions(+), 149 deletions(-) delete mode 100644 SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/AnyEncodable.swift delete mode 100644 SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Controllers/.gitkeep delete mode 100644 SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/EnvironmentExtensions.swift diff --git a/SeleniumGridNodeMachineAutoscaler/Package.resolved b/SeleniumGridNodeMachineAutoscaler/Package.resolved index bdf8d8b..064b0a5 100644 --- a/SeleniumGridNodeMachineAutoscaler/Package.resolved +++ b/SeleniumGridNodeMachineAutoscaler/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "9802e2b29a308fe5162b9b309bddbd9258f741cce1a70edc40ccec72b1597a4c", + "originHash" : "364aa31ddb591204d017baeb5b7ea1fe5c6ab793835d018b2b9582ff8392a7ff", "pins" : [ { "identity" : "async-http-client", "kind" : "remoteSourceControl", "location" : "https://github.com/swift-server/async-http-client.git", "state" : { - "revision" : "60235983163d040f343a489f7e2e77c1918a8bd9", - "version" : "1.26.1" + "revision" : "7dc119c7edf3c23f52638faadb89182861dee853", + "version" : "1.28.0" } }, { @@ -15,8 +15,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/async-kit.git", "state" : { - "revision" : "e048c8ee94967e8d8a1c2ec0e1156d6f7fa34d31", - "version" : "1.20.0" + "revision" : "6f3615ccf2ac3c2ae0c8087d527546e9544a43dd", + "version" : "1.21.0" + } + }, + { + "identity" : "automautilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/GetAutomaApp/AutomaUtilities", + "state" : { + "branch" : "main", + "revision" : "60e4a28b9f54ab9556e5cbbb176a5bcf41337161" } }, { @@ -87,8 +96,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-certificates.git", "state" : { - "revision" : "c059d9c9d08d6654b9a92dda93d9049a278964c6", - "version" : "1.12.0" + "revision" : "20c451f1ad8e344e61ddbb34ef196653d4b73ea6", + "version" : "1.13.0" } }, { @@ -105,8 +114,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-crypto.git", "state" : { - "revision" : "334e682869394ee239a57dbe9262bff3cd9495bd", - "version" : "3.14.0" + "revision" : "d1c6b70f7c5f19fb0b8750cb8dcdf2ea6e2d8c34", + "version" : "3.15.0" } }, { @@ -114,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-distributed-tracing.git", "state" : { - "revision" : "b78796709d243d5438b36e74ce3c5ec2d2ece4d8", - "version" : "1.2.1" + "revision" : "149fa4cc7d54c513288108142e7af9ceaf8def8f", + "version" : "1.2.2" } }, { @@ -186,8 +195,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-ssl.git", "state" : { - "revision" : "385f5bd783ffbfff46b246a7db7be8e4f04c53bd", - "version" : "2.33.0" + "revision" : "737e550e607d82bf15bdfddf158ec61652ce836f", + "version" : "2.34.0" } }, { @@ -204,8 +213,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-numerics.git", "state" : { - "revision" : "e0ec0f5f3af6f3e4d5e7a19d2af26b481acb6ba8", - "version" : "1.0.3" + "revision" : "bbadd4b853a33fd78c4ae977d17bb2af15eb3f2a", + "version" : "1.1.0" } }, { @@ -240,8 +249,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/vapor.git", "state" : { - "revision" : "3636f443474769147828a5863e81a31f6f30e92c", - "version" : "4.115.1" + "revision" : "636fbe688e001dc1f8d6f6026a52bcc396025d19", + "version" : "4.116.0" } }, { diff --git a/SeleniumGridNodeMachineAutoscaler/Package.swift b/SeleniumGridNodeMachineAutoscaler/Package.swift index 62c22d9..6765967 100644 --- a/SeleniumGridNodeMachineAutoscaler/Package.swift +++ b/SeleniumGridNodeMachineAutoscaler/Package.swift @@ -4,13 +4,14 @@ import PackageDescription let package = Package( name: "SeleniumGridNodeMachineAutoscaler", platforms: [ - .macOS(.v13) + .macOS(.v15), ], dependencies: [ // 💧 A server-side Swift web framework. .package(url: "https://github.com/vapor/vapor.git", from: "4.115.0"), // 🔵 Non-blocking, event-driven networking for Swift. Used for custom executors .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"), + .package(url: "https://github.com/GetAutomaApp/AutomaUtilities", branch: "main"), ], targets: [ .executableTarget( @@ -19,6 +20,7 @@ let package = Package( .product(name: "Vapor", package: "vapor"), .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"), + .product(name: "AutomaUtilities", package: "AutomaUtilities"), ], swiftSettings: swiftSettings ), diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/AnyEncodable.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/AnyEncodable.swift deleted file mode 100644 index 65fe5b1..0000000 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/AnyEncodable.swift +++ /dev/null @@ -1,61 +0,0 @@ -// AnyEncodable.swift -// Copyright (c) 2025 GetAutomaApp -// All source code and related assets are the property of GetAutomaApp. -// All rights reserved. - -import Foundation - -struct AnyEncodable: Encodable { - private let value: Any - - init(_ value: Any) { - self.value = value - } - - func encode(to encoder: any Encoder) throws { - var container = encoder.singleValueContainer() - - switch value { - case let v as Bool: - try container.encode(v) - case let v as Int: - try container.encode(v) - case let v as Int8: - try container.encode(v) - case let v as Int16: - try container.encode(v) - case let v as Int32: - try container.encode(v) - case let v as Int64: - try container.encode(v) - case let v as UInt: - try container.encode(v) - case let v as UInt8: - try container.encode(v) - case let v as UInt16: - try container.encode(v) - case let v as UInt32: - try container.encode(v) - case let v as UInt64: - try container.encode(v) - case let v as Double: - try container.encode(v) - case let v as Float: - try container.encode(v) - case let v as String: - try container.encode(v) - case let v as [Any]: - try container.encode(v.map { AnyEncodable($0) }) - case let v as [String: Any]: - try container.encode(v.mapValues { AnyEncodable($0) }) - case Optional.none: - try container.encodeNil() - default: - let context = EncodingError.Context( - codingPath: container.codingPath, - debugDescription: "Unsupported type: \(type(of: value))" - ) - throw EncodingError.invalidValue(value, context) - } - } -} diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Controllers/.gitkeep b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Controllers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/EnvironmentExtensions.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/EnvironmentExtensions.swift deleted file mode 100644 index 466805a..0000000 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/EnvironmentExtensions.swift +++ /dev/null @@ -1,17 +0,0 @@ -// EnvironmentExtensions.swift -// Copyright (c) 2025 GetAutomaApp -// All source code and related assets are the property of GetAutomaApp. -// All rights reserved. - -import Vapor - -public extension Environment { - static func getOrThrow(_ key: String) throws -> String { - guard - let value = get(key) - else { - throw Abort(.internalServerError, reason: "Value for key \(key) not found") - } - return value - } -} diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift index 6afd896..066bd80 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift @@ -3,81 +3,150 @@ // All source code and related assets are the property of GetAutomaApp. // All rights reserved. +import AutomaUtilities import Vapor internal protocol SeleniumGridNodeAppInteractorBase { - var nodesAppMachineAPIURL: String { get } - var flyAPIToken: String { get } - var authHeader: [(String, String)] { get } + var payload: SeleniumGridNodeAppInteractorPayload { get } } -internal class SeleniumGridNodeAppInteractor: SeleniumGridNodeAppInteractorBase { +internal struct SeleniumGridNodeAppInteractorPayload: Content { let nodesAppMachineAPIURL: String let flyAPIToken: String - let authHeader: [(String, String)] + let flyAPIHTTPRequestAuthenticationHeader: FlyAPIHTTPRequestAuthenticationHeader + + internal struct FlyAPIHTTPRequestAuthenticationHeader: Content { + let Authorization: String + + func getHeaderList() -> [(String, String)] { + return [("Authorization", Authorization)] + } + } +} + +internal class SeleniumGridNodeAppInteractor: SeleniumGridNodeAppInteractorBase { + let payload: SeleniumGridNodeAppInteractorPayload let logger: Logger let client: any Client init(logger: Logger, client: any Client) throws { - guard - let flyAPIURL = try URL(string: Environment.getOrThrow("FLY_API_URL")) - else { - throw Abort(.internalServerError) - } + self.logger = logger + self.client = client + + let flyAPIURL = try URL.fromString(payload: .init(string: Environment.getOrThrow("FLY_API_URL"))) + let flyAPIToken = try Environment.getOrThrow("SELENIUM_GRID_NODE_FLY_APP_API_TOKEN") + + payload = .init( + nodesAppMachineAPIURL: "\(flyAPIURL.absoluteString)/v1/apps/automa-web-core-seleniumgrid-node/machines", + flyAPIToken: flyAPIToken, + flyAPIHTTPRequestAuthenticationHeader: .init(Authorization: "Bearer \(flyAPIToken)") + ) + } + + internal func getListOfAllNodeMachines() async throws -> [SeleniumGridNodeAppNodeMachinesFinder.NodeMachine] { + try await SeleniumGridNodeAppNodeMachinesFinder( + logger: logger, + client: client, + payload: payload + ) + .getListOfAllNodeMachines() + } +} + +internal struct SeleniumGridNodeAppNodeMachinesFinder: SeleniumGridNodeAppInteractorBase { + let payload: SeleniumGridNodeAppInteractorPayload + let logger: Logger + let client: any Client + internal init( + logger: Logger, + client: any Client, + payload: SeleniumGridNodeAppInteractorPayload + ) { self.logger = logger self.client = client - nodesAppMachineAPIURL = "\(flyAPIURL.absoluteString)/v1/apps/automa-web-core-seleniumgrid-node/machines" - flyAPIToken = try Environment.getOrThrow("SELENIUM_GRID_NODE_FLY_APP_API_TOKEN") - authHeader = [("Authorization", "Bearer \(flyAPIToken)")] + self.payload = payload } - internal struct NodeMachine: Content { - let id: String - let state: String - let createdAt: Date + public func getListOfAllNodeMachines() async throws -> [NodeMachine] { + return try await getAllNodeMachinesList() + } - public enum CodingKeys: String, CodingKey { - case id - case state - case createdAt = "created_at" - } + private func getAllNodeMachinesList() async throws -> [NodeMachine] { + logGetListOfAllMachinesStarted() + let allMachines = try await validateAndGetAllMachines() + logGetListOfAllMachinesSuccess(totalMachines: allMachines.count) + return allMachines + } + + private func validateAndGetAllMachines() async throws -> [NodeMachine] { + let response = try await getAllNodeMachinesResponse() + try validateAllNodeMachinesResponseStatus(response: response) + + return try getNodeMachineListFromAppNodeMachinesResponse(response) } - internal func getListOfAllNodeMachines() async throws -> [NodeMachine] { + private func logGetListOfAllMachinesSuccess(totalMachines: Int) { logger.info( - "Getting a list of all machines.", + "Got list of all machines.", metadata: [ "to": .string("\(String(describing: Self.self)).\(#function)"), + "total_machines": .string(String(totalMachines)) ] ) - let res = try await client.get( - .init(stringLiteral: nodesAppMachineAPIURL), - headers: .init(authHeader) - ) + } + + private func getNodeMachineListFromAppNodeMachinesResponse(_ response: ClientResponse) throws -> [NodeMachine] { + try response.content.decode([NodeMachine].self) + } - if res.status != .ok { - let responseContent = try res.content.decode([String: String].self) - logger.info( - "Failed to get a list of all machines in nodes app", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - "response_content": .string("\(responseContent)"), - ] - ) - throw Abort(.internalServerError) + private func validateAllNodeMachinesResponseStatus(response: ClientResponse) throws { + if isInvalidAllNodeMachinesResponseStatus(status: response.status) { + try handleInvalidAllNodeMachinesResponse(res: response) } + } + + private func handleInvalidAllNodeMachinesResponse(res: ClientResponse) throws { + let responseContent = try res.content.decode([String: String].self) + logger.info( + "Failed to get a list of all machines in nodes app", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + "response_content": .string("\(responseContent)"), + ] + ) + throw Abort(.internalServerError) + } + + private func isInvalidAllNodeMachinesResponseStatus(status: HTTPStatus) -> Bool { + return status != .ok + } - let allMachines = try res.content.decode([NodeMachine].self) + private func getAllNodeMachinesResponse() async throws -> ClientResponse { + return try await client.get( + .init(stringLiteral: payload.nodesAppMachineAPIURL), + headers: .init(payload.flyAPIHTTPRequestAuthenticationHeader.getHeaderList()) + ) + } + private func logGetListOfAllMachinesStarted() { logger.info( - "Got list of all machines.", + "Getting a list of all machines.", metadata: [ "to": .string("\(String(describing: Self.self)).\(#function)"), - "total_machines": .string(String(allMachines.count)) ] ) + } - return allMachines + internal struct NodeMachine: Content { + let id: String + let state: String + let createdAt: Date + + public enum CodingKeys: String, CodingKey { + case id + case state + case createdAt = "created_at" + } } } diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoDestroyer.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoDestroyer.swift index 6a86af4..d6f4d17 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoDestroyer.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoDestroyer.swift @@ -50,7 +50,9 @@ internal class SeleniumGridNodeAutoDestroyer: SeleniumGridNodeAppInteractor { try await autoDestroyAllOffNodeMachines(cycleCount: cycleCount + 1, cyclePauseDuration: cyclePauseDuration) } - private func destroyAllOldNodeMachines(_ allMachines: [NodeMachine]) async throws { + private func destroyAllOldNodeMachines(_ allMachines: [SeleniumGridNodeAppNodeMachinesFinder + .NodeMachine]) async throws + { if allMachines.count == 0 { return } @@ -83,7 +85,9 @@ internal class SeleniumGridNodeAutoDestroyer: SeleniumGridNodeAppInteractor { } } - private func destroyAllOffNodeMachines(_ allMachines: [NodeMachine]) async throws { + private func destroyAllOffNodeMachines(_ allMachines: [SeleniumGridNodeAppNodeMachinesFinder + .NodeMachine]) async throws + { if allMachines.count == 0 { return } @@ -125,8 +129,8 @@ internal class SeleniumGridNodeAutoDestroyer: SeleniumGridNodeAppInteractor { ) let res = try await client.delete( - .init(stringLiteral: "\(nodesAppMachineAPIURL)/\(id)?force=true"), - headers: .init(authHeader) + .init(stringLiteral: "\(payload.nodesAppMachineAPIURL)/\(id)?force=true"), + headers: .init(payload.flyAPIHTTPRequestAuthenticationHeader.getHeaderList()) ) if res.status != .ok { diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoscaler.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoscaler.swift index 5273434..b5ebc77 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoscaler.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoscaler.swift @@ -3,6 +3,7 @@ // All source code and related assets are the property of GetAutomaApp. // All rights reserved. +import AutomaUtilities import Vapor internal class SeleniumGridNodeAutoscaler: SeleniumGridNodeAppInteractor { @@ -105,8 +106,8 @@ internal class SeleniumGridNodeAutoscaler: SeleniumGridNodeAppInteractor { ) ) - let res = try await client.post(.init(stringLiteral: nodesAppMachineAPIURL)) { req in - req.headers = .init(authHeader) + let res = try await client.post(.init(stringLiteral: payload.nodesAppMachineAPIURL)) { req in + req.headers = .init(payload.flyAPIHTTPRequestAuthenticationHeader.getHeaderList()) try req.content.encode(machineConfiguration) } @@ -137,7 +138,7 @@ internal class SeleniumGridNodeAutoscaler: SeleniumGridNodeAppInteractor { try await updateMachineNodeHostURLEnvironmentVariable( machineIdentifier: machineIdentifier, machineConfiguration: machineConfiguration, - flyAPIToken: flyAPIToken + flyAPIToken: payload.flyAPIToken ) try await Task.sleep(for: .seconds(20)) @@ -165,10 +166,11 @@ internal class SeleniumGridNodeAutoscaler: SeleniumGridNodeAppInteractor { ] ) - let res = try await client.post(.init(stringLiteral: "\(nodesAppMachineAPIURL)/\(machineIdentifier)")) { req in - req.headers = .init(authHeader) - try req.content.encode(["config": updatedConfiguration.config]) - } + let res = try await client + .post(.init(stringLiteral: "\(payload.nodesAppMachineAPIURL)/\(machineIdentifier)")) { req in + req.headers = .init(payload.flyAPIHTTPRequestAuthenticationHeader.getHeaderList()) + try req.content.encode(["config": updatedConfiguration.config]) + } if res.status != .ok { let responseContent = try res.content.decode([String: String].self) From f5e639bd5063f1a95f706a9aca0e860f8ff93f50 Mon Sep 17 00:00:00 2001 From: William Date: Thu, 11 Sep 2025 08:33:07 +0200 Subject: [PATCH 02/10] refactor: refactored a lot of code, around 50% done --- ...SeleniumGridNodeAutoDestroyerCommand.swift | 7 +- .../SeleniumGridNodeAutoScalerCommand.swift | 5 +- .../NodeMachineDeleter.swift | 79 +++++++++ .../SeleniumGridNodeAppInteractor.swift | 125 +++++++------- .../SeleniumGridNodeAutoDestroyer.swift | 159 +++--------------- ...eniumGridNodeAutoOffMachineDestroyer.swift | 71 ++++++++ ...eniumGridNodeAutoOldMachineDestroyer.swift | 74 ++++++++ .../SeleniumGridNodeAutoscaler.swift | 7 +- 8 files changed, 321 insertions(+), 206 deletions(-) create mode 100644 SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineDeleter.swift create mode 100644 SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOffMachineDestroyer.swift create mode 100644 SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOldMachineDestroyer.swift diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoDestroyerCommand.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoDestroyerCommand.swift index 7d33790..1974707 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoDestroyerCommand.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoDestroyerCommand.swift @@ -23,13 +23,14 @@ struct SeleniumGridNodeAutoDestroyerCommand: AsyncCommand { func run(using context: CommandContext, signature: Signature) async throws { let destroyer = try SeleniumGridNodeAutoDestroyer( logger: context.application.logger, - client: context.application.client + client: context.application.client, + cyclePauseDurationSeconds: 10 ) switch signature.type { case "offMachines": - try await destroyer.autoDestroyAllOffNodeMachines(cyclePauseDuration: 10) + try await destroyer.autoDestroyAllOffNodeMachines() case "oldMachines": - try await destroyer.autoDestroyAllOldNodeMachines(cyclePauseDuration: 10) + try await destroyer.autoDestroyAllOldNodeMachines() default: throw Abort( .internalServerError, diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoScalerCommand.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoScalerCommand.swift index 2ed95e2..06b2bb1 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoScalerCommand.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoScalerCommand.swift @@ -15,8 +15,9 @@ struct SeleniumGridNodeAutoScalerCommand: AsyncCommand { func run(using context: CommandContext, signature _: Signature) async throws { let autoscaler = try SeleniumGridNodeAutoscaler( client: context.application.client, - logger: context.application.logger + logger: context.application.logger, + cyclePauseDurationSeconds: 10 ) - try await autoscaler.autoscale(cyclePauseDuration: 10) + try await autoscaler.autoscale() } } diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineDeleter.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineDeleter.swift new file mode 100644 index 0000000..3dd09a9 --- /dev/null +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineDeleter.swift @@ -0,0 +1,79 @@ +// NodeMachineDeleter.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +import Vapor + +// TODO: Use logger.error instead of logger.info in places where errors occur. Create custom errors instead of using internalServerError everywhere. +// add more and better logs + +internal class NodeMachineDeleter: SeleniumGridNodeAppInteractor { + let machineID: String + + init(logger: Logger, client: any Client, machineID: String) throws { + self.machineID = machineID + try super.init(logger: logger, client: client) + } + + public func delete() async throws { + logDeleteNodeMachineStarted() + try await getAndValidateDeleteNodeMachineResponse() + logDeleteNodeMachineSuccess() + } + + private func getAndValidateDeleteNodeMachineResponse() async throws { + let response = try await getDeleteNodeMachineResponse() + try validateDeleteNodeMachineResponseStatus(response: response) + } + + private func validateDeleteNodeMachineResponseStatus(response: ClientResponse) throws { + if isInvalidHTTPResponseStatus(status: response.status) { + try handleInvalidDeleteNodeMachineResponse(response: response) + } + } + + private func logDeleteNodeMachineSuccess() { + logger.info( + "Successfully deleted node machine.", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + "deletd_node_machine_id": .string(machineID) + ] + ) + } + + private func handleInvalidDeleteNodeMachineResponse(response: ClientResponse) throws { + let responseContent = try decodeErrorFromResponse(response) + logDeleteNodeMachineFailed(responseContent: responseContent) + throw Abort(.internalServerError) + } + + private func logDeleteNodeMachineFailed(responseContent: [String: String]) { + logger.error( + "Failed to delete machine.", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + "response_content": .string("\(responseContent)"), + "node_machine_id_to_delete": .string(machineID) + ] + ) + } + + private func getDeleteNodeMachineResponse() async throws -> ClientResponse { + try await client.delete( + .init(stringLiteral: "\(payload.nodesAppMachineAPIURL)/\(machineID)?force=true"), + headers: .init(flyAPIHTTPRequestAuthenticationHeader) + ) + } + + private func logDeleteNodeMachineStarted() { + logger.info( + "Deleting node machine started.", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + "node_machine_id_to_delete": .string(machineID) + ] + ) + } +} diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift index 066bd80..465dbe0 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift @@ -6,26 +6,9 @@ import AutomaUtilities import Vapor -internal protocol SeleniumGridNodeAppInteractorBase { - var payload: SeleniumGridNodeAppInteractorPayload { get } -} - -internal struct SeleniumGridNodeAppInteractorPayload: Content { - let nodesAppMachineAPIURL: String - let flyAPIToken: String - let flyAPIHTTPRequestAuthenticationHeader: FlyAPIHTTPRequestAuthenticationHeader - - internal struct FlyAPIHTTPRequestAuthenticationHeader: Content { - let Authorization: String - - func getHeaderList() -> [(String, String)] { - return [("Authorization", Authorization)] - } - } -} - internal class SeleniumGridNodeAppInteractor: SeleniumGridNodeAppInteractorBase { let payload: SeleniumGridNodeAppInteractorPayload + let flyAPIHTTPRequestAuthenticationHeader: [(String, String)] let logger: Logger let client: any Client @@ -36,10 +19,11 @@ internal class SeleniumGridNodeAppInteractor: SeleniumGridNodeAppInteractorBase let flyAPIURL = try URL.fromString(payload: .init(string: Environment.getOrThrow("FLY_API_URL"))) let flyAPIToken = try Environment.getOrThrow("SELENIUM_GRID_NODE_FLY_APP_API_TOKEN") + flyAPIHTTPRequestAuthenticationHeader = [("Authorization", "Bearer \(flyAPIToken)")] + payload = .init( nodesAppMachineAPIURL: "\(flyAPIURL.absoluteString)/v1/apps/automa-web-core-seleniumgrid-node/machines", - flyAPIToken: flyAPIToken, - flyAPIHTTPRequestAuthenticationHeader: .init(Authorization: "Bearer \(flyAPIToken)") + flyAPIToken: flyAPIToken ) } @@ -47,31 +31,35 @@ internal class SeleniumGridNodeAppInteractor: SeleniumGridNodeAppInteractorBase try await SeleniumGridNodeAppNodeMachinesFinder( logger: logger, client: client, - payload: payload + payload: payload, + flyAPIHTTPRequestAuthenticationHeader: flyAPIHTTPRequestAuthenticationHeader, ) .getListOfAllNodeMachines() } } internal struct SeleniumGridNodeAppNodeMachinesFinder: SeleniumGridNodeAppInteractorBase { - let payload: SeleniumGridNodeAppInteractorPayload let logger: Logger let client: any Client - - internal init( - logger: Logger, - client: any Client, - payload: SeleniumGridNodeAppInteractorPayload - ) { - self.logger = logger - self.client = client - self.payload = payload - } + var payload: SeleniumGridNodeAppInteractorPayload + var flyAPIHTTPRequestAuthenticationHeader: [(String, String)] public func getListOfAllNodeMachines() async throws -> [NodeMachine] { return try await getAllNodeMachinesList() } + internal struct NodeMachine: Content { + let id: String + let state: String + let createdAt: Date + + public enum CodingKeys: String, CodingKey { + case id + case state + case createdAt = "created_at" + } + } + private func getAllNodeMachinesList() async throws -> [NodeMachine] { logGetListOfAllMachinesStarted() let allMachines = try await validateAndGetAllMachines() @@ -79,6 +67,15 @@ internal struct SeleniumGridNodeAppNodeMachinesFinder: SeleniumGridNodeAppIntera return allMachines } + private func logGetListOfAllMachinesStarted() { + logger.info( + "Getting a list of all machines.", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + ] + ) + } + private func validateAndGetAllMachines() async throws -> [NodeMachine] { let response = try await getAllNodeMachinesResponse() try validateAllNodeMachinesResponseStatus(response: response) @@ -86,29 +83,27 @@ internal struct SeleniumGridNodeAppNodeMachinesFinder: SeleniumGridNodeAppIntera return try getNodeMachineListFromAppNodeMachinesResponse(response) } - private func logGetListOfAllMachinesSuccess(totalMachines: Int) { - logger.info( - "Got list of all machines.", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - "total_machines": .string(String(totalMachines)) - ] + private func getAllNodeMachinesResponse() async throws -> ClientResponse { + return try await client.get( + .init(stringLiteral: payload.nodesAppMachineAPIURL), + headers: .init(flyAPIHTTPRequestAuthenticationHeader) ) } - private func getNodeMachineListFromAppNodeMachinesResponse(_ response: ClientResponse) throws -> [NodeMachine] { - try response.content.decode([NodeMachine].self) - } - private func validateAllNodeMachinesResponseStatus(response: ClientResponse) throws { - if isInvalidAllNodeMachinesResponseStatus(status: response.status) { + if isInvalidHTTPResponseStatus(status: response.status) { try handleInvalidAllNodeMachinesResponse(res: response) } } + internal func isInvalidHTTPResponseStatus(status: HTTPStatus) -> Bool { + return status != .ok + } + private func handleInvalidAllNodeMachinesResponse(res: ClientResponse) throws { - let responseContent = try res.content.decode([String: String].self) - logger.info( + let responseContent = try decodeErrorFromResponse(res) + // TODO: finish refactoring + logger.error( "Failed to get a list of all machines in nodes app", metadata: [ "to": .string("\(String(describing: Self.self)).\(#function)"), @@ -118,35 +113,33 @@ internal struct SeleniumGridNodeAppNodeMachinesFinder: SeleniumGridNodeAppIntera throw Abort(.internalServerError) } - private func isInvalidAllNodeMachinesResponseStatus(status: HTTPStatus) -> Bool { - return status != .ok - } - - private func getAllNodeMachinesResponse() async throws -> ClientResponse { - return try await client.get( - .init(stringLiteral: payload.nodesAppMachineAPIURL), - headers: .init(payload.flyAPIHTTPRequestAuthenticationHeader.getHeaderList()) - ) + private func getNodeMachineListFromAppNodeMachinesResponse(_ response: ClientResponse) throws -> [NodeMachine] { + try response.content.decode([NodeMachine].self) } - private func logGetListOfAllMachinesStarted() { + private func logGetListOfAllMachinesSuccess(totalMachines: Int) { logger.info( - "Getting a list of all machines.", + "Got list of all machines.", metadata: [ "to": .string("\(String(describing: Self.self)).\(#function)"), + "total_machines": .string(String(totalMachines)) ] ) } +} - internal struct NodeMachine: Content { - let id: String - let state: String - let createdAt: Date +internal protocol SeleniumGridNodeAppInteractorBase { + var payload: SeleniumGridNodeAppInteractorPayload { get } + var flyAPIHTTPRequestAuthenticationHeader: [(String, String)] { get } +} - public enum CodingKeys: String, CodingKey { - case id - case state - case createdAt = "created_at" - } +extension SeleniumGridNodeAppInteractorBase { + func decodeErrorFromResponse(_ response: ClientResponse) throws -> [String: String] { + return try response.content.decode([String: String].self) } } + +internal struct SeleniumGridNodeAppInteractorPayload: Content { + let nodesAppMachineAPIURL: String + let flyAPIToken: String +} diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoDestroyer.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoDestroyer.swift index d6f4d17..bd099ca 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoDestroyer.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoDestroyer.swift @@ -5,153 +5,48 @@ import Vapor -internal class SeleniumGridNodeAutoDestroyer: SeleniumGridNodeAppInteractor { - public func autoDestroyAllOldNodeMachines(cyclePauseDuration: Int) async throws { - try await autoDestroyAllOldNodeMachines(cycleCount: 1, cyclePauseDuration: cyclePauseDuration) - } +internal class SeleniumGridNodeAutoDestroyerBase: SeleniumGridNodeAppInteractor { + let cyclePauseDurationSeconds: Int + internal var cycleCount: Int = 1 - public func autoDestroyAllOffNodeMachines(cyclePauseDuration: Int) async throws { - try await autoDestroyAllOffNodeMachines(cycleCount: 1, cyclePauseDuration: cyclePauseDuration) + internal init(logger: Logger, client: any Client, cyclePauseDurationSeconds: Int) throws { + self.cyclePauseDurationSeconds = cyclePauseDurationSeconds + try super.init(logger: logger, client: client) } - private func autoDestroyAllOldNodeMachines( - cycleCount: Int, - cyclePauseDuration: Int - ) async throws { - logger.info( - "Auto destroy all old node machines cycle started (cycle: \(cycleCount))", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - ] - ) - - let allMachines = try await getListOfAllNodeMachines() - try await destroyAllOldNodeMachines(allMachines) - - try await Task.sleep(for: .seconds(cyclePauseDuration)) - try await autoDestroyAllOldNodeMachines( - cycleCount: cycleCount + 1, - cyclePauseDuration: cyclePauseDuration - ) + internal func sleepBetweenCycle() async throws { + logSleepBetweenCycleStarted() + try await Task.sleep(for: .seconds(cyclePauseDurationSeconds)) } - private func autoDestroyAllOffNodeMachines(cycleCount: Int, cyclePauseDuration: Int) async throws { + private func logSleepBetweenCycleStarted() { logger.info( - "Auto destroy all off node machines cycle started (cycle: \(cycleCount))", + "Pausing for \(cyclePauseDurationSeconds) before next cycle starts.", metadata: [ "to": .string("\(String(describing: Self.self)).\(#function)"), ] ) - - let allMachines = try await getListOfAllNodeMachines() - try await destroyAllOffNodeMachines(allMachines) - - try await Task.sleep(for: .seconds(cyclePauseDuration)) - try await autoDestroyAllOffNodeMachines(cycleCount: cycleCount + 1, cyclePauseDuration: cyclePauseDuration) } - private func destroyAllOldNodeMachines(_ allMachines: [SeleniumGridNodeAppNodeMachinesFinder - .NodeMachine]) async throws - { - if allMachines.count == 0 { - return - } - - let machinesToStop = allMachines.filter { machine in - let identifyAsOldAt = machine.createdAt.addingTimeInterval(60 * 60) - return Date() > identifyAsOldAt - } - - if machinesToStop.count == 0 { - logger.info( - "None of the \(allMachines.count) machines in a considered old. No machines will be destroyed.", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - ] - ) - return - } - - logger.info( - "Destroying all old node machines started.", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - "total_machines_to_destroy": .string(String(machinesToStop.count)) - ] - ) - - for machine in machinesToStop { - try await deleteNodeMachine(id: machine.id) - } + internal func deleteNodeMachine(id: String) async throws { + try await NodeMachineDeleter(logger: logger, client: client, machineID: id).delete() } +} - private func destroyAllOffNodeMachines(_ allMachines: [SeleniumGridNodeAppNodeMachinesFinder - .NodeMachine]) async throws - { - if allMachines.count == 0 { - return - } - - let machinesToStop = allMachines.filter { machine in - ["stopped", "suspended"].contains(machine.state) - } - - if machinesToStop.count == 0 { - logger.info( - "None of the \(allMachines.count) machines in a stopped or suspended state. No machines will be destroyed.", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - ] - ) - return - } - - logger.info( - "Destroying all off node machines started.", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - "total_machines_to_destroy": .string(String(machinesToStop.count)) - ] - ) - - for machine in machinesToStop { - try await deleteNodeMachine(id: machine.id) - } +internal class SeleniumGridNodeAutoDestroyer: SeleniumGridNodeAutoDestroyerBase { + public func autoDestroyAllOldNodeMachines() async throws { + try await SeleniumGridNodeAutoOldMachineDestroyer( + logger: logger, + client: client, + cyclePauseDurationSeconds: cyclePauseDurationSeconds + ).autoDestroyAllOldNodeMachines() } - private func deleteNodeMachine(id: String) async throws { - logger.info( - "Deleting node machine started.", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - "node_machine_id_to_delete": .string(id) - ] - ) - - let res = try await client.delete( - .init(stringLiteral: "\(payload.nodesAppMachineAPIURL)/\(id)?force=true"), - headers: .init(payload.flyAPIHTTPRequestAuthenticationHeader.getHeaderList()) - ) - - if res.status != .ok { - let responseContent = try res.content.decode([String: String].self) - logger.info( - "Failed to delete machine.", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - "response_content": .string("\(responseContent)"), - "node_machine_id_to_delete": .string(id) - ] - ) - throw Abort(.internalServerError) - } - - logger.info( - "Successfully deleted node machine.", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - "deletd_node_machine_id": .string(id) - ] - ) + public func autoDestroyAllOffNodeMachines() async throws { + try await SeleniumGridNodeAutoOffMachineDestroyer( + logger: logger, + client: client, + cyclePauseDurationSeconds: cyclePauseDurationSeconds + ).autoDestroyAllOffNodeMachines() } } diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOffMachineDestroyer.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOffMachineDestroyer.swift new file mode 100644 index 0000000..6c741fe --- /dev/null +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOffMachineDestroyer.swift @@ -0,0 +1,71 @@ +// SeleniumGridNodeAutoOffMachineDestroyer.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +internal class SeleniumGridNodeAutoOffMachineDestroyer: SeleniumGridNodeAutoDestroyerBase { + public func autoDestroyAllOffNodeMachines() async throws { + try await autoDestroyAllOffNodeMachinesImpl() + } + + private func autoDestroyAllOffNodeMachinesImpl() async throws { + try await destroyAllCurrentlyOffNodeMachines() + + try await recursivelyAutoDestroyAllOffNodeMachines() + } + + private func recursivelyAutoDestroyAllOffNodeMachines() async throws { + try await sleepBetweenCycle() + cycleCount += 1 + try await autoDestroyAllOffNodeMachines() + } + + private func destroyAllCurrentlyOffNodeMachines() async throws { + logAutoDestroyAllOffNodeMachinesStarted() + let allMachines = try await getListOfAllNodeMachines() + try await destroyAllOffNodeMachines(allMachines) + } + + private func logAutoDestroyAllOffNodeMachinesStarted() { + logger.info( + "Auto destroy all off node machines cycle started (cycle: \(cycleCount))", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + ] + ) + } + + private func destroyAllOffNodeMachines(_ allMachines: [SeleniumGridNodeAppNodeMachinesFinder + .NodeMachine]) async throws + { + if allMachines.count == 0 { + return + } + + let machinesToStop = allMachines.filter { machine in + ["stopped", "suspended"].contains(machine.state) + } + + if machinesToStop.count == 0 { + logger.info( + "None of the \(allMachines.count) machines in a stopped or suspended state. No machines will be destroyed.", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + ] + ) + return + } + + logger.info( + "Destroying all off node machines started.", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + "total_machines_to_destroy": .string(String(machinesToStop.count)) + ] + ) + + for machine in machinesToStop { + try await deleteNodeMachine(id: machine.id) + } + } +} diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOldMachineDestroyer.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOldMachineDestroyer.swift new file mode 100644 index 0000000..d233d2b --- /dev/null +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOldMachineDestroyer.swift @@ -0,0 +1,74 @@ +// SeleniumGridNodeAutoOldMachineDestroyer.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +import Vapor + +internal class SeleniumGridNodeAutoOldMachineDestroyer: SeleniumGridNodeAutoDestroyerBase { + public func autoDestroyAllOldNodeMachines() async throws { + try await autoDestroyAllOldNodeMachinesImpl() + } + + private func autoDestroyAllOldNodeMachinesImpl() async throws { + try await destroyAllCurrentlyOldNodeMachines() + + try await recursivelyAutoDestroyAllOldNodeMachines() + } + + private func recursivelyAutoDestroyAllOldNodeMachines() async throws { + try await sleepBetweenCycle() + cycleCount += 1 + try await autoDestroyAllOldNodeMachinesImpl() + } + + private func destroyAllCurrentlyOldNodeMachines() async throws { + logAutoDestroyAllOldNodeMachinesStarted() + let allMachines = try await getListOfAllNodeMachines() + try await destroyAllOldNodeMachines(allMachines) + } + + private func logAutoDestroyAllOldNodeMachinesStarted() { + logger.info( + "Auto destroy all old node machines cycle started (cycle: \(cycleCount))", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + ] + ) + } + + private func destroyAllOldNodeMachines(_ allMachines: [SeleniumGridNodeAppNodeMachinesFinder + .NodeMachine]) async throws + { + if allMachines.count == 0 { + return + } + + let machinesToStop = allMachines.filter { machine in + let identifyAsOldAt = machine.createdAt.addingTimeInterval(60 * 60) + return Date() > identifyAsOldAt + } + + if machinesToStop.count == 0 { + logger.info( + "None of the \(allMachines.count) machines in a considered old. No machines will be destroyed.", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + ] + ) + return + } + + logger.info( + "Destroying all old node machines started.", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + "total_machines_to_destroy": .string(String(machinesToStop.count)) + ] + ) + + for machine in machinesToStop { + try await deleteNodeMachine(id: machine.id) + } + } +} diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoscaler.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoscaler.swift index b5ebc77..99b1fad 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoscaler.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoscaler.swift @@ -107,11 +107,12 @@ internal class SeleniumGridNodeAutoscaler: SeleniumGridNodeAppInteractor { ) let res = try await client.post(.init(stringLiteral: payload.nodesAppMachineAPIURL)) { req in - req.headers = .init(payload.flyAPIHTTPRequestAuthenticationHeader.getHeaderList()) + req.headers = .init(flyAPIHTTPRequestAuthenticationHeader) try req.content.encode(machineConfiguration) } if res.status != .ok { + hv let responseContent = try res.content.decode([String: String].self) logger.info( "Node machine creation failed.", @@ -168,12 +169,12 @@ internal class SeleniumGridNodeAutoscaler: SeleniumGridNodeAppInteractor { let res = try await client .post(.init(stringLiteral: "\(payload.nodesAppMachineAPIURL)/\(machineIdentifier)")) { req in - req.headers = .init(payload.flyAPIHTTPRequestAuthenticationHeader.getHeaderList()) + req.headers = .init(flyAPIHTTPRequestAuthenticationHeader) try req.content.encode(["config": updatedConfiguration.config]) } if res.status != .ok { - let responseContent = try res.content.decode([String: String].self) + let responseContent = try decodeErrorFromResponse(res) logger.info( "Failed to updated machine node 'SE_NODE_HOST' environment variable to URL of the machine", metadata: [ From 3a9040d37ee9017668b1f91256877f45d1315289 Mon Sep 17 00:00:00 2001 From: William Date: Fri, 12 Sep 2025 06:43:07 +0200 Subject: [PATCH 03/10] refactor(SeleniumGridNodeAppInteractor): done with refactoring --- .../NodeMachineDeleter.swift | 8 +++--- .../SeleniumGridNodeAppInteractor.swift | 26 +++++++++++-------- .../SeleniumGridNodeAutoscaler.swift | 4 +-- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineDeleter.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineDeleter.swift index 3dd09a9..91dc038 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineDeleter.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineDeleter.swift @@ -44,17 +44,17 @@ internal class NodeMachineDeleter: SeleniumGridNodeAppInteractor { } private func handleInvalidDeleteNodeMachineResponse(response: ClientResponse) throws { - let responseContent = try decodeErrorFromResponse(response) - logDeleteNodeMachineFailed(responseContent: responseContent) + let error = try decodeErrorFromResponse(response) + logDeleteNodeMachineFailed(error: error) throw Abort(.internalServerError) } - private func logDeleteNodeMachineFailed(responseContent: [String: String]) { + private func logDeleteNodeMachineFailed(error: [String: String]) { logger.error( "Failed to delete machine.", metadata: [ "to": .string("\(String(describing: Self.self)).\(#function)"), - "response_content": .string("\(responseContent)"), + "error": .string("\(error)"), "node_machine_id_to_delete": .string(machineID) ] ) diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift index 465dbe0..ed5fdbc 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift @@ -78,9 +78,9 @@ internal struct SeleniumGridNodeAppNodeMachinesFinder: SeleniumGridNodeAppIntera private func validateAndGetAllMachines() async throws -> [NodeMachine] { let response = try await getAllNodeMachinesResponse() - try validateAllNodeMachinesResponseStatus(response: response) + try validateFindAllNodeMachinesResponseStatus(response: response) - return try getNodeMachineListFromAppNodeMachinesResponse(response) + return try findNodeMachineListFromResponse(response) } private func getAllNodeMachinesResponse() async throws -> ClientResponse { @@ -90,30 +90,30 @@ internal struct SeleniumGridNodeAppNodeMachinesFinder: SeleniumGridNodeAppIntera ) } - private func validateAllNodeMachinesResponseStatus(response: ClientResponse) throws { + private func validateFindAllNodeMachinesResponseStatus(response: ClientResponse) throws { if isInvalidHTTPResponseStatus(status: response.status) { - try handleInvalidAllNodeMachinesResponse(res: response) + try handleInvalidFindAllNodeMachinesResponse(res: response) } } - internal func isInvalidHTTPResponseStatus(status: HTTPStatus) -> Bool { - return status != .ok + private func handleInvalidFindAllNodeMachinesResponse(res: ClientResponse) throws { + let error = try decodeErrorFromResponse(res) + try logInvalidFindAllNodeMachinesResponse(error: error) } - private func handleInvalidAllNodeMachinesResponse(res: ClientResponse) throws { - let responseContent = try decodeErrorFromResponse(res) - // TODO: finish refactoring + private func logInvalidFindAllNodeMachinesResponse(error: [String: String]) throws { logger.error( "Failed to get a list of all machines in nodes app", metadata: [ "to": .string("\(String(describing: Self.self)).\(#function)"), - "response_content": .string("\(responseContent)"), + "error": .string("\(error)"), ] ) + throw Abort(.internalServerError) } - private func getNodeMachineListFromAppNodeMachinesResponse(_ response: ClientResponse) throws -> [NodeMachine] { + private func findNodeMachineListFromResponse(_ response: ClientResponse) throws -> [NodeMachine] { try response.content.decode([NodeMachine].self) } @@ -137,6 +137,10 @@ extension SeleniumGridNodeAppInteractorBase { func decodeErrorFromResponse(_ response: ClientResponse) throws -> [String: String] { return try response.content.decode([String: String].self) } + + func isInvalidHTTPResponseStatus(status: HTTPStatus) -> Bool { + return status != .ok + } } internal struct SeleniumGridNodeAppInteractorPayload: Content { diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoscaler.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoscaler.swift index 99b1fad..753e5fc 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoscaler.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoscaler.swift @@ -174,12 +174,12 @@ internal class SeleniumGridNodeAutoscaler: SeleniumGridNodeAppInteractor { } if res.status != .ok { - let responseContent = try decodeErrorFromResponse(res) + let error = try decodeErrorFromResponse(res) logger.info( "Failed to updated machine node 'SE_NODE_HOST' environment variable to URL of the machine", metadata: [ "to": .string("\(String(describing: Self.self)).\(#function)"), - "response_content": .string("\(responseContent)"), + "error": .string("\(error)"), "machine_identifier": .string(machineIdentifier) ] ) From 719fdb05919002c643e95e0cf1505d1904d170d4 Mon Sep 17 00:00:00 2001 From: William Date: Fri, 12 Sep 2025 07:30:20 +0200 Subject: [PATCH 04/10] refactor(SeleniumGridNodeAppInteractor): refactored fly api error handler to its own class to reduce function parameter passing and length of interactor struct --- .../SeleniumGridNodeAutoScalerCommand.swift | 3 +- .../FlyMachinesAPIErrorHandler.swift | 56 +++++++++ .../NodeMachineDeleter.swift | 8 +- .../SeleniumGridNodeAppInteractor.swift | 109 +++--------------- ...eleniumGridNodeAppNodeMachinesFinder.swift | 96 +++++++++++++++ .../SeleniumGridNodeAutoscaler.swift | 3 +- ...leniumGridNodeMachineAutoscalerError.swift | 8 ++ 7 files changed, 181 insertions(+), 102 deletions(-) create mode 100644 SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/FlyMachinesAPIErrorHandler.swift create mode 100644 SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppNodeMachinesFinder.swift create mode 100644 SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeMachineAutoscalerError.swift diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoScalerCommand.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoScalerCommand.swift index 06b2bb1..0688ffb 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoScalerCommand.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoScalerCommand.swift @@ -16,8 +16,7 @@ struct SeleniumGridNodeAutoScalerCommand: AsyncCommand { let autoscaler = try SeleniumGridNodeAutoscaler( client: context.application.client, logger: context.application.logger, - cyclePauseDurationSeconds: 10 ) - try await autoscaler.autoscale() + try await autoscaler.autoscale(cyclePauseDuration: 10) } } diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/FlyMachinesAPIErrorHandler.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/FlyMachinesAPIErrorHandler.swift new file mode 100644 index 0000000..b23ec85 --- /dev/null +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/FlyMachinesAPIErrorHandler.swift @@ -0,0 +1,56 @@ +// FlyMachinesAPIErrorHandler.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +import Vapor + +internal struct FlyMachinesAPIErrorHandler { + let payload: FlyMachinesAPIErrorHandlerPayload + let logger: Logger + + func handle() throws { + logFlyAPIError() + try throwFlyMachinesAPIError() + } + + private func logFlyAPIError() { + let finalMetadata = getFlyAPIErrorLogFinalMetadata() + logFlyAPIErrorWithFinalMetadata(finalMetadata: finalMetadata) + } + + private func getFlyAPIErrorLogFinalMetadata() -> Logger.Metadata { + let metadataBase = getFlyAPIErrorLogFinalMetadataBase() + return mergeFlyAPIErrorLogMetadataBaseWithMetadata(base: metadataBase) + } + + private func getFlyAPIErrorLogFinalMetadataBase() -> Logger.Metadata { + [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + "error": .string("\(payload.error)") + ] + } + + private func mergeFlyAPIErrorLogMetadataBaseWithMetadata(base: Logger.Metadata) -> Logger + .Metadata + { + payload.metadata.merging(base, uniquingKeysWith: { first, _ in first }) + } + + private func logFlyAPIErrorWithFinalMetadata(finalMetadata: Logger.Metadata) { + logger.error( + "\(payload.message).", + metadata: finalMetadata + ) + } + + private func throwFlyMachinesAPIError() throws { + throw SeleniumGridNodeMachineAutoscalerError.flyMachinesAPIError(error: payload.error) + } +} + +internal struct FlyMachinesAPIErrorHandlerPayload { + let message: String + let metadata: Logger.Metadata = [:] + let error: SeleniumGridNodeAppInteractorBase.FlyAPIError +} diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineDeleter.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineDeleter.swift index 91dc038..fc97953 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineDeleter.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineDeleter.swift @@ -46,10 +46,10 @@ internal class NodeMachineDeleter: SeleniumGridNodeAppInteractor { private func handleInvalidDeleteNodeMachineResponse(response: ClientResponse) throws { let error = try decodeErrorFromResponse(response) logDeleteNodeMachineFailed(error: error) - throw Abort(.internalServerError) + try throwInvalidDeleteNodeMachineResponseError(error: error) } - private func logDeleteNodeMachineFailed(error: [String: String]) { + private func logDeleteNodeMachineFailed(error: FlyAPIError) { logger.error( "Failed to delete machine.", metadata: [ @@ -60,6 +60,10 @@ internal class NodeMachineDeleter: SeleniumGridNodeAppInteractor { ) } + private func throwInvalidDeleteNodeMachineResponseError(error: FlyAPIError) throws { + throw SeleniumGridNodeMachineAutoscalerError.flyMachinesAPIError(error: error) + } + private func getDeleteNodeMachineResponse() async throws -> ClientResponse { try await client.delete( .init(stringLiteral: "\(payload.nodesAppMachineAPIURL)/\(machineID)?force=true"), diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift index ed5fdbc..d511aae 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift @@ -36,114 +36,31 @@ internal class SeleniumGridNodeAppInteractor: SeleniumGridNodeAppInteractorBase ) .getListOfAllNodeMachines() } -} - -internal struct SeleniumGridNodeAppNodeMachinesFinder: SeleniumGridNodeAppInteractorBase { - let logger: Logger - let client: any Client - var payload: SeleniumGridNodeAppInteractorPayload - var flyAPIHTTPRequestAuthenticationHeader: [(String, String)] - public func getListOfAllNodeMachines() async throws -> [NodeMachine] { - return try await getAllNodeMachinesList() - } - - internal struct NodeMachine: Content { - let id: String - let state: String - let createdAt: Date - - public enum CodingKeys: String, CodingKey { - case id - case state - case createdAt = "created_at" - } - } - - private func getAllNodeMachinesList() async throws -> [NodeMachine] { - logGetListOfAllMachinesStarted() - let allMachines = try await validateAndGetAllMachines() - logGetListOfAllMachinesSuccess(totalMachines: allMachines.count) - return allMachines - } - - private func logGetListOfAllMachinesStarted() { - logger.info( - "Getting a list of all machines.", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - ] - ) - } - - private func validateAndGetAllMachines() async throws -> [NodeMachine] { - let response = try await getAllNodeMachinesResponse() - try validateFindAllNodeMachinesResponseStatus(response: response) - - return try findNodeMachineListFromResponse(response) - } - - private func getAllNodeMachinesResponse() async throws -> ClientResponse { - return try await client.get( - .init(stringLiteral: payload.nodesAppMachineAPIURL), - headers: .init(flyAPIHTTPRequestAuthenticationHeader) - ) - } - - private func validateFindAllNodeMachinesResponseStatus(response: ClientResponse) throws { - if isInvalidHTTPResponseStatus(status: response.status) { - try handleInvalidFindAllNodeMachinesResponse(res: response) - } - } - - private func handleInvalidFindAllNodeMachinesResponse(res: ClientResponse) throws { - let error = try decodeErrorFromResponse(res) - try logInvalidFindAllNodeMachinesResponse(error: error) - } - - private func logInvalidFindAllNodeMachinesResponse(error: [String: String]) throws { - logger.error( - "Failed to get a list of all machines in nodes app", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - "error": .string("\(error)"), - ] - ) - - throw Abort(.internalServerError) - } - - private func findNodeMachineListFromResponse(_ response: ClientResponse) throws -> [NodeMachine] { - try response.content.decode([NodeMachine].self) - } - - private func logGetListOfAllMachinesSuccess(totalMachines: Int) { - logger.info( - "Got list of all machines.", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - "total_machines": .string(String(totalMachines)) - ] - ) + internal func handleFlyMachinesAPIError(payload: FlyMachinesAPIErrorHandlerPayload) throws { + try FlyMachinesAPIErrorHandler(payload: payload, logger: logger).handle() } } internal protocol SeleniumGridNodeAppInteractorBase { + var client: any Client { get } + var logger: Logger { get } var payload: SeleniumGridNodeAppInteractorPayload { get } var flyAPIHTTPRequestAuthenticationHeader: [(String, String)] { get } } -extension SeleniumGridNodeAppInteractorBase { - func decodeErrorFromResponse(_ response: ClientResponse) throws -> [String: String] { - return try response.content.decode([String: String].self) +internal struct SeleniumGridNodeAppInteractorPayload: Content { + let nodesAppMachineAPIURL: String + let flyAPIToken: String +} + +internal extension SeleniumGridNodeAppInteractorBase { + typealias FlyAPIError = [String: String] + func decodeErrorFromResponse(_ response: ClientResponse) throws -> FlyAPIError { + return try response.content.decode(FlyAPIError.self) } func isInvalidHTTPResponseStatus(status: HTTPStatus) -> Bool { return status != .ok } } - -internal struct SeleniumGridNodeAppInteractorPayload: Content { - let nodesAppMachineAPIURL: String - let flyAPIToken: String -} diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppNodeMachinesFinder.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppNodeMachinesFinder.swift new file mode 100644 index 0000000..c71f9b8 --- /dev/null +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppNodeMachinesFinder.swift @@ -0,0 +1,96 @@ +// SeleniumGridNodeAppNodeMachinesFinder.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +import Vapor + +internal struct SeleniumGridNodeAppNodeMachinesFinder: SeleniumGridNodeAppInteractorBase { + let logger: Logger + let client: any Client + var payload: SeleniumGridNodeAppInteractorPayload + var flyAPIHTTPRequestAuthenticationHeader: [(String, String)] + + public func getListOfAllNodeMachines() async throws -> [NodeMachine] { + return try await getAllNodeMachinesList() + } + + internal struct NodeMachine: Content { + let id: String + let state: String + let createdAt: Date + + public enum CodingKeys: String, CodingKey { + case id + case state + case createdAt = "created_at" + } + } + + private func getAllNodeMachinesList() async throws -> [NodeMachine] { + logGetListOfAllMachinesStarted() + let allMachines = try await validateAndGetAllMachines() + logGetListOfAllMachinesSuccess(totalMachines: allMachines.count) + return allMachines + } + + private func logGetListOfAllMachinesStarted() { + logger.info( + "Getting a list of all machines.", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + ] + ) + } + + private func validateAndGetAllMachines() async throws -> [NodeMachine] { + let response = try await getAllNodeMachinesResponse() + try validateFindAllNodeMachinesResponseStatus(response: response) + + return try findNodeMachineListFromResponse(response) + } + + private func getAllNodeMachinesResponse() async throws -> ClientResponse { + return try await client.get( + .init(stringLiteral: payload.nodesAppMachineAPIURL), + headers: .init(flyAPIHTTPRequestAuthenticationHeader) + ) + } + + private func validateFindAllNodeMachinesResponseStatus(response: ClientResponse) throws { + if isInvalidHTTPResponseStatus(status: response.status) { + try handleInvalidFindAllNodeMachinesResponse(res: response) + } + } + + private func handleInvalidFindAllNodeMachinesResponse(res: ClientResponse) throws { + let error = try decodeErrorFromResponse(res) + try logInvalidFindAllNodeMachinesResponse(error: error) + } + + private func logInvalidFindAllNodeMachinesResponse(error: FlyAPIError) throws { + logger.error( + "Failed to get a list of all machines in nodes app", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + "error": .string("\(error)"), + ] + ) + + throw Abort(.internalServerError) + } + + private func findNodeMachineListFromResponse(_ response: ClientResponse) throws -> [NodeMachine] { + try response.content.decode([NodeMachine].self) + } + + private func logGetListOfAllMachinesSuccess(totalMachines: Int) { + logger.info( + "Got list of all machines.", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + "total_machines": .string(String(totalMachines)) + ] + ) + } +} diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoscaler.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoscaler.swift index 753e5fc..72b6230 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoscaler.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoscaler.swift @@ -112,8 +112,7 @@ internal class SeleniumGridNodeAutoscaler: SeleniumGridNodeAppInteractor { } if res.status != .ok { - hv - let responseContent = try res.content.decode([String: String].self) + let responseContent = try res.content.decode(FlyAPIError.self) logger.info( "Node machine creation failed.", metadata: [ diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeMachineAutoscalerError.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeMachineAutoscalerError.swift new file mode 100644 index 0000000..eadc7a5 --- /dev/null +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeMachineAutoscalerError.swift @@ -0,0 +1,8 @@ +// SeleniumGridNodeMachineAutoscalerError.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +internal enum SeleniumGridNodeMachineAutoscalerError: Error { + case flyMachinesAPIError(error: SeleniumGridNodeAppInteractorBase.FlyAPIError) +} From 22e56fa80f0c177add87b5208dcff8112e256f2d Mon Sep 17 00:00:00 2001 From: William Date: Fri, 12 Sep 2025 07:46:12 +0200 Subject: [PATCH 05/10] refactor(SeleniumGridNodeAutoCreator): refactored name from autoscaler to autocreator --- .../Commands/SeleniumGridNodeAutoScalerCommand.swift | 9 +++++---- ...oscaler.swift => SeleniumGridNodeAutoCreator.swift} | 10 +++++----- .../SeleniumGridNodeAutoDestroyer.swift | 6 ++++-- .../SeleniumGridNodeAutoOffMachineDestroyer.swift | 2 +- .../SeleniumGridNodeAutoOldMachineDestroyer.swift | 2 +- .../SeleniumGridNodeMachineAutoscaler/configure.swift | 2 +- .../infra/autoscaler.toml | 9 ++------- 7 files changed, 19 insertions(+), 21 deletions(-) rename SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/{SeleniumGridNodeAutoscaler.swift => SeleniumGridNodeAutoCreator.swift} (96%) diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoScalerCommand.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoScalerCommand.swift index 0688ffb..92ddfb9 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoScalerCommand.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoScalerCommand.swift @@ -5,18 +5,19 @@ import Vapor -struct SeleniumGridNodeAutoScalerCommand: AsyncCommand { +struct SeleniumGridNodeAutoCreatorCommand: AsyncCommand { struct Signature: CommandSignature {} var help: String { - "Autoscales fly.io SeleniumGrid Node App machines" + "Auto-creates fly.io SeleniumGrid Node App machines" } func run(using context: CommandContext, signature _: Signature) async throws { - let autoscaler = try SeleniumGridNodeAutoscaler( + let autoCreator = try SeleniumGridNodeAutoCreator( client: context.application.client, logger: context.application.logger, + cyclePauseDurationSeconds: 10 ) - try await autoscaler.autoscale(cyclePauseDuration: 10) + try await autoCreator.autoCreate() } } diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoscaler.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoCreator.swift similarity index 96% rename from SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoscaler.swift rename to SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoCreator.swift index 72b6230..749a47f 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoscaler.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoCreator.swift @@ -6,20 +6,20 @@ import AutomaUtilities import Vapor -internal class SeleniumGridNodeAutoscaler: SeleniumGridNodeAppInteractor { +internal class SeleniumGridNodeAutoCreator: SeleniumGridNodeMachineAutoscaler { let seleniumGridHubBase: String let seleniumGridNodeBase: String let maxNodeMachinesAllowed: Int = 10 - init(client: any Client, logger: Logger) throws { + init(client: any Client, logger: Logger, cyclePauseDurationSeconds: Int) throws { seleniumGridHubBase = try Environment.getOrThrow("SELENIUM_GRID_HUB_BASE") seleniumGridNodeBase = try Environment.getOrThrow("SELENIUM_GRID_NODE_BASE") - try super.init(logger: logger, client: client) + try super.init(logger: logger, client: client, cyclePauseDurationSeconds: cyclePauseDurationSeconds) } - public func autoscale(cyclePauseDuration: Int) async throws { - try await autoscaleNodes(cyclePauseDuration: cyclePauseDuration) + public func autoCreate() async throws { + try await autoscaleNodes(cyclePauseDuration: cyclePauseDurationSeconds) } private func autoscaleNodes(cyclePauseDuration: Int, cycleCount: Int = 1) async throws { diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoDestroyer.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoDestroyer.swift index bd099ca..ea8b236 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoDestroyer.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoDestroyer.swift @@ -5,7 +5,7 @@ import Vapor -internal class SeleniumGridNodeAutoDestroyerBase: SeleniumGridNodeAppInteractor { +internal class SeleniumGridNodeMachineAutoscalerBase: SeleniumGridNodeAppInteractor { let cyclePauseDurationSeconds: Int internal var cycleCount: Int = 1 @@ -13,7 +13,9 @@ internal class SeleniumGridNodeAutoDestroyerBase: SeleniumGridNodeAppInteractor self.cyclePauseDurationSeconds = cyclePauseDurationSeconds try super.init(logger: logger, client: client) } +} +internal class SeleniumGridNodeMachineAutoscaler: SeleniumGridNodeMachineAutoscalerBase { internal func sleepBetweenCycle() async throws { logSleepBetweenCycleStarted() try await Task.sleep(for: .seconds(cyclePauseDurationSeconds)) @@ -33,7 +35,7 @@ internal class SeleniumGridNodeAutoDestroyerBase: SeleniumGridNodeAppInteractor } } -internal class SeleniumGridNodeAutoDestroyer: SeleniumGridNodeAutoDestroyerBase { +internal class SeleniumGridNodeAutoDestroyer: SeleniumGridNodeMachineAutoscalerBase { public func autoDestroyAllOldNodeMachines() async throws { try await SeleniumGridNodeAutoOldMachineDestroyer( logger: logger, diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOffMachineDestroyer.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOffMachineDestroyer.swift index 6c741fe..71215d5 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOffMachineDestroyer.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOffMachineDestroyer.swift @@ -3,7 +3,7 @@ // All source code and related assets are the property of GetAutomaApp. // All rights reserved. -internal class SeleniumGridNodeAutoOffMachineDestroyer: SeleniumGridNodeAutoDestroyerBase { +internal class SeleniumGridNodeAutoOffMachineDestroyer: SeleniumGridNodeMachineAutoscaler { public func autoDestroyAllOffNodeMachines() async throws { try await autoDestroyAllOffNodeMachinesImpl() } diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOldMachineDestroyer.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOldMachineDestroyer.swift index d233d2b..3930e09 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOldMachineDestroyer.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOldMachineDestroyer.swift @@ -5,7 +5,7 @@ import Vapor -internal class SeleniumGridNodeAutoOldMachineDestroyer: SeleniumGridNodeAutoDestroyerBase { +internal class SeleniumGridNodeAutoOldMachineDestroyer: SeleniumGridNodeMachineAutoscaler { public func autoDestroyAllOldNodeMachines() async throws { try await autoDestroyAllOldNodeMachinesImpl() } diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/configure.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/configure.swift index adf8c70..701313c 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/configure.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/configure.swift @@ -10,6 +10,6 @@ public func configure(_ app: Application) async throws { // uncomment to serve files from /Public folder // app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)) - app.asyncCommands.use(SeleniumGridNodeAutoScalerCommand(), as: "autoscaler") + app.asyncCommands.use(SeleniumGridNodeAutoCreatorCommand(), as: "autocreator") app.asyncCommands.use(SeleniumGridNodeAutoDestroyerCommand(), as: "autodestroyer") } diff --git a/SeleniumGridNodeMachineAutoscaler/infra/autoscaler.toml b/SeleniumGridNodeMachineAutoscaler/infra/autoscaler.toml index 408f977..493e6da 100644 --- a/SeleniumGridNodeMachineAutoscaler/infra/autoscaler.toml +++ b/SeleniumGridNodeMachineAutoscaler/infra/autoscaler.toml @@ -1,8 +1,3 @@ -# fly.toml app configuration file generated for automa-web-core-seleniumgrid-node-autoscaler on 2025-09-01T18:06:11+02:00 -# -# See https://fly.io/docs/reference/configuration/ for information about how to use this file. -# - app = 'automa-web-core-seleniumgrid-node-autoscaler' primary_region = 'jnb' @@ -15,13 +10,13 @@ auto_stop_machines = 'off' auto_start_machines = false min_machines_running = 0 processes = [ - 'app', + 'machines_auto_creator', 'off_machines_auto_destroyer', 'old_machines_auto_destroyer', ] [processes] -app = "./SeleniumGridNodeMachineAutoscaler autoscaler" +machines_auto_creator = "./SeleniumGridNodeMachineAutoscaler autocreator" off_machines_auto_destroyer = "./SeleniumGridNodeMachineAutoscaler autodestroyer offMachines" old_machines_auto_destroyer = "./SeleniumGridNodeMachineAutoscaler autodestroyer oldMachines" From 39914b657d133802d422adbc48d5c7bffc30a948 Mon Sep 17 00:00:00 2001 From: William Date: Fri, 12 Sep 2025 08:20:37 +0200 Subject: [PATCH 06/10] refactor(SeleniumGridAutoCreator): refactored some code in auto creator Still have a lot of todos left (left comment on top). --- .../NodeMachineDeleter.swift | 3 - ...eleniumGridNodeAppNodeMachinesFinder.swift | 3 + .../SeleniumGridNodeAutoCreator.swift | 105 +++++++++++++----- 3 files changed, 79 insertions(+), 32 deletions(-) diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineDeleter.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineDeleter.swift index fc97953..c4ebc62 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineDeleter.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineDeleter.swift @@ -5,9 +5,6 @@ import Vapor -// TODO: Use logger.error instead of logger.info in places where errors occur. Create custom errors instead of using internalServerError everywhere. -// add more and better logs - internal class NodeMachineDeleter: SeleniumGridNodeAppInteractor { let machineID: String diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppNodeMachinesFinder.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppNodeMachinesFinder.swift index c71f9b8..4c4bb8b 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppNodeMachinesFinder.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppNodeMachinesFinder.swift @@ -5,6 +5,9 @@ import Vapor +// TODO: Use logger.error instead of logger.info in places where errors occur. Create custom errors instead of using internalServerError everywhere. +// add more and better logs + internal struct SeleniumGridNodeAppNodeMachinesFinder: SeleniumGridNodeAppInteractorBase { let logger: Logger let client: any Client diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoCreator.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoCreator.swift index 749a47f..edda8d7 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoCreator.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoCreator.swift @@ -1,4 +1,4 @@ -// SeleniumGridNodeAutoscaler.swift +// SeleniumGridNodeAutoCreator.swift // Copyright (c) 2025 GetAutomaApp // All source code and related assets are the property of GetAutomaApp. // All rights reserved. @@ -6,6 +6,12 @@ import AutomaUtilities import Vapor +// TODO: Use logger.error instead of logger.info in places where errors occur. Create custom errors instead of using internalServerError everywhere. +// add more and better logs +// refactor remaining methods +// order functions in logical order, like a well-ordered document +// refactor helper methods to a single helper method in its own struct to reduce class length + internal class SeleniumGridNodeAutoCreator: SeleniumGridNodeMachineAutoscaler { let seleniumGridHubBase: String let seleniumGridNodeBase: String @@ -18,55 +24,96 @@ internal class SeleniumGridNodeAutoCreator: SeleniumGridNodeMachineAutoscaler { try super.init(logger: logger, client: client, cyclePauseDurationSeconds: cyclePauseDurationSeconds) } - public func autoCreate() async throws { - try await autoscaleNodes(cyclePauseDuration: cyclePauseDurationSeconds) + public func autoCreateNodeMachines() async throws { + try await autoCreateNodeMachinesImpl() + } + + private func autoCreateNodeMachinesImpl() async throws { + try await autoCreateNodeMachinesBasedOnSessionsInQueue() + try await recursivelyAutoCreateNodeMachines() + } + + private func autoCreateNodeMachinesBasedOnSessionsInQueue() async throws { + logAutoCreateNodeMachinesStarted() + try await handleMaxNodeMachinesReached() + + let totalSessionQueueRequests = try await getTotalSessionQueueRequests() + try await createSeleniumGridNodeFlyMachines(totalSessionsInQueue: totalSessionQueueRequests) } - private func autoscaleNodes(cyclePauseDuration: Int, cycleCount: Int = 1) async throws { + private func createSeleniumGridNodeFlyMachines(totalSessionsInQueue: Int) async throws { + if totalSessionsInQueue > 0 { + logFoundPendingSessionsInQueue(totalSessions: totalSessionsInQueue) + try await createNewSeleniumGridNodeFlyMachines(amount: totalSessionsInQueue) + } + } + + private func createNewSeleniumGridNodeFlyMachines(amount: Int) async throws { + for _ in 1 ... amount { + try await createNewSeleniumGridNodeFlyMachine() + } + } + + private func logFoundPendingSessionsInQueue(totalSessions: Int) { logger.info( - "Node autoscaler cycle started (cycle: \(cycleCount)).", + "Found a total of \(totalSessions) pending sessions in queue.", metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)") + "to": .string("\(String(describing: Self.self)).\(#function)"), ] ) + } + private func getTotalSessionQueueRequests() async throws -> Int { + let response = try await getGridSessionQueueReponse() + return response.data.sessionsInfo.sessionQueueRequests.count + } + + private func handleMaxNodeMachinesReached() async throws { if try await maxNodeMachinesReached() { - return + try await recursivelyAutoCreateNodeMachines() } + } - let response = try await getGridSessionQueueReponse() - let totalSessionsInQueue = response.data.sessionsInfo.sessionQueueRequests.count - if totalSessionsInQueue > 0 { - logger.info( - "Found a total of \(totalSessionsInQueue) pending sessions in queue.", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - ] - ) - // create a selenium node fly machine for every session - for _ in 1 ... totalSessionsInQueue { - try await createNewSeleniumGridNodeFlyMachine() - } - } - try await Task.sleep(for: .seconds(cyclePauseDuration)) - try await autoscaleNodes(cyclePauseDuration: cyclePauseDuration, cycleCount: cycleCount + 1) + private func recursivelyAutoCreateNodeMachines() async throws { + try await sleepBetweenCycle() + cycleCount += 1 + try await autoCreateNodeMachinesImpl() } - private func maxNodeMachinesReached() async throws -> Bool { - let totalNodeMachines = try await getListOfAllNodeMachines().count + private func logAutoCreateNodeMachinesStarted() { + logger.info( + "Node auto-creator cycle started (cycle: \(cycleCount)).", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)") + ] + ) + } - if totalNodeMachines < maxNodeMachinesAllowed { - return false + private func maxNodeMachinesReached() async throws -> Bool { + let totalMachines = try await getTotalNodeMachines() + let reached = reachedMaxNodeMachines(totalMachines: totalMachines) + if reached { + logReachedMaxNodeMachines(totalMachines: totalMachines) } + return reached + } + private func logReachedMaxNodeMachines(totalMachines: Int) { logger.info( "The threshold of \(maxNodeMachinesAllowed) running node machines reached. No additional node machines will be created.", metadata: [ "to": .string("\(String(describing: Self.self)).\(#function)"), - "total_node_machines": .string(String(totalNodeMachines)) + "total_node_machines": .string(String(totalMachines)) ] ) - return true + } + + private func reachedMaxNodeMachines(totalMachines: Int) -> Bool { + return totalMachines >= maxNodeMachinesAllowed + } + + private func getTotalNodeMachines() async throws -> Int { + try await getListOfAllNodeMachines().count } private func getGridSessionQueueReponse() async throws -> SessionQueueResponse { From cc47d896643454e402889435ef88935fab7facf0 Mon Sep 17 00:00:00 2001 From: William Date: Mon, 15 Sep 2025 19:49:07 +0200 Subject: [PATCH 07/10] refactor: semi done with refactoring all code --- .../Package.resolved | 10 +- .../CycleSleeper.swift | 35 +++ .../FlyMachinesAPIErrorHandler.swift | 6 - .../MaxNodeMachinesReachedHandler.swift | 41 +++ .../NodeMachineCreator.swift | 151 ++++++++++ .../NodeMachineDeleter.swift | 54 ++-- .../NodeMachineUpdater.swift | 87 ++++++ .../SeleniumGridNodeAppInteractor.swift | 61 ++-- ...eleniumGridNodeAppNodeMachinesFinder.swift | 23 +- .../SeleniumGridNodeAutoCreator.swift | 269 +++--------------- .../SeleniumGridNodeAutoDestroyer.swift | 30 -- ...eniumGridNodeAutoOffMachineDestroyer.swift | 13 +- ...eniumGridNodeAutoOldMachineDestroyer.swift | 2 +- .../SeleniumGridNodeMachineAutoscaler.swift | 22 ++ ...eniumGridSessionQueueRequestsHandler.swift | 76 +++++ 15 files changed, 520 insertions(+), 360 deletions(-) create mode 100644 SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/CycleSleeper.swift create mode 100644 SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/MaxNodeMachinesReachedHandler.swift create mode 100644 SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineCreator.swift create mode 100644 SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineUpdater.swift create mode 100644 SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeMachineAutoscaler.swift create mode 100644 SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridSessionQueueRequestsHandler.swift diff --git a/SeleniumGridNodeMachineAutoscaler/Package.resolved b/SeleniumGridNodeMachineAutoscaler/Package.resolved index 064b0a5..506abf5 100644 --- a/SeleniumGridNodeMachineAutoscaler/Package.resolved +++ b/SeleniumGridNodeMachineAutoscaler/Package.resolved @@ -25,7 +25,7 @@ "location" : "https://github.com/GetAutomaApp/AutomaUtilities", "state" : { "branch" : "main", - "revision" : "60e4a28b9f54ab9556e5cbbb176a5bcf41337161" + "revision" : "bc7212b7994ee496b0d3684dd77be92f23961c8f" } }, { @@ -123,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-distributed-tracing.git", "state" : { - "revision" : "149fa4cc7d54c513288108142e7af9ceaf8def8f", - "version" : "1.2.2" + "revision" : "6600888f4cb5bbf1bcac51000f60b2cbd224c91b", + "version" : "1.3.0" } }, { @@ -249,8 +249,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/vapor.git", "state" : { - "revision" : "636fbe688e001dc1f8d6f6026a52bcc396025d19", - "version" : "4.116.0" + "revision" : "773ea6a63595ae4f6bc46a366d78769d4cb8b08c", + "version" : "4.117.0" } }, { diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/CycleSleeper.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/CycleSleeper.swift new file mode 100644 index 0000000..e889ce0 --- /dev/null +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/CycleSleeper.swift @@ -0,0 +1,35 @@ +// CycleSleeper.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +import Vapor + +internal struct CycleSleeper { + let config: CycleSleeperConfig + let logger: Logger + + init(_ config: CycleSleeperConfig, logger: Logger) { + self.config = config + self.logger = logger + } + + internal struct CycleSleeperConfig { + let duration: Int + let message: String? = nil + } + + public func sleep() async throws { + logSleepBetweenCycleStarted() + try await Task.sleep(for: .seconds(config.duration)) + } + + private func logSleepBetweenCycleStarted() { + logger.info( + Logger.Message(stringLiteral: config.message ?? "Pausing for \(config.duration) before next cycle starts."), + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + ] + ) + } +} diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/FlyMachinesAPIErrorHandler.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/FlyMachinesAPIErrorHandler.swift index b23ec85..0d7fb30 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/FlyMachinesAPIErrorHandler.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/FlyMachinesAPIErrorHandler.swift @@ -48,9 +48,3 @@ internal struct FlyMachinesAPIErrorHandler { throw SeleniumGridNodeMachineAutoscalerError.flyMachinesAPIError(error: payload.error) } } - -internal struct FlyMachinesAPIErrorHandlerPayload { - let message: String - let metadata: Logger.Metadata = [:] - let error: SeleniumGridNodeAppInteractorBase.FlyAPIError -} diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/MaxNodeMachinesReachedHandler.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/MaxNodeMachinesReachedHandler.swift new file mode 100644 index 0000000..665c067 --- /dev/null +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/MaxNodeMachinesReachedHandler.swift @@ -0,0 +1,41 @@ +// MaxNodeMachinesReachedHandler.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +import Vapor + +internal class MaxNodeMachinesReachedHandler: SeleniumGridNodeMachineAutoscaler { + let maxNodeMachinesAllowed: Int = 10 + + init(logger: Logger, client: any Client) throws { + try super.init(logger: logger, client: client, cyclePauseDurationSeconds: 0) + } + + public func reached() async throws -> Bool { + let totalMachines = try await getTotalNodeMachines() + let reached = reachedMaxNodeMachines(totalMachines: totalMachines) + if reached { + logReachedMaxNodeMachines(totalMachines: totalMachines) + } + return reached + } + + private func getTotalNodeMachines() async throws -> Int { + try await getListOfAllNodeMachines().count + } + + private func reachedMaxNodeMachines(totalMachines: Int) -> Bool { + return totalMachines >= maxNodeMachinesAllowed + } + + private func logReachedMaxNodeMachines(totalMachines: Int) { + logger.info( + "The threshold of \(maxNodeMachinesAllowed) running node machines reached. No additional node machines will be created.", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + "total_node_machines": .string(String(totalMachines)) + ] + ) + } +} diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineCreator.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineCreator.swift new file mode 100644 index 0000000..2816239 --- /dev/null +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineCreator.swift @@ -0,0 +1,151 @@ +// NodeMachineCreator.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +import AutomaUtilities +import Vapor + +internal class NodeMachineCreationBase: SeleniumGridNodeAppInteractor, SeleniumGridInteractor { + let machineConfiguration: MachinePropertyConfiguration + let seleniumGridNodeBase: String + let seleniumGridHubBase: String + + internal struct MachinePropertyConfiguration: Content { + let region: String + var config: MachineConfiguration + } + + internal struct MachineConfiguration: Content { + let image: String + var skipLaunch: Bool + var env: [String: String] + let autoDestroy: Bool + let restart: [String: String] + let guest: MachineGuessConfiguration + + public enum CodingKeys: String, CodingKey { + case image + case skipLaunch = "skip_launch" + case env + case autoDestroy = "auto_destroy" + case restart + case guest + } + } + + internal struct MachineGuessConfiguration: Content { + let cpuKind: String + let cpus: Int + let memoryMb: Int + + public enum CodingKeys: String, CodingKey { + case cpuKind = "cpu_kind" + case cpus + case memoryMb = "memory_mb" + } + } + + typealias MachineIdentifier = String + + init( + logger: Logger, + client: any Client, + seleniumGridHubBase: String + ) throws { + seleniumGridNodeBase = try Environment.getOrThrow("SELENIUM_GRID_NODE_BASE") + self.seleniumGridHubBase = seleniumGridHubBase + machineConfiguration = Self.getCreateNodeMachineConfiguration() + try super.init(logger: logger, client: client) + } + + private static func getCreateNodeMachineConfiguration() -> MachinePropertyConfiguration { + MachinePropertyConfiguration( + region: "jnb", + config: .init( + image: "selenium/node-chrome:latest", + skipLaunch: true, + env: ["SE_OPTS": "--drain-after-session-count 1"], + autoDestroy: false, + restart: [ + "policy": "always" + ], + guest: .init(cpuKind: "shared", cpus: 1, memoryMb: 2_048) + ) + ) + } +} + +internal class NodeMachineCreator: NodeMachineCreationBase { + public func create() async throws { + try await createImpl() + } + + private func createImpl() async throws { + logCreateNodeMachineStarted() + let machineID = try await createNodeMachine() + try await updateAndStartMachine(machineID: machineID) + } + + private func logCreateNodeMachineStarted() { + logger.info( + "Creating new node machine.", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + ] + ) + } + + private func createNodeMachine() async throws -> MachineIdentifier { + let response = try await getCreateNodeMachineResponse() + try handleInvalidCreateNodeMachineResponse(response: response) + let machineID = try getMachineIDFromCreateMachineResponse(response) + logNodeMachineCreationSuccess(machineID: machineID) + return machineID + } + + private func getCreateNodeMachineResponse() async throws + -> ClientResponse + { + return try await client.post(.init(stringLiteral: payload.nodesAppMachineAPIURL)) { req in + req.headers = .init(flyAPIHTTPRequestAuthenticationHeader) + try req.content.encode(machineConfiguration) + } + } + + private func handleInvalidCreateNodeMachineResponse(response: ClientResponse) throws { + if isInvalidHTTPResponseStatus(status: response.status) { + let error = try decodeErrorFromResponse(response) + try handleFlyMachinesAPIError(payload: .init(message: "Node machine creation failed", error: error)) + } + } + + private func getMachineIDFromCreateMachineResponse(_ response: ClientResponse) throws -> MachineIdentifier { + struct CreateMachineResponseContent: Content { + let id: String + } + return try response.content.decode(CreateMachineResponseContent.self).id + } + + private func logNodeMachineCreationSuccess(machineID: MachineIdentifier) { + logger.info( + "Node machine creation success.", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + "machine_id": .string(machineID), + ] + ) + } + + private func updateAndStartMachine(machineID: MachineIdentifier) async throws { + try await NodeMachineUpdater( + logger: logger, + client: client, + seleniumGridHubBase: seleniumGridHubBase, + machineID: machineID + ) + .updateNodeHostURLEnvironmentVariable() + + try await sleepBetweenCycle(config: .init(duration: 20)) + } +} diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineDeleter.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineDeleter.swift index c4ebc62..52cad9d 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineDeleter.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineDeleter.swift @@ -19,46 +19,19 @@ internal class NodeMachineDeleter: SeleniumGridNodeAppInteractor { logDeleteNodeMachineSuccess() } - private func getAndValidateDeleteNodeMachineResponse() async throws { - let response = try await getDeleteNodeMachineResponse() - try validateDeleteNodeMachineResponseStatus(response: response) - } - - private func validateDeleteNodeMachineResponseStatus(response: ClientResponse) throws { - if isInvalidHTTPResponseStatus(status: response.status) { - try handleInvalidDeleteNodeMachineResponse(response: response) - } - } - - private func logDeleteNodeMachineSuccess() { + private func logDeleteNodeMachineStarted() { logger.info( - "Successfully deleted node machine.", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - "deletd_node_machine_id": .string(machineID) - ] - ) - } - - private func handleInvalidDeleteNodeMachineResponse(response: ClientResponse) throws { - let error = try decodeErrorFromResponse(response) - logDeleteNodeMachineFailed(error: error) - try throwInvalidDeleteNodeMachineResponseError(error: error) - } - - private func logDeleteNodeMachineFailed(error: FlyAPIError) { - logger.error( - "Failed to delete machine.", + "Deleting node machine started.", metadata: [ "to": .string("\(String(describing: Self.self)).\(#function)"), - "error": .string("\(error)"), "node_machine_id_to_delete": .string(machineID) ] ) } - private func throwInvalidDeleteNodeMachineResponseError(error: FlyAPIError) throws { - throw SeleniumGridNodeMachineAutoscalerError.flyMachinesAPIError(error: error) + private func getAndValidateDeleteNodeMachineResponse() async throws { + let response = try await getDeleteNodeMachineResponse() + try validateDeleteNodeMachineResponseStatus(response: response) } private func getDeleteNodeMachineResponse() async throws -> ClientResponse { @@ -68,12 +41,23 @@ internal class NodeMachineDeleter: SeleniumGridNodeAppInteractor { ) } - private func logDeleteNodeMachineStarted() { + private func validateDeleteNodeMachineResponseStatus(response: ClientResponse) throws { + if isInvalidHTTPResponseStatus(status: response.status) { + try handleInvalidDeleteNodeMachineResponse(response: response) + } + } + + private func handleInvalidDeleteNodeMachineResponse(response: ClientResponse) throws { + let error = try decodeErrorFromResponse(response) + try handleFlyMachinesAPIError(payload: .init(message: "Failed to delete machine", error: error)) + } + + private func logDeleteNodeMachineSuccess() { logger.info( - "Deleting node machine started.", + "Successfully deleted node machine.", metadata: [ "to": .string("\(String(describing: Self.self)).\(#function)"), - "node_machine_id_to_delete": .string(machineID) + "deletd_node_machine_id": .string(machineID) ] ) } diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineUpdater.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineUpdater.swift new file mode 100644 index 0000000..98a08ca --- /dev/null +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineUpdater.swift @@ -0,0 +1,87 @@ +// NodeMachineUpdater.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +import AutomaUtilities +import Vapor + +internal class NodeMachineUpdater: NodeMachineCreationBase { + let machineID: MachineIdentifier + + init( + logger: Logger, + client: any Client, + seleniumGridHubBase: String, + machineID: MachineIdentifier + ) throws { + self.machineID = machineID + try super.init(logger: logger, client: client, seleniumGridHubBase: seleniumGridHubBase) + } + + public func updateNodeHostURLEnvironmentVariable() async throws { + let updatedConfig = updateMachineConfiguation() + + let response = try await getUpdateNodeMachineResponse(updatedConfig: updatedConfig) + try handleInvalidUpdateMachineResponse(response: response) + + let updateResponseBody = try getResponseBodyFromUpdateNodeMachineResponse(response) + + logUpdateMachineSuccess(updateResponseBody: updateResponseBody) + } + + private func updateMachineConfiguation() -> MachinePropertyConfiguration { + var updatedConfiguration = machineConfiguration + updatedConfiguration.config.env = [ + "SE_OPTS": "--drain-after-session-count 1", + "SE_EVENT_BUS_HOST": seleniumGridHubBase, + "SE_NODE_HOST": "\(machineID).vm.\(seleniumGridNodeBase)" + ] + updatedConfiguration.config.skipLaunch = false + + return updatedConfiguration + } + + private func getUpdateNodeMachineResponse( + updatedConfig: MachinePropertyConfiguration + ) async throws -> ClientResponse { + let uri = getUpdateMachineURI() + + return try await client + .post(uri) { req in + req.headers = .init(flyAPIHTTPRequestAuthenticationHeader) + try req.content.encode(["config": updatedConfig.config]) + } + } + + private func getUpdateMachineURI() -> URI { + URI(stringLiteral: "\(payload.nodesAppMachineAPIURL)/\(machineID)") + } + + private func handleInvalidUpdateMachineResponse(response: ClientResponse) throws { + if isInvalidHTTPResponseStatus(status: response.status) { + try handleFlyMachinesAPIError(payload: .init( + message: "Failed to updated machine node 'SE_NODE_HOST' environment variable to URL of the machine", + error: decodeErrorFromResponse(response) + )) + } + } + + private func logUpdateMachineSuccess(updateResponseBody: ByteBuffer) { + logger.info( + "Updating node 'SE_NODE_HOST' environment variable success. Machine will start automatically.", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + "machine_identifier": .string(machineID), + "update_machine_response": .string(String(buffer: updateResponseBody)) + ] + ) + } + + private func getResponseBodyFromUpdateNodeMachineResponse(_ response: ClientResponse) throws -> ByteBuffer { + try response + .unwrapBodyOrThrow( + errorMessage: "Failed to get update node machine response for machine with ID '\(machineID)'" + ) + } +} diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift index d511aae..853fa06 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift @@ -6,6 +6,40 @@ import AutomaUtilities import Vapor +internal protocol SeleniumGridNodeAppInteractorBase { + var client: any Client { get } + var logger: Logger { get } + var payload: SeleniumGridNodeAppInteractorPayload { get } + var flyAPIHTTPRequestAuthenticationHeader: [(String, String)] { get } +} + +internal struct SeleniumGridNodeAppInteractorPayload: Content { + let nodesAppMachineAPIURL: String + let flyAPIToken: String +} + +internal extension SeleniumGridNodeAppInteractorBase { + typealias FlyAPIError = [String: String] + + func decodeErrorFromResponse(_ response: ClientResponse) throws -> FlyAPIError { + return try response.content.decode(FlyAPIError.self) + } + + func isInvalidHTTPResponseStatus(status: HTTPStatus) -> Bool { + return status != .ok + } + + func handleFlyMachinesAPIError(payload: FlyMachinesAPIErrorHandlerPayload) throws { + try FlyMachinesAPIErrorHandler(payload: payload, logger: logger).handle() + } +} + +internal struct FlyMachinesAPIErrorHandlerPayload { + let message: String + let metadata: Logger.Metadata = [:] + let error: SeleniumGridNodeAppInteractorBase.FlyAPIError +} + internal class SeleniumGridNodeAppInteractor: SeleniumGridNodeAppInteractorBase { let payload: SeleniumGridNodeAppInteractorPayload let flyAPIHTTPRequestAuthenticationHeader: [(String, String)] @@ -37,30 +71,7 @@ internal class SeleniumGridNodeAppInteractor: SeleniumGridNodeAppInteractorBase .getListOfAllNodeMachines() } - internal func handleFlyMachinesAPIError(payload: FlyMachinesAPIErrorHandlerPayload) throws { - try FlyMachinesAPIErrorHandler(payload: payload, logger: logger).handle() - } -} - -internal protocol SeleniumGridNodeAppInteractorBase { - var client: any Client { get } - var logger: Logger { get } - var payload: SeleniumGridNodeAppInteractorPayload { get } - var flyAPIHTTPRequestAuthenticationHeader: [(String, String)] { get } -} - -internal struct SeleniumGridNodeAppInteractorPayload: Content { - let nodesAppMachineAPIURL: String - let flyAPIToken: String -} - -internal extension SeleniumGridNodeAppInteractorBase { - typealias FlyAPIError = [String: String] - func decodeErrorFromResponse(_ response: ClientResponse) throws -> FlyAPIError { - return try response.content.decode(FlyAPIError.self) - } - - func isInvalidHTTPResponseStatus(status: HTTPStatus) -> Bool { - return status != .ok + internal func sleepBetweenCycle(config: CycleSleeper.CycleSleeperConfig) async throws { + try await CycleSleeper(config, logger: logger).sleep() } } diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppNodeMachinesFinder.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppNodeMachinesFinder.swift index 4c4bb8b..70986b8 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppNodeMachinesFinder.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppNodeMachinesFinder.swift @@ -62,25 +62,16 @@ internal struct SeleniumGridNodeAppNodeMachinesFinder: SeleniumGridNodeAppIntera private func validateFindAllNodeMachinesResponseStatus(response: ClientResponse) throws { if isInvalidHTTPResponseStatus(status: response.status) { - try handleInvalidFindAllNodeMachinesResponse(res: response) + try handleInvalidFindAllNodeMachinesResponse(response: response) } } - private func handleInvalidFindAllNodeMachinesResponse(res: ClientResponse) throws { - let error = try decodeErrorFromResponse(res) - try logInvalidFindAllNodeMachinesResponse(error: error) - } - - private func logInvalidFindAllNodeMachinesResponse(error: FlyAPIError) throws { - logger.error( - "Failed to get a list of all machines in nodes app", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - "error": .string("\(error)"), - ] - ) - - throw Abort(.internalServerError) + private func handleInvalidFindAllNodeMachinesResponse(response: ClientResponse) throws { + let error = try decodeErrorFromResponse(response) + try handleFlyMachinesAPIError(payload: .init( + message: "Failed to get a list of all machines in nodes app", + error: error + )) } private func findNodeMachineListFromResponse(_ response: ClientResponse) throws -> [NodeMachine] { diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoCreator.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoCreator.swift index edda8d7..493a646 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoCreator.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoCreator.swift @@ -6,21 +6,19 @@ import AutomaUtilities import Vapor -// TODO: Use logger.error instead of logger.info in places where errors occur. Create custom errors instead of using internalServerError everywhere. +// TODO: // add more and better logs -// refactor remaining methods -// order functions in logical order, like a well-ordered document -// refactor helper methods to a single helper method in its own struct to reduce class length -internal class SeleniumGridNodeAutoCreator: SeleniumGridNodeMachineAutoscaler { - let seleniumGridHubBase: String - let seleniumGridNodeBase: String - - let maxNodeMachinesAllowed: Int = 10 +internal protocol SeleniumGridInteractor { + var client: any Client { get } + var logger: Logger { get } + var seleniumGridHubBase: String { get } +} +internal class SeleniumGridNodeAutoCreator: SeleniumGridNodeMachineAutoscaler, SeleniumGridInteractor { + let seleniumGridHubBase: String init(client: any Client, logger: Logger, cyclePauseDurationSeconds: Int) throws { seleniumGridHubBase = try Environment.getOrThrow("SELENIUM_GRID_HUB_BASE") - seleniumGridNodeBase = try Environment.getOrThrow("SELENIUM_GRID_NODE_BASE") try super.init(logger: logger, client: client, cyclePauseDurationSeconds: cyclePauseDurationSeconds) } @@ -38,264 +36,63 @@ internal class SeleniumGridNodeAutoCreator: SeleniumGridNodeMachineAutoscaler { try await handleMaxNodeMachinesReached() let totalSessionQueueRequests = try await getTotalSessionQueueRequests() - try await createSeleniumGridNodeFlyMachines(totalSessionsInQueue: totalSessionQueueRequests) - } - - private func createSeleniumGridNodeFlyMachines(totalSessionsInQueue: Int) async throws { - if totalSessionsInQueue > 0 { - logFoundPendingSessionsInQueue(totalSessions: totalSessionsInQueue) - try await createNewSeleniumGridNodeFlyMachines(amount: totalSessionsInQueue) - } - } - - private func createNewSeleniumGridNodeFlyMachines(amount: Int) async throws { - for _ in 1 ... amount { - try await createNewSeleniumGridNodeFlyMachine() - } + try await createNodeMachines(totalSessionsInQueue: totalSessionQueueRequests) } - private func logFoundPendingSessionsInQueue(totalSessions: Int) { + private func logAutoCreateNodeMachinesStarted() { logger.info( - "Found a total of \(totalSessions) pending sessions in queue.", + "Node auto-creator cycle started (cycle: \(cycleCount)).", metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), + "to": .string("\(String(describing: Self.self)).\(#function)") ] ) } - private func getTotalSessionQueueRequests() async throws -> Int { - let response = try await getGridSessionQueueReponse() - return response.data.sessionsInfo.sessionQueueRequests.count - } - private func handleMaxNodeMachinesReached() async throws { - if try await maxNodeMachinesReached() { + if try await MaxNodeMachinesReachedHandler(logger: logger, client: client) + .reached() + { try await recursivelyAutoCreateNodeMachines() } } private func recursivelyAutoCreateNodeMachines() async throws { - try await sleepBetweenCycle() + try await sleepBetweenCycle(config: .init(duration: cyclePauseDurationSeconds)) cycleCount += 1 try await autoCreateNodeMachinesImpl() } - private func logAutoCreateNodeMachinesStarted() { - logger.info( - "Node auto-creator cycle started (cycle: \(cycleCount)).", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)") - ] - ) - } - - private func maxNodeMachinesReached() async throws -> Bool { - let totalMachines = try await getTotalNodeMachines() - let reached = reachedMaxNodeMachines(totalMachines: totalMachines) - if reached { - logReachedMaxNodeMachines(totalMachines: totalMachines) - } - return reached - } - - private func logReachedMaxNodeMachines(totalMachines: Int) { - logger.info( - "The threshold of \(maxNodeMachinesAllowed) running node machines reached. No additional node machines will be created.", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - "total_node_machines": .string(String(totalMachines)) - ] - ) - } - - private func reachedMaxNodeMachines(totalMachines: Int) -> Bool { - return totalMachines >= maxNodeMachinesAllowed - } - - private func getTotalNodeMachines() async throws -> Int { - try await getListOfAllNodeMachines().count - } - - private func getGridSessionQueueReponse() async throws -> SessionQueueResponse { - logger.info( - "Looking for sessions in queue.", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - ] - ) - let url = "http://\(seleniumGridHubBase):4444/graphql" - let res = try await client.post(.init(stringLiteral: url)) { req in - try req.content - .encode(SeleniumGridGraphQLQuery(query: "query SessionsInfo { sessionsInfo { sessionQueueRequests }}")) - } - return try res.content.decode(SessionQueueResponse.self) + private func getTotalSessionQueueRequests() async throws -> Int { + try await SeleniumGridSessionQueueRequestsHandler( + client: client, + logger: logger, + seleniumGridHubBase: seleniumGridHubBase + ).getTotalRequests() } - private func createNewSeleniumGridNodeFlyMachine() async throws { - logger.info( - "Creating new node machine.", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - ] - ) - - let machineConfiguration = MachinePropertyConfiguration( - region: "jnb", - config: .init( - image: "selenium/node-chrome:latest", - skipLaunch: true, - env: ["SE_OPTS": "--drain-after-session-count 1"], - autoDestroy: false, - restart: [ - "policy": "always" - ], - guest: .init(cpuKind: "shared", cpus: 1, memoryMb: 2_048) - ) - ) - - let res = try await client.post(.init(stringLiteral: payload.nodesAppMachineAPIURL)) { req in - req.headers = .init(flyAPIHTTPRequestAuthenticationHeader) - try req.content.encode(machineConfiguration) - } - - if res.status != .ok { - let responseContent = try res.content.decode(FlyAPIError.self) - logger.info( - "Node machine creation failed.", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - "response_content": .string("\(responseContent)") - ] - ) - throw Abort(.internalServerError) - } - - logger.info( - "Node machine creation success.", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - ] - ) - - struct CreateMachineResponseContent: Content { - let id: String + private func createNodeMachines(totalSessionsInQueue: Int) async throws { + if totalSessionsInQueue > 0 { + logFoundPendingSessionsInQueue(totalSessions: totalSessionsInQueue) + try await createNodeMachines(amount: totalSessionsInQueue) } - - let machineIdentifier = try res.content.decode(CreateMachineResponseContent.self).id - try await updateMachineNodeHostURLEnvironmentVariable( - machineIdentifier: machineIdentifier, - machineConfiguration: machineConfiguration, - flyAPIToken: payload.flyAPIToken - ) - - try await Task.sleep(for: .seconds(20)) } - private func updateMachineNodeHostURLEnvironmentVariable( - machineIdentifier: String, - machineConfiguration: MachinePropertyConfiguration, - flyAPIToken _: String - ) async throws { - var updatedConfiguration = machineConfiguration - - updatedConfiguration.config.env = [ - "SE_OPTS": "--drain-after-session-count 1", - "SE_EVENT_BUS_HOST": seleniumGridHubBase, - "SE_NODE_HOST": "\(machineIdentifier).vm.\(seleniumGridNodeBase)" - ] - - updatedConfiguration.config.skipLaunch = false - logger.info( - "Machine updated environment.", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - "updated_configuration": .string("\(updatedConfiguration.config)") - ] - ) - - let res = try await client - .post(.init(stringLiteral: "\(payload.nodesAppMachineAPIURL)/\(machineIdentifier)")) { req in - req.headers = .init(flyAPIHTTPRequestAuthenticationHeader) - try req.content.encode(["config": updatedConfiguration.config]) - } - - if res.status != .ok { - let error = try decodeErrorFromResponse(res) - logger.info( - "Failed to updated machine node 'SE_NODE_HOST' environment variable to URL of the machine", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - "error": .string("\(error)"), - "machine_identifier": .string(machineIdentifier) - ] - ) - throw Abort(.internalServerError) - } - - guard - let body = res.body - else { - throw Abort(.internalServerError) - } - + private func logFoundPendingSessionsInQueue(totalSessions: Int) { logger.info( - "Updating node 'SE_NODE_HOST' environment variable success. Machine will start automatically.", + "Found a total of \(totalSessions) pending sessions in queue.", metadata: [ "to": .string("\(String(describing: Self.self)).\(#function)"), - "machine_identifier": .string(machineIdentifier), - "update_machine_response": .string(String(buffer: body)) ] ) } - internal struct SeleniumGridGraphQLQuery: Content { - let query: String - } - - internal struct SessionQueueResponse: Content { - let data: SessionQueueResponseData - } - - internal struct SessionQueueResponseData: Content { - let sessionsInfo: SessionsInfo - } - - internal struct SessionsInfo: Content { - let sessionQueueRequests: [String] - } - - internal struct MachinePropertyConfiguration: Content { - let region: String - var config: MachineConfiguration - } - - internal struct MachineConfiguration: Content { - let image: String - var skipLaunch: Bool - var env: [String: String] - let autoDestroy: Bool - let restart: [String: String] - let guest: MachineGuessConfiguration - - public enum CodingKeys: String, CodingKey { - case image - case skipLaunch = "skip_launch" - case env - case autoDestroy = "auto_destroy" - case restart - case guest + private func createNodeMachines(amount: Int) async throws { + for _ in 1 ... amount { + try await createNodeMachine() } } - internal struct MachineGuessConfiguration: Content { - let cpuKind: String - let cpus: Int - let memoryMb: Int - - public enum CodingKeys: String, CodingKey { - case cpuKind = "cpu_kind" - case cpus - case memoryMb = "memory_mb" - } + private func createNodeMachine() async throws { + try await NodeMachineCreator(logger: logger, client: client, seleniumGridHubBase: seleniumGridHubBase).create() } } diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoDestroyer.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoDestroyer.swift index ea8b236..1db21e0 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoDestroyer.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoDestroyer.swift @@ -5,36 +5,6 @@ import Vapor -internal class SeleniumGridNodeMachineAutoscalerBase: SeleniumGridNodeAppInteractor { - let cyclePauseDurationSeconds: Int - internal var cycleCount: Int = 1 - - internal init(logger: Logger, client: any Client, cyclePauseDurationSeconds: Int) throws { - self.cyclePauseDurationSeconds = cyclePauseDurationSeconds - try super.init(logger: logger, client: client) - } -} - -internal class SeleniumGridNodeMachineAutoscaler: SeleniumGridNodeMachineAutoscalerBase { - internal func sleepBetweenCycle() async throws { - logSleepBetweenCycleStarted() - try await Task.sleep(for: .seconds(cyclePauseDurationSeconds)) - } - - private func logSleepBetweenCycleStarted() { - logger.info( - "Pausing for \(cyclePauseDurationSeconds) before next cycle starts.", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - ] - ) - } - - internal func deleteNodeMachine(id: String) async throws { - try await NodeMachineDeleter(logger: logger, client: client, machineID: id).delete() - } -} - internal class SeleniumGridNodeAutoDestroyer: SeleniumGridNodeMachineAutoscalerBase { public func autoDestroyAllOldNodeMachines() async throws { try await SeleniumGridNodeAutoOldMachineDestroyer( diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOffMachineDestroyer.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOffMachineDestroyer.swift index 71215d5..3c85e4e 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOffMachineDestroyer.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOffMachineDestroyer.swift @@ -14,12 +14,6 @@ internal class SeleniumGridNodeAutoOffMachineDestroyer: SeleniumGridNodeMachineA try await recursivelyAutoDestroyAllOffNodeMachines() } - private func recursivelyAutoDestroyAllOffNodeMachines() async throws { - try await sleepBetweenCycle() - cycleCount += 1 - try await autoDestroyAllOffNodeMachines() - } - private func destroyAllCurrentlyOffNodeMachines() async throws { logAutoDestroyAllOffNodeMachinesStarted() let allMachines = try await getListOfAllNodeMachines() @@ -38,6 +32,7 @@ internal class SeleniumGridNodeAutoOffMachineDestroyer: SeleniumGridNodeMachineA private func destroyAllOffNodeMachines(_ allMachines: [SeleniumGridNodeAppNodeMachinesFinder .NodeMachine]) async throws { + // TODO: refactor if allMachines.count == 0 { return } @@ -68,4 +63,10 @@ internal class SeleniumGridNodeAutoOffMachineDestroyer: SeleniumGridNodeMachineA try await deleteNodeMachine(id: machine.id) } } + + private func recursivelyAutoDestroyAllOffNodeMachines() async throws { + try await sleepBetweenCycle(config: .init(duration: cyclePauseDurationSeconds)) + cycleCount += 1 + try await autoDestroyAllOffNodeMachines() + } } diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOldMachineDestroyer.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOldMachineDestroyer.swift index 3930e09..7fc2134 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOldMachineDestroyer.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOldMachineDestroyer.swift @@ -17,7 +17,7 @@ internal class SeleniumGridNodeAutoOldMachineDestroyer: SeleniumGridNodeMachineA } private func recursivelyAutoDestroyAllOldNodeMachines() async throws { - try await sleepBetweenCycle() + try await sleepBetweenCycle(config: .init(duration: cyclePauseDurationSeconds)) cycleCount += 1 try await autoDestroyAllOldNodeMachinesImpl() } diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeMachineAutoscaler.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeMachineAutoscaler.swift new file mode 100644 index 0000000..1c80b36 --- /dev/null +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeMachineAutoscaler.swift @@ -0,0 +1,22 @@ +// SeleniumGridNodeMachineAutoscaler.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +import Vapor + +internal class SeleniumGridNodeMachineAutoscalerBase: SeleniumGridNodeAppInteractor { + let cyclePauseDurationSeconds: Int + internal var cycleCount: Int = 1 + + internal init(logger: Logger, client: any Client, cyclePauseDurationSeconds: Int) throws { + self.cyclePauseDurationSeconds = cyclePauseDurationSeconds + try super.init(logger: logger, client: client) + } +} + +internal class SeleniumGridNodeMachineAutoscaler: SeleniumGridNodeMachineAutoscalerBase { + internal func deleteNodeMachine(id: String) async throws { + try await NodeMachineDeleter(logger: logger, client: client, machineID: id).delete() + } +} diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridSessionQueueRequestsHandler.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridSessionQueueRequestsHandler.swift new file mode 100644 index 0000000..d164981 --- /dev/null +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridSessionQueueRequestsHandler.swift @@ -0,0 +1,76 @@ +// SeleniumGridSessionQueueRequestsHandler.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +import Vapor + +internal struct SeleniumGridSessionQueueRequestsHandler: SeleniumGridInteractor { + var client: any Client + var logger: Logger + var seleniumGridHubBase: String + + public func getTotalRequests() async throws -> Int { + let response = try await getGridSessionQueueReponse() + return getTotalSessionQueueRequestsFromResponse(response) + } + + private func getGridSessionQueueReponse() async throws -> SessionQueueResponse { + logGetGridSessionQueueResponseStarted() + let response = try await querySeleniumGridHubForSessionQueueRequests() + return try decodeSessionQueueResponseFromClientResponse(response) + } + + internal struct SessionQueueResponse: Content { + let data: SessionQueueResponseData + } + + internal struct SessionQueueResponseData: Content { + let sessionsInfo: SessionsInfo + } + + internal struct SessionsInfo: Content { + let sessionQueueRequests: [String] + } + + private func logGetGridSessionQueueResponseStarted() { + logger.info( + "Looking for sessions in queue.", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + ] + ) + } + + private func querySeleniumGridHubForSessionQueueRequests() async throws -> ClientResponse { + let uri = getSeleniumGridGraphQLURI() + let query = getSessionQueueRequestsGraphQLQuery() + return try await client.post(uri) { req in + try req.content.encode(query) + } + } + + private func getSeleniumGridGraphQLURI() -> URI { + URI(stringLiteral: "http://\(seleniumGridHubBase):4444/graphql") + } + + private func getSessionQueueRequestsGraphQLQuery() -> SeleniumGridGraphQLQuery { + SeleniumGridGraphQLQuery(query: "query SessionsInfo { sessionsInfo { sessionQueueRequests }}") + } + + internal struct SeleniumGridGraphQLQuery: Content { + let query: String + } + + private func decodeSessionQueueResponseFromClientResponse(_ response: ClientResponse) throws + -> SessionQueueResponse + { + return try response.content.decode(SessionQueueResponse.self) + } + + private func getTotalSessionQueueRequestsFromResponse(_ response: SessionQueueResponse) + -> Int + { + response.data.sessionsInfo.sessionQueueRequests.count + } +} From a1ee5f361995214ce61c2a2daa0eb93a251e928e Mon Sep 17 00:00:00 2001 From: William Date: Mon, 15 Sep 2025 20:02:51 +0200 Subject: [PATCH 08/10] refactor: more cleanup work --- .../SeleniumGridNodeAppInteractor.swift | 16 +++++- ...eleniumGridNodeAppNodeMachinesFinder.swift | 25 ++-------- .../SeleniumGridNodeAutoCreator.swift | 3 +- ...eniumGridNodeAutoOffMachineDestroyer.swift | 50 +++++++++++-------- ...eniumGridNodeAutoOldMachineDestroyer.swift | 2 +- 5 files changed, 53 insertions(+), 43 deletions(-) diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift index 853fa06..439e87d 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift @@ -61,7 +61,7 @@ internal class SeleniumGridNodeAppInteractor: SeleniumGridNodeAppInteractorBase ) } - internal func getListOfAllNodeMachines() async throws -> [SeleniumGridNodeAppNodeMachinesFinder.NodeMachine] { + internal func getListOfAllNodeMachines() async throws -> NodeMachines { try await SeleniumGridNodeAppNodeMachinesFinder( logger: logger, client: client, @@ -75,3 +75,17 @@ internal class SeleniumGridNodeAppInteractor: SeleniumGridNodeAppInteractorBase try await CycleSleeper(config, logger: logger).sleep() } } + +internal struct NodeMachine: Content { + let id: String + let state: String + let createdAt: Date + + public enum CodingKeys: String, CodingKey { + case id + case state + case createdAt = "created_at" + } +} + +typealias NodeMachines = [NodeMachine] diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppNodeMachinesFinder.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppNodeMachinesFinder.swift index 70986b8..c72ebd5 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppNodeMachinesFinder.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppNodeMachinesFinder.swift @@ -5,32 +5,17 @@ import Vapor -// TODO: Use logger.error instead of logger.info in places where errors occur. Create custom errors instead of using internalServerError everywhere. -// add more and better logs - internal struct SeleniumGridNodeAppNodeMachinesFinder: SeleniumGridNodeAppInteractorBase { let logger: Logger let client: any Client var payload: SeleniumGridNodeAppInteractorPayload var flyAPIHTTPRequestAuthenticationHeader: [(String, String)] - public func getListOfAllNodeMachines() async throws -> [NodeMachine] { + public func getListOfAllNodeMachines() async throws -> NodeMachines { return try await getAllNodeMachinesList() } - internal struct NodeMachine: Content { - let id: String - let state: String - let createdAt: Date - - public enum CodingKeys: String, CodingKey { - case id - case state - case createdAt = "created_at" - } - } - - private func getAllNodeMachinesList() async throws -> [NodeMachine] { + private func getAllNodeMachinesList() async throws -> NodeMachines { logGetListOfAllMachinesStarted() let allMachines = try await validateAndGetAllMachines() logGetListOfAllMachinesSuccess(totalMachines: allMachines.count) @@ -46,7 +31,7 @@ internal struct SeleniumGridNodeAppNodeMachinesFinder: SeleniumGridNodeAppIntera ) } - private func validateAndGetAllMachines() async throws -> [NodeMachine] { + private func validateAndGetAllMachines() async throws -> NodeMachines { let response = try await getAllNodeMachinesResponse() try validateFindAllNodeMachinesResponseStatus(response: response) @@ -74,8 +59,8 @@ internal struct SeleniumGridNodeAppNodeMachinesFinder: SeleniumGridNodeAppIntera )) } - private func findNodeMachineListFromResponse(_ response: ClientResponse) throws -> [NodeMachine] { - try response.content.decode([NodeMachine].self) + private func findNodeMachineListFromResponse(_ response: ClientResponse) throws -> NodeMachines { + try response.content.decode(NodeMachines.self) } private func logGetListOfAllMachinesSuccess(totalMachines: Int) { diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoCreator.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoCreator.swift index 493a646..fd4f14a 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoCreator.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoCreator.swift @@ -7,7 +7,8 @@ import AutomaUtilities import Vapor // TODO: -// add more and better logs +// - [ ] add more logs +// - [ ] better error handling internal protocol SeleniumGridInteractor { var client: any Client { get } diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOffMachineDestroyer.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOffMachineDestroyer.swift index 3c85e4e..4b355e7 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOffMachineDestroyer.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOffMachineDestroyer.swift @@ -29,39 +29,49 @@ internal class SeleniumGridNodeAutoOffMachineDestroyer: SeleniumGridNodeMachineA ) } - private func destroyAllOffNodeMachines(_ allMachines: [SeleniumGridNodeAppNodeMachinesFinder - .NodeMachine]) async throws - { - // TODO: refactor - if allMachines.count == 0 { + private func destroyAllOffNodeMachines(_ allMachines: NodeMachines) async throws { + if allMachines.isEmpty { return } - let machinesToStop = allMachines.filter { machine in - ["stopped", "suspended"].contains(machine.state) - } + let machinesToStop = getAllOffNodeMachines(allMachines) + let totalMachines = allMachines.count - if machinesToStop.count == 0 { - logger.info( - "None of the \(allMachines.count) machines in a stopped or suspended state. No machines will be destroyed.", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - ] - ) + if machinesToStop.isEmpty { + logNoOffMachines(totalMachines: totalMachines) return } + logDestroyAllOffMachinesStarted(totalMachines: totalMachines) + + for machine in machinesToStop { + try await deleteNodeMachine(id: machine.id) + } + } + + private func getAllOffNodeMachines(_ machines: NodeMachines) -> NodeMachines { + machines.filter { machine in + ["stopped", "suspended"].contains(machine.state) + } + } + + private func logNoOffMachines(totalMachines: Int) { logger.info( - "Destroying all off node machines started.", + "None of the \(totalMachines) machines in a stopped or suspended state. No machines will be destroyed.", metadata: [ "to": .string("\(String(describing: Self.self)).\(#function)"), - "total_machines_to_destroy": .string(String(machinesToStop.count)) ] ) + } - for machine in machinesToStop { - try await deleteNodeMachine(id: machine.id) - } + private func logDestroyAllOffMachinesStarted(totalMachines: Int) { + logger.info( + "Destroying all off node machines started.", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + "total_machines_to_destroy": .string(String(totalMachines)) + ] + ) } private func recursivelyAutoDestroyAllOffNodeMachines() async throws { diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOldMachineDestroyer.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOldMachineDestroyer.swift index 7fc2134..55cc712 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOldMachineDestroyer.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOldMachineDestroyer.swift @@ -68,7 +68,7 @@ internal class SeleniumGridNodeAutoOldMachineDestroyer: SeleniumGridNodeMachineA ) for machine in machinesToStop { - try await deleteNodeMachine(id: machine.id) + try await destroyNodeMachine(id: machine.id) } } } From 96ca3733dbd7e2fb0dfb8eebb600e370a0965415 Mon Sep 17 00:00:00 2001 From: William Date: Tue, 16 Sep 2025 16:22:42 +0200 Subject: [PATCH 09/10] refactor: added meaningful logs --- .../CycleSleeper.swift | 19 +++++++++++++++++-- .../NodeMachineUpdater.swift | 11 +++++++++++ ...eniumGridNodeAutoOldMachineDestroyer.swift | 6 ++---- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/CycleSleeper.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/CycleSleeper.swift index e889ce0..fcc15e0 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/CycleSleeper.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/CycleSleeper.swift @@ -16,17 +16,32 @@ internal struct CycleSleeper { internal struct CycleSleeperConfig { let duration: Int - let message: String? = nil + let startMessage: String? = nil + let completionMessage: String? = nil } public func sleep() async throws { logSleepBetweenCycleStarted() try await Task.sleep(for: .seconds(config.duration)) + logSleepBetweenCycleCompleted() } private func logSleepBetweenCycleStarted() { logger.info( - Logger.Message(stringLiteral: config.message ?? "Pausing for \(config.duration) before next cycle starts."), + Logger + .Message(stringLiteral: config + .startMessage ?? "Pausing for \(config.duration) before next cycle starts."), + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + ] + ) + } + + private func logSleepBetweenCycleCompleted() { + logger.info( + Logger + .Message(stringLiteral: config + .completionMessage ?? "Pausing for \(config.duration) before completed."), metadata: [ "to": .string("\(String(describing: Self.self)).\(#function)"), ] diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineUpdater.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineUpdater.swift index 98a08ca..144a5d9 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineUpdater.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineUpdater.swift @@ -20,6 +20,7 @@ internal class NodeMachineUpdater: NodeMachineCreationBase { } public func updateNodeHostURLEnvironmentVariable() async throws { + logUpdateMachineStarted() let updatedConfig = updateMachineConfiguation() let response = try await getUpdateNodeMachineResponse(updatedConfig: updatedConfig) @@ -30,6 +31,16 @@ internal class NodeMachineUpdater: NodeMachineCreationBase { logUpdateMachineSuccess(updateResponseBody: updateResponseBody) } + private func logUpdateMachineStarted() { + logger.info( + "Updating node machine SE_NODE_HOST url.", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + "machine_id": .string(machineID) + ] + ) + } + private func updateMachineConfiguation() -> MachinePropertyConfiguration { var updatedConfiguration = machineConfiguration updatedConfiguration.config.env = [ diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOldMachineDestroyer.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOldMachineDestroyer.swift index 55cc712..f5390e2 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOldMachineDestroyer.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOldMachineDestroyer.swift @@ -37,9 +37,7 @@ internal class SeleniumGridNodeAutoOldMachineDestroyer: SeleniumGridNodeMachineA ) } - private func destroyAllOldNodeMachines(_ allMachines: [SeleniumGridNodeAppNodeMachinesFinder - .NodeMachine]) async throws - { + private func destroyAllOldNodeMachines(_ allMachines: [NodeMachine]) async throws { if allMachines.count == 0 { return } @@ -68,7 +66,7 @@ internal class SeleniumGridNodeAutoOldMachineDestroyer: SeleniumGridNodeMachineA ) for machine in machinesToStop { - try await destroyNodeMachine(id: machine.id) + try await deleteNodeMachine(id: machine.id) } } } From 5b3996caf1655c3bb1514bb69163c371cf1830e0 Mon Sep 17 00:00:00 2001 From: William Date: Tue, 16 Sep 2025 16:59:43 +0200 Subject: [PATCH 10/10] fix: fixed all swiftlint linting errors --- ... SeleniumGridNodeAutoCreatorCommand.swift} | 12 +++--- ...SeleniumGridNodeAutoDestroyerCommand.swift | 14 ++++--- .../CycleSleeper.swift | 14 ++++--- .../FlyMachinesAPIErrorHandler.swift | 10 +++-- .../MaxNodeMachinesReachedHandler.swift | 14 +++++-- .../NodeMachineCreator.swift | 42 +++++++++++-------- .../NodeMachineDeleter.swift | 8 +++- .../NodeMachineUpdater.swift | 8 +++- .../SeleniumGridNodeAppInteractor.swift | 31 +++++++------- ...eleniumGridNodeAppNodeMachinesFinder.swift | 11 +++-- .../SeleniumGridNodeAutoCreator.swift | 13 +++--- .../SeleniumGridNodeAutoDestroyer.swift | 6 +++ ...eniumGridNodeAutoOffMachineDestroyer.swift | 4 ++ ...eniumGridNodeAutoOldMachineDestroyer.swift | 8 +++- .../SeleniumGridNodeMachineAutoscaler.swift | 6 ++- ...eniumGridSessionQueueRequestsHandler.swift | 17 ++++---- .../configure.swift | 2 +- .../entrypoint.swift | 8 ++-- .../routes.swift | 11 ----- ...leniumGridNodeMachineAutoscalerTests.swift | 16 ++++--- 20 files changed, 155 insertions(+), 100 deletions(-) rename SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/{SeleniumGridNodeAutoScalerCommand.swift => SeleniumGridNodeAutoCreatorCommand.swift} (55%) delete mode 100644 SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/routes.swift diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoScalerCommand.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoCreatorCommand.swift similarity index 55% rename from SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoScalerCommand.swift rename to SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoCreatorCommand.swift index 92ddfb9..1d45a24 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoScalerCommand.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoCreatorCommand.swift @@ -1,23 +1,23 @@ -// SeleniumGridNodeAutoScalerCommand.swift +// SeleniumGridNodeAutoCreatorCommand.swift // Copyright (c) 2025 GetAutomaApp // All source code and related assets are the property of GetAutomaApp. // All rights reserved. import Vapor -struct SeleniumGridNodeAutoCreatorCommand: AsyncCommand { - struct Signature: CommandSignature {} +internal struct SeleniumGridNodeAutoCreatorCommand: AsyncCommand { + internal struct Signature: CommandSignature {} - var help: String { + internal var help: String { "Auto-creates fly.io SeleniumGrid Node App machines" } - func run(using context: CommandContext, signature _: Signature) async throws { + internal func run(using context: CommandContext, signature _: Signature) async throws { let autoCreator = try SeleniumGridNodeAutoCreator( client: context.application.client, logger: context.application.logger, cyclePauseDurationSeconds: 10 ) - try await autoCreator.autoCreate() + try await autoCreator.autoCreateNodeMachines() } } diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoDestroyerCommand.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoDestroyerCommand.swift index 1974707..4f8d748 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoDestroyerCommand.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/Commands/SeleniumGridNodeAutoDestroyerCommand.swift @@ -5,22 +5,24 @@ import Vapor -enum SeleniumGridNodeAutoDestroyerType: String, Codable { +internal enum SeleniumGridNodeAutoDestroyerType: String, Codable { case offMachines case oldMachines } -struct SeleniumGridNodeAutoDestroyerCommand: AsyncCommand { - struct Signature: CommandSignature { +internal struct SeleniumGridNodeAutoDestroyerCommand: AsyncCommand { + internal struct Signature: CommandSignature { @Argument(name: "type") - var type: SeleniumGridNodeAutoDestroyerType.RawValue + internal var type: SeleniumGridNodeAutoDestroyerType.RawValue } - var help: String { + internal var help: String { "Auto destroys fly.io SeleniumGrid Node App machines" } - func run(using context: CommandContext, signature: Signature) async throws { + /// Run auto-destroyer command + /// - Throws: An eror if there was a problem destroying machines + public func run(using context: CommandContext, signature: Signature) async throws { let destroyer = try SeleniumGridNodeAutoDestroyer( logger: context.application.logger, client: context.application.client, diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/CycleSleeper.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/CycleSleeper.swift index fcc15e0..7b67651 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/CycleSleeper.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/CycleSleeper.swift @@ -6,20 +6,22 @@ import Vapor internal struct CycleSleeper { - let config: CycleSleeperConfig - let logger: Logger + internal let config: CycleSleeperConfig + internal let logger: Logger - init(_ config: CycleSleeperConfig, logger: Logger) { + internal init(_ config: CycleSleeperConfig, logger: Logger) { self.config = config self.logger = logger } internal struct CycleSleeperConfig { - let duration: Int - let startMessage: String? = nil - let completionMessage: String? = nil + internal let duration: Int + internal let startMessage: String? = nil + internal let completionMessage: String? = nil } + /// Sleep for a cycle + /// - Throws: An error if `Task.sleep()` failed for some reason public func sleep() async throws { logSleepBetweenCycleStarted() try await Task.sleep(for: .seconds(config.duration)) diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/FlyMachinesAPIErrorHandler.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/FlyMachinesAPIErrorHandler.swift index 0d7fb30..b0f1591 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/FlyMachinesAPIErrorHandler.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/FlyMachinesAPIErrorHandler.swift @@ -6,10 +6,12 @@ import Vapor internal struct FlyMachinesAPIErrorHandler { - let payload: FlyMachinesAPIErrorHandlerPayload - let logger: Logger + internal let payload: FlyMachinesAPIErrorHandlerPayload + internal let logger: Logger - func handle() throws { + /// Generic handler for handling fly.io machines API errors + /// - Throws: `SeleniumGridNodeMachineAutoscalerError.flyMachinesAPIError` + public func handle() throws { logFlyAPIError() try throwFlyMachinesAPIError() } @@ -34,7 +36,7 @@ internal struct FlyMachinesAPIErrorHandler { private func mergeFlyAPIErrorLogMetadataBaseWithMetadata(base: Logger.Metadata) -> Logger .Metadata { - payload.metadata.merging(base, uniquingKeysWith: { first, _ in first }) + payload.metadata.merging(base) { first, _ in first } } private func logFlyAPIErrorWithFinalMetadata(finalMetadata: Logger.Metadata) { diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/MaxNodeMachinesReachedHandler.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/MaxNodeMachinesReachedHandler.swift index 665c067..0e98ffd 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/MaxNodeMachinesReachedHandler.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/MaxNodeMachinesReachedHandler.swift @@ -6,12 +6,15 @@ import Vapor internal class MaxNodeMachinesReachedHandler: SeleniumGridNodeMachineAutoscaler { - let maxNodeMachinesAllowed: Int = 10 + internal let maxNodeMachinesAllowed: Int = 10 - init(logger: Logger, client: any Client) throws { + internal init(logger: Logger, client: any Client) throws { try super.init(logger: logger, client: client, cyclePauseDurationSeconds: 0) } + /// Returns whether maximum allowed machines existing at once reached + /// - Returns: Boolean, whether max node machines were reached or not + /// - Throws: An error if there was a problem getting total node machines public func reached() async throws -> Bool { let totalMachines = try await getTotalNodeMachines() let reached = reachedMaxNodeMachines(totalMachines: totalMachines) @@ -31,11 +34,16 @@ internal class MaxNodeMachinesReachedHandler: SeleniumGridNodeMachineAutoscaler private func logReachedMaxNodeMachines(totalMachines: Int) { logger.info( - "The threshold of \(maxNodeMachinesAllowed) running node machines reached. No additional node machines will be created.", + """ + The threshold of \(maxNodeMachinesAllowed) running node machines reached. + No additional node machines will be created. + """, metadata: [ "to": .string("\(String(describing: Self.self)).\(#function)"), "total_node_machines": .string(String(totalMachines)) ] ) } + + deinit {} } diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineCreator.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineCreator.swift index 2816239..b55c56e 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineCreator.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineCreator.swift @@ -7,23 +7,24 @@ import AutomaUtilities import Vapor internal class NodeMachineCreationBase: SeleniumGridNodeAppInteractor, SeleniumGridInteractor { - let machineConfiguration: MachinePropertyConfiguration - let seleniumGridNodeBase: String - let seleniumGridHubBase: String + internal let machineConfiguration: MachinePropertyConfiguration + internal let seleniumGridNodeBase: String + internal let seleniumGridHubBase: String internal struct MachinePropertyConfiguration: Content { - let region: String - var config: MachineConfiguration + internal let region: String + internal var config: MachineConfiguration } internal struct MachineConfiguration: Content { - let image: String - var skipLaunch: Bool - var env: [String: String] - let autoDestroy: Bool - let restart: [String: String] - let guest: MachineGuessConfiguration - + internal let image: String + internal var skipLaunch: Bool + internal var env: [String: String] + internal let autoDestroy: Bool + internal let restart: [String: String] + internal let guest: MachineGuessConfiguration + + /// Coding keys for `NodeMachineCreationBase` public enum CodingKeys: String, CodingKey { case image case skipLaunch = "skip_launch" @@ -35,10 +36,11 @@ internal class NodeMachineCreationBase: SeleniumGridNodeAppInteractor, SeleniumG } internal struct MachineGuessConfiguration: Content { - let cpuKind: String - let cpus: Int - let memoryMb: Int + internal let cpuKind: String + internal let cpus: Int + internal let memoryMb: Int + /// Coding keys for `MachineGuessConfiguration` public enum CodingKeys: String, CodingKey { case cpuKind = "cpu_kind" case cpus @@ -46,9 +48,9 @@ internal class NodeMachineCreationBase: SeleniumGridNodeAppInteractor, SeleniumG } } - typealias MachineIdentifier = String + internal typealias MachineIdentifier = String - init( + internal init( logger: Logger, client: any Client, seleniumGridHubBase: String @@ -74,9 +76,13 @@ internal class NodeMachineCreationBase: SeleniumGridNodeAppInteractor, SeleniumG ) ) } + + deinit {} } internal class NodeMachineCreator: NodeMachineCreationBase { + /// Create new node machine for Selenium Grid Node app on fly.io + /// - Throws: An error if creating machine fails public func create() async throws { try await createImpl() } @@ -148,4 +154,6 @@ internal class NodeMachineCreator: NodeMachineCreationBase { try await sleepBetweenCycle(config: .init(duration: 20)) } + + deinit {} } diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineDeleter.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineDeleter.swift index 52cad9d..025e00a 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineDeleter.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineDeleter.swift @@ -6,13 +6,15 @@ import Vapor internal class NodeMachineDeleter: SeleniumGridNodeAppInteractor { - let machineID: String + internal let machineID: String - init(logger: Logger, client: any Client, machineID: String) throws { + internal init(logger: Logger, client: any Client, machineID: String) throws { self.machineID = machineID try super.init(logger: logger, client: client) } + /// Delete node machine using fly.io machines API + /// - Throws: An error if deletion of machine failed public func delete() async throws { logDeleteNodeMachineStarted() try await getAndValidateDeleteNodeMachineResponse() @@ -61,4 +63,6 @@ internal class NodeMachineDeleter: SeleniumGridNodeAppInteractor { ] ) } + + deinit {} } diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineUpdater.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineUpdater.swift index 144a5d9..7297d4e 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineUpdater.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/NodeMachineUpdater.swift @@ -7,9 +7,9 @@ import AutomaUtilities import Vapor internal class NodeMachineUpdater: NodeMachineCreationBase { - let machineID: MachineIdentifier + internal let machineID: MachineIdentifier - init( + internal init( logger: Logger, client: any Client, seleniumGridHubBase: String, @@ -19,6 +19,8 @@ internal class NodeMachineUpdater: NodeMachineCreationBase { try super.init(logger: logger, client: client, seleniumGridHubBase: seleniumGridHubBase) } + /// Update node machnine `SE_NODE_HOST` environment variable to node machine url + /// - Throws: An error if updating machine response failed public func updateNodeHostURLEnvironmentVariable() async throws { logUpdateMachineStarted() let updatedConfig = updateMachineConfiguation() @@ -95,4 +97,6 @@ internal class NodeMachineUpdater: NodeMachineCreationBase { errorMessage: "Failed to get update node machine response for machine with ID '\(machineID)'" ) } + + deinit {} } diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift index 439e87d..58ea97e 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppInteractor.swift @@ -14,8 +14,8 @@ internal protocol SeleniumGridNodeAppInteractorBase { } internal struct SeleniumGridNodeAppInteractorPayload: Content { - let nodesAppMachineAPIURL: String - let flyAPIToken: String + internal let nodesAppMachineAPIURL: String + internal let flyAPIToken: String } internal extension SeleniumGridNodeAppInteractorBase { @@ -35,18 +35,18 @@ internal extension SeleniumGridNodeAppInteractorBase { } internal struct FlyMachinesAPIErrorHandlerPayload { - let message: String - let metadata: Logger.Metadata = [:] - let error: SeleniumGridNodeAppInteractorBase.FlyAPIError + internal let message: String + internal let metadata: Logger.Metadata = [:] + internal let error: SeleniumGridNodeAppInteractorBase.FlyAPIError } internal class SeleniumGridNodeAppInteractor: SeleniumGridNodeAppInteractorBase { - let payload: SeleniumGridNodeAppInteractorPayload - let flyAPIHTTPRequestAuthenticationHeader: [(String, String)] - let logger: Logger - let client: any Client + internal let payload: SeleniumGridNodeAppInteractorPayload + internal let flyAPIHTTPRequestAuthenticationHeader: [(String, String)] + internal let logger: Logger + internal let client: any Client - init(logger: Logger, client: any Client) throws { + internal init(logger: Logger, client: any Client) throws { self.logger = logger self.client = client @@ -74,13 +74,16 @@ internal class SeleniumGridNodeAppInteractor: SeleniumGridNodeAppInteractorBase internal func sleepBetweenCycle(config: CycleSleeper.CycleSleeperConfig) async throws { try await CycleSleeper(config, logger: logger).sleep() } + + deinit {} } internal struct NodeMachine: Content { - let id: String - let state: String - let createdAt: Date + internal let id: String + internal let state: String + internal let createdAt: Date + /// Coding keys for `NodeMachine` public enum CodingKeys: String, CodingKey { case id case state @@ -88,4 +91,4 @@ internal struct NodeMachine: Content { } } -typealias NodeMachines = [NodeMachine] +internal typealias NodeMachines = [NodeMachine] diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppNodeMachinesFinder.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppNodeMachinesFinder.swift index c72ebd5..e8758d5 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppNodeMachinesFinder.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAppNodeMachinesFinder.swift @@ -6,11 +6,14 @@ import Vapor internal struct SeleniumGridNodeAppNodeMachinesFinder: SeleniumGridNodeAppInteractorBase { - let logger: Logger - let client: any Client - var payload: SeleniumGridNodeAppInteractorPayload - var flyAPIHTTPRequestAuthenticationHeader: [(String, String)] + internal let logger: Logger + internal let client: any Client + internal var payload: SeleniumGridNodeAppInteractorPayload + internal var flyAPIHTTPRequestAuthenticationHeader: [(String, String)] + /// Get a list of all node machines + /// - Throws: An error if there was a problem getting list of all machines from fly.io machines API + /// - Returns: NodeMachines public func getListOfAllNodeMachines() async throws -> NodeMachines { return try await getAllNodeMachinesList() } diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoCreator.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoCreator.swift index fd4f14a..7bde83a 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoCreator.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoCreator.swift @@ -6,10 +6,6 @@ import AutomaUtilities import Vapor -// TODO: -// - [ ] add more logs -// - [ ] better error handling - internal protocol SeleniumGridInteractor { var client: any Client { get } var logger: Logger { get } @@ -17,12 +13,15 @@ internal protocol SeleniumGridInteractor { } internal class SeleniumGridNodeAutoCreator: SeleniumGridNodeMachineAutoscaler, SeleniumGridInteractor { - let seleniumGridHubBase: String - init(client: any Client, logger: Logger, cyclePauseDurationSeconds: Int) throws { + internal let seleniumGridHubBase: String + + internal init(client: any Client, logger: Logger, cyclePauseDurationSeconds: Int) throws { seleniumGridHubBase = try Environment.getOrThrow("SELENIUM_GRID_HUB_BASE") try super.init(logger: logger, client: client, cyclePauseDurationSeconds: cyclePauseDurationSeconds) } + /// Auto-create node machines + /// - Throws: An error if there was a problem auto-creating node machines public func autoCreateNodeMachines() async throws { try await autoCreateNodeMachinesImpl() } @@ -96,4 +95,6 @@ internal class SeleniumGridNodeAutoCreator: SeleniumGridNodeMachineAutoscaler, S private func createNodeMachine() async throws { try await NodeMachineCreator(logger: logger, client: client, seleniumGridHubBase: seleniumGridHubBase).create() } + + deinit {} } diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoDestroyer.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoDestroyer.swift index 1db21e0..208e72c 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoDestroyer.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoDestroyer.swift @@ -6,6 +6,8 @@ import Vapor internal class SeleniumGridNodeAutoDestroyer: SeleniumGridNodeMachineAutoscalerBase { + /// Auto-destroy all old node machines in Selenium Grid Node app hosted on fly.io + /// - Throws: An error if auto-destroying all old node machines failed public func autoDestroyAllOldNodeMachines() async throws { try await SeleniumGridNodeAutoOldMachineDestroyer( logger: logger, @@ -14,6 +16,8 @@ internal class SeleniumGridNodeAutoDestroyer: SeleniumGridNodeMachineAutoscalerB ).autoDestroyAllOldNodeMachines() } + /// Auto-destroy all off node machines in Selenium Grid Node app hosted on fly.io + /// - Throws: An error if auto-destroying all off node machines failed public func autoDestroyAllOffNodeMachines() async throws { try await SeleniumGridNodeAutoOffMachineDestroyer( logger: logger, @@ -21,4 +25,6 @@ internal class SeleniumGridNodeAutoDestroyer: SeleniumGridNodeMachineAutoscalerB cyclePauseDurationSeconds: cyclePauseDurationSeconds ).autoDestroyAllOffNodeMachines() } + + deinit {} } diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOffMachineDestroyer.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOffMachineDestroyer.swift index 4b355e7..af59b95 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOffMachineDestroyer.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOffMachineDestroyer.swift @@ -4,6 +4,8 @@ // All rights reserved. internal class SeleniumGridNodeAutoOffMachineDestroyer: SeleniumGridNodeMachineAutoscaler { + /// Auto-destroy off machines + /// - Throws: An error if there was a problem auto-destroying off machines public func autoDestroyAllOffNodeMachines() async throws { try await autoDestroyAllOffNodeMachinesImpl() } @@ -79,4 +81,6 @@ internal class SeleniumGridNodeAutoOffMachineDestroyer: SeleniumGridNodeMachineA cycleCount += 1 try await autoDestroyAllOffNodeMachines() } + + deinit {} } diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOldMachineDestroyer.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOldMachineDestroyer.swift index f5390e2..cdb6126 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOldMachineDestroyer.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeAutoOldMachineDestroyer.swift @@ -6,6 +6,8 @@ import Vapor internal class SeleniumGridNodeAutoOldMachineDestroyer: SeleniumGridNodeMachineAutoscaler { + /// Auto-destroy all old node machines on fly.io + /// - Throws: An error if there was a problem auto-destroying all old node machines public func autoDestroyAllOldNodeMachines() async throws { try await autoDestroyAllOldNodeMachinesImpl() } @@ -38,7 +40,7 @@ internal class SeleniumGridNodeAutoOldMachineDestroyer: SeleniumGridNodeMachineA } private func destroyAllOldNodeMachines(_ allMachines: [NodeMachine]) async throws { - if allMachines.count == 0 { + if allMachines.isEmpty { return } @@ -47,7 +49,7 @@ internal class SeleniumGridNodeAutoOldMachineDestroyer: SeleniumGridNodeMachineA return Date() > identifyAsOldAt } - if machinesToStop.count == 0 { + if machinesToStop.isEmpty { logger.info( "None of the \(allMachines.count) machines in a considered old. No machines will be destroyed.", metadata: [ @@ -69,4 +71,6 @@ internal class SeleniumGridNodeAutoOldMachineDestroyer: SeleniumGridNodeMachineA try await deleteNodeMachine(id: machine.id) } } + + deinit {} } diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeMachineAutoscaler.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeMachineAutoscaler.swift index 1c80b36..a499170 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeMachineAutoscaler.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridNodeMachineAutoscaler.swift @@ -6,17 +6,21 @@ import Vapor internal class SeleniumGridNodeMachineAutoscalerBase: SeleniumGridNodeAppInteractor { - let cyclePauseDurationSeconds: Int + internal let cyclePauseDurationSeconds: Int internal var cycleCount: Int = 1 internal init(logger: Logger, client: any Client, cyclePauseDurationSeconds: Int) throws { self.cyclePauseDurationSeconds = cyclePauseDurationSeconds try super.init(logger: logger, client: client) } + + deinit {} } internal class SeleniumGridNodeMachineAutoscaler: SeleniumGridNodeMachineAutoscalerBase { internal func deleteNodeMachine(id: String) async throws { try await NodeMachineDeleter(logger: logger, client: client, machineID: id).delete() } + + deinit {} } diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridSessionQueueRequestsHandler.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridSessionQueueRequestsHandler.swift index d164981..b2f9524 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridSessionQueueRequestsHandler.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/SeleniumGridSessionQueueRequestsHandler.swift @@ -6,10 +6,13 @@ import Vapor internal struct SeleniumGridSessionQueueRequestsHandler: SeleniumGridInteractor { - var client: any Client - var logger: Logger - var seleniumGridHubBase: String + internal let client: any Client + internal let logger: Logger + internal let seleniumGridHubBase: String + /// Get total session requests from selenium grid hub + /// - Throws: An error if there was a problem making a request to Selenium hub + /// - Returns: Total session requests public func getTotalRequests() async throws -> Int { let response = try await getGridSessionQueueReponse() return getTotalSessionQueueRequestsFromResponse(response) @@ -22,15 +25,15 @@ internal struct SeleniumGridSessionQueueRequestsHandler: SeleniumGridInteractor } internal struct SessionQueueResponse: Content { - let data: SessionQueueResponseData + internal let data: SessionQueueResponseData } internal struct SessionQueueResponseData: Content { - let sessionsInfo: SessionsInfo + internal let sessionsInfo: SessionsInfo } internal struct SessionsInfo: Content { - let sessionQueueRequests: [String] + internal let sessionQueueRequests: [String] } private func logGetGridSessionQueueResponseStarted() { @@ -59,7 +62,7 @@ internal struct SeleniumGridSessionQueueRequestsHandler: SeleniumGridInteractor } internal struct SeleniumGridGraphQLQuery: Content { - let query: String + internal let query: String } private func decodeSessionQueueResponseFromClientResponse(_ response: ClientResponse) throws diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/configure.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/configure.swift index 701313c..ea1df19 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/configure.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/configure.swift @@ -6,7 +6,7 @@ import Vapor // configures your application -public func configure(_ app: Application) async throws { +internal func configure(_ app: Application) throws { // uncomment to serve files from /Public folder // app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)) diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/entrypoint.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/entrypoint.swift index 04a3806..cad2faa 100644 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/entrypoint.swift +++ b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/entrypoint.swift @@ -9,8 +9,10 @@ import NIOPosix import Vapor @main -enum Entrypoint { - static func main() async throws { +internal enum Entrypoint { + /// The main function that initializes and runs the application. + /// - Throws: Throws an error if the application fails to start or execute. + public static func main() async throws { var env = try Environment.detect() try LoggingSystem.bootstrap(from: &env) @@ -27,7 +29,7 @@ enum Entrypoint { // metadata: ["success": .stringConvertible(executorTakeoverSuccess)]) do { - try await configure(app) + try configure(app) try await app.execute() } catch { app.logger.report(error: error) diff --git a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/routes.swift b/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/routes.swift deleted file mode 100644 index 2edcc8f..0000000 --- a/SeleniumGridNodeMachineAutoscaler/Sources/SeleniumGridNodeMachineAutoscaler/routes.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Vapor - -func routes(_ app: Application) throws { - app.get { req async in - "It works!" - } - - app.get("hello") { req async -> String in - "Hello, world!" - } -} diff --git a/SeleniumGridNodeMachineAutoscaler/Tests/SeleniumGridNodeMachineAutoscalerTests/SeleniumGridNodeMachineAutoscalerTests.swift b/SeleniumGridNodeMachineAutoscaler/Tests/SeleniumGridNodeMachineAutoscalerTests/SeleniumGridNodeMachineAutoscalerTests.swift index eb4f1ce..2d35b85 100644 --- a/SeleniumGridNodeMachineAutoscaler/Tests/SeleniumGridNodeMachineAutoscalerTests/SeleniumGridNodeMachineAutoscalerTests.swift +++ b/SeleniumGridNodeMachineAutoscaler/Tests/SeleniumGridNodeMachineAutoscalerTests/SeleniumGridNodeMachineAutoscalerTests.swift @@ -1,16 +1,22 @@ +// SeleniumGridNodeMachineAutoscalerTests.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + @testable import SeleniumGridNodeMachineAutoscaler -import VaporTesting import Testing +import VaporTesting @Suite("App Tests") -struct SeleniumGridNodeMachineAutoscalerTests { +internal struct SeleniumGridNodeMachineAutoscalerTests { + /// Example Test @Test("Test Hello World Route") - func helloWorld() async throws { + public func helloWorld() async throws { try await withApp(configure: configure) { app in - try await app.testing().test(.GET, "hello", afterResponse: { res async in + try await app.testing().test(.GET, "hello") { res async in #expect(res.status == .ok) #expect(res.body.string == "Hello, world!") - }) + } } } }