Skip to content

Commit

Permalink
Cleanup Lifecycle & ServiceLifecycle
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuawright11 committed Jul 16, 2024
1 parent 82c39e2 commit 2b51325
Show file tree
Hide file tree
Showing 19 changed files with 379 additions and 523 deletions.
12 changes: 3 additions & 9 deletions Alchemy/AlchemyX/Router+Resource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,15 @@ extension Application {
) -> Self where R.Identifier: SQLValueConvertible & LosslessStringConvertible {
use(ResourceController<R>(db: db, tableName: table))
if updateTable {
Container.main.lifecycleServices.append(ResourceMigrationService<R>(db: db))
Container.onStart {
try await db.updateSchema(R.self)
}
}

return self
}
}

struct ResourceMigrationService<R: Resource>: ServiceLifecycle.Service, @unchecked Sendable {
let db: Database

func run() async throws {
try await db.updateSchema(R.self)
}
}

extension Router {
@discardableResult
public func useResource<R: Resource>(
Expand Down
174 changes: 42 additions & 132 deletions Alchemy/Application/Application.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import HummingbirdCore

/// The core type for an Alchemy application.
///
/// @Application
Expand All @@ -14,15 +16,16 @@ public protocol Application: Router {
var container: Container { get }
/// Any custom plugins of this application.
var plugins: [Plugin] { get }

/// Build the hummingbird server
var server: HTTPServerBuilder { get }

init()

/// Boots the app's dependencies. Don't override the default for this unless
/// you want to prevent default Alchemy services from loading.
func bootPlugins() async throws
/// Setup your application here. Called after all services are registered.
func boot() throws

/// Optional shutdown logic here.
func shutdown() throws

// MARK: Default Plugin Configurations

/// This application's HTTP configuration.
Expand All @@ -47,151 +50,58 @@ public protocol Application: Router {
// MARK: Defaults

public extension Application {
var container: Container { .main }
var plugins: [Plugin] { [] }

func bootPlugins() async throws {
let alchemyPlugins: [Plugin] = [
Core(),
Schedules(),
EventStreams(),
http,
commands,
filesystems,
databases,
caches,
queues,
]

let allPlugins = alchemyPlugins + plugins
for plugin in alchemyPlugins + plugins {
plugin.registerServices(in: self)
}

Container.onStart {
for plugin in allPlugins {
try await plugin.boot(app: self)
}
}

Container.onShutdown {
for plugin in allPlugins.reversed() {
try await plugin.shutdownServices(in: self)
}
}
}

func boot() throws {
//
}

func bootRouter() {
(self as? Controller)?.route(self)
}

// MARK: Plugin Defaults

var http: HTTPConfiguration { HTTPConfiguration() }
var caches: Caches { Caches() }
var commands: Commands { [] }
var container: Container { .main }
var databases: Databases { Databases() }
var caches: Caches { Caches() }
var queues: Queues { Queues() }
var filesystems: Filesystems { Filesystems() }
var http: HTTPConfiguration { HTTPConfiguration() }
var loggers: Loggers { Loggers() }

func schedule(on schedule: Scheduler) {
//
}
}
var plugins: [Plugin] { [] }
var queues: Queues { Queues() }
var server: HTTPServerBuilder { .http1() }

import ServiceLifecycle
func boot() throws {}
func shutdown() throws {}
func schedule(on schedule: Scheduler) {}
}

// MARK: Running

public extension Application {
func run() async throws {
extension Application {
// @main support
public static func main() async throws {
let app = Self()
do {
try await bootPlugins()
try boot()
bootRouter()
try await start()
try await app.willRun()
try await app.run()
try await app.didRun()
} catch {
commander.exit(error: error)
app.commander.exit(error: error)
}
}

/// Starts the application with the given arguments.
func start(_ args: String..., waitOrShutdown: Bool = true) async throws {
try await start(args: args.isEmpty ? nil : args, waitOrShutdown: waitOrShutdown)
}

/// Starts the application with the given arguments.
///
/// @MainActor ensures that calls to `wait()` doesn't block an `EventLoop`.
@MainActor
func start(args: [String]? = nil, waitOrShutdown: Bool = true) async throws {

// 0. Add service

Container.main
.lifecycleServices
.append(
ApplicationService(
args: args,
waitOrShutdown: waitOrShutdown,
app: self
)
)

// 1. Start the application lifecycle.

try await serviceGroup.run()
}

/// Stops the application.
func stop() async {
await serviceGroup.triggerGracefulShutdown()
/// Runs the application with the given arguments.
public func run(_ args: String...) async throws {
try await lifecycle.start(args: args.isEmpty ? nil : args)
}

// @main support
static func main() async throws {
try await Self().run()
}
}

private actor ApplicationService: ServiceLifecycle.Service {
var args: [String]? = nil
var waitOrShutdown: Bool = true
let app: Application

init(args: [String]? = nil, waitOrShutdown: Bool, app: Application) {
self.args = args
self.waitOrShutdown = waitOrShutdown
self.app = app
/// Sets up the app for running.
public func willRun() async throws {
let lifecycle = Lifecycle(app: self)
try await lifecycle.start()
(self as? Controller)?.route(self)
try boot()
}

func run() async throws {

// 0. Parse and run a `Command` based on the application arguments.

let command = try await app.commander.runCommand(args: args)
guard waitOrShutdown else { return }

// 1. Wait for lifecycle or immediately shut down depending on if the
// command should run indefinitely.

if !command.runUntilStopped {
await app.stop()
}

// 2. this is required to not throw service lifecycle errors

try await gracefulShutdown()
/// Any cleanup after the app finishes running.
public func didRun() async throws {
try shutdown()
try await lifecycle.shutdown()
}
}

fileprivate extension ParsableCommand {
var runUntilStopped: Bool {
(Self.self as? Command.Type)?.runUntilStopped ?? false
/// Stops the application.
public func stop() async {
await lifecycle.stop()
}
}
86 changes: 73 additions & 13 deletions Alchemy/Application/Lifecycle.swift
Original file line number Diff line number Diff line change
@@ -1,32 +1,58 @@
import ServiceLifecycle

actor Lifecycle: ServiceLifecycle.Service {
fileprivate var startTasks: [() async throws -> Void] = []
fileprivate var shutdownTasks: [() async throws -> Void] = []
/// Manages the startup and shutdown of an Application as well as it's various
/// services and configurations.
actor Lifecycle {
typealias Action = () async throws -> Void

var didStart = false
var didStop = false
fileprivate var startTasks: [Action] = []
fileprivate var shutdownTasks: [Action] = []

func run() async throws {
try await start()
try await gracefulShutdown()
try await shutdown()
let app: Application
let plugins: [Plugin]

private var group: ServiceGroup?
private var services: [ServiceLifecycle.Service] = []

init(app: Application) {
self.app = app
self.plugins = [
Core(),
Schedules(),
EventStreams(),
app.http,
app.commands,
app.filesystems,
app.databases,
app.caches,
app.queues,
] + app.plugins
}

func start() async throws {
guard !didStart else { return }
didStart = true
app.container.register(self).singleton()

for plugin in plugins {
plugin.registerServices(in: app)
}

for plugin in plugins {
try await plugin.boot(app: app)
}

for start in startTasks {
try await start()
}
}

func shutdown() async throws {
guard !didStop else { return }
didStop = true
for shutdown in shutdownTasks.reversed() {
try await shutdown()
}

for plugin in plugins.reversed() {
try await plugin.shutdownServices(in: app)
}
}

func onStart(action: @escaping () async throws -> Void) {
Expand All @@ -36,6 +62,36 @@ actor Lifecycle: ServiceLifecycle.Service {
func onShutdown(action: @escaping () async throws -> Void) {
self.shutdownTasks.append(action)
}

func addService(_ service: ServiceLifecycle.Service) {
services.append(service)
}

func start(args: [String]? = nil) async throws {
let commander = Container.require(Commander.self)
commander.setArgs(args)
let allServices = services + [commander]
let group = ServiceGroup(
configuration: ServiceGroupConfiguration(
services: allServices.map {
.init(
service: $0,
successTerminationBehavior: .gracefullyShutdownGroup,
failureTerminationBehavior: .gracefullyShutdownGroup
)
},
gracefulShutdownSignals: [.sigterm, .sigint],
logger: Log
)
)

self.group = group
try await group.run()
}

func stop() async {
await group?.triggerGracefulShutdown()
}
}

extension Application {
Expand All @@ -56,4 +112,8 @@ extension Container {
public static func onShutdown(action: @escaping () async throws -> Void) {
Task { await lifecycle.onShutdown(action: action) }
}

public static func addService(_ service: ServiceLifecycle.Service) {
Task { await lifecycle.addService(service) }
}
}
Loading

0 comments on commit 2b51325

Please sign in to comment.