From d597758e634ab84639a12bfb8eb756a0b151b231 Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Sat, 14 Dec 2024 13:37:50 +0100 Subject: [PATCH] Add error forcing mechanics --- Sources/App/Commands/Ingest.swift | 33 +++++++++++++++++-- .../Core/Dependencies/EnvironmentClient.swift | 19 ++++++++++- app.yml | 1 + 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/Sources/App/Commands/Ingest.swift b/Sources/App/Commands/Ingest.swift index 820c68c7f..96d63fd43 100644 --- a/Sources/App/Commands/Ingest.swift +++ b/Sources/App/Commands/Ingest.swift @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import Dependencies import Fluent import PostgresKit import Vapor @@ -168,13 +169,17 @@ func ingest(client: Client, extension Ingestion { static func ingest(client: Client, database: Database, package: Joined) async { let result = await Result { () async throws(Ingestion.Error) -> Joined in + @Dependency(\.environment) var environment Current.logger().info("Ingesting \(package.package.url)") // Even though we have a `Joined` as a parameter, we must not rely // on `repository` for owner/name as it will be nil when a package is first ingested. // The only way to get `owner` and `repository` here is by parsing them from the URL. let (owner, repository) = try await run { - try Github.parseOwnerName(url: package.model.url) + if environment.shouldFail(failureMode: .invalidURL) { + throw Github.Error.invalidURL(package.model.url) + } + return try Github.parseOwnerName(url: package.model.url) } rethrowing: { _ in Ingestion.Error.invalidURL(packageId: package.model.id!, url: package.model.url) } @@ -223,7 +228,12 @@ extension Ingestion { static func findOrCreateRepository(on database: Database, for package: Joined) async throws(Ingestion.Error) -> Repository { try await run { - try await Repository.findOrCreate(on: database, for: package.model) + @Dependency(\.environment) var environment + if environment.shouldFail(failureMode: .findOrCreateRepositoryFailed) { + throw Abort(.internalServerError) + } + + return try await Repository.findOrCreate(on: database, for: package.model) } rethrowing: { Ingestion.Error( packageId: package.model.id!, @@ -251,6 +261,11 @@ extension Ingestion { static func fetchMetadata(client: Client, package: Package, owner: String, repository: String) async throws(Github.Error) -> (Github.Metadata, Github.License?, Github.Readme?) { + @Dependency(\.environment) var environment + if environment.shouldFail(failureMode: .fetchMetadataFailed) { + throw Github.Error.requestFailed(.internalServerError) + } + async let metadata = try await Current.fetchMetadata(client, owner, repository) async let license = await Current.fetchLicense(client, owner, repository) async let readme = await Current.fetchReadme(client, owner, repository) @@ -286,6 +301,20 @@ func updateRepository(on database: Database, readmeInfo: Github.Readme?, s3Readme: S3Readme?, fork: Fork? = nil) async throws(Ingestion.Error.UnderlyingError) { + @Dependency(\.environment) var environment + if environment.shouldFail(failureMode: .noRepositoryMetadata) { + throw .noRepositoryMetadata(owner: repository.owner, name: repository.name) + } + if environment.shouldFail(failureMode: .repositorySaveFailed) { + throw .repositorySaveFailed(owner: repository.owner, + name: repository.name, + details: "TestError") + } + if environment.shouldFail(failureMode: .repositorySaveUniqueViolation) { + throw .repositorySaveUniqueViolation(owner: repository.owner, + name: repository.name, + details: "TestError") + } guard let repoMetadata = metadata.repository else { throw .noRepositoryMetadata(owner: repository.owner, name: repository.name) } diff --git a/Sources/App/Core/Dependencies/EnvironmentClient.swift b/Sources/App/Core/Dependencies/EnvironmentClient.swift index b3da31ab7..166993620 100644 --- a/Sources/App/Core/Dependencies/EnvironmentClient.swift +++ b/Sources/App/Core/Dependencies/EnvironmentClient.swift @@ -44,6 +44,16 @@ struct EnvironmentClient { var mastodonCredentials: @Sendable () -> Mastodon.Credentials? var mastodonPost: @Sendable (_ client: Client, _ post: String) async throws -> Void var random: @Sendable (_ range: ClosedRange) -> Double = { XCTFail("random"); return Double.random(in: $0) } + + enum FailureMode: String { + case fetchMetadataFailed + case findOrCreateRepositoryFailed + case invalidURL + case noRepositoryMetadata + case repositorySaveFailed + case repositorySaveUniqueViolation + } + var shouldFail: @Sendable (_ failureMode: FailureMode) -> Bool = { _ in false } } @@ -100,7 +110,14 @@ extension EnvironmentClient: DependencyKey { .map(Mastodon.Credentials.init(accessToken:)) }, mastodonPost: { client, message in try await Mastodon.post(client: client, message: message) }, - random: { range in Double.random(in: range) } + random: { range in Double.random(in: range) }, + shouldFail: { failureMode in + let shouldFail = Environment.get("FAILURE_MODE") + .map { Data($0.utf8) } + .flatMap { try? JSONDecoder().decode([String: Double].self, from: $0) } ?? [:] + guard let rate = shouldFail[failureMode.rawValue] else { return false } + return Double.random(in: 0...1) <= rate + } ) } } diff --git a/app.yml b/app.yml index 0245834ff..e2306bc2b 100644 --- a/app.yml +++ b/app.yml @@ -46,6 +46,7 @@ x-shared: &shared DATABASE_USERNAME: ${DATABASE_USERNAME} DATABASE_PASSWORD: ${DATABASE_PASSWORD} DATABASE_USE_TLS: ${DATABASE_USE_TLS} + FAILURE_MODE: ${FAILURE_MODE} GITHUB_TOKEN: ${GITHUB_TOKEN} GITLAB_API_TOKEN: ${GITLAB_API_TOKEN} GITLAB_PIPELINE_LIMIT: ${GITLAB_PIPELINE_LIMIT}