Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework error handling #3549

Merged
merged 33 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
1576f6c
Expand test case to ensure we're advancing to analysis in case of an …
finestructure Jul 27, 2023
64c6028
Temporarily disable new check
finestructure Aug 30, 2024
f3a4609
Move existing ingestion logic into ingestOriginal
finestructure Aug 30, 2024
81b770f
Make s3 readme functions throws(S3ReadmeError)
finestructure Aug 30, 2024
ff18387
wip
finestructure Sep 1, 2024
a7f9fb1
Add back getFork
finestructure Dec 7, 2024
7f1e474
Add warning about S3ReadmeError → S3Store.Error
finestructure Dec 7, 2024
76cfd4d
Add id, underlyingError to Ingestion.Error
finestructure Dec 8, 2024
c39639f
Avoid repository.package.id!
finestructure Dec 9, 2024
bbdfbbe
Update recordError to handle Ingestion.Error
finestructure Dec 9, 2024
0e4f349
Ingestion specific updatePackage - all tests pass
finestructure Dec 9, 2024
c64cfa2
Update test_ingest_unique_owner_name_violation to reflect new error h…
finestructure Dec 11, 2024
6fba70e
Disentangle updatePackage/recordError into Common, Analyze, Ingestion…
finestructure Dec 11, 2024
eb4abf7
Remove Common.updatePackage, clean up
finestructure Dec 11, 2024
9d6551d
Move things around
finestructure Dec 12, 2024
ba4c7c5
Cleanup
finestructure Dec 12, 2024
0f1c2bb
Add protocol ProcessingError
finestructure Dec 12, 2024
43a7ca9
Address warnings in Common
finestructure Dec 12, 2024
0849080
Extended test_ingest_continue_on_error now passing
finestructure Dec 12, 2024
644a87e
S3ReadmeError → S3Readme.Error
finestructure Dec 12, 2024
ff81141
Add run { } throwing: { }
finestructure Dec 12, 2024
481d641
Ingestion.ingestNew → Ingestion.ingest
finestructure Dec 13, 2024
1edf345
Rename run(_:throwing:) to run(_:rethrowing), bundle with run(_:defer:)
finestructure Dec 13, 2024
b09fe99
Cleanup
finestructure Dec 13, 2024
1dcbd09
Restore async let in fetchMetadata
finestructure Dec 13, 2024
ec8b61d
Cleanup
finestructure Dec 13, 2024
b2c2ae5
Re-enable test
finestructure Dec 13, 2024
04ef182
Explicitly analyse packages and verify processing stage
finestructure Dec 13, 2024
0c0ebf6
Turn `Ingestion.fetchMetadata` into `throws(Github.Error)`
finestructure Dec 14, 2024
d597758
Add error forcing mechanics
finestructure Dec 14, 2024
11e3052
Improve error logging
finestructure Dec 14, 2024
0079140
Update error message expectation
finestructure Dec 15, 2024
475ce9d
Make IngestCommand.run non-throwing
finestructure Dec 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions Sources/App/Commands/Analyze.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,7 @@ extension Analyze {
}
}

try await updatePackages(client: client,
database: database,
results: packageResults,
stage: .analysis)
try await updatePackages(client: client, database: database, results: packageResults)

try await RecentPackage.refresh(on: database)
try await RecentRelease.refresh(on: database)
Expand Down
194 changes: 104 additions & 90 deletions Sources/App/Commands/Common.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,111 +17,125 @@ import PostgresKit
import Vapor


/// Update packages (in the `[Result<Joined<Package, Repository>, Error>]` array).
///
/// - Parameters:
/// - client: `Client` object
/// - database: `Database` object
/// - results: `Joined<Package, Repository>` results to update
/// - stage: Processing stage
func updatePackages(client: Client,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updatePackages (note the plural) was only called from analysis. Moving this into the Analyze namespace (which I'll update to Analysis at some point in the future) makes this explicit.

database: Database,
results: [Result<Joined<Package, Repository>, Error>],
stage: Package.ProcessingStage) async throws {
do {
let total = results.count
let errors = results.filter(\.isError).count
let errorRate = total > 0 ? 100.0 * Double(errors) / Double(total) : 0.0
switch errorRate {
case 0:
Current.logger().info("Updating \(total) packages for stage '\(stage)'")
case 0..<20:
Current.logger().info("Updating \(total) packages for stage '\(stage)' (errors: \(errors))")
default:
Current.logger().critical("updatePackages: unusually high error rate: \(errors)/\(total) = \(errorRate)%")
}
}
for result in results {
do {
try await updatePackage(client: client, database: database, result: result, stage: stage)
} catch {
Current.logger().critical("updatePackage failed: \(error)")
}
}

Current.logger().debug("updateStatus ops: \(results.count)")
// TODO: Adopt ProcessingError also in Analysis and then factor out generic parts back into Common
protocol ProcessingError: Error, CustomStringConvertible {
associatedtype UnderlyingError: Error & CustomStringConvertible
var packageId: Package.Id { get }
var underlyingError: UnderlyingError { get }
var level: Logger.Level { get }
var status: Package.Status { get }
}


func updatePackage(client: Client,
database: Database,
result: Result<Joined<Package, Repository>, Error>,
stage: Package.ProcessingStage) async throws {
switch result {
case .success(let res):
let pkg = res.package
if stage == .ingestion && pkg.status == .new {
// newly ingested package: leave status == .new for fast-track
// analysis
} else {
pkg.status = .ok
// TODO: Leaving this extension here for now in order to group the updating/error reporting in one place for both Ingestion and Analysis. Eventually these should either go to their respective files or move common parts into a Common namespace.
extension Analyze {
/// Update packages (in the `[Result<Joined<Package, Repository>, Error>]` array).
///
/// - Parameters:
/// - client: `Client` object
/// - database: `Database` object
/// - results: `Joined<Package, Repository>` results to update
/// - stage: Processing stage
static func updatePackages(client: Client,
database: Database,
results: [Result<Joined<Package, Repository>, Error>]) async throws {
do {
let total = results.count
let errors = results.filter(\.isError).count
let errorRate = total > 0 ? 100.0 * Double(errors) / Double(total) : 0.0
switch errorRate {
case 0:
Current.logger().info("Updating \(total) packages for stage 'analysis'")
case 0..<20:
Current.logger().info("Updating \(total) packages for stage 'analysis' (errors: \(errors))")
default:
Current.logger().critical("updatePackages: unusually high error rate: \(errors)/\(total) = \(errorRate)%")
}
pkg.processingStage = stage
}
for result in results {
do {
try await pkg.update(on: database)
try await updatePackage(client: client, database: database, result: result)
} catch {
Current.logger().report(error: error)
Current.logger().critical("updatePackage failed: \(error)")
}
}

// PSQLError also conforms to DatabaseError but we want to intercept it specifically,
// because it allows us to log more concise error messages via serverInfo[.message]
case let .failure(error) where error is PSQLError:
// Escalate database errors to critical
let error = error as! PSQLError
let msg = error.serverInfo?[.message] ?? String(reflecting: error)
Current.logger().critical("\(msg)")
try await recordError(database: database, error: error, stage: stage)
Current.logger().debug("updateStatus ops: \(results.count)")
}

case let .failure(error) where error is DatabaseError:
// Escalate database errors to critical
Current.logger().critical("\(String(reflecting: error))")
try await recordError(database: database, error: error, stage: stage)
static func updatePackage(client: Client,
database: Database,
result: Result<Joined<Package, Repository>, Error>) async throws {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We temporarily introduce specific static updatePackage functions on Analysis (and further down Ingestion) to deal with the fact that Ingestion has transitioned over to a new error type which we're throwing in typed throws while analysis is still using untyped throws and any Error as its error type.

The plan is to consolidate the two again once analysis moves to typed throws as well and base them both on the shared protocol ProcessingError.

switch result {
case .success(let res):
try await res.package.update(on: database, status: .ok, stage: .analysis)

case let .failure(error):
Current.logger().report(error: error)
try await recordError(database: database, error: error, stage: stage)
// PSQLError also conforms to DatabaseError but we want to intercept it specifically,
// because it allows us to log more concise error messages via serverInfo[.message]
case let .failure(error) where error is PSQLError:
// Escalate database errors to critical
let error = error as! PSQLError
let msg = error.serverInfo?[.message] ?? String(reflecting: error)
Current.logger().critical("\(msg)")
try await recordError(database: database, error: error)

case let .failure(error) where error is DatabaseError:
// Escalate database errors to critical
Current.logger().critical("\(String(reflecting: error))")
try await recordError(database: database, error: error)

case let .failure(error):
Current.logger().report(error: error)
try await recordError(database: database, error: error)
}
}
}

static func recordError(database: Database, error: Error) async throws {
func setStatus(id: Package.Id?, status: Package.Status) async throws {
guard let id = id else { return }
try await Package.query(on: database)
.filter(\.$id == id)
.set(\.$processingStage, to: .analysis)
.set(\.$status, to: status)
.update()
}

guard let error = error as? AppError else { return }

func recordError(database: Database,
error: Error,
stage: Package.ProcessingStage) async throws {
func setStatus(id: Package.Id?, status: Package.Status) async throws {
guard let id = id else { return }
try await Package.query(on: database)
.filter(\.$id == id)
.set(\.$processingStage, to: stage)
.set(\.$status, to: status)
.update()
switch error {
case let .analysisError(id, _):
try await setStatus(id: id, status: .analysisFailed)
case .envVariableNotSet, .shellCommandFailed:
break
case let .genericError(id, _):
try await setStatus(id: id, status: .ingestionFailed)
case let .invalidPackageCachePath(id, _):
try await setStatus(id: id, status: .invalidCachePath)
case let .cacheDirectoryDoesNotExist(id, _):
try await setStatus(id: id, status: .cacheDirectoryDoesNotExist)
case let .invalidRevision(id, _):
try await setStatus(id: id, status: .analysisFailed)
case let .noValidVersions(id, _):
try await setStatus(id: id, status: .noValidVersions)
}
}
}

guard let error = error as? AppError else { return }

switch error {
case let .analysisError(id, _):
try await setStatus(id: id, status: .analysisFailed)
case .envVariableNotSet, .shellCommandFailed:
break
case let .genericError(id, _):
try await setStatus(id: id, status: .ingestionFailed)
case let .invalidPackageCachePath(id, _):
try await setStatus(id: id, status: .invalidCachePath)
case let .cacheDirectoryDoesNotExist(id, _):
try await setStatus(id: id, status: .cacheDirectoryDoesNotExist)
case let .invalidRevision(id, _):
try await setStatus(id: id, status: .analysisFailed)
case let .noValidVersions(id, _):
try await setStatus(id: id, status: .noValidVersions)
// TODO: Leaving this extension here for now in order to group the updating/error reporting in one place for both Ingestion and Analysis. Eventually these should either go to their respective files or move common parts into a Common namespace.
extension Ingestion {
static func updatePackage(client: Client,
database: Database,
result: Result<Joined<Package, Repository>, Ingestion.Error>,
stage: Package.ProcessingStage) async throws {
switch result {
case .success(let res):
// for newly ingested package leave status == .new in order to fast-track analysis
let updatedStatus: Package.Status = res.package.status == .new ? .new : .ok
try await res.package.update(on: database, status: updatedStatus, stage: stage)
case .failure(let failure):
Current.logger().log(level: failure.level, "\(failure)")
try await Package.update(for: failure.packageId, on: database, status: failure.status, stage: stage)
}
}
}
Loading
Loading