From 15d2abf7b771fb910fa1328e75d1c20040d1fdf4 Mon Sep 17 00:00:00 2001 From: William Date: Fri, 14 Nov 2025 08:58:18 +0200 Subject: [PATCH 01/16] fix(Package): update swift tools version to v6.2, temporal as backend dependency package --- Backend/DevDockerfile | 2 +- Backend/Dockerfile | 2 +- Backend/Package.resolved | 74 +++++++++++++++++++++++++++++++++++++++- Backend/Package.swift | 3 +- 4 files changed, 77 insertions(+), 4 deletions(-) diff --git a/Backend/DevDockerfile b/Backend/DevDockerfile index d5c31edf..b6ed5f50 100644 --- a/Backend/DevDockerfile +++ b/Backend/DevDockerfile @@ -1,4 +1,4 @@ -FROM swift:6.1.0-jammy AS build +FROM swift:6.2.0-jammy AS build RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ && apt-get -q update \ diff --git a/Backend/Dockerfile b/Backend/Dockerfile index a2eeb6ed..0987eec2 100644 --- a/Backend/Dockerfile +++ b/Backend/Dockerfile @@ -1,7 +1,7 @@ # ================================ # Build image # ================================ -FROM swift:6.1.0-jammy AS build +FROM swift:6.2.0-jammy AS build # Install OS updates RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ diff --git a/Backend/Package.resolved b/Backend/Package.resolved index 1f991034..1c1d0974 100644 --- a/Backend/Package.resolved +++ b/Backend/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "70949b7001f2afbc09ce48fe8e74c79342a0ebb035c9f66150debe1305d29cd3", + "originHash" : "bcfbb95bbe37661be0ba4312be4913ea89844607b2721e445cb5b308db2530d6", "pins" : [ { "identity" : "alamofire", @@ -100,6 +100,42 @@ "version" : "0.25.0" } }, + { + "identity" : "grpc-swift-2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/grpc/grpc-swift-2.git", + "state" : { + "revision" : "8bc237c565deb14cd2ffda0b23759bdcdaa244b7", + "version" : "2.2.0" + } + }, + { + "identity" : "grpc-swift-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/grpc/grpc-swift-extras.git", + "state" : { + "revision" : "f6fc3a28a896cb5e01365758c717a3d56c16a2b2", + "version" : "2.1.0" + } + }, + { + "identity" : "grpc-swift-nio-transport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/grpc/grpc-swift-nio-transport.git", + "state" : { + "revision" : "11d1f0917aa30ca50e2c78d216e8ae8c72f42af1", + "version" : "2.3.0" + } + }, + { + "identity" : "grpc-swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/grpc/grpc-swift-protobuf.git", + "state" : { + "revision" : "c008d356d9e9c2255602711888bf25b542a30919", + "version" : "2.1.1" + } + }, { "identity" : "jmespath.swift", "kind" : "remoteSourceControl", @@ -298,6 +334,15 @@ "version" : "1.3.0" } }, + { + "identity" : "swift-configuration", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-configuration.git", + "state" : { + "revision" : "9d5e40727efd060fb9b41b69932738f478abaa43", + "version" : "0.2.0" + } + }, { "identity" : "swift-crypto", "kind" : "remoteSourceControl", @@ -424,6 +469,15 @@ "version" : "1.8.3" } }, + { + "identity" : "swift-otel-semantic-conventions", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-otel/swift-otel-semantic-conventions.git", + "state" : { + "revision" : "69185e48088c4abdd438b74d332d103431849f63", + "version" : "1.38.0" + } + }, { "identity" : "swift-prometheus", "kind" : "remoteSourceControl", @@ -478,6 +532,15 @@ "version" : "1.0.0" } }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax.git", + "state" : { + "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", + "version" : "601.0.1" + } + }, { "identity" : "swift-system", "kind" : "remoteSourceControl", @@ -487,6 +550,15 @@ "version" : "1.6.3" } }, + { + "identity" : "swift-temporal-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-temporal-sdk.git", + "state" : { + "revision" : "35235775afe122a2a5e1fd3e400f02d3dc3b6d1b", + "version" : "0.4.0" + } + }, { "identity" : "swiftsoup", "kind" : "remoteSourceControl", diff --git a/Backend/Package.swift b/Backend/Package.swift index dab30892..bbd229ae 100644 --- a/Backend/Package.swift +++ b/Backend/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:6.0 +// swift-tools-version:6.2 import PackageDescription /// Initializes Package @@ -34,6 +34,7 @@ public let package = Package( .package(url: "https://github.com/GetAutomaApp/AutomaUtilities", branch: "main"), .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.11.1"), .package(url: "https://github.com/lovetodream/swift-log-loki.git", branch: "main"), + .package(url: "https://github.com/apple/swift-temporal-sdk.git", from: "0.4.0") ], targets: [ .executableTarget( From 4152db36c9bfe20cf36fe863495d6a042ac871e4 Mon Sep 17 00:00:00 2001 From: William Date: Tue, 18 Nov 2025 17:09:06 +0200 Subject: [PATCH 02/16] feat(docker-compose.yaml): added temporal services in docker-compose, along with configurations --- Backend/docker-compose.yml | 80 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/Backend/docker-compose.yml b/Backend/docker-compose.yml index 09fe063d..77f8bdda 100644 --- a/Backend/docker-compose.yml +++ b/Backend/docker-compose.yml @@ -1,6 +1,9 @@ networks: monitoring: backend: + temporal-network: + driver: bridge + name: temporal-network services: api-dev: @@ -129,6 +132,83 @@ services: - prom_data:/prometheus networks: - monitoring + elasticsearch: + container_name: temporal-elasticsearch + environment: + - cluster.routing.allocation.disk.threshold_enabled=true + - cluster.routing.allocation.disk.watermark.low=512mb + - cluster.routing.allocation.disk.watermark.high=256mb + - cluster.routing.allocation.disk.watermark.flood_stage=128mb + - discovery.type=single-node + - ES_JAVA_OPTS=-Xms256m -Xmx256m + - xpack.security.enabled=false + image: elasticsearch:${ELASTICSEARCH_VERSION} + networks: + - temporal-network + expose: + - 9200 + volumes: + - /var/lib/elasticsearch/data + postgresql: + container_name: temporal-postgresql + environment: + POSTGRES_PASSWORD: temporal + POSTGRES_USER: temporal + image: postgres:${POSTGRESQL_VERSION} + networks: + - temporal-network + expose: + - 5432 + volumes: + - /var/lib/postgresql/data + temporal: + container_name: temporal + depends_on: + - postgresql + - elasticsearch + environment: + - DB=postgres12 + - DB_PORT=5432 + - POSTGRES_USER=temporal + - POSTGRES_PWD=temporal + - POSTGRES_SEEDS=postgresql + - DYNAMIC_CONFIG_FILE_PATH=config/dynamicconfig/development-sql.yaml + - ENABLE_ES=true + - ES_SEEDS=elasticsearch + - ES_VERSION=v7 + - TEMPORAL_ADDRESS=temporal:7233 + - TEMPORAL_CLI_ADDRESS=temporal:7233 + image: temporalio/auto-setup:${TEMPORAL_VERSION} + networks: + - temporal-network + ports: + - 7233:7233 + volumes: + - ./dynamicconfig:/etc/temporal/config/dynamicconfig + temporal-admin-tools: + container_name: temporal-admin-tools + depends_on: + - temporal + environment: + - TEMPORAL_ADDRESS=temporal:7233 + - TEMPORAL_CLI_ADDRESS=temporal:7233 + image: temporalio/admin-tools:${TEMPORAL_ADMINTOOLS_VERSION} + networks: + - temporal-network + stdin_open: true + tty: true + temporal-ui: + container_name: temporal-ui + depends_on: + - temporal + environment: + - TEMPORAL_ADDRESS=temporal:7233 + - TEMPORAL_CORS_ORIGINS=http://localhost:3000 + image: temporalio/ui:${TEMPORAL_UI_VERSION} + networks: + - temporal-network + ports: + - 8080:8080 volumes: postgres_data: From 68c984a5337f5516ddff1c08ecd36faff54b52b0 Mon Sep 17 00:00:00 2001 From: William Date: Wed, 19 Nov 2025 18:45:27 +0200 Subject: [PATCH 03/16] refactor(ProfilePictureAsyncJob): refactored to temporal activity and container, updated job usage to use temporal workflow Code will not work currently. I'm going to build application, and fix all errors. --- Backend/README.md | 5 ++ .../Vapor/EnvironmentExtensions.swift | 28 -------- .../ProfilePictureAsyncJob.swift | 72 ------------------- .../TransactionalMessageAsyncJob.swift | 56 --------------- .../Activites/ProfilePictureActivities.swift | 67 +++++++++++++++++ .../TransactionalMessageActivities.swift | 53 ++++++++++++++ .../CreateProfilePictureWorkflow.swift | 18 +++++ .../SendTransactionalMessageWorkflow.swift | 0 .../UserRegistrationService.swift | 10 ++- Backend/Sources/App/configure.swift | 63 +++++++++------- ...ofilePictureAsyncJobIntegrationTests.swift | 18 ----- .../ProfilePictureAsyncJobUnitTests.swift | 18 ----- 12 files changed, 187 insertions(+), 221 deletions(-) delete mode 100644 Backend/Sources/App/Extensions/Vapor/EnvironmentExtensions.swift delete mode 100644 Backend/Sources/App/Procs/Jobs/ProfilePictureAsyncJob/ProfilePictureAsyncJob.swift delete mode 100644 Backend/Sources/App/Procs/Jobs/TransactionalMessageAsyncJob/TransactionalMessageAsyncJob.swift create mode 100644 Backend/Sources/App/Procs/Temporal/Activites/ProfilePictureActivities.swift create mode 100644 Backend/Sources/App/Procs/Temporal/Activites/TransactionalMessageActivities.swift create mode 100644 Backend/Sources/App/Procs/Temporal/Workflows/CreateProfilePictureWorkflow.swift create mode 100644 Backend/Sources/App/Procs/Temporal/Workflows/SendTransactionalMessageWorkflow.swift delete mode 100644 Backend/Tests/Procs/Jobs/ProfilePictureAsyncJobTests/ProfilePictureAsyncJobIntegrationTests.swift delete mode 100644 Backend/Tests/Procs/Jobs/ProfilePictureAsyncJobTests/ProfilePictureAsyncJobUnitTests.swift diff --git a/Backend/README.md b/Backend/README.md index 351eafa9..012a9f14 100644 --- a/Backend/README.md +++ b/Backend/README.md @@ -1,2 +1,7 @@ 1. Create `./infra/docker-secrets/` directory with the following files and secrets: 1. "GITHUB_SSH_AUTHENTICATION_TOKEN": A Github fine-grained token that allows cloning AutomaUtilities repository (private) + +TODOS: +- [ ] Fix swift protobuf package warnings (find upstream package, make a pull request to swift protobuf to support latest + swift version - fix warnings) and update upstream package `Package.swift` to use + latest protobuf version. diff --git a/Backend/Sources/App/Extensions/Vapor/EnvironmentExtensions.swift b/Backend/Sources/App/Extensions/Vapor/EnvironmentExtensions.swift deleted file mode 100644 index 6006e470..00000000 --- a/Backend/Sources/App/Extensions/Vapor/EnvironmentExtensions.swift +++ /dev/null @@ -1,28 +0,0 @@ -// EnvironmentExtensions.swift -// Copyright (c) 2025 GetAutomaApp -// All source code and related assets are the property of GetAutomaApp. -// All rights reserved. - -import Vapor - -internal extension Environment { - enum AppMode { - case http - case queue(name: String) - } - - var appMode: AppMode { - if - CommandLine.arguments.contains("queues") || - CommandLine.arguments.contains("vapor-queues") - { - guard let name = CommandLine.arguments.firstIndex(of: "--queue") else { - return .queue(name: "default") - } - - return .queue(name: CommandLine.arguments[name + 1]) - } else { - return .http - } - } -} diff --git a/Backend/Sources/App/Procs/Jobs/ProfilePictureAsyncJob/ProfilePictureAsyncJob.swift b/Backend/Sources/App/Procs/Jobs/ProfilePictureAsyncJob/ProfilePictureAsyncJob.swift deleted file mode 100644 index 83e0fea4..00000000 --- a/Backend/Sources/App/Procs/Jobs/ProfilePictureAsyncJob/ProfilePictureAsyncJob.swift +++ /dev/null @@ -1,72 +0,0 @@ -// ProfilePictureAsyncJob.swift -// Copyright (c) 2025 GetAutomaApp -// All source code and related assets are the property of GetAutomaApp. -// All rights reserved. - -import DataTypes -import Foundation -import Queues -import Vapor - -/// Input payload for the profile picture job. -internal struct ProfilePictureJobInput: Codable { - /// The user data transfer object. - public let payload: UserDTO -} - -/// Asynchronous job to create a profile picture. -internal struct ProfilePictureAsyncJob: AsyncJob { - public typealias Payload = ProfilePictureJobInput - - /// Processes the job to create a profile picture. - /// - Parameters: - /// - context: The queue context. - /// - payload: The input payload containing user data. - /// - Throws: Throws an error if the profile picture creation fails. - public func dequeue(_ context: QueueContext, _ payload: ProfilePictureJobInput) async throws { - let logger = context.logger - let profilePictureService = ProfilePictureService(logger: context.logger) - - // Create a profile picture for the user - let profilePictureKey = try await profilePictureService.createProfilePicture( - for: payload.payload - ) - - // Log the successful creation of the profile picture - logger.info( - "Profile picture created", - metadata: [ - "to": .string("ProfilePictureAsyncJob.dequeue"), - "userId": .string(payload.payload.id?.uuidString ?? ""), - "username": .string(payload.payload.username), - "profilePictureKey": .string(profilePictureKey), - ] - ) - } - - /// Handles errors that occur during the job processing. - /// - Parameters: - /// - context: The queue context. - /// - error: The error that occurred. - /// - payload: The input payload containing user data. - /// - Throws: Throws an error if logging fails. - public func error(_ context: QueueContext, _ error: any Error, _ payload: ProfilePictureJobInput) throws { - let logger = context.logger - - // Capture the current stack trace - let stackTrace = Thread.callStackSymbols.joined(separator: "\n") - - // Log the error details - logger.error( - "Failed to create profile picture", - metadata: [ - "to": .string("ProfilePictureAsyncJob.error"), - "userId": .string(payload.payload.id?.uuidString ?? ""), - "username": .string(payload.payload.username), - "error": .string(error.localizedDescription), - "debugInfo": .string("\(error)"), - "stackTrace": .string(stackTrace), - ] - ) - } -} diff --git a/Backend/Sources/App/Procs/Jobs/TransactionalMessageAsyncJob/TransactionalMessageAsyncJob.swift b/Backend/Sources/App/Procs/Jobs/TransactionalMessageAsyncJob/TransactionalMessageAsyncJob.swift deleted file mode 100644 index 598ea6e9..00000000 --- a/Backend/Sources/App/Procs/Jobs/TransactionalMessageAsyncJob/TransactionalMessageAsyncJob.swift +++ /dev/null @@ -1,56 +0,0 @@ -// TransactionalMessageAsyncJob.swift -// Copyright (c) 2025 GetAutomaApp -// All source code and related assets are the property of GetAutomaApp. -// All rights reserved. - -import Foundation -import Queues -import Vapor - -/// Input payload for the transactional message job. -internal struct TransactionalMessageJobInput: Codable { - /// The content of the message. - public let content: String - /// The phone number to send the message to. - public let toPhoneNumber: String -} - -/// Asynchronous job to send a transactional message. -internal struct TransactionalMessageAsyncJob: AsyncJob { - public typealias Payload = TransactionalMessageJobInput - - /// Processes the job to send a transactional message. - /// - Parameters: - /// - context: The queue context. - /// - payload: The input payload containing message data. - /// - Throws: Throws an error if the message sending fails. - public func dequeue(_ context: QueueContext, _ payload: TransactionalMessageJobInput) async throws { - let snsService = SNSService() - - // Send the SMS message - _ = try await snsService - .sendSmS( - to: payload.toPhoneNumber, - message: payload.content, - logger: context.logger - ) - } - - /// Handles errors that occur during the job processing. - /// - Parameters: - /// - context: The queue context. - /// - error: The error that occurred. - /// - payload: The input payload containing message data. - /// - Throws: Throws an error if logging fails. - public func error(_ context: QueueContext, _ error: Error, _ payload: TransactionalMessageJobInput) throws { - // Log the error details - context.logger.info( - "Error occurred while processing job", - metadata: [ - "to": .string("TransactionalMessageAsyncJob.dequeue"), - "error": .string(error.localizedDescription), - "payload": .string("\(payload.content)"), - ] - ) - } -} diff --git a/Backend/Sources/App/Procs/Temporal/Activites/ProfilePictureActivities.swift b/Backend/Sources/App/Procs/Temporal/Activites/ProfilePictureActivities.swift new file mode 100644 index 00000000..4480032b --- /dev/null +++ b/Backend/Sources/App/Procs/Temporal/Activites/ProfilePictureActivities.swift @@ -0,0 +1,67 @@ +// ProfilePictureActivities.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +import DataTypes +import Foundation +import Temporal +import Vapor + +internal struct CreateProfilePictureActivityInput { + let logger: Logger + let payload: CreateProfilePictureActivityPayload +} + +/// Input payload for the create profile picture activity. +internal struct CreateProfilePictureActivityPayload: Codable { + /// The user data transfer object. + public let payload: UserDTO +} + +/// Temporal activity for user profile picture creation. +@ActivityContainer +internal struct ProfilePictureActivities { + /// Create + /// - Parameters: + /// - payload: The input payload containing user data. + /// - Throws: Throws an error if the profile picture creation fails. + @Activity + public func createPicture(input: CreateProfilePictureActivityInput) async throws { + let innerPayload = input.payload.payload + let logger = input.logger + let profilePictureService = ProfilePictureService(logger: logger) + + do { + // Create a profile picture for the user + let profilePictureKey = try await profilePictureService.createProfilePicture( + for: innerPayload + ) + // Log the successful creation of the profile picture + logger.info( + "Profile picture created", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + "userId": .string(innerPayload.id?.uuidString ?? ""), + "username": .string(innerPayload.username), + "profilePictureKey": .string(profilePictureKey), + ] + ) + } catch { + // Capture the current stack trace + let stackTrace = Thread.callStackSymbols.joined(separator: "\n") + + // Log the error details + logger.error( + "Failed to create profile picture", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + "userId": .string(innerPayload.id?.uuidString ?? ""), + "username": .string(innerPayload.username), + "error": .string(error.localizedDescription), + "stackTrace": .string(stackTrace), + ] + ) + } + } +} diff --git a/Backend/Sources/App/Procs/Temporal/Activites/TransactionalMessageActivities.swift b/Backend/Sources/App/Procs/Temporal/Activites/TransactionalMessageActivities.swift new file mode 100644 index 00000000..8a9b576e --- /dev/null +++ b/Backend/Sources/App/Procs/Temporal/Activites/TransactionalMessageActivities.swift @@ -0,0 +1,53 @@ +// TransactionalMessageActivities.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +import Foundation +import Temporal +import Vapor + +/// Input payload for send transactional message activity. +internal struct SendTransactionalMessageActivityInput: Codable { + /// The content of the message. + public let content: String + /// The phone number to send the message to. + public let toPhoneNumber: String +} + +/// Activites for sending a transactional message. +@ActivitiesContainer +internal struct TransactionalMessageActivities { + /// Activity for sending a transactional message. + /// - Parameters: + /// - payload: The input payload containing message data. + /// - Throws: Throws an error if the message sending fails. + @Activity + public func sendMessage(app: any Application, payload: SendTransactionalMessageActivityInput) async throws { + let logger = app.logger + let snsService = SNSService() + + // Send the SMS message + do { + _ = try await snsService + .sendSmS( + to: payload.toPhoneNumber, + message: payload.content, + logger: logger + ) + } catch { + // Capture the current stack trace + let stackTrace = Thread.callStackSymbols.joined(separator: "\n") + + logger.info( + "Error occurred while processing activity", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + "error": .string(error.localizedDescription), + "payload": .string("\(payload.content)"), + "stackTrace": .string(stackTrace), + ] + ) + } + } +} diff --git a/Backend/Sources/App/Procs/Temporal/Workflows/CreateProfilePictureWorkflow.swift b/Backend/Sources/App/Procs/Temporal/Workflows/CreateProfilePictureWorkflow.swift new file mode 100644 index 00000000..1ea25161 --- /dev/null +++ b/Backend/Sources/App/Procs/Temporal/Workflows/CreateProfilePictureWorkflow.swift @@ -0,0 +1,18 @@ +// CreateProfilePictureWorkflow.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +import Temporal +import Vapor + +@Workflow +internal final class CreateProfilePictureWorkflow { + func run(input: CreateProfilePictureActivityInput) async throws { + try await Workflow.executeActivity( + ProfilePictureActivities.Activities.CreatePicture.self, + options: ActivityOptions(startToCloseTimeout: .seconds(30)), + input: input + ) + } +} diff --git a/Backend/Sources/App/Procs/Temporal/Workflows/SendTransactionalMessageWorkflow.swift b/Backend/Sources/App/Procs/Temporal/Workflows/SendTransactionalMessageWorkflow.swift new file mode 100644 index 00000000..e69de29b diff --git a/Backend/Sources/App/Services/AuthenticationService/UserRegistrationService.swift b/Backend/Sources/App/Services/AuthenticationService/UserRegistrationService.swift index e0a44ece..a718bc78 100644 --- a/Backend/Sources/App/Services/AuthenticationService/UserRegistrationService.swift +++ b/Backend/Sources/App/Services/AuthenticationService/UserRegistrationService.swift @@ -18,8 +18,9 @@ internal struct UserRegistrationService: AuthenticationService { private var user: UserModel /// Initializes User Registration Service - public init(_ config: UserRegistrationConfig) { + public init(_ config: UserRegistrationConfig, temporalClient: TemporalClient) { self.config = config + self.temporalClient = temporalClient helper = .init(.init(writeDb: config.writeDb, readDb: config.readDb, logger: config.logger)) identifier = Self.generateNewUserIdentifier() user = Self.createUserModel(identifier: identifier, config: config) @@ -70,7 +71,12 @@ internal struct UserRegistrationService: AuthenticationService { let userDTO = try user.toDTO(logger: config.logger) let profilePictureKey = try ProfilePictureService(logger: config.logger).generateImageKey(for: userDTO) - try await config.queue.dispatch(ProfilePictureAsyncJob.self, .init(payload: userDTO)) + Task { + try await temporalClient.executeWorkflow( + type: CreateProfilePictureWorkflow.self, + input: .init(logger: logger, payload: userDTO) + ) + } user.profilePictureKey = profilePictureKey diff --git a/Backend/Sources/App/configure.swift b/Backend/Sources/App/configure.swift index 3a6818f1..7cfb8824 100644 --- a/Backend/Sources/App/configure.swift +++ b/Backend/Sources/App/configure.swift @@ -7,8 +7,7 @@ import AutomaUtilities import Fluent import FluentPostgresDriver import JWT -import Queues -import QueuesFluentDriver +import Temporal import Vapor internal func configure(_ app: Application) async throws { @@ -29,7 +28,6 @@ internal struct AppConfigurator { /// Configures the entire application public func configure() async throws { registerMiddleware() - registerQueues() let hasDatabaseURLs = (primaryDatabaseURL != nil) || (regionalDatabaseURL != nil) if hasDatabaseURLs { try await configureWhenDatabaseURLsAvailable() @@ -40,19 +38,12 @@ internal struct AppConfigurator { app.middleware.use(ErrorStringMiddleware()) } - private func registerQueues() { - if ["local", "testing"].contains(environment) == false { - app.asyncCommands.use(QueuesCommand(application: app), as: "vapor-queues") - } - } - private func configureWhenDatabaseURLsAvailable() async throws { try await DatabaseConfigurator(app: app).configureDatabases() try registerControllers() try await startPrometheusService() + try await setupTemporal() try await addAuthenticationJWTKey() - configureQueues() - addJobsToQueue() configureServer() } @@ -68,13 +59,7 @@ internal struct AppConfigurator { try await PrometheusService().startServer() return } - - switch app.environment.appMode { - case .queue: - try await PrometheusService().startServer(port: 6_835) - default: - try await PrometheusService().startServer() - } + try await PrometheusService().startServer() } private func addAuthenticationJWTKey() async throws { @@ -82,15 +67,28 @@ internal struct AppConfigurator { await app.jwt.keys.add(hmac: .init(stringLiteral: encryptionSecret), digestAlgorithm: .sha256) } - private func configureQueues() { - app.queues.use(.fluent(useSoftDeletes: true)) - app.queues.configuration.workerCount = 1 - app.queues.configuration.refreshInterval = .seconds(5) - } - - private func addJobsToQueue() { - app.queues.add(TransactionalMessageAsyncJob()) - app.queues.add(ProfilePictureAsyncJob()) + private func setupTemporal() async throws { + let worker = try TemporalWorker( + configuration: .init( + namespace: "default", + taskQueue: "default-queue", + instrumentation: .init(serverHostname: "127.0.0.1") + ), + target: .ipv4(address: "127.0.0.1", port: 7_233), + transportSecurity: .plaintext, + activityContainers: GreetingActivities(), + workflows: [GreetingWorkflow.self], + logger: Logger(label: "temporal-worker") + ) + try await withThrowingTaskGroup { group in + group.addTask { + try await worker.run() + } + + group.addTask { + try await app.temporalClient.run() + } + } } private func configureServer() { @@ -221,3 +219,14 @@ internal enum DatabaseURLs { /// regional url most likely doesn't have write access, but allows for extremely fast reads public static let regional: Result = Result { try Environment.getOrThrow("REGIONAL_POSTGRES_URL") } } + +public extension Application { + let temporalClient: TemporalClient { + try TemporalClient( + target: .ipv4(address: "127.0.0.1", port: 7_233), + transportSecurity: .plaintext, + configuration: .init(instrumentation: .init(serverHostname: "127.0.0.1")), + logger: Logger(label: "temporal-client") + ) + } +} diff --git a/Backend/Tests/Procs/Jobs/ProfilePictureAsyncJobTests/ProfilePictureAsyncJobIntegrationTests.swift b/Backend/Tests/Procs/Jobs/ProfilePictureAsyncJobTests/ProfilePictureAsyncJobIntegrationTests.swift deleted file mode 100644 index a52a8864..00000000 --- a/Backend/Tests/Procs/Jobs/ProfilePictureAsyncJobTests/ProfilePictureAsyncJobIntegrationTests.swift +++ /dev/null @@ -1,18 +0,0 @@ -// ProfilePictureAsyncJobIntegrationTests.swift -// Copyright (c) 2025 GetAutomaApp -// All source code and related assets are the property of GetAutomaApp. -// All rights reserved. - -import Testing - -/// Integration tests for the `ProfilePictureAsyncJob`. -/// These tests verify the job's ability to process profile picture generation tasks. -@Suite("ProfilePictureAsyncJobIntegrationTests") -internal struct ProfilePictureAsyncJobIntegrationTests { - /// Placeholder test for the profile picture async job. - /// This test will be implemented with specific scenarios for the job. - @Test("test name here") - public func nameHere() { - // Placeholder for test implementation - } -} diff --git a/Backend/Tests/Procs/Jobs/ProfilePictureAsyncJobTests/ProfilePictureAsyncJobUnitTests.swift b/Backend/Tests/Procs/Jobs/ProfilePictureAsyncJobTests/ProfilePictureAsyncJobUnitTests.swift deleted file mode 100644 index e54cbe28..00000000 --- a/Backend/Tests/Procs/Jobs/ProfilePictureAsyncJobTests/ProfilePictureAsyncJobUnitTests.swift +++ /dev/null @@ -1,18 +0,0 @@ -// ProfilePictureAsyncJobUnitTests.swift -// Copyright (c) 2025 GetAutomaApp -// All source code and related assets are the property of GetAutomaApp. -// All rights reserved. - -import Testing - -/// Unit tests for the `ProfilePictureAsyncJob`. -/// These tests verify the job's internal logic and functionality. -@Suite("ProfilePictureAsyncJobUnitTests") -internal struct ProfilePictureAsyncJobUnitTests { - /// Placeholder test for the profile picture async job. - /// This test will be implemented with specific unit test scenarios. - @Test("Test Name") - public func test() { - // Placeholder for test implementation - } -} From 57cb66425fada952696ec42ac58f712bc6924f3f Mon Sep 17 00:00:00 2001 From: William Date: Sat, 22 Nov 2025 06:03:11 +0200 Subject: [PATCH 04/16] refactor: in process of fixing all errors when refactoring queues to temporal activities & workflows Currently fixing sendable errors --- Backend/Package.resolved | 20 +---------- Backend/Package.swift | 6 +--- .../AuthenticationController.swift | 10 ++++-- .../Activites/ProfilePictureActivities.swift | 27 ++++++-------- .../TransactionalMessageActivities.swift | 16 ++++----- .../SendTransactionalMessageWorkflow.swift | 18 ++++++++++ .../AuthenticationService.swift | 20 ++++++----- .../AuthenticationServiceHelper.swift | 36 +++++++++++-------- .../UserRegistrationService.swift | 14 ++++---- Backend/Sources/App/configure.swift | 20 +++++++---- Backend/docker-compose.yml | 22 ------------ 11 files changed, 98 insertions(+), 111 deletions(-) diff --git a/Backend/Package.resolved b/Backend/Package.resolved index 1c1d0974..31c571db 100644 --- a/Backend/Package.resolved +++ b/Backend/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "bcfbb95bbe37661be0ba4312be4913ea89844607b2721e445cb5b308db2530d6", + "originHash" : "25741725534bf349d85342c3c4f09746825ae3902642ee651a2b1cc0ac192cd4", "pins" : [ { "identity" : "alamofire", @@ -235,15 +235,6 @@ "version" : "1.29.0" } }, - { - "identity" : "queues", - "kind" : "remoteSourceControl", - "location" : "https://github.com/vapor/queues.git", - "state" : { - "revision" : "f1764d5fef7e8ede8f3585deb63ece01ea25c2db", - "version" : "1.17.2" - } - }, { "identity" : "routing-kit", "kind" : "remoteSourceControl", @@ -586,15 +577,6 @@ "version" : "4.117.0" } }, - { - "identity" : "vapor-queues-fluent-driver", - "kind" : "remoteSourceControl", - "location" : "https://github.com/m-barthelemy/vapor-queues-fluent-driver.git", - "state" : { - "revision" : "e2ce6775850bdbe277cb2e5792d05eff42434f52", - "version" : "3.0.0-beta1" - } - }, { "identity" : "websocket-kit", "kind" : "remoteSourceControl", diff --git a/Backend/Package.swift b/Backend/Package.swift index bbd229ae..2ca1c9ea 100644 --- a/Backend/Package.swift +++ b/Backend/Package.swift @@ -24,10 +24,6 @@ public let package = Package( .package(url: "https://github.com/MacPaw/OpenAI.git", from: "0.4.5"), .package(url: "https://github.com/soto-project/soto.git", from: "7.3.0"), .package(url: "https://github.com/swift-server/swift-prometheus.git", from: "2.0.0"), - .package( - url: "https://github.com/m-barthelemy/vapor-queues-fluent-driver.git", - from: "3.0.0-beta1" - ), .package(url: "https://github.com/nmdias/FeedKit.git", from: "10.0.0-rc.3"), .package(url: "https://github.com/GetAutomaApp/swift-retry.git", branch: "main"), .package(url: "https://github.com/GetAutomaApp/Fakery", branch: "master"), @@ -53,13 +49,13 @@ public let package = Package( .product(name: "SotoS3", package: "soto"), .product(name: "SotoSNS", package: "soto"), .product(name: "Prometheus", package: "swift-prometheus"), - .product(name: "QueuesFluentDriver", package: "vapor-queues-fluent-driver"), .product(name: "SotoTextract", package: "soto"), .product(name: "FeedKit", package: "FeedKit"), .product(name: "DMRetry", package: "swift-retry"), .product(name: "AutomaUtilities", package: "AutomaUtilities"), "SwiftSoup", .product(name: "LoggingLoki", package: "swift-log-loki"), + .product(name: "Temporal", package: "swift-temporal-sdk"), ], exclude: [ "Documentation.md", diff --git a/Backend/Sources/App/Controllers/AuthenticationController/AuthenticationController.swift b/Backend/Sources/App/Controllers/AuthenticationController/AuthenticationController.swift index 0e351da2..003e253a 100644 --- a/Backend/Sources/App/Controllers/AuthenticationController/AuthenticationController.swift +++ b/Backend/Sources/App/Controllers/AuthenticationController/AuthenticationController.swift @@ -5,6 +5,7 @@ import DataTypes import Fluent +import Temporal import Vapor /// Controller for handling authentication-related routes. @@ -48,7 +49,7 @@ internal struct AuthenticationController: RouteCollection { } return try await authService.sendAuthCode( - phoneNumber: dto.phoneNumber, queue: req.queue + phoneNumber: dto.phoneNumber, temporalClient: req.temporalClient ) } @@ -69,7 +70,10 @@ internal struct AuthenticationController: RouteCollection { throw GenericErrors.userAlreadyExists } - return try await authService.register(.init(authCodePayload: dto, signer: req.jwt), queue: req.queue) + return try await authService.register( + .init(authCodePayload: dto, signer: req.jwt), + temporalClient: req.temporalClient + ) } /// Sends a login code to the user's phone number. @@ -91,7 +95,7 @@ internal struct AuthenticationController: RouteCollection { } return try await authService.sendAuthCode( - phoneNumber: dto.phoneNumber, queue: req.queue + phoneNumber: dto.phoneNumber, temporalClient: req.temporalClient ) } diff --git a/Backend/Sources/App/Procs/Temporal/Activites/ProfilePictureActivities.swift b/Backend/Sources/App/Procs/Temporal/Activites/ProfilePictureActivities.swift index 4480032b..e7c7a2cb 100644 --- a/Backend/Sources/App/Procs/Temporal/Activites/ProfilePictureActivities.swift +++ b/Backend/Sources/App/Procs/Temporal/Activites/ProfilePictureActivities.swift @@ -8,15 +8,8 @@ import Foundation import Temporal import Vapor -internal struct CreateProfilePictureActivityInput { - let logger: Logger - let payload: CreateProfilePictureActivityPayload -} - -/// Input payload for the create profile picture activity. -internal struct CreateProfilePictureActivityPayload: Codable { - /// The user data transfer object. - public let payload: UserDTO +internal struct CreateProfilePictureActivityInput: Codable { + let payload: UserDTO } /// Temporal activity for user profile picture creation. @@ -24,26 +17,26 @@ internal struct CreateProfilePictureActivityPayload: Codable { internal struct ProfilePictureActivities { /// Create /// - Parameters: - /// - payload: The input payload containing user data. + /// - input: The input for creating a profile picture. /// - Throws: Throws an error if the profile picture creation fails. @Activity public func createPicture(input: CreateProfilePictureActivityInput) async throws { - let innerPayload = input.payload.payload - let logger = input.logger + let payload = input.payload + let logger = Logger("temporal") let profilePictureService = ProfilePictureService(logger: logger) do { // Create a profile picture for the user let profilePictureKey = try await profilePictureService.createProfilePicture( - for: innerPayload + for: payload ) // Log the successful creation of the profile picture logger.info( "Profile picture created", metadata: [ "to": .string("\(String(describing: Self.self)).\(#function)"), - "userId": .string(innerPayload.id?.uuidString ?? ""), - "username": .string(innerPayload.username), + "userId": .string(payload.id?.uuidString ?? ""), + "username": .string(payload.username), "profilePictureKey": .string(profilePictureKey), ] ) @@ -56,8 +49,8 @@ internal struct ProfilePictureActivities { "Failed to create profile picture", metadata: [ "to": .string("\(String(describing: Self.self)).\(#function)"), - "userId": .string(innerPayload.id?.uuidString ?? ""), - "username": .string(innerPayload.username), + "userId": .string(payload.id?.uuidString ?? ""), + "username": .string(payload.username), "error": .string(error.localizedDescription), "stackTrace": .string(stackTrace), ] diff --git a/Backend/Sources/App/Procs/Temporal/Activites/TransactionalMessageActivities.swift b/Backend/Sources/App/Procs/Temporal/Activites/TransactionalMessageActivities.swift index 8a9b576e..779b5861 100644 --- a/Backend/Sources/App/Procs/Temporal/Activites/TransactionalMessageActivities.swift +++ b/Backend/Sources/App/Procs/Temporal/Activites/TransactionalMessageActivities.swift @@ -8,7 +8,7 @@ import Temporal import Vapor /// Input payload for send transactional message activity. -internal struct SendTransactionalMessageActivityInput: Codable { +internal struct SendTransactionalMessageActivityInput { /// The content of the message. public let content: String /// The phone number to send the message to. @@ -16,23 +16,23 @@ internal struct SendTransactionalMessageActivityInput: Codable { } /// Activites for sending a transactional message. -@ActivitiesContainer +@ActivityContainer internal struct TransactionalMessageActivities { /// Activity for sending a transactional message. /// - Parameters: - /// - payload: The input payload containing message data. + /// - input: The input for sending a transactional message. /// - Throws: Throws an error if the message sending fails. @Activity - public func sendMessage(app: any Application, payload: SendTransactionalMessageActivityInput) async throws { - let logger = app.logger + public func sendMessage(input: SendTransactionalMessageActivityInput) async throws { + let logger = Logger("temporal") let snsService = SNSService() // Send the SMS message do { _ = try await snsService .sendSmS( - to: payload.toPhoneNumber, - message: payload.content, + to: input.toPhoneNumber, + message: input.content, logger: logger ) } catch { @@ -44,7 +44,7 @@ internal struct TransactionalMessageActivities { metadata: [ "to": .string("\(String(describing: Self.self)).\(#function)"), "error": .string(error.localizedDescription), - "payload": .string("\(payload.content)"), + "input": .string("\(input.content)"), "stackTrace": .string(stackTrace), ] ) diff --git a/Backend/Sources/App/Procs/Temporal/Workflows/SendTransactionalMessageWorkflow.swift b/Backend/Sources/App/Procs/Temporal/Workflows/SendTransactionalMessageWorkflow.swift index e69de29b..7247a889 100644 --- a/Backend/Sources/App/Procs/Temporal/Workflows/SendTransactionalMessageWorkflow.swift +++ b/Backend/Sources/App/Procs/Temporal/Workflows/SendTransactionalMessageWorkflow.swift @@ -0,0 +1,18 @@ +// SendTransactionalMessageWorkflow.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +import Temporal +import Vapor + +@Workflow +internal final class SendTransactionalMessageWorkflow { + func run(input: SendTransactionalMessageActivityInput) async throws { + try await Workflow.executeActivity( + TransactionalMessageActivities.Activities.SendMessage.self, + options: ActivityOptions(startToCloseTimeout: .seconds(30)), + input: input + ) + } +} diff --git a/Backend/Sources/App/Services/AuthenticationService/AuthenticationService.swift b/Backend/Sources/App/Services/AuthenticationService/AuthenticationService.swift index 0cd29453..13c16528 100644 --- a/Backend/Sources/App/Services/AuthenticationService/AuthenticationService.swift +++ b/Backend/Sources/App/Services/AuthenticationService/AuthenticationService.swift @@ -7,7 +7,7 @@ import AutomaUtilities import DataTypes import Fluent import JWT -import Queues +import Temporal import Vapor internal struct RootAuthenticationService: AuthenticationService { @@ -24,13 +24,13 @@ internal struct RootAuthenticationService: AuthenticationService { /// Register a user public func register( _ payload: UserRegistrationPayload, - queue: Queue + temporalClient: TemporalClient ) async throws -> AuthenticationTokensPayloadDTO { var registrator = UserRegistrationService(.init( writeDb: config.writeDb, readDb: config.readDb, logger: config.logger, - queue: queue, + temporalClient: temporalClient, payload: payload )) return try await registrator.register() @@ -48,7 +48,9 @@ internal struct RootAuthenticationService: AuthenticationService { } /// Send authentication code - public func sendAuthCode(phoneNumber: String, queue: Queue) async throws -> AuthenticationCodeResponseDTO { + public func sendAuthCode(phoneNumber: String, + temporalClient: TemporalClient) async throws -> AuthenticationCodeResponseDTO + { let code = RandomService.randomCode() logAuthCodeAttempt(phoneNumber: phoneNumber, code: code) @@ -60,9 +62,9 @@ internal struct RootAuthenticationService: AuthenticationService { return try await sendOrHandleAuthCode( phoneNumber: phoneNumber, - queue: queue, code: code, - codeModelId: codeModelId + codeModelId: codeModelId, + temporalClient: temporalClient ) } @@ -145,9 +147,9 @@ internal struct RootAuthenticationService: AuthenticationService { private func sendOrHandleAuthCode( phoneNumber: String, - queue: Queue, code: String, - codeModelId: UUID + codeModelId: UUID, + temporalClient: TemporalClient, ) async throws -> AuthenticationCodeResponseDTO { do { return try await helper.sendAuthCode( @@ -156,7 +158,7 @@ internal struct RootAuthenticationService: AuthenticationService { phoneNumber: phoneNumber, codeModelId: codeModelId ), - queue: queue + temporalClient: temporalClient ) } catch let error as GenericErrors { await helper.sendTelemetryDataOnAuthCodeSendFail( diff --git a/Backend/Sources/App/Services/AuthenticationService/AuthenticationServiceHelper.swift b/Backend/Sources/App/Services/AuthenticationService/AuthenticationServiceHelper.swift index 571a9338..0357c1d3 100644 --- a/Backend/Sources/App/Services/AuthenticationService/AuthenticationServiceHelper.swift +++ b/Backend/Sources/App/Services/AuthenticationService/AuthenticationServiceHelper.swift @@ -7,7 +7,7 @@ import AutomaUtilities import DataTypes import Fluent import JWT -import Queues +import Temporal import Vapor internal actor AuthenticationServiceHelper { @@ -70,13 +70,14 @@ internal actor AuthenticationServiceHelper { } /// Sends a verification code to the given phone number and returns the result. - public func sendAuthCode(_ payload: SendAuthCodePayload, queue: Queue) async throws -> AuthenticationCodeResponseDTO + public func sendAuthCode(_ payload: SendAuthCodePayload, + temporalClient: TemporalClient) async throws -> AuthenticationCodeResponseDTO { try await AuthCodeSender(.init( writeDb: config.writeDb, readDb: config.writeDb, logger: config.logger, - queue: queue, + temporalClient: temporalClient, payload: payload )).send() } @@ -570,16 +571,23 @@ internal struct AuthCodeSender { } private func startSendCodeJob() async throws { - try await config.queue.dispatch( - TransactionalMessageAsyncJob.self, - .init( - content: MessageFormatterService - .craftVerificationCodeMessage( - code: config.payload.code - ), - toPhoneNumber: config.payload.phoneNumber + Task { + try await config.temporalClient.executeWorkflow( + type: SendTransactionalMessageWorkflow.self, + options: .init( + id: "send-transactional-message-\(config.payload.codeModelId)", + taskQueue: "default-queue" + ), + input: .init( + content: MessageFormatterService + .craftVerificationCodeMessage( + code: config.payload.code + ), + toPhoneNumber: config.payload.phoneNumber, + logger: config.logger + ) ) - ) + } } private func getCodeDeletionTime() -> Date { @@ -594,8 +602,8 @@ internal struct AuthCodeSenderConfig: AuthenticationServiceConfig { public let readDb: Database /// Logger public let logger: Logger - /// Queue to submit token to - public let queue: Queue + // Temporal client to execute workflows + public let temporalClient: TemporalClient /// Payload to submit public let payload: SendAuthCodePayload } diff --git a/Backend/Sources/App/Services/AuthenticationService/UserRegistrationService.swift b/Backend/Sources/App/Services/AuthenticationService/UserRegistrationService.swift index a718bc78..17aa2f67 100644 --- a/Backend/Sources/App/Services/AuthenticationService/UserRegistrationService.swift +++ b/Backend/Sources/App/Services/AuthenticationService/UserRegistrationService.swift @@ -7,7 +7,7 @@ import AutomaUtilities import DataTypes import Fluent import JWT -import Queues +import Temporal import Vapor internal struct UserRegistrationService: AuthenticationService { @@ -18,9 +18,8 @@ internal struct UserRegistrationService: AuthenticationService { private var user: UserModel /// Initializes User Registration Service - public init(_ config: UserRegistrationConfig, temporalClient: TemporalClient) { + public init(_ config: UserRegistrationConfig) { self.config = config - self.temporalClient = temporalClient helper = .init(.init(writeDb: config.writeDb, readDb: config.readDb, logger: config.logger)) identifier = Self.generateNewUserIdentifier() user = Self.createUserModel(identifier: identifier, config: config) @@ -72,9 +71,10 @@ internal struct UserRegistrationService: AuthenticationService { let profilePictureKey = try ProfilePictureService(logger: config.logger).generateImageKey(for: userDTO) Task { - try await temporalClient.executeWorkflow( + try await config.temporalClient.executeWorkflow( type: CreateProfilePictureWorkflow.self, - input: .init(logger: logger, payload: userDTO) + options: .init(id: "create-user-profile-pic-\(userDTO.id)-\(Date())", taskQueue: "default-queue"), + input: .init(logger: config.logger, payload: userDTO) ) } @@ -105,8 +105,8 @@ internal struct UserRegistrationConfig: AuthenticationServiceConfig { public let readDb: Database /// Logger public let logger: Logger - /// Queue to submit messages & generation stuff to - public let queue: Queue + // Temporal client to execute workflows + public let temporalClient: TemporalClient /// Payload to register user public let payload: UserRegistrationPayload } diff --git a/Backend/Sources/App/configure.swift b/Backend/Sources/App/configure.swift index 7cfb8824..196583a8 100644 --- a/Backend/Sources/App/configure.swift +++ b/Backend/Sources/App/configure.swift @@ -76,11 +76,11 @@ internal struct AppConfigurator { ), target: .ipv4(address: "127.0.0.1", port: 7_233), transportSecurity: .plaintext, - activityContainers: GreetingActivities(), - workflows: [GreetingWorkflow.self], + activityContainers: ProfilePictureActivities(), + workflows: [CreateProfilePictureWorkflow.self], logger: Logger(label: "temporal-worker") ) - try await withThrowingTaskGroup { group in + await withThrowingTaskGroup { group in group.addTask { try await worker.run() } @@ -131,7 +131,6 @@ internal struct DatabaseConfigurator { app.migrations.add(JWTTokenShouldBeBoundToParentUserObjectMigration1735140054()) app.migrations.add(UserProfileAddProfilePictureMigration1735216565()) app.migrations.add(UserProfileConvertIdToImageKeyMigration1735294202()) - app.migrations.add(JobMetadataMigrate()) app.migrations.add(RemoveUserStorageMigration1739456565()) app.migrations.add(AddAcceptedColumnMigration1740658649()) app.migrations.add(TwitterOAuthTokenMigration1741687313()) @@ -220,9 +219,10 @@ internal enum DatabaseURLs { public static let regional: Result = Result { try Environment.getOrThrow("REGIONAL_POSTGRES_URL") } } -public extension Application { - let temporalClient: TemporalClient { - try TemporalClient( +// TODO: refactor extensions to extensions directory +internal extension Application { + var temporalClient: TemporalClient { + try! TemporalClient( target: .ipv4(address: "127.0.0.1", port: 7_233), transportSecurity: .plaintext, configuration: .init(instrumentation: .init(serverHostname: "127.0.0.1")), @@ -230,3 +230,9 @@ public extension Application { ) } } + +internal extension Request { + var temporalClient: TemporalClient { + application.temporalClient + } +} diff --git a/Backend/docker-compose.yml b/Backend/docker-compose.yml index 77f8bdda..a73a7af0 100644 --- a/Backend/docker-compose.yml +++ b/Backend/docker-compose.yml @@ -30,28 +30,6 @@ services: - monitoring - backend - worker-dev: - volumes: - - ./:/app/ - - /app/.build - - worker_build:/app/.build - build: - context: . - dockerfile: ./DevDockerfile - secrets: - - GITHUB_SSH_AUTHENTICATION_TOKEN - depends_on: - postgres: - condition: service_healthy - localstack: - condition: service_healthy - environment: - RUN_COMMAND: "queues" - METRICS_PORT: "6835" - networks: - - monitoring - - backend - postgres: image: postgres:latest container_name: postgres-container From beb7182a74bf9e47e573dffee29d7ec20c0150bb Mon Sep 17 00:00:00 2001 From: William Date: Sat, 22 Nov 2025 12:27:01 +0200 Subject: [PATCH 05/16] fix: fixed all sendable errors --- .swift-version | 2 +- .../Temporal/Activites/ProfilePictureActivities.swift | 6 +++--- .../Activites/TransactionalMessageActivities.swift | 6 +++--- .../AuthenticationServiceHelper.swift | 3 +-- .../UserRegistrationService.swift | 10 +++++++--- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/.swift-version b/.swift-version index 358e78e6..0df17dd0 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -6.1.0 \ No newline at end of file +6.2.1 \ No newline at end of file diff --git a/Backend/Sources/App/Procs/Temporal/Activites/ProfilePictureActivities.swift b/Backend/Sources/App/Procs/Temporal/Activites/ProfilePictureActivities.swift index e7c7a2cb..70ecda8d 100644 --- a/Backend/Sources/App/Procs/Temporal/Activites/ProfilePictureActivities.swift +++ b/Backend/Sources/App/Procs/Temporal/Activites/ProfilePictureActivities.swift @@ -8,7 +8,7 @@ import Foundation import Temporal import Vapor -internal struct CreateProfilePictureActivityInput: Codable { +internal struct CreateProfilePictureActivityInput: Sendable, Codable { let payload: UserDTO } @@ -19,10 +19,10 @@ internal struct ProfilePictureActivities { /// - Parameters: /// - input: The input for creating a profile picture. /// - Throws: Throws an error if the profile picture creation fails. - @Activity + @Sendable @Activity public func createPicture(input: CreateProfilePictureActivityInput) async throws { let payload = input.payload - let logger = Logger("temporal") + let logger = Logger(label: "temporal") let profilePictureService = ProfilePictureService(logger: logger) do { diff --git a/Backend/Sources/App/Procs/Temporal/Activites/TransactionalMessageActivities.swift b/Backend/Sources/App/Procs/Temporal/Activites/TransactionalMessageActivities.swift index 779b5861..3430094e 100644 --- a/Backend/Sources/App/Procs/Temporal/Activites/TransactionalMessageActivities.swift +++ b/Backend/Sources/App/Procs/Temporal/Activites/TransactionalMessageActivities.swift @@ -8,7 +8,7 @@ import Temporal import Vapor /// Input payload for send transactional message activity. -internal struct SendTransactionalMessageActivityInput { +internal struct SendTransactionalMessageActivityInput: Sendable, Codable { /// The content of the message. public let content: String /// The phone number to send the message to. @@ -22,9 +22,9 @@ internal struct TransactionalMessageActivities { /// - Parameters: /// - input: The input for sending a transactional message. /// - Throws: Throws an error if the message sending fails. - @Activity + @Sendable @Activity public func sendMessage(input: SendTransactionalMessageActivityInput) async throws { - let logger = Logger("temporal") + let logger = Logger(label: "temporal") let snsService = SNSService() // Send the SMS message diff --git a/Backend/Sources/App/Services/AuthenticationService/AuthenticationServiceHelper.swift b/Backend/Sources/App/Services/AuthenticationService/AuthenticationServiceHelper.swift index 0357c1d3..0886701c 100644 --- a/Backend/Sources/App/Services/AuthenticationService/AuthenticationServiceHelper.swift +++ b/Backend/Sources/App/Services/AuthenticationService/AuthenticationServiceHelper.swift @@ -583,8 +583,7 @@ internal struct AuthCodeSender { .craftVerificationCodeMessage( code: config.payload.code ), - toPhoneNumber: config.payload.phoneNumber, - logger: config.logger + toPhoneNumber: config.payload.phoneNumber ) ) } diff --git a/Backend/Sources/App/Services/AuthenticationService/UserRegistrationService.swift b/Backend/Sources/App/Services/AuthenticationService/UserRegistrationService.swift index 17aa2f67..ae9dacb8 100644 --- a/Backend/Sources/App/Services/AuthenticationService/UserRegistrationService.swift +++ b/Backend/Sources/App/Services/AuthenticationService/UserRegistrationService.swift @@ -70,11 +70,15 @@ internal struct UserRegistrationService: AuthenticationService { let userDTO = try user.toDTO(logger: config.logger) let profilePictureKey = try ProfilePictureService(logger: config.logger).generateImageKey(for: userDTO) + let temporalClient = config.temporalClient Task { - try await config.temporalClient.executeWorkflow( + try await temporalClient.executeWorkflow( type: CreateProfilePictureWorkflow.self, - options: .init(id: "create-user-profile-pic-\(userDTO.id)-\(Date())", taskQueue: "default-queue"), - input: .init(logger: config.logger, payload: userDTO) + options: .init( + id: "create-user-profile-pic-\(userDTO.id?.uuidString ?? userDTO.username)-\(Date())", + taskQueue: "default-queue" + ), + input: .init(payload: userDTO) ) } From 1f7c8decc49014e6b9208249d08c5b0be4f4d488 Mon Sep 17 00:00:00 2001 From: William Date: Mon, 24 Nov 2025 08:10:58 +0200 Subject: [PATCH 06/16] fix: in process of debugging why send message temporal workflow doens't execute --- .../AuthenticationController.swift | 45 ++++++++++++++----- .../TransactionalMessageActivities.swift | 2 +- Backend/Sources/App/configure.swift | 27 ++++++----- 3 files changed, 49 insertions(+), 25 deletions(-) diff --git a/Backend/Sources/App/Controllers/AuthenticationController/AuthenticationController.swift b/Backend/Sources/App/Controllers/AuthenticationController/AuthenticationController.swift index 003e253a..86886693 100644 --- a/Backend/Sources/App/Controllers/AuthenticationController/AuthenticationController.swift +++ b/Backend/Sources/App/Controllers/AuthenticationController/AuthenticationController.swift @@ -48,9 +48,30 @@ internal struct AuthenticationController: RouteCollection { throw GenericErrors.userAlreadyExists } - return try await authService.sendAuthCode( - phoneNumber: dto.phoneNumber, temporalClient: req.temporalClient - ) + return try await useTemporalClient(request: req) { temporalClient in + try await authService.sendAuthCode( + phoneNumber: dto.phoneNumber, + temporalClient: temporalClient + ) + } + } + + private func useTemporalClient( + request: Request, + _ callback: @escaping (TemporalClient) async throws -> T + ) async throws -> T { + let temporalClient = request.temporalClient + return try await withThrowingTaskGroup { group in + group.addTask { + try await temporalClient.run() + } + + try await Task.sleep(for: .seconds(1)) + + let result = try await callback(temporalClient) + group.cancelAll() + return result + } } /// Registers a new user with the provided phone number and code. @@ -70,10 +91,12 @@ internal struct AuthenticationController: RouteCollection { throw GenericErrors.userAlreadyExists } - return try await authService.register( - .init(authCodePayload: dto, signer: req.jwt), - temporalClient: req.temporalClient - ) + return try await useTemporalClient(request: req) { temporalClient in + try await authService.register( + .init(authCodePayload: dto, signer: req.jwt), + temporalClient: temporalClient + ) + } } /// Sends a login code to the user's phone number. @@ -94,9 +117,11 @@ internal struct AuthenticationController: RouteCollection { throw GenericErrors.userNotFound } - return try await authService.sendAuthCode( - phoneNumber: dto.phoneNumber, temporalClient: req.temporalClient - ) + return try await useTemporalClient(request: req) { temporalClient in + try await authService.sendAuthCode( + phoneNumber: dto.phoneNumber, temporalClient: temporalClient + ) + } } /// Logs in a user with the provided phone number and code. diff --git a/Backend/Sources/App/Procs/Temporal/Activites/TransactionalMessageActivities.swift b/Backend/Sources/App/Procs/Temporal/Activites/TransactionalMessageActivities.swift index 3430094e..9a100d05 100644 --- a/Backend/Sources/App/Procs/Temporal/Activites/TransactionalMessageActivities.swift +++ b/Backend/Sources/App/Procs/Temporal/Activites/TransactionalMessageActivities.swift @@ -25,7 +25,7 @@ internal struct TransactionalMessageActivities { @Sendable @Activity public func sendMessage(input: SendTransactionalMessageActivityInput) async throws { let logger = Logger(label: "temporal") - let snsService = SNSService() + let snsService = try SNSService() // Send the SMS message do { diff --git a/Backend/Sources/App/configure.swift b/Backend/Sources/App/configure.swift index 196583a8..f4801edd 100644 --- a/Backend/Sources/App/configure.swift +++ b/Backend/Sources/App/configure.swift @@ -72,23 +72,22 @@ internal struct AppConfigurator { configuration: .init( namespace: "default", taskQueue: "default-queue", - instrumentation: .init(serverHostname: "127.0.0.1") + instrumentation: .init(serverHostname: "temporal") ), - target: .ipv4(address: "127.0.0.1", port: 7_233), + target: .dns(host: "temporal", port: 7_233), transportSecurity: .plaintext, - activityContainers: ProfilePictureActivities(), - workflows: [CreateProfilePictureWorkflow.self], + activities: [ + ProfilePictureActivities().activities.createPicture, + TransactionalMessageActivities().activities.sendMessage + ], + workflows: [CreateProfilePictureWorkflow.self, SendTransactionalMessageWorkflow.self], logger: Logger(label: "temporal-worker") ) - await withThrowingTaskGroup { group in - group.addTask { - try await worker.run() - } - - group.addTask { - try await app.temporalClient.run() - } + Task { + try await worker.run() } + // wait for worker to startup + try await Task.sleep(for: .seconds(1)) } private func configureServer() { @@ -223,9 +222,9 @@ internal enum DatabaseURLs { internal extension Application { var temporalClient: TemporalClient { try! TemporalClient( - target: .ipv4(address: "127.0.0.1", port: 7_233), + target: .dns(host: "temporal", port: 7_233), transportSecurity: .plaintext, - configuration: .init(instrumentation: .init(serverHostname: "127.0.0.1")), + configuration: .init(instrumentation: .init(serverHostname: "temporal")), logger: Logger(label: "temporal-client") ) } From b756a7c5a8037719e3ebefb60c1c3ddba3b7d344 Mon Sep 17 00:00:00 2001 From: William Date: Tue, 25 Nov 2025 09:25:11 +0200 Subject: [PATCH 07/16] fix: fixed temporal workflows not executing it currently executes, but temporal ui doesn't work --- .../AuthenticationController.swift | 27 +++++++++++++++---- .../AuthenticationServiceHelper.swift | 19 ++++++++++++- Backend/docker-compose.yml | 1 + 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/Backend/Sources/App/Controllers/AuthenticationController/AuthenticationController.swift b/Backend/Sources/App/Controllers/AuthenticationController/AuthenticationController.swift index 86886693..533ac1a3 100644 --- a/Backend/Sources/App/Controllers/AuthenticationController/AuthenticationController.swift +++ b/Backend/Sources/App/Controllers/AuthenticationController/AuthenticationController.swift @@ -61,17 +61,34 @@ internal struct AuthenticationController: RouteCollection { _ callback: @escaping (TemporalClient) async throws -> T ) async throws -> T { let temporalClient = request.temporalClient - return try await withThrowingTaskGroup { group in - group.addTask { + + // Start the Temporal worker in the background + let workerTask = Task { + do { try await temporalClient.run() + } catch { + request.logger.error("Temporal worker failed: \(error)") + throw error } + } + // Give the worker a moment to start + try await Task.sleep(for: .seconds(2)) + + var result: T + do { + // Execute the callback with the client + result = try await callback(temporalClient) + + // Give any workflows a moment to start try await Task.sleep(for: .seconds(1)) - let result = try await callback(temporalClient) - group.cancelAll() - return result + } catch { + request.logger.error("Error in Temporal operation: \(error)") + throw error } + workerTask.cancel() + return result } /// Registers a new user with the provided phone number and code. diff --git a/Backend/Sources/App/Services/AuthenticationService/AuthenticationServiceHelper.swift b/Backend/Sources/App/Services/AuthenticationService/AuthenticationServiceHelper.swift index 0886701c..da9f60ea 100644 --- a/Backend/Sources/App/Services/AuthenticationService/AuthenticationServiceHelper.swift +++ b/Backend/Sources/App/Services/AuthenticationService/AuthenticationServiceHelper.swift @@ -571,7 +571,7 @@ internal struct AuthCodeSender { } private func startSendCodeJob() async throws { - Task { + do { try await config.temporalClient.executeWorkflow( type: SendTransactionalMessageWorkflow.self, options: .init( @@ -586,6 +586,23 @@ internal struct AuthCodeSender { toPhoneNumber: config.payload.phoneNumber ) ) + + config.logger.info( + "Successfully started workflow", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + ] + ) + } catch { + config.logger.error( + "Failed to start workflow", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + "error": .string(error.localizedDescription), + "phoneNumber": .string(config.payload.phoneNumber) + ] + ) + throw error } } diff --git a/Backend/docker-compose.yml b/Backend/docker-compose.yml index a73a7af0..ca533047 100644 --- a/Backend/docker-compose.yml +++ b/Backend/docker-compose.yml @@ -159,6 +159,7 @@ services: image: temporalio/auto-setup:${TEMPORAL_VERSION} networks: - temporal-network + - backend ports: - 7233:7233 volumes: From c83df3b91427209cf359e11ca0611b1ff0c15a32 Mon Sep 17 00:00:00 2001 From: William Date: Tue, 25 Nov 2025 10:10:25 +0200 Subject: [PATCH 08/16] fix(DebugMenu,AuthenticationServiceHelper): use new dev-port, run workflows asynchronously --- .../Screens/Debug/DebugMenu.swift | 2 +- .../AuthenticationServiceHelper.swift | 62 ++++++++++--------- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/App/AutomaAppShared/Sources/AutomaAppShared/Screens/Debug/DebugMenu.swift b/App/AutomaAppShared/Sources/AutomaAppShared/Screens/Debug/DebugMenu.swift index 98f45c96..fba4f189 100644 --- a/App/AutomaAppShared/Sources/AutomaAppShared/Screens/Debug/DebugMenu.swift +++ b/App/AutomaAppShared/Sources/AutomaAppShared/Screens/Debug/DebugMenu.swift @@ -17,7 +17,7 @@ import SwiftUI /// Used to switch between different API environments internal enum BaseEnvironmentUrl: String, CaseIterable { /// Local development environment - case localhost = "http://localhost:8080" + case localhost = "http://localhost:6886" /// Production environment case production = "https://api-production.getautoma.app" /// Sandbox testing environment diff --git a/Backend/Sources/App/Services/AuthenticationService/AuthenticationServiceHelper.swift b/Backend/Sources/App/Services/AuthenticationService/AuthenticationServiceHelper.swift index da9f60ea..f8912e3b 100644 --- a/Backend/Sources/App/Services/AuthenticationService/AuthenticationServiceHelper.swift +++ b/Backend/Sources/App/Services/AuthenticationService/AuthenticationServiceHelper.swift @@ -571,38 +571,40 @@ internal struct AuthCodeSender { } private func startSendCodeJob() async throws { - do { - try await config.temporalClient.executeWorkflow( - type: SendTransactionalMessageWorkflow.self, - options: .init( - id: "send-transactional-message-\(config.payload.codeModelId)", - taskQueue: "default-queue" - ), - input: .init( - content: MessageFormatterService - .craftVerificationCodeMessage( - code: config.payload.code - ), - toPhoneNumber: config.payload.phoneNumber + Task { + do { + try await config.temporalClient.executeWorkflow( + type: SendTransactionalMessageWorkflow.self, + options: .init( + id: "send-transactional-message-\(config.payload.codeModelId)", + taskQueue: "default-queue" + ), + input: .init( + content: MessageFormatterService + .craftVerificationCodeMessage( + code: config.payload.code + ), + toPhoneNumber: config.payload.phoneNumber + ) ) - ) - config.logger.info( - "Successfully started workflow", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - ] - ) - } catch { - config.logger.error( - "Failed to start workflow", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - "error": .string(error.localizedDescription), - "phoneNumber": .string(config.payload.phoneNumber) - ] - ) - throw error + config.logger.info( + "Successfully started workflow", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + ] + ) + } catch { + config.logger.error( + "Failed to start workflow", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + "error": .string(error.localizedDescription), + "phoneNumber": .string(config.payload.phoneNumber) + ] + ) + throw error + } } } From b80b07e4852161edad5e6964e9703d8fe594684f Mon Sep 17 00:00:00 2001 From: William Date: Thu, 27 Nov 2025 10:52:45 +0200 Subject: [PATCH 09/16] fix(docker-compose): using temporal cli to startup dev-server, as service in docker-compose It almost works, just need to fix out of memory issue when setting volume for temporal service --- Backend/docker-compose.yml | 89 +++++--------------------------------- package.json | 4 +- 2 files changed, 14 insertions(+), 79 deletions(-) diff --git a/Backend/docker-compose.yml b/Backend/docker-compose.yml index ca533047..224613eb 100644 --- a/Backend/docker-compose.yml +++ b/Backend/docker-compose.yml @@ -1,9 +1,6 @@ networks: monitoring: backend: - temporal-network: - driver: bridge - name: temporal-network services: api-dev: @@ -21,6 +18,8 @@ services: condition: service_healthy localstack: condition: service_healthy + temporal: + condition: service_started environment: RUN_COMMAND: "serve --hostname 0.0.0.0" METRICS_PORT: "6834" @@ -110,86 +109,20 @@ services: - prom_data:/prometheus networks: - monitoring - elasticsearch: - container_name: temporal-elasticsearch - environment: - - cluster.routing.allocation.disk.threshold_enabled=true - - cluster.routing.allocation.disk.watermark.low=512mb - - cluster.routing.allocation.disk.watermark.high=256mb - - cluster.routing.allocation.disk.watermark.flood_stage=128mb - - discovery.type=single-node - - ES_JAVA_OPTS=-Xms256m -Xmx256m - - xpack.security.enabled=false - image: elasticsearch:${ELASTICSEARCH_VERSION} - networks: - - temporal-network - expose: - - 9200 - volumes: - - /var/lib/elasticsearch/data - postgresql: - container_name: temporal-postgresql - environment: - POSTGRES_PASSWORD: temporal - POSTGRES_USER: temporal - image: postgres:${POSTGRESQL_VERSION} - networks: - - temporal-network - expose: - - 5432 - volumes: - - /var/lib/postgresql/data temporal: + image: temporalio/temporal container_name: temporal - depends_on: - - postgresql - - elasticsearch - environment: - - DB=postgres12 - - DB_PORT=5432 - - POSTGRES_USER=temporal - - POSTGRES_PWD=temporal - - POSTGRES_SEEDS=postgresql - - DYNAMIC_CONFIG_FILE_PATH=config/dynamicconfig/development-sql.yaml - - ENABLE_ES=true - - ES_SEEDS=elasticsearch - - ES_VERSION=v7 - - TEMPORAL_ADDRESS=temporal:7233 - - TEMPORAL_CLI_ADDRESS=temporal:7233 - image: temporalio/auto-setup:${TEMPORAL_VERSION} - networks: - - temporal-network - - backend - ports: - - 7233:7233 + entrypoint: [] + command: /bin/sh -c "mkdir -p ~/data && temporal server start-dev --ip 0.0.0.0 --db-filename ~/data/temporal.db" volumes: - - ./dynamicconfig:/etc/temporal/config/dynamicconfig - temporal-admin-tools: - container_name: temporal-admin-tools - depends_on: - - temporal - environment: - - TEMPORAL_ADDRESS=temporal:7233 - - TEMPORAL_CLI_ADDRESS=temporal:7233 - image: temporalio/admin-tools:${TEMPORAL_ADMINTOOLS_VERSION} - networks: - - temporal-network - stdin_open: true - tty: true - temporal-ui: - container_name: temporal-ui - depends_on: - - temporal - environment: - - TEMPORAL_ADDRESS=temporal:7233 - - TEMPORAL_CORS_ORIGINS=http://localhost:3000 - image: temporalio/ui:${TEMPORAL_UI_VERSION} - networks: - - temporal-network + - temporal_data:/home/temporal/data ports: - - 8080:8080 - + - 8233:8233 + networks: + - backend volumes: + temporal_data: + driver: local postgres_data: driver: local api_build: diff --git a/package.json b/package.json index 01da9961..79d8744a 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,9 @@ "install:swiftlint": "brew install swiftlint", "install:swiftgen": "brew install swiftgen", "install:swiftformat": "brew install swiftformat", - "install:all": "npx npm-run-all --sequential install:swiftlint install:swiftgen install:swiftformat config", + "install:temporal": "brew install temporal", + "install:all": "npx npm-run-all --sequential install:swiftlint install:temporal install:swiftgen install:swiftformat config", + "temporal:up": "temporal server start-dev", "compose:up": "npm run compose -- up -d", "compose:build": "npm run compose -- build", "compose:build:no-cache": "npm run compose -- build --no-cache", From 1b7c819c387bdb2f1d5cae7df6d8d09bb6b78807 Mon Sep 17 00:00:00 2001 From: William Date: Fri, 28 Nov 2025 08:29:38 +0200 Subject: [PATCH 10/16] fix(docker-compose): don't use data dir, it's fine to not have a persistent database using a volume for database leads to database readonly errors --- Backend/docker-compose.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Backend/docker-compose.yml b/Backend/docker-compose.yml index 224613eb..3c3c0938 100644 --- a/Backend/docker-compose.yml +++ b/Backend/docker-compose.yml @@ -112,17 +112,12 @@ services: temporal: image: temporalio/temporal container_name: temporal - entrypoint: [] - command: /bin/sh -c "mkdir -p ~/data && temporal server start-dev --ip 0.0.0.0 --db-filename ~/data/temporal.db" - volumes: - - temporal_data:/home/temporal/data + command: ["server", "start-dev", "--ip", "0.0.0.0"] ports: - 8233:8233 networks: - backend volumes: - temporal_data: - driver: local postgres_data: driver: local api_build: From 344a01084603efa102dfda569906aa31d0be3042 Mon Sep 17 00:00:00 2001 From: William Date: Mon, 1 Dec 2025 11:23:51 +0200 Subject: [PATCH 11/16] fix(configure.swift): refactor code, temporal worker command to startup worker as separate process, use worker host to initialize worker client and temporal client, small code enhancements --- Backend/README.md | 4 +- .../App/Commands/TemporalWorkerCommand.swift | 74 ++++++++++++++++ .../Vapor/ApplicationExtensions.swift | 19 +++++ .../Extensions/Vapor/RequestExtensions.swift | 7 ++ Backend/Sources/App/Models/UserModel.swift | 23 +---- .../AuthenticationServiceHelper.swift | 47 +++-------- .../UserRegistrationService.swift | 18 ++-- Backend/Sources/App/configure.swift | 84 +++++++------------ Backend/docker-compose.yml | 23 +++++ 9 files changed, 182 insertions(+), 117 deletions(-) create mode 100644 Backend/Sources/App/Commands/TemporalWorkerCommand.swift create mode 100644 Backend/Sources/App/Extensions/Vapor/ApplicationExtensions.swift diff --git a/Backend/README.md b/Backend/README.md index 012a9f14..62040457 100644 --- a/Backend/README.md +++ b/Backend/README.md @@ -2,6 +2,8 @@ 1. "GITHUB_SSH_AUTHENTICATION_TOKEN": A Github fine-grained token that allows cloning AutomaUtilities repository (private) TODOS: -- [ ] Fix swift protobuf package warnings (find upstream package, make a pull request to swift protobuf to support latest +- [ ] CURRENT BRANCH: deploy temporal and temporal worker on fly.io. Delete vapor-queues worker fly.io config and apps in organizations +- [ ] Fix profile picture image generation not working (current problem: OpenAI platform billing hard limit reached) +- [ ] FUTURE: Fix swift protobuf package warnings (find upstream package, make a pull request to swift protobuf to support latest swift version - fix warnings) and update upstream package `Package.swift` to use latest protobuf version. diff --git a/Backend/Sources/App/Commands/TemporalWorkerCommand.swift b/Backend/Sources/App/Commands/TemporalWorkerCommand.swift new file mode 100644 index 00000000..c24ec52c --- /dev/null +++ b/Backend/Sources/App/Commands/TemporalWorkerCommand.swift @@ -0,0 +1,74 @@ +// TemporalWorkerCommand.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +import Temporal +import Vapor + +struct TemporalWorkerCommand: AsyncCommand { + struct Signature: CommandSignature {} + + var help: String { + "startup temporal worker" + } + + func run(using context: CommandContext, signature _: Signature) async throws { + try await AppConfigurator( + app: context.application, + config: .init( + shouldSetupAPI: false, + metricsPort: 6_835 + ) + ).configure() + try await setupTemporal(logger: context.application.logger) + } + + private func setupTemporal(logger: Logger) async throws { + logSetupTemporalWorkerStarted(logger) + let temporalServerHostname = try getTemporalServerHostname() + let worker = try getTemporalWorker(hostname: temporalServerHostname) + logSetupTemporalWorkerConfigured(logger) + try await worker.run() + } + + private func logSetupTemporalWorkerStarted(_ logger: Logger) { + logger.info( + "Setting up temporal worker started.", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + ] + ) + } + + private func getTemporalServerHostname() throws -> String { + try Environment.getOrThrow("TEMPORAL_WORKER_HOSTNAME") + } + + private func getTemporalWorker(hostname temporalServerHostname: String) throws -> TemporalWorker { + try TemporalWorker( + configuration: .init( + namespace: "default", + taskQueue: "default-queue", + instrumentation: .init(serverHostname: temporalServerHostname) + ), + target: .dns(host: temporalServerHostname, port: 7_233), + transportSecurity: .plaintext, + activities: [ + ProfilePictureActivities().activities.createPicture, + TransactionalMessageActivities().activities.sendMessage + ], + workflows: [CreateProfilePictureWorkflow.self, SendTransactionalMessageWorkflow.self], + logger: Logger(label: "temporal-worker") + ) + } + + private func logSetupTemporalWorkerConfigured(_ logger: Logger) { + logger.info( + "Configured temporal worker instance.", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + ] + ) + } +} diff --git a/Backend/Sources/App/Extensions/Vapor/ApplicationExtensions.swift b/Backend/Sources/App/Extensions/Vapor/ApplicationExtensions.swift new file mode 100644 index 00000000..99fb1fd7 --- /dev/null +++ b/Backend/Sources/App/Extensions/Vapor/ApplicationExtensions.swift @@ -0,0 +1,19 @@ +// ApplicationExtensions.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +import Temporal +import Vapor + +internal extension Application { + var temporalClient: TemporalClient { + let temporalServerHostname = try! Environment.getOrThrow("TEMPORAL_WORKER_HOSTNAME") + return try! TemporalClient( + target: .dns(host: temporalServerHostname, port: 7_233), + transportSecurity: .plaintext, + configuration: .init(instrumentation: .init(serverHostname: "temporal")), + logger: Logger(label: "temporal-client") + ) + } +} diff --git a/Backend/Sources/App/Extensions/Vapor/RequestExtensions.swift b/Backend/Sources/App/Extensions/Vapor/RequestExtensions.swift index a1054fb3..43658898 100644 --- a/Backend/Sources/App/Extensions/Vapor/RequestExtensions.swift +++ b/Backend/Sources/App/Extensions/Vapor/RequestExtensions.swift @@ -4,6 +4,7 @@ // All rights reserved. import Fluent +import Temporal import Vapor /// Extension on rquest to add DB aliases @@ -18,3 +19,9 @@ internal extension Request { db(.readOnly) } } + +internal extension Request { + var temporalClient: TemporalClient { + application.temporalClient + } +} diff --git a/Backend/Sources/App/Models/UserModel.swift b/Backend/Sources/App/Models/UserModel.swift index f19ab0cf..90b3a0d6 100644 --- a/Backend/Sources/App/Models/UserModel.swift +++ b/Backend/Sources/App/Models/UserModel.swift @@ -86,25 +86,10 @@ public final class UserModel: Model, @unchecked Sendable { /// Converts the model to a `UserDTO`. /// - Returns: An instance of `UserDTO`. public func toDTO(logger: Logger) throws -> UserDTO { - let profilePictureUrl: String? - do { - guard - let profilePictureKey - else { - let userId = try requireID().uuidString - logger.error( - "Profile picture key is nil, could not convert user model to DTO.", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - "user_id": .string(userId), - ] - ) - throw URLError(.badURL) - } - - profilePictureUrl = try TigrisService(logger: logger).getTigrisUrl(profilePictureKey) - } catch { - profilePictureUrl = nil + let profilePictureUrl: String? = if let profilePictureKey { + try TigrisService(logger: logger).getTigrisUrl(profilePictureKey) + } else { + nil } return .init( diff --git a/Backend/Sources/App/Services/AuthenticationService/AuthenticationServiceHelper.swift b/Backend/Sources/App/Services/AuthenticationService/AuthenticationServiceHelper.swift index f8912e3b..ad06b5f5 100644 --- a/Backend/Sources/App/Services/AuthenticationService/AuthenticationServiceHelper.swift +++ b/Backend/Sources/App/Services/AuthenticationService/AuthenticationServiceHelper.swift @@ -571,41 +571,20 @@ internal struct AuthCodeSender { } private func startSendCodeJob() async throws { - Task { - do { - try await config.temporalClient.executeWorkflow( - type: SendTransactionalMessageWorkflow.self, - options: .init( - id: "send-transactional-message-\(config.payload.codeModelId)", - taskQueue: "default-queue" + _ = try await config.temporalClient.startWorkflow( + type: SendTransactionalMessageWorkflow.self, + options: .init( + id: "send-transactional-message-\(config.payload.codeModelId)", + taskQueue: "default-queue" + ), + input: .init( + content: MessageFormatterService + .craftVerificationCodeMessage( + code: config.payload.code ), - input: .init( - content: MessageFormatterService - .craftVerificationCodeMessage( - code: config.payload.code - ), - toPhoneNumber: config.payload.phoneNumber - ) - ) - - config.logger.info( - "Successfully started workflow", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - ] - ) - } catch { - config.logger.error( - "Failed to start workflow", - metadata: [ - "to": .string("\(String(describing: Self.self)).\(#function)"), - "error": .string(error.localizedDescription), - "phoneNumber": .string(config.payload.phoneNumber) - ] - ) - throw error - } - } + toPhoneNumber: config.payload.phoneNumber + ) + ) } private func getCodeDeletionTime() -> Date { diff --git a/Backend/Sources/App/Services/AuthenticationService/UserRegistrationService.swift b/Backend/Sources/App/Services/AuthenticationService/UserRegistrationService.swift index ae9dacb8..d41ede18 100644 --- a/Backend/Sources/App/Services/AuthenticationService/UserRegistrationService.swift +++ b/Backend/Sources/App/Services/AuthenticationService/UserRegistrationService.swift @@ -71,16 +71,14 @@ internal struct UserRegistrationService: AuthenticationService { let profilePictureKey = try ProfilePictureService(logger: config.logger).generateImageKey(for: userDTO) let temporalClient = config.temporalClient - Task { - try await temporalClient.executeWorkflow( - type: CreateProfilePictureWorkflow.self, - options: .init( - id: "create-user-profile-pic-\(userDTO.id?.uuidString ?? userDTO.username)-\(Date())", - taskQueue: "default-queue" - ), - input: .init(payload: userDTO) - ) - } + _ = try await temporalClient.startWorkflow( + type: CreateProfilePictureWorkflow.self, + options: .init( + id: "create-user-profile-pic-\(userDTO.id?.uuidString ?? userDTO.username)-\(Date())", + taskQueue: "default-queue" + ), + input: .init(payload: userDTO) + ) user.profilePictureKey = profilePictureKey diff --git a/Backend/Sources/App/configure.swift b/Backend/Sources/App/configure.swift index f4801edd..0b530efd 100644 --- a/Backend/Sources/App/configure.swift +++ b/Backend/Sources/App/configure.swift @@ -7,26 +7,33 @@ import AutomaUtilities import Fluent import FluentPostgresDriver import JWT -import Temporal import Vapor internal func configure(_ app: Application) async throws { - try await AppConfigurator(app: app).configure() + try await AppConfigurator( + app: app, + config: .init( + shouldSetupAPI: true + ) + ).configure() } internal struct AppConfigurator { private let app: Application + private let config: AppConfiguratorConfig private let environment = Environment.get("ENVIRONMENT") ?? "local" private let primaryDatabaseURL: String? = try? DatabaseURLs.primary.get() private let regionalDatabaseURL: String? = try? DatabaseURLs.regional.get() /// Initializes Application - public init(app: Application) { + public init(app: Application, config: AppConfiguratorConfig) { self.app = app + self.config = config } /// Configures the entire application public func configure() async throws { + registerCommands() registerMiddleware() let hasDatabaseURLs = (primaryDatabaseURL != nil) || (regionalDatabaseURL != nil) if hasDatabaseURLs { @@ -34,17 +41,23 @@ internal struct AppConfigurator { } } + private func registerCommands() { + app.asyncCommands.use(TemporalWorkerCommand(), as: "temporal-worker") + } + private func registerMiddleware() { app.middleware.use(ErrorStringMiddleware()) } private func configureWhenDatabaseURLsAvailable() async throws { try await DatabaseConfigurator(app: app).configureDatabases() - try registerControllers() try await startPrometheusService() - try await setupTemporal() - try await addAuthenticationJWTKey() - configureServer() + + if config.shouldSetupAPI { + try registerControllers() + try await addAuthenticationJWTKey() + configureServer() + } } private func registerControllers() throws { @@ -55,11 +68,7 @@ internal struct AppConfigurator { } private func startPrometheusService() async throws { - if environment != "local" { - try await PrometheusService().startServer() - return - } - try await PrometheusService().startServer() + try await PrometheusService().startServer(port: config.metricsPort) } private func addAuthenticationJWTKey() async throws { @@ -67,32 +76,19 @@ internal struct AppConfigurator { await app.jwt.keys.add(hmac: .init(stringLiteral: encryptionSecret), digestAlgorithm: .sha256) } - private func setupTemporal() async throws { - let worker = try TemporalWorker( - configuration: .init( - namespace: "default", - taskQueue: "default-queue", - instrumentation: .init(serverHostname: "temporal") - ), - target: .dns(host: "temporal", port: 7_233), - transportSecurity: .plaintext, - activities: [ - ProfilePictureActivities().activities.createPicture, - TransactionalMessageActivities().activities.sendMessage - ], - workflows: [CreateProfilePictureWorkflow.self, SendTransactionalMessageWorkflow.self], - logger: Logger(label: "temporal-worker") - ) - Task { - try await worker.run() - } - // wait for worker to startup - try await Task.sleep(for: .seconds(1)) - } - private func configureServer() { app.http.server.configuration.responseCompression = .enabled } + + public struct AppConfiguratorConfig { + let shouldSetupAPI: Bool + let metricsPort: UInt16 + + init(shouldSetupAPI: Bool = true, metricsPort: UInt16 = 6_834) { + self.shouldSetupAPI = shouldSetupAPI + self.metricsPort = metricsPort + } + } } internal struct DatabaseConfigurator { @@ -217,21 +213,3 @@ internal enum DatabaseURLs { /// regional url most likely doesn't have write access, but allows for extremely fast reads public static let regional: Result = Result { try Environment.getOrThrow("REGIONAL_POSTGRES_URL") } } - -// TODO: refactor extensions to extensions directory -internal extension Application { - var temporalClient: TemporalClient { - try! TemporalClient( - target: .dns(host: "temporal", port: 7_233), - transportSecurity: .plaintext, - configuration: .init(instrumentation: .init(serverHostname: "temporal")), - logger: Logger(label: "temporal-client") - ) - } -} - -internal extension Request { - var temporalClient: TemporalClient { - application.temporalClient - } -} diff --git a/Backend/docker-compose.yml b/Backend/docker-compose.yml index 3c3c0938..b7eed9e1 100644 --- a/Backend/docker-compose.yml +++ b/Backend/docker-compose.yml @@ -29,6 +29,29 @@ services: - monitoring - backend + temporal-worker: + volumes: + - ./:/app/ + - /app/.build + - api_build:/app/.build + build: + context: . + dockerfile: ./DevDockerfile + secrets: + - GITHUB_SSH_AUTHENTICATION_TOKEN + depends_on: + postgres: + condition: service_healthy + localstack: + condition: service_healthy + temporal: + condition: service_started + environment: + RUN_COMMAND: "temporal-worker" + METRICS_PORT: "6835" + networks: + - monitoring + - backend postgres: image: postgres:latest container_name: postgres-container From e0ffc9ed7d9cf02aff4e0b8fec724260ee59b08a Mon Sep 17 00:00:00 2001 From: William Date: Tue, 9 Dec 2025 11:21:13 +0200 Subject: [PATCH 12/16] feat: deploy temporal service, temporal worker refactor, debug steps and temporary debug code to get symbolicated stack trace in production fly.io --- Backend/Dockerfile | 48 +++++---- Backend/OldDockerfile | 34 +++++++ Backend/OlderDockerfile | 97 +++++++++++++++++++ .../App/Commands/TemporalWorkerCommand.swift | 28 ++++-- .../Temporal/TemporalClientExtensions.swift | 13 +++ .../Vapor/ApplicationExtensions.swift | 8 +- .../SendTransactionalMessageWorkflow.swift | 26 ++++- .../AuthenticationServiceHelper.swift | 44 ++++++--- Backend/Sources/App/configure.swift | 26 ++--- Backend/docker-compose.yml | 18 +++- Backend/infra/fly/fly-jobs.toml | 22 ----- Backend/infra/fly/temporal-worker.toml | 21 ++++ Backend/infra/fly/temporal/Dockerfile | 27 ++++++ Backend/infra/fly/temporal/database.toml | 5 + Backend/infra/fly/temporal/fly.toml | 49 ++++++++++ Backend/infra/fly/temporal/start-ui.sh | 9 ++ Backend/infra/fly/temporal/start.sh | 10 ++ package.json | 4 +- 18 files changed, 403 insertions(+), 86 deletions(-) create mode 100644 Backend/OldDockerfile create mode 100644 Backend/OlderDockerfile create mode 100644 Backend/Sources/App/Extensions/Temporal/TemporalClientExtensions.swift delete mode 100644 Backend/infra/fly/fly-jobs.toml create mode 100644 Backend/infra/fly/temporal-worker.toml create mode 100644 Backend/infra/fly/temporal/Dockerfile create mode 100644 Backend/infra/fly/temporal/database.toml create mode 100644 Backend/infra/fly/temporal/fly.toml create mode 100755 Backend/infra/fly/temporal/start-ui.sh create mode 100755 Backend/infra/fly/temporal/start.sh diff --git a/Backend/Dockerfile b/Backend/Dockerfile index 0987eec2..d442425e 100644 --- a/Backend/Dockerfile +++ b/Backend/Dockerfile @@ -1,7 +1,7 @@ # ================================ # Build image # ================================ -FROM swift:6.2.0-jammy AS build +FROM swift:6.2-noble AS build # Install OS updates RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ @@ -13,41 +13,44 @@ RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ # Set up a build area WORKDIR /build -# First just resolve dependencies. -# This creates a cached layer that can be reused -# as long as your Package.swift/Package.resolved -# files do not change. - RUN --mount=type=secret,id=GITHUB_SSH_AUTHENTICATION_TOKEN \ TOKEN=$(cat /run/secrets/GITHUB_SSH_AUTHENTICATION_TOKEN) && \ git config --global url."https://x-access-token:${TOKEN}@github.com/".insteadOf "https://github.com/" -COPY Package.swift . -COPY Package.resolved . +# First just resolve dependencies. +# This creates a cached layer that can be reused +# as long as your Package.swift/Package.resolved +# files do not change. +COPY ./Package.* ./ COPY DataTypes ./DataTypes -RUN --mount=type=cache,target=/build/.build swift package resolve +RUN swift package resolve \ + $([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true) + # Copy entire repo into container COPY . . -# Build everything, with optimizations, with static linking, and using jemalloc +RUN mkdir /staging + +# Build the application, with optimizations, with static linking, and using jemalloc # N.B.: The static version of jemalloc is incompatible with the static Swift runtime. -RUN --mount=type=cache,target=/build/.build swift build -c release \ +RUN --mount=type=cache,target=/build/.build \ + swift build -c release \ + --product App \ --static-swift-stdlib \ - -Xlinker -ljemalloc + -Xlinker -ljemalloc \ + -Xswiftc -g && \ + # Copy main executable to staging area + cp "$(swift build -c release --show-bin-path)/App" /staging && \ + # Copy resources bundled by SPM to staging area + find -L "$(swift build -c release --show-bin-path)" -regex '.*\.resources$' -exec cp -Ra {} /staging \; # Switch to the staging area WORKDIR /staging -# Copy main executable to staging area -RUN --mount=type=cache,target=/build/.build cp "$(swift build --package-path /build -c release --show-bin-path)/App" ./ - # Copy static swift backtracer binary to staging area RUN cp "/usr/libexec/swift/linux/swift-backtrace-static" ./ -# Copy resources bundled by SPM to staging area -RUN --mount=type=cache,target=/build/.build find -L "$(swift build --package-path /build -c release --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \; - # Copy any resources from the public directory and views directory if the directories exist # Ensure that by default, neither the directory nor any of its contents are writable. RUN [ -d /build/Public ] && { mv /build/Public ./Public && chmod -R a-w ./Public; } || true @@ -56,7 +59,7 @@ RUN [ -d /build/Resources ] && { mv /build/Resources ./Resources && chmod -R a-w # ================================ # Run image # ================================ -FROM ubuntu:jammy +FROM ubuntu:noble # Make sure all system packages are up to date, and install only essential packages. RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ @@ -66,8 +69,10 @@ RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ libjemalloc2 \ ca-certificates \ tzdata \ - && apt-get install bash \ - && apt-get install -y openssl libssl-dev libcurl4 libxml2 \ + # If your app or its dependencies import FoundationNetworking, also install `libcurl4`. + libcurl4 \ + # If your app or its dependencies import FoundationXML, also install `libxml2`. + libxml2 \ && rm -r /var/lib/apt/lists/* # Create a vapor user and group with /app as its home directory @@ -92,3 +97,4 @@ EXPOSE 8080 EXPOSE 6834 CMD /bin/bash -c "eval './App $RUN_COMMAND'" +# CMD /bin/bash -c "sleep 100000" diff --git a/Backend/OldDockerfile b/Backend/OldDockerfile new file mode 100644 index 00000000..39694fcc --- /dev/null +++ b/Backend/OldDockerfile @@ -0,0 +1,34 @@ +FROM swift:6.2.0-jammy AS build + +RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ + && apt-get -q update \ + && apt-get -q dist-upgrade -y \ + && apt-get -q install -y \ + libjemalloc2 \ + ca-certificates \ + tzdata \ + && apt-get install bash \ + && apt-get install -y openssl libssl-dev libcurl4 libxml2 \ + && rm -r /var/lib/apt/lists/* + +WORKDIR /build + +RUN --mount=type=secret,id=GITHUB_SSH_AUTHENTICATION_TOKEN \ + TOKEN=$(cat /run/secrets/GITHUB_SSH_AUTHENTICATION_TOKEN) && \ + git config --global url."https://x-access-token:${TOKEN}@github.com/".insteadOf "https://github.com/" + +COPY Package.swift . +COPY Package.resolved . + +COPY DataTypes ./DataTypes +RUN --mount=type=cache,target=/build/.build swift package resolve + +COPY . . + +# Let Docker bind to port 8080 +EXPOSE 8080 + +# Expose PrometheusService server port +EXPOSE 6834 + +CMD /bin/bash -c "sleep 100000" diff --git a/Backend/OlderDockerfile b/Backend/OlderDockerfile new file mode 100644 index 00000000..f81e0a66 --- /dev/null +++ b/Backend/OlderDockerfile @@ -0,0 +1,97 @@ +# ================================ +# Build image +# ================================ +FROM swift:6.2.0-jammy AS build + +# Install OS updates +RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ + && apt-get -q update \ + && apt-get -q dist-upgrade -y \ + && apt-get install -y libjemalloc-dev \ + && apt-get install -y openssl libssl-dev + +# Set up a build area +WORKDIR /build + +# First just resolve dependencies. +# This creates a cached layer that can be reused +# as long as your Package.swift/Package.resolved +# files do not change. + +RUN --mount=type=secret,id=GITHUB_SSH_AUTHENTICATION_TOKEN \ + TOKEN=$(cat /run/secrets/GITHUB_SSH_AUTHENTICATION_TOKEN) && \ + git config --global url."https://x-access-token:${TOKEN}@github.com/".insteadOf "https://github.com/" + +COPY Package.swift . +COPY Package.resolved . + +COPY DataTypes ./DataTypes +RUN --mount=type=cache,target=/build/.build swift package resolve +# Copy entire repo into container +COPY . . + +# Build everything, with optimizations, with static linking, and using jemalloc +# N.B.: The static version of jemalloc is incompatible with the static Swift runtime. +# RUN --mount=type=cache,target=/build/.build swift build -c release \ +RUN --mount=type=cache,target=/build/.build swift build \ + --static-swift-stdlib \ + -Xlinker -ljemalloc + +# Switch to the staging area +WORKDIR /staging + +# Copy main executable to staging area +# RUN --mount=type=cache,target=/build/.build cp "$(swift build --package-path /build -c release --show-bin-path)/App" ./ +RUN --mount=type=cache,target=/build/.build cp "$(swift build --package-path /build --show-bin-path)/App" ./ + +# Copy static swift backtracer binary to staging area +RUN cp "/usr/libexec/swift/linux/swift-backtrace-static" ./ + +# Copy resources bundled by SPM to staging area +# RUN --mount=type=cache,target=/build/.build find -L "$(swift build --package-path /build -c release --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \; +RUN --mount=type=cache,target=/build/.build find -L "$(swift build --package-path /build --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \; + +# Copy any resources from the public directory and views directory if the directories exist +# Ensure that by default, neither the directory nor any of its contents are writable. +RUN [ -d /build/Public ] && { mv /build/Public ./Public && chmod -R a-w ./Public; } || true +RUN [ -d /build/Resources ] && { mv /build/Resources ./Resources && chmod -R a-w ./Resources; } || true + +# ================================ +# Run image +# ================================ +FROM ubuntu:jammy + +# Make sure all system packages are up to date, and install only essential packages. +RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ + && apt-get -q update \ + && apt-get -q dist-upgrade -y \ + && apt-get -q install -y \ + libjemalloc2 \ + ca-certificates \ + tzdata \ + && apt-get install bash \ + && apt-get install -y openssl libssl-dev libcurl4 libxml2 \ + && rm -r /var/lib/apt/lists/* + +# Create a vapor user and group with /app as its home directory +RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app vapor + +# Switch to the new home directory +WORKDIR /app + +# Copy built executable and any staged resources from builder +COPY --from=build --chown=vapor:vapor /staging /app + +# Provide configuration needed by the built-in crash reporter and some sensible default behaviors. +ENV SWIFT_BACKTRACE=enable=yes,sanitize=yes,threads=all,images=all,interactive=no,swift-backtrace=./swift-backtrace-static + +# Ensure all further commands run as the vapor user +USER vapor:vapor + +# Let Docker bind to port 8080 +EXPOSE 8080 + +# Expose PrometheusService server port +EXPOSE 6834 + +CMD /bin/bash -c "eval './App $RUN_COMMAND'" diff --git a/Backend/Sources/App/Commands/TemporalWorkerCommand.swift b/Backend/Sources/App/Commands/TemporalWorkerCommand.swift index c24ec52c..00e00cbe 100644 --- a/Backend/Sources/App/Commands/TemporalWorkerCommand.swift +++ b/Backend/Sources/App/Commands/TemporalWorkerCommand.swift @@ -14,6 +14,7 @@ struct TemporalWorkerCommand: AsyncCommand { } func run(using context: CommandContext, signature _: Signature) async throws { + let result = 5 ... 1 try await AppConfigurator( app: context.application, config: .init( @@ -26,10 +27,27 @@ struct TemporalWorkerCommand: AsyncCommand { private func setupTemporal(logger: Logger) async throws { logSetupTemporalWorkerStarted(logger) - let temporalServerHostname = try getTemporalServerHostname() - let worker = try getTemporalWorker(hostname: temporalServerHostname) + let worker = try getTemporalWorker( + hostname: TemporalClient.getServerHostnameFromEnv() + ) logSetupTemporalWorkerConfigured(logger) - try await worker.run() + do { + try await worker.run() + } catch { + // Capture the current stack trace + let stackTrace = Thread.callStackSymbols.joined(separator: "\n") + + logger.info( + "Error occurred while running tmeporal worker", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + "error": .string(error.localizedDescription), + "stackTrace": .string(stackTrace), + ] + ) + + throw error + } } private func logSetupTemporalWorkerStarted(_ logger: Logger) { @@ -41,10 +59,6 @@ struct TemporalWorkerCommand: AsyncCommand { ) } - private func getTemporalServerHostname() throws -> String { - try Environment.getOrThrow("TEMPORAL_WORKER_HOSTNAME") - } - private func getTemporalWorker(hostname temporalServerHostname: String) throws -> TemporalWorker { try TemporalWorker( configuration: .init( diff --git a/Backend/Sources/App/Extensions/Temporal/TemporalClientExtensions.swift b/Backend/Sources/App/Extensions/Temporal/TemporalClientExtensions.swift new file mode 100644 index 00000000..0c690f39 --- /dev/null +++ b/Backend/Sources/App/Extensions/Temporal/TemporalClientExtensions.swift @@ -0,0 +1,13 @@ +// TemporalClientExtensions.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +import Temporal +import Vapor + +internal extension TemporalClient { + static func getServerHostnameFromEnv() throws -> String { + try Environment.getOrThrow("TEMPORAL_SERVER_HOSTNAME") + } +} diff --git a/Backend/Sources/App/Extensions/Vapor/ApplicationExtensions.swift b/Backend/Sources/App/Extensions/Vapor/ApplicationExtensions.swift index 99fb1fd7..40176a88 100644 --- a/Backend/Sources/App/Extensions/Vapor/ApplicationExtensions.swift +++ b/Backend/Sources/App/Extensions/Vapor/ApplicationExtensions.swift @@ -8,9 +8,11 @@ import Vapor internal extension Application { var temporalClient: TemporalClient { - let temporalServerHostname = try! Environment.getOrThrow("TEMPORAL_WORKER_HOSTNAME") - return try! TemporalClient( - target: .dns(host: temporalServerHostname, port: 7_233), + try! TemporalClient( + target: .dns( + host: TemporalClient.getServerHostnameFromEnv(), + port: 7_233 + ), transportSecurity: .plaintext, configuration: .init(instrumentation: .init(serverHostname: "temporal")), logger: Logger(label: "temporal-client") diff --git a/Backend/Sources/App/Procs/Temporal/Workflows/SendTransactionalMessageWorkflow.swift b/Backend/Sources/App/Procs/Temporal/Workflows/SendTransactionalMessageWorkflow.swift index 7247a889..31c7e2cc 100644 --- a/Backend/Sources/App/Procs/Temporal/Workflows/SendTransactionalMessageWorkflow.swift +++ b/Backend/Sources/App/Procs/Temporal/Workflows/SendTransactionalMessageWorkflow.swift @@ -9,10 +9,26 @@ import Vapor @Workflow internal final class SendTransactionalMessageWorkflow { func run(input: SendTransactionalMessageActivityInput) async throws { - try await Workflow.executeActivity( - TransactionalMessageActivities.Activities.SendMessage.self, - options: ActivityOptions(startToCloseTimeout: .seconds(30)), - input: input - ) + do { + try await Workflow.executeActivity( + TransactionalMessageActivities.Activities.SendMessage.self, + options: ActivityOptions(startToCloseTimeout: .seconds(30)), + input: input + ) + } catch { + // Capture the current stack trace + let stackTrace = Thread.callStackSymbols.joined(separator: "\n") + + let logger = Logger(label: "send-transactional-message-workflow") + logger.info( + "Error occurred while processing workflow", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + "error": .string(error.localizedDescription), + "input": .string("\(input.content)"), + "stackTrace": .string(stackTrace), + ] + ) + } } } diff --git a/Backend/Sources/App/Services/AuthenticationService/AuthenticationServiceHelper.swift b/Backend/Sources/App/Services/AuthenticationService/AuthenticationServiceHelper.swift index ad06b5f5..a4d2a20e 100644 --- a/Backend/Sources/App/Services/AuthenticationService/AuthenticationServiceHelper.swift +++ b/Backend/Sources/App/Services/AuthenticationService/AuthenticationServiceHelper.swift @@ -103,7 +103,7 @@ internal actor AuthenticationServiceHelper { } private func getRateLimitString() throws -> String { - try Environment.getOrThrow("AUTHENTICATION_CODE_RATE_LIMIT") + try Environment.getOrThrow("AUTHENTICATION_CODE_DISTANCE") } private func castCodeRateLimitToNumber(_ rateLimitString: String) throws -> Double { @@ -571,20 +571,36 @@ internal struct AuthCodeSender { } private func startSendCodeJob() async throws { - _ = try await config.temporalClient.startWorkflow( - type: SendTransactionalMessageWorkflow.self, - options: .init( - id: "send-transactional-message-\(config.payload.codeModelId)", - taskQueue: "default-queue" - ), - input: .init( - content: MessageFormatterService - .craftVerificationCodeMessage( - code: config.payload.code - ), - toPhoneNumber: config.payload.phoneNumber + do { + _ = try await config.temporalClient.startWorkflow( + type: SendTransactionalMessageWorkflow.self, + options: .init( + id: "send-transactional-message-\(config.payload.codeModelId)", + taskQueue: "default-queue" + ), + input: .init( + content: MessageFormatterService + .craftVerificationCodeMessage( + code: config.payload.code + ), + toPhoneNumber: config.payload.phoneNumber + ) ) - ) + } catch { + // Capture the current stack trace + let stackTrace = Thread.callStackSymbols.joined(separator: "\n") + + config.logger.info( + "Error occurred while starting workflow to send authentication code", + metadata: [ + "to": .string("\(String(describing: Self.self)).\(#function)"), + "error": .string(error.localizedDescription), + "stackTrace": .string(stackTrace), + ] + ) + + throw error + } } private func getCodeDeletionTime() -> Date { diff --git a/Backend/Sources/App/configure.swift b/Backend/Sources/App/configure.swift index 0b530efd..3d7e3d55 100644 --- a/Backend/Sources/App/configure.swift +++ b/Backend/Sources/App/configure.swift @@ -10,12 +10,20 @@ import JWT import Vapor internal func configure(_ app: Application) async throws { - try await AppConfigurator( - app: app, - config: .init( - shouldSetupAPI: true - ) - ).configure() + registerCommands(app: app) + let shouldSetupAPI = !["temporal-worker"].contains(CommandLine.arguments.last) + if shouldSetupAPI { + try await AppConfigurator( + app: app, + config: .init( + shouldSetupAPI: true + ) + ).configure() + } +} + +internal func registerCommands(app: Application) { + app.asyncCommands.use(TemporalWorkerCommand(), as: "temporal-worker") } internal struct AppConfigurator { @@ -33,7 +41,6 @@ internal struct AppConfigurator { /// Configures the entire application public func configure() async throws { - registerCommands() registerMiddleware() let hasDatabaseURLs = (primaryDatabaseURL != nil) || (regionalDatabaseURL != nil) if hasDatabaseURLs { @@ -41,10 +48,6 @@ internal struct AppConfigurator { } } - private func registerCommands() { - app.asyncCommands.use(TemporalWorkerCommand(), as: "temporal-worker") - } - private func registerMiddleware() { app.middleware.use(ErrorStringMiddleware()) } @@ -53,6 +56,7 @@ internal struct AppConfigurator { try await DatabaseConfigurator(app: app).configureDatabases() try await startPrometheusService() + app.logger.info("Should setup API: \(config.shouldSetupAPI). Environment: \(environment)") if config.shouldSetupAPI { try registerControllers() try await addAuthenticationJWTKey() diff --git a/Backend/docker-compose.yml b/Backend/docker-compose.yml index b7eed9e1..3571b5e4 100644 --- a/Backend/docker-compose.yml +++ b/Backend/docker-compose.yml @@ -20,6 +20,14 @@ services: condition: service_healthy temporal: condition: service_started + grafana: + condition: service_started + prometheus: + condition: service_started + loki: + condition: service_started + promtail: + condition: service_started environment: RUN_COMMAND: "serve --hostname 0.0.0.0" METRICS_PORT: "6834" @@ -33,7 +41,7 @@ services: volumes: - ./:/app/ - /app/.build - - api_build:/app/.build + - worker_build:/app/.build build: context: . dockerfile: ./DevDockerfile @@ -46,6 +54,14 @@ services: condition: service_healthy temporal: condition: service_started + grafana: + condition: service_started + prometheus: + condition: service_started + loki: + condition: service_started + promtail: + condition: service_started environment: RUN_COMMAND: "temporal-worker" METRICS_PORT: "6835" diff --git a/Backend/infra/fly/fly-jobs.toml b/Backend/infra/fly/fly-jobs.toml deleted file mode 100644 index 273fe1fb..00000000 --- a/Backend/infra/fly/fly-jobs.toml +++ /dev/null @@ -1,22 +0,0 @@ -# fly.toml app configuration file generated for backend-solitary-voice-65 on 2024-12-14T04:09:07-08:00 -# -# See https://fly.io/docs/reference/configuration/ for information about how to use this file. -# - -app = 'automa-backend-workers-__FLY_ENVIRONMENT__' -primary_region = 'jnb' - -[build] - -[env] -RUN_COMMAND = "vapor-queues" - -[[vm]] -memory = '1024mb' -cpu_kind = 'shared' -cpus = 2 -min_machines_running = 1 - -[metrics] -port = 6834 -path = "/metrics" diff --git a/Backend/infra/fly/temporal-worker.toml b/Backend/infra/fly/temporal-worker.toml new file mode 100644 index 00000000..e40e760f --- /dev/null +++ b/Backend/infra/fly/temporal-worker.toml @@ -0,0 +1,21 @@ +app = 'automa-backend-temporal-worker-__FLY_ENVIRONMENT__' +primary_region = 'jnb' + +[env] +RUN_COMMAND = 'temporal-worker' + +[http_service] +internal_port = 8080 +force_https = true +auto_stop_machines = 'off' +auto_start_machines = false +min_machines_running = 0 + +[[vm]] +memory = '1gb' +cpu_kind = 'shared' +cpus = 2 + +[[metrics]] +port = 6834 +path = '/metrics' diff --git a/Backend/infra/fly/temporal/Dockerfile b/Backend/infra/fly/temporal/Dockerfile new file mode 100644 index 00000000..76f1febf --- /dev/null +++ b/Backend/infra/fly/temporal/Dockerfile @@ -0,0 +1,27 @@ +FROM temporalio/ui:2.27.2 as ui +FROM temporalio/server:1.24.1.0 as server + +WORKDIR /etc/temporal + +FROM temporalio/auto-setup:1.24.1.0 as final + +COPY --from=ui --chown=temporal:temporal /home/ui-server /home/ui-server +RUN rm -rf /home/ui-server/config/* + +EXPOSE 7233 8080 + +ENV DB=postgres12 +ENV DB_PORT=5432 + +ENV BIND_ON_IP=0.0.0.0 +ENV TEMPORAL_BROADCAST_ADDRESS=0.0.0.0 +ENV DEFAULT_NAMESPACE=default + +ENV DYNAMIC_CONFIG_FILE_PATH=/etc/temporal/config/dynamicconfig/docker.yaml + +# These two .sh files are defined below +COPY ./start.sh /etc/temporal/start.sh +COPY ./start-ui.sh /etc/temporal/start-ui.sh +CMD ["autosetup"] + +ENTRYPOINT ["/etc/temporal/start.sh"] diff --git a/Backend/infra/fly/temporal/database.toml b/Backend/infra/fly/temporal/database.toml new file mode 100644 index 00000000..d394ee63 --- /dev/null +++ b/Backend/infra/fly/temporal/database.toml @@ -0,0 +1,5 @@ +app = 'automa-backend-temporal-postgres-sandbox' +primary_region = 'jnb' + +[build] +- diff --git a/Backend/infra/fly/temporal/fly.toml b/Backend/infra/fly/temporal/fly.toml new file mode 100644 index 00000000..23701275 --- /dev/null +++ b/Backend/infra/fly/temporal/fly.toml @@ -0,0 +1,49 @@ +app = 'automa-backend-temporal-sandbox' +primary_region = 'jnb' + +[processes] +app = '/etc/temporal/entrypoint.sh autosetup' +ui = '/etc/temporal/start-ui.sh' + +[env] +LOG_LEVEL = 'debug' + +[[services]] +protocol = 'tcp' +internal_port = 7233 +auto_stop_machines = true +auto_start_machines = true +min_machines_running = 1 +processes = ['app'] + +[[services.ports]] +port = 7233 +# handlers = ['http'] # Do not expose to public, public ipv4/6 should be removed already +# alpn h2 is needed for the grpc protocol +[services.ports.tls_options] +alpn = ['h2'] + +[[services.tcp_checks]] +interval = '10s' +timeout = '2s' +grace_period = '5s' + +[[services]] +protocol = 'tcp' +internal_port = 8080 +min_machines_running = 0 +processes = ['ui'] + +# Ideally, don't expose the UI to the public, keep it behind a CDN (eg Cloudflare) and whitelist the IP +# or make it public but set up SSO +[[services.ports]] +port = 8080 +# handlers = ['http'] + +# [[services.ports]] +# port = 443 +# handlers = ['tls', 'http'] + +[[vm]] +size = 'shared-cpu-1x' +memory = "1gb" diff --git a/Backend/infra/fly/temporal/start-ui.sh b/Backend/infra/fly/temporal/start-ui.sh new file mode 100755 index 00000000..1c1d58e3 --- /dev/null +++ b/Backend/infra/fly/temporal/start-ui.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +# The Temporal UI Server expects the script to be executed at the `/home/ui-server` +cd /home/ui-server +# Assuming your server/ui is running in the same Fly app (but different process) +# Change this to another fly app or IP if running elsewhere. +export TEMPORAL_ADDRESS="${FLY_APP_NAME}.flycast:7233" +export TEMPORAL_CSRF_COOKIE_INSECURE=true +./start-ui-server.sh diff --git a/Backend/infra/fly/temporal/start.sh b/Backend/infra/fly/temporal/start.sh new file mode 100755 index 00000000..b4203bd1 --- /dev/null +++ b/Backend/infra/fly/temporal/start.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +# This is called via the fly.toml: +# [processes] +# server = "/etc/temporal/entrypoint.sh autosetup" +# ui = "/etc/temporal/start-ui.sh" +# +# This script itself is called in the Dockerfile: +# ENTRYPOINT ["/etc/temporal/start.sh"] +exec "$@" diff --git a/package.json b/package.json index 79d8744a..161f433d 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "deploy:backend:staging": "npm run fly:config -- fly staging | npm run deploy:to:fly", "deploy:backend:production": "npm run fly:config -- fly production | npm run deploy:to:fly", "deploy:to:fly": "tail -n 1 | xargs -Ixx cp xx Backend/.fly.toml && cd Backend && flyctl deploy --ha=false --config=.fly.toml --build-secret GITHUB_SSH_AUTHENTICATION_TOKEN=$(cat ./infra/docker-secrets/GITHUB_SSH_AUTHENTICATION_TOKEN)", - "deploy:worker:sandbox": "npm run fly:config -- fly-jobs sandbox | npm run deploy:to:fly", - "deploy:worker:production": "npm run fly:config -- fly-jobs production | npm run deploy:to:fly", + "deploy:worker:sandbox": "npm run fly:config -- temporal-worker sandbox | npm run deploy:to:fly", + "deploy:worker:production": "npm run fly:config -- temporal-worker production | npm run deploy:to:fly", "build:all": "npx npm-run-all --parallel build:ui-kit build:backend", "build:ui-kit": "cd App/AutomaUIKit && swift build", "build:backend": "cd Backend && swift build", From d8ee66e08600ea108ce5c362fc8c4bce3ebce07a Mon Sep 17 00:00:00 2001 From: William Date: Thu, 11 Dec 2025 09:44:07 +0200 Subject: [PATCH 13/16] fix(ApplicationExtensions): use hostname, not static string for instrumentation hostname temporal client --- .../Sources/App/Extensions/Vapor/ApplicationExtensions.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Backend/Sources/App/Extensions/Vapor/ApplicationExtensions.swift b/Backend/Sources/App/Extensions/Vapor/ApplicationExtensions.swift index 40176a88..42614804 100644 --- a/Backend/Sources/App/Extensions/Vapor/ApplicationExtensions.swift +++ b/Backend/Sources/App/Extensions/Vapor/ApplicationExtensions.swift @@ -8,13 +8,14 @@ import Vapor internal extension Application { var temporalClient: TemporalClient { + let hostname = try! TemporalClient.getServerHostnameFromEnv() ?? "temporal" try! TemporalClient( target: .dns( - host: TemporalClient.getServerHostnameFromEnv(), + host: hostname, port: 7_233 ), transportSecurity: .plaintext, - configuration: .init(instrumentation: .init(serverHostname: "temporal")), + configuration: .init(instrumentation: .init(serverHostname: hostname)), logger: Logger(label: "temporal-client") ) } From 391415e9c932540b0379a6376b06c80ec0912029 Mon Sep 17 00:00:00 2001 From: William Date: Thu, 11 Dec 2025 10:46:36 +0200 Subject: [PATCH 14/16] fixup! feat: deploy temporal service, temporal worker refactor, debug steps and temporary debug code to get symbolicated stack trace in production fly.io --- .../Sources/App/Extensions/Vapor/ApplicationExtensions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Backend/Sources/App/Extensions/Vapor/ApplicationExtensions.swift b/Backend/Sources/App/Extensions/Vapor/ApplicationExtensions.swift index 42614804..8c6a7eb2 100644 --- a/Backend/Sources/App/Extensions/Vapor/ApplicationExtensions.swift +++ b/Backend/Sources/App/Extensions/Vapor/ApplicationExtensions.swift @@ -9,7 +9,7 @@ import Vapor internal extension Application { var temporalClient: TemporalClient { let hostname = try! TemporalClient.getServerHostnameFromEnv() ?? "temporal" - try! TemporalClient( + return try! TemporalClient( target: .dns( host: hostname, port: 7_233 From 6ddffd0f8b894b30722cc6a04cd018b7f726a4ec Mon Sep 17 00:00:00 2001 From: William Date: Thu, 11 Dec 2025 10:47:33 +0200 Subject: [PATCH 15/16] fix(Dockerfile): use old develop dockerfile --- Backend/Dockerfile | 48 ++++++++++++++++++++-------------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/Backend/Dockerfile b/Backend/Dockerfile index d442425e..0987eec2 100644 --- a/Backend/Dockerfile +++ b/Backend/Dockerfile @@ -1,7 +1,7 @@ # ================================ # Build image # ================================ -FROM swift:6.2-noble AS build +FROM swift:6.2.0-jammy AS build # Install OS updates RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ @@ -13,44 +13,41 @@ RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ # Set up a build area WORKDIR /build -RUN --mount=type=secret,id=GITHUB_SSH_AUTHENTICATION_TOKEN \ - TOKEN=$(cat /run/secrets/GITHUB_SSH_AUTHENTICATION_TOKEN) && \ - git config --global url."https://x-access-token:${TOKEN}@github.com/".insteadOf "https://github.com/" - # First just resolve dependencies. # This creates a cached layer that can be reused # as long as your Package.swift/Package.resolved # files do not change. -COPY ./Package.* ./ -COPY DataTypes ./DataTypes -RUN swift package resolve \ - $([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true) +RUN --mount=type=secret,id=GITHUB_SSH_AUTHENTICATION_TOKEN \ + TOKEN=$(cat /run/secrets/GITHUB_SSH_AUTHENTICATION_TOKEN) && \ + git config --global url."https://x-access-token:${TOKEN}@github.com/".insteadOf "https://github.com/" + +COPY Package.swift . +COPY Package.resolved . +COPY DataTypes ./DataTypes +RUN --mount=type=cache,target=/build/.build swift package resolve # Copy entire repo into container COPY . . -RUN mkdir /staging - -# Build the application, with optimizations, with static linking, and using jemalloc +# Build everything, with optimizations, with static linking, and using jemalloc # N.B.: The static version of jemalloc is incompatible with the static Swift runtime. -RUN --mount=type=cache,target=/build/.build \ - swift build -c release \ - --product App \ +RUN --mount=type=cache,target=/build/.build swift build -c release \ --static-swift-stdlib \ - -Xlinker -ljemalloc \ - -Xswiftc -g && \ - # Copy main executable to staging area - cp "$(swift build -c release --show-bin-path)/App" /staging && \ - # Copy resources bundled by SPM to staging area - find -L "$(swift build -c release --show-bin-path)" -regex '.*\.resources$' -exec cp -Ra {} /staging \; + -Xlinker -ljemalloc # Switch to the staging area WORKDIR /staging +# Copy main executable to staging area +RUN --mount=type=cache,target=/build/.build cp "$(swift build --package-path /build -c release --show-bin-path)/App" ./ + # Copy static swift backtracer binary to staging area RUN cp "/usr/libexec/swift/linux/swift-backtrace-static" ./ +# Copy resources bundled by SPM to staging area +RUN --mount=type=cache,target=/build/.build find -L "$(swift build --package-path /build -c release --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \; + # Copy any resources from the public directory and views directory if the directories exist # Ensure that by default, neither the directory nor any of its contents are writable. RUN [ -d /build/Public ] && { mv /build/Public ./Public && chmod -R a-w ./Public; } || true @@ -59,7 +56,7 @@ RUN [ -d /build/Resources ] && { mv /build/Resources ./Resources && chmod -R a-w # ================================ # Run image # ================================ -FROM ubuntu:noble +FROM ubuntu:jammy # Make sure all system packages are up to date, and install only essential packages. RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ @@ -69,10 +66,8 @@ RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ libjemalloc2 \ ca-certificates \ tzdata \ - # If your app or its dependencies import FoundationNetworking, also install `libcurl4`. - libcurl4 \ - # If your app or its dependencies import FoundationXML, also install `libxml2`. - libxml2 \ + && apt-get install bash \ + && apt-get install -y openssl libssl-dev libcurl4 libxml2 \ && rm -r /var/lib/apt/lists/* # Create a vapor user and group with /app as its home directory @@ -97,4 +92,3 @@ EXPOSE 8080 EXPOSE 6834 CMD /bin/bash -c "eval './App $RUN_COMMAND'" -# CMD /bin/bash -c "sleep 100000" From f1bf65c9b08a2317b9be65dc727a0e98d42eab24 Mon Sep 17 00:00:00 2001 From: William Date: Wed, 11 Feb 2026 13:16:01 +0200 Subject: [PATCH 16/16] fix(Package.swift): update temporal-swift-sdk, new verions fixes linux runtime + unsymbolicated stacktrace errors --- Backend/Package.resolved | 10 +++++----- Backend/Package.swift | 2 +- .../Sources/App/Commands/TemporalWorkerCommand.swift | 3 +-- .../App/Extensions/Vapor/ApplicationExtensions.swift | 4 ++-- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Backend/Package.resolved b/Backend/Package.resolved index 31c571db..2cb07f95 100644 --- a/Backend/Package.resolved +++ b/Backend/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "25741725534bf349d85342c3c4f09746825ae3902642ee651a2b1cc0ac192cd4", + "originHash" : "da402b8112bb9effe58819a094f653dad53d0e1dc5c9038d949d2b991c47a36e", "pins" : [ { "identity" : "alamofire", @@ -330,8 +330,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-configuration.git", "state" : { - "revision" : "9d5e40727efd060fb9b41b69932738f478abaa43", - "version" : "0.2.0" + "revision" : "6ffef195ed4ba98ee98029970c94db7edc60d4c6", + "version" : "1.0.1" } }, { @@ -546,8 +546,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-temporal-sdk.git", "state" : { - "revision" : "35235775afe122a2a5e1fd3e400f02d3dc3b6d1b", - "version" : "0.4.0" + "revision" : "55a222f02f279333489c7a1657195d7489a92cf2", + "version" : "0.6.0" } }, { diff --git a/Backend/Package.swift b/Backend/Package.swift index 2ca1c9ea..bd3e9e2b 100644 --- a/Backend/Package.swift +++ b/Backend/Package.swift @@ -30,7 +30,7 @@ public let package = Package( .package(url: "https://github.com/GetAutomaApp/AutomaUtilities", branch: "main"), .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.11.1"), .package(url: "https://github.com/lovetodream/swift-log-loki.git", branch: "main"), - .package(url: "https://github.com/apple/swift-temporal-sdk.git", from: "0.4.0") + .package(url: "https://github.com/apple/swift-temporal-sdk.git", from: "0.6.0") ], targets: [ .executableTarget( diff --git a/Backend/Sources/App/Commands/TemporalWorkerCommand.swift b/Backend/Sources/App/Commands/TemporalWorkerCommand.swift index 00e00cbe..24c670a4 100644 --- a/Backend/Sources/App/Commands/TemporalWorkerCommand.swift +++ b/Backend/Sources/App/Commands/TemporalWorkerCommand.swift @@ -1,5 +1,5 @@ // TemporalWorkerCommand.swift -// Copyright (c) 2025 GetAutomaApp +// Copyright (c) 2026 GetAutomaApp // All source code and related assets are the property of GetAutomaApp. // All rights reserved. @@ -14,7 +14,6 @@ struct TemporalWorkerCommand: AsyncCommand { } func run(using context: CommandContext, signature _: Signature) async throws { - let result = 5 ... 1 try await AppConfigurator( app: context.application, config: .init( diff --git a/Backend/Sources/App/Extensions/Vapor/ApplicationExtensions.swift b/Backend/Sources/App/Extensions/Vapor/ApplicationExtensions.swift index 8c6a7eb2..efbc00af 100644 --- a/Backend/Sources/App/Extensions/Vapor/ApplicationExtensions.swift +++ b/Backend/Sources/App/Extensions/Vapor/ApplicationExtensions.swift @@ -1,5 +1,5 @@ // ApplicationExtensions.swift -// Copyright (c) 2025 GetAutomaApp +// Copyright (c) 2026 GetAutomaApp // All source code and related assets are the property of GetAutomaApp. // All rights reserved. @@ -8,7 +8,7 @@ import Vapor internal extension Application { var temporalClient: TemporalClient { - let hostname = try! TemporalClient.getServerHostnameFromEnv() ?? "temporal" + let hostname = try! TemporalClient.getServerHostnameFromEnv() return try! TemporalClient( target: .dns( host: hostname,