GithubHelp home page GithubHelp logo

swift-server / swift-service-lifecycle Goto Github PK

View Code? Open in Web Editor NEW
376.0 376.0 36.0 375 KB

Cleanly startup and shutdown server application, freeing resources in order before exiting.

Home Page: https://swiftpackageindex.com/swift-server/swift-service-lifecycle/documentation/servicelifecycle

License: Apache License 2.0

Swift 95.93% Dockerfile 0.41% Shell 3.66%

swift-service-lifecycle's People

Contributors

adam-fowler avatar buratti avatar czechboy0 avatar darrellroot avatar fabianfett avatar florianreinhart avatar franzbusch avatar gjcairo avatar glbrntt avatar hamzahrmalik avatar hassila avatar ktoso avatar mahdibm avatar maxdesiatov avatar peteradams-a avatar rnro avatar sajjon avatar sidepelican avatar simonjbeaumont avatar slashmo avatar sliemeobn avatar stevapple avatar t089 avatar tib avatar tomerd avatar weissi avatar yim-lee avatar zg avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

swift-service-lifecycle's Issues

`Lifecycle.wait()` hangs in tests

I've just moved Vapor's Penny bot to use swift-service-lifecycle (here).

What happens is, if I use await Penny.main() in tests (here), the test reaches the ServiceLifecycle.wait() call, but hangs there (Penny type is the @main entry point).

Using ServiceLifecycle.wait() when running the executable works as expected.

Optimally this shouldn't happen and tests should run without hanging. Before this change, we were using RunLoop.main.run() to stop the executable from exiting immediately (with a non-async main()) and this was working fine with tests.

The solution i've found is to not call ServiceLifecycle.wait() at all in tests. As you can see, the tests call another await Penny.start() function which is the same as await Penny.main(), except that it doesn't call ServiceLifecycle.wait().

Environment:

swift-driver version: 1.75.2 Apple Swift version 5.8 (swiftlang-5.8.0.124.2 clang-1403.0.22.11.100)
Target: arm64-apple-macosx13.0
Darwin MahdiBM-Macbook.local 22.4.0 Darwin Kernel Version 22.4.0: Mon Mar  6 20:59:28 PST 2023; root:xnu-8796.101.5~3/RELEASE_ARM64_T6000 arm64

with swift-service-lifecycle alpha.11.

Support optional daemonization

What are your thoughts of supporting daemonization as part of this framework during startup? Useful for background services and looking at the proposed server side frameworks this looks like a natural place.

Provide a `CancelOnGracefulShutdownService`

If an adopter already has a reference type that implements a run() method it is currently impossible to support ServiceLifecycle, even though the implementation works easily:

extension MyExistingService: Service {}

The reason for this is, that ServiceLifecycle uses gracefulShutdown to signal shutdown instead of Task cancellation. That is the correct call. However it is impossible to add gracefulShutdown support to object that already expose a run() method. That's why im proposing that ServiceLifecycle offers a CancelOnGracefulShutdownService<Underlying: Service>.

is ServiceLauncher a good name?

I'm wondering whether ServiceLauncher is a good name for this. See #14 , I'm planning to use this for systems that start/stop sub-systems too. But I wouldn't really call my systems a "service" so I'm unsure if ServiceLauncher is a good name.

Shutdown must ALWAYS be called for shutdownIfNotStarted=true

To use Lifecycle in lambda runtime, we need to find a way that it can shutdown instances that don't need a startup. Example handler:

import AWSLambdaRuntime
import AsyncHTTPClient

// introductory example, the obligatory "hello, world!"
@main
struct HelloWorldHandler: AsyncLambdaHandler {
    typealias In = String
    typealias Out = String

    let httpClient: HTTPClient

    init(context: Lambda.InitializationContext) async throws {
        self.httpClient = HTTPClient(eventLoopGroupProvider: .shared(context.eventLoop))
        context.lifecycle.register(label: "shutdown", start: .none, shutdown: .async {
            self.httpClient.shutdown($0)
        }, shutdownIfNotStarted: true)

        throw MyError()
    }

    func handle(event: String, context: Lambda.Context) async throws -> String {
        "hello, world"
    }
}

In this case the httpClient still needs a call to shutdown. This is currently not happening. The use case that we should enable is abstracted below:

Failing test case

func testShutdownIfNotStarted() {
    var shutdownCalled = false
    
    let lifecycle = ComponentLifecycle(label: "test")
    lifecycle.register(label: "shutdown", start: .none, shutdown: .sync {
        shutdownCalled = true
    }, shutdownIfNotStarted: true)
    
    lifecycle.shutdown()
    XCTAssertTrue(shutdownCalled)
}

should be allow specifying a DispatchQueue per registered item?

Often, sub-systems won't be thread-safe so we rely on start & stop being called from the same thread. That is achievable with Configuration however only per Lifecycle and not once per item. What if two separate items rely on being each started/stopped on their own queue?

Crash on start if no tasks have been registered

If we use ServiceLifecycle without registering any tasks, a precondition fails in ComponentLifecycle._start.

        precondition(tasks.count > 0, "invalid number of tasks, must be > 0")

I think this case should be handled and should not lead to a crash. The ServiceLifecycle may be used in tests in which no tasks are registered, but the class, that is tested, still expects to be initialized with a ServiceLifecycle.

remove label from completion trailing closure arguments

We have functions like

public func shutdown(callback: @escaping ([String: Error]?)

which everybody probably calls as

thing.shutdown { stuff in ... }

If you're however forced to add parentheses because say you'll use it in a if ... (doesn't strictly apply to -> Void but bear with me, you need to go from

thing.func { stuff in }

to

if thing.func(callback: { stuff in }) { ... }

Therefore I recommend making all trailing closure arguments label-free. Ie. all should be migrated from

public func shutdown(callback: @escaping ([String: Error]?)

to

public func shutdown(_ callback: @escaping ([String: Error]?)

Impossible to mix "closure-style" shutdown/start with "LifecycleHandler.bla { ... } style"

When one starts out with lifecycle one will likely use the simple start overload:

       let thing: Thing
        lifecycle.register(
            label: "thing",
            start: thing.start,
// ... 
        )

Then one realizes it's best if the shutdown (or start, either) was async.

There's only an func register(label: String, start: @escaping () throws -> Void, shutdown: @escaping () throws -> Void) { overload, and none for ((Error?) -> Void) -> Void so one can be left confused, or just miss that erroring the lifecycle is a thing.

One then discovers shutdown: LifecycleHandler.async { (callback: @escaping (Error?) -> Void) -> Void in // callback: Error? -> Void

so:

       let thing: Thing
        lifecycle.register(
            label: "thing",
            start: thing.start,
            shutdown: .async { callback in thing.shutdown(callback) }
        )

which won't compile as well, and neither would shutdown: { callback in thing.shutdown(callback) }.

The issue is that one has to make BOTH start and shutdown using the explicit .async/.sync API:

func register(label: String, start: LifecycleHandler, shutdown: LifecycleHandler) {

mixing the callback style with this is not possible.


We should either:

  • provide all overloads for the .sync/.async in all permutations so people can use just plain closures whenever they want (a bit annoying and could yield not so good compiler errors)
  • remove the closure overloads completely -- always explicitly say .sync { ... } (or async)

I'd vote for the latter, because it helps discoverability -- it's easy to discover there's an async way to init and shutdown, and if we ever need new ones (say, some future type etc), we can easily add those there.

Add support for SIGUSR1/SIGUSR2

Would be nice to add support for them similar to the other signals for custom behavior (eg. Cache evictions, dump of debug state, etc, etc). Propose to just add those two signals in an analogue manner.

Tasks with dependsOn and topological sort of task start order

The current way of defining lifecycle tasks is simple and works well for single or two frameworks or libraries adding tasks.

It gets complex if many libraries need to depend on some specific one thing, that some other library provides.

We could offer:

// Module X
    lifecycle.register(
        label: "service-y",
        dependsOn: "database", // ["database", "http-server"]
        start: .async { ... },
        shutdown: .sync { }
    )

// Module Y
    lifecycle.register(
        label: "service-y",
        dependsOn: "database", // ["database", "http-server"]
        start: .async { ... },
        shutdown: .sync { }
    )
// Main
// ModuleY(lifecycle)

    lifecycle.register(
        label: "database",
        start: .async { ... },
        shutdown: .sync { }
    )
ModuleX(lifecycle)
// ModuleY(lifecycle)

lifecycle.start()

^ it does not matter where I start ModuleY in "source order" and the dependencies will always be run in a predictable order.

and perform a Topological sort for starting the tasks in the right order.

This allows one library to start the "database" and other modules of a project to say i depend on "database" without having to specify more -- if the resolver notices there is no database task defined if can bail out. Otherwise, the order is made such that all tasks that depend on the "database" are spawned after it is ready.

Crash cancelling ServiceGroup.run() in Swift 6

With Swift 6 nightly: swift-6.0-DEVELOPMENT-SNAPSHOT-2024-04-06-a
I get a crash when cancelling ServiceGroup.run

struct TestService: Service {
    func run() async throws {
        try await gracefulShutdown()
    }
}
await withThrowingTaskGroup(of: Void.self) { group in
    let serviceGroup = ServiceGroup(
        configuration: .init(
            services: [TestService()],
            gracefulShutdownSignals: [.sigterm, .sigint],
            logger: Logger(label: "JobQueueService")
        )
    )
    group.addTask {
        try await serviceGroup.run()
    }
    group.cancelAll()
}

It crashes on this line with a EXC_BAD_ACCESS

Swift --version output

swift-driver version: 1.90.11.1 Apple Swift version 5.10 (swiftlang-5.10.0.13 clang-1500.3.9.4)
Target: arm64-apple-macosx14.0

Future idea: if @main lands and SPM supports it, offer an "entry point"?

The ongoing pitch https://forums.swift.org/t/main-type-based-program-execution/34624 introduces @main define-able entry points.

It is also aimed for libraries to provide their patterns / entry points.

It currently will NOT work with Swift Package Manager... but if we were able to make it work there, we could offer APIs similar to

@main 
struct MyService: ServiceLifecycleMain {
// or struct MyService: ServiceBootstrapMain {

    // some command line things, using ParsableCommand
    @Argument(help: "What port to bind to")
    var port: [Int]

    // NOT ACTUAL API; not through through how it'd look like exactly,
    // but we'd allow registering the phases here?
    func configurePhases/Tasks() {}

    func onShutdown(error: [String: Error]?) {}
}

finalize naming

current state

repo name: swift-service-launcher
package name: swift-service-launcher
main module name: ServiceLauncher
main type name: Lifecycle

Using signals for actions other that shutdown

Is there support for trapping signals for anything other than shutdown hooks?

Linux services will often trap SIGHUP to reload configuration without stopping the service.

I believe that a Windows service can handle a SERVICE_ACCEPT_PARAMCHANGE state request to do the same thing.

This would be a useful feature for production services.

Question: return type of `cancelOnGracefulShutdown`

public func cancelOnGracefulShutdown<T: Sendable>(_ operation: @Sendable @escaping () async throws -> T) async rethrows -> T? {

I noticed that the cancelOnGracefulShutdown method signature suggests it returns an optional (T?).
However, I couldn’t find any code paths in the implementation where a nil value would be returned.

Could you please clarify why the return type is optional in this context?
Is there a scenario where this method might return nil that I might have missed?

cancelOnGracefulShutdown never returns

In version 2.0.0-alpha.1

swift-driver version: 1.75.2 Apple Swift version 5.8 (swiftlang-5.8.0.124.2 clang-1403.0.22.11.100)
Target: arm64-apple-macosx13.0

Calling cancelOnGracefulShutdown currently never finishes, because the withThrowingTaskGroup keeps running the "waiting-for-graceful-shutdown" task.

Afaik this is due to some unfortunate inconsistency with task groups with their "cancelAll on return" behavior.
edit: this has nothing to do with environment or inconsistencies, was just a plain bug.

This test will never finish in my environment:

    func testResumesCancelOnGracefulShutdownWithResult() async throws {
        await testGracefulShutdown { _ in
            let result = await cancelOnGracefulShutdown {
                await Task.yield()
                return "hello"
            }

            XCTAssertEqual(result, "hello")
        }
    }

Adding a "manual" group.cancelAll() should do the trick.

Convenience method for cancelling repeated tasks

LifecycleNIOCompat could provide a convenience method to cancel scheduled RepeatedTasks. I came up with the following extension:

extension LifecycleHandler {
    public static func cancelRepeatedTask(_ task: RepeatedTask, on eventLoopGroup: EventLoopGroup) -> LifecycleHandler {
        return self.eventLoopFuture {
            let promise = eventLoopGroup.next().makePromise(of: Void.self)
            task.cancel(promise: promise)
            return promise.futureResult
        }
    }
}

What do you think? I am happy to create a PR for this.

Make `CancellationWaiter` public

I've implemented a version (I forgot to add cancellation support as well as graceful shutdown), I know other people have done the same as well. Should we be just using the one that is already written here

docs about nesting ServiceLaunchers

Very clearly ServiceLaucher is helpful for the top-level thing in probably main.swift and that's why it has builtin support for handling signals & doing backtraces.

However, the lifecycle concept is much bigger than what you do in main.swift.

Consider the following example:

final class MySystem {
    private let subSys1
    private let subSys2
    private let lifecycle
    init() {
        self.subSys1 = SubSys1()
        self.sybSys2 = SubSys2()
        self.lifecycle.register(name: "sub sys 1", start: self.subSys1.start, shutdown: self.subSys1.shutdown)
        self.lifecycle.register(name: "sub sys 2", start: self.subSys2.start, shutdown: self.subSys2.shutdown)
    }
    func start() {
        self.lifecycle.start() // calls subSys1.start and subSys2.start
    }
    func shutdown() {
        self.lifecycle.shutdown()
    }
}

There, lifecycle is also super useful and I think we should add an example like this to the docs. And the API docs need a big fat warning that catching signals & registering backtraces is only for the top-level ServiceLauncher.

think about associated data when started

Manual state tracking has the advantage that you're able to associate data with a state. For example

enum LifecycleState {
    case stopped
    case started(Channel)
    case stopping(Channel)
}

migrating this to Lifecycle currently would require to add an optional

var serverChannel: Channel?

and then the code would start to contain self.serverChannel!. The other option is to duplicate the state tracking in Lifecycle as well as the application but that's not nice either because the states can get out of sync.

withCancellationOrGracefulShutdownHandler

A common pattern with services is

try await withTaskCancellationHandler {
    try await withGracefulShutdownHandler {
        doStuff()
    } onGracefulShutdown {
        stop()
    }
} onCancel: {
    stop()
}

Is it worthwhile providing a small helper API to do this eg

@_unsafeInheritExecutor
public func withCancellationOrGracefulShutdownHandler<T>(
    operation: () async throws -> T,
    onCancellationOrGracefulShutdown handler: @Sendable @escaping () -> Void
) async rethrows -> T {
    return try await withTaskCancellationHandler {
        try await withGracefulShutdownHandler(operation: operation, onGracefulShutdown: handler)
    } onCancel: {
        handler()
    }
}

@available for iOS12, but uses types from iOS13.

I'm trying to use the Hummingbird server as a test server for UI testing. After adding Hummingbird and trying to compile I'm getting a series of compilation errors from the swift-service-lifecycle code. One of which looks like this:

checkouts/swift-service-lifecycle/Sources/Lifecycle/Lifecycle.swift:103:13: error: 'Task' is only available in iOS 13.0 or newer
            Task {
            ^

Looking at Lifecycle.swift I can see this code:

#if compiler(>=5.5) && canImport(_Concurrency)
@available(macOS 12.0, *)
extension LifecycleHandler {
    public init(_ handler: @escaping () async throws -> Void) {
        self = LifecycleHandler { callback in
            Task {
                do {
                    try await handler()

Now if I'm reading this right, it's saying that it should be available for iOS12 (which is what this project is still supporting :-( ) but it's using Task which only came with iOS13 as the error indicates.

How do I fix this?

support unregistering a service

In certain cases it could be useful to register and then unregister a service, maybe because that service has been shut down independently for some reason.

Today, this is possible but just forgetting about a registered service (and making the shutdown function do nothing). Unfortunately, that would cause the registration to consume memory forever...

I'd like to see an API like this:

// the return value is usually ignore (`@discardableResult`)
let registrationToken = lifecycle.register(...)
...
lifecycle.unregister(registrationToken, runShutdown: false)

Fails to compile Xcode 13 RC (note: needs macOS release)

Hey folks, trying to build with Xcode 13 RC.

Steps to reproduce

git clone https://github.com/swift-server/swift-service-lifecycle.git
cd swift-service-lifecycle
swift build

.../swift-service-lifecycle/Sources/Lifecycle/Lifecycle.swift:16:8: error: no such module '_Concurrency'
import _Concurrency

And swift --version is

swift-driver version: 1.26.9 Apple Swift version 5.5 (swiftlang-1300.0.31.1 clang-1300.0.29.1)
Target: x86_64-apple-macosx11.0

Assume the fix is just to delete the import _Concurrency? (didn't actually fix it, as discussed below)

Wait for availability of remote services before starting an application

When developing Swift services locally (macOS), I usually use Docker to run one or more databases. When quickly wanting to test on Linux, I use docker-compose to start both the database and my Swift service at the same time.

When doing so I'm running into the issue of the database container being booted quickly but not actually being available.
If the Swift service is not set up to retry connecting to the database it might crash when the connection can't yet be established. To work around this I often use something like wait-for-it, which is a simple bash script that you add to the Dockerfile of the Swift service and use it as the entry point, before actually specifying your application's real entry point. The script then pings a specified port as long as it's not available every few seconds.

Long story short, I'm thinking there may be an opportunity for ServiceLifecycle to tackle this issue without including such a script that lets you wait for a specific port to be available before starting certain tasks such as starting the main app or running migrations. I didn't look into the source code deep enough to figure out how this might work but just wanted to get the idea out there so we can discuss it together.

v1.0 alpha is reliant on Swift Backtrace

When Swift 5.9 is released swift-backtrace is no longer needed and is deprecated.
While v1.0.0-alpha.11 is not a full release many projects are still reliant on it. Is it possible to patch it to remove the backtrace install if compiling with Swift 5.9

state machine bug: calls shutdown if start fails

consider this code

        service.register(label: "thing",
                         start: .sync {
            print("THIS WILL FAIL")
            struct SomeError: Error {}
            throw SomeError()
        },
                         shutdown: .sync {
            print("SHUTDOWN <<< SHOULD NEVER HAPPEN")
        },
        shutdownIfNotStarted: false)

        try service.startAndWait()

clearly, it should never invoke the shutdown handler because the start one failed but it still will. It prints:

THIS WILL FAIL
SHUTDOWN <<< SHOULD NEVER HAPPEN

Lifecycle.Handler.async is overloaded

The idea of disambiguating through .async { ... } was to not use overloading. Now, we still use overloading at least for .async { ... }.

    public static func async(_ callback: @escaping (@escaping (Error?) -> Void) -> Void) -> Handler
    public static func async(_ future: @escaping () -> EventLoopFuture<Void>) -> Lifecycle.Handler

and in the future, we'll probably also need

    public static func async(_ callback: @escaping (DispatchQueue, @escaping (Error?) -> Void) -> Void) -> Handler

so maybe we should use the following names instead of .async ?

  • .errorCompletion
  • .errorCompletionOnQueue
  • .nio

Not really sure. If we do use overloading, then we can also overload them with the synchronous version I think...

Logger argument missing in docs, or it should be an optional argument

The Readme and docs ('How to adopt ServiceLifecycle in applications') have this exact same line

configuration: .init(gracefulShutdownSignals: [.sigterm])

which is missing the required argument logger: Logger.

We should either add the argument with a sample Logger to those three occurrences in the docs, or make the argument optional.

As per discussion with Fabian: /cc @FranzBusch

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.