SSWG - March 1st, 2023

Attendees: @adam-fowler, @tomerd, @FranzBusch, @ktoso, @0xtim, @graskind, @davmoser

Carry over

  • @adam-fowler Graphiti and GraphQL need new async based docs - need to reach out
  • @FranzBusch to make gaps in ecosystem post public (waiting on thumbs up)
  • @davmoser - blog post for AWS sample update to swift concurrency
  • @graskind finish updating swift evolution for property wrappers in protocols
  • @tomerd Community survey
  • @tachyonics: set up middleware meeting
  • @0xTim to investigate slim containers based on Canonical's work
  • @patrick Swiftly - Move to the SSWG GitHub org
  • @0xTim SwiftPM support in Dependabot - working on this. Looking through old PRs will have something for next meeting
  • @jdmcd to put together a pitch for blog (Transeo scaling Swift)
  • @FranzBusch and @0xTim write 2022 Review Post

Action Points

  • @FranzBusch Start conversation on Windows CI on Slack.
  • @0xTim ask Mishal what is plan going forward for swift docker files. Are we going to use Swiftly?
  • Everyone - review ServiceLifecycle and provide feedback where necessary

General

AWS blog posts

  • @tomerd Difficultly finding info, need blog posts
  • @davmoser Main issue seems to be how do you deploy? SAM, CoPilot CDK, Cloudformation etc
  • @0xtim We should keep it simple and supply Deploy fargate and lambda blogs only. It is not our job to demostrate every method available.
  • @davmoser we suggest you use SAM for lambda and use CoPilot for fargate

Windows networking

  • @FranzBusch talked with Cory to discuss how we feel about proper Windows support in NIO
    • There was concern about Windows CI being available before moving forward
    • SwiftPM on Windows is also a concern
    • @tomerd - Start conversation with Mishal and others about starting Windows CI

AWS SAM CLI Rust support

  • Can we get first class support for Swift as well?
  • @davmoser Need some templates, check with AWS service team to see what they need.

Summmer of Code 2023

  • @ktoso Apple open to mentorship by people outside of Apple, If you have any suggestions or ideas for projects please provide input
  • @FranzBusch Zookeeper project, don't have any knowledge if someone else does, maybe they could take it on.
  • @ktoso month left for input

Structured Service Lifecycle

  • @FranzBusch presented code for new ServiceLifecycle
  • Looking for feedback
  • How do we get libraries using this
  • It would clean up systems that run code in background. It isn't always clear where these processes are being initiated
  • How do we get it in swift-service-lifecycle library?

No longer possible to request code coverage output path without running tests

  • Holding back Vapor CI with nightlies

VSCode Devcontainer features

  • @adam-fowler Devcontainer features are used to augment existing devcontainers
  • Many languages use features for building their devcontainer templates, unlike Swift which uses a base docker image.
  • It would mean replicating work from swift-docker repo. Work that is also possibly needed by Swiftly
  • @0xTim Are the swift-docker images going to move to using Swiftly
4 Likes

Hi,

We just had a look at the new ServiceLifecycle API (ping @FranzBusch) and have some feedback as we looked to adopt it.

Let's use the sample code as the basis for discussion:

actor FooService {
    func run() async throws {
        print("FooService starting")
        try await Task.sleep(for: .seconds(10)) // the assumption is that this basically blocks
        print("FooService done")
    }
}

@main
struct Application {
    static func main() async throws {
        let service1 = FooService()
        let service2 = FooService()
        
        let serviceGroup = ServiceGroup(
            services: [service1, service2],
            configuration: .init(gracefulShutdownSignals: [.sigterm])
        )
        try await serviceGroup.run()
    }
}

and the previous API:

let migrator = DatabaseMigrator(eventLoopGroup: eventLoopGroup)
lifecycle.register(
    name: "migrator",
    start: .async(migrator.migrate),
    shutdown: .async(migrator.shutdown)
)

The problem we have with the new API is that it assumes that the work takes the structure of start, blocked run and shutdown.

Most of our services do not necessarily block, but can in turn start other asynchronous work (which would be shut down by the shutdown handles).

This worked just fine with the old API, as there were no such assumption.

What would be the recommended way to migrate? It doesn't seem that nice to just run an infinite loop and check for task cancellation / sleep between checks. I guess we could create an async stream where we could yield the 'shutdown' when it's time, but it seems perhaps that would better be part of the API then, or are we missing something?

1 Like

Thanks for providing feedback @hassila!

This question of how to bridge an existing service that exposes start/stop APIs came up a few times. The gist of the problem is what you call out here:

This is a totally fine pattern and we want services to do other asynchronous work. The problem that you are encountering here is that your service is doing the asynchronous work in an unstructured way. Presumably it runs this work on some NIO event loop. This is totally fine and the way we have done this in the past; however, the new ServiceLifecycle is fully embracing Structured Concurrency and wants to nudge you into making your asynchronous work part of the task tree.
Though we can't change everything to be fully structured right away; therefore, it is totally fine to create a bridge that wraps your start/stop service into a "structured" service. Wrapping it is quite simple:

actor StartStopService: Service {
  private var continuation: CheckedContinuation<Void, Error>?

  func run() async throws {
    self.underlyingService.start()
    try await withTaskCancellationHandler {
        try await withCheckedContinuation { continuation in
          self.continuation = continuation
        }
    } onCancel: {
      Task { self.cancel() } // We have to spawn an unstructured task here since we don't have actor send yet
    }
  }

  private func cancel() {
    self.continuation?.resume(throwing: CancellationError())
  }
}

We don't provide such a bridging construct out of the box because in the fullness of time we expect that start/stop services to face out and be replaced with run() based services.

On a related note:
I am unsure if you want to model a database migrator as a service. Personally, I think that database migrations have to run before any other part of the application is started. Meaning your main method should look more like this

@main
struct Application {
    static func main() async throws {
        let databaseMigrator = DatabaseMigrator()
        try await databaseMigrator.migrate()

        let service1 = FooService()
        let service2 = FooService()
        
        let serviceGroup = ServiceGroup(
            services: [service1, service2],
            configuration: .init(gracefulShutdownSignals: [.sigterm])
        )
        try await serviceGroup.run()
    }
}
1 Like

Thanks @FranzBusch we'll have a look!

The Database migrator isn't ours, it was the original sample code from the Service Lifecycle blog entry so I just used that for contrast of discussion ;-)

Hello @FranzBusch! Thanks for your example, that's definitely makes sense.

I managed this to work, with small change of try await withTaskCancellationHandler to be await withGracefulShutdownHandler and using none error version of continuation.

One more thing that we are using in previous version is order of start operations. Basically if I register lifecycle tasks A, B, C, then when C starts I know that A and B have started. This allows us to avoid logic of checking and awaiting of dependencies (like C do not check and wait for A/B to be started).

I guess it can be solved by logic like: C is awaiting on some continuation which is resumed when both A and B are done, but this make the adaptation even more complex and not straightforward.

So is there any plans to support ordering of service startup and if yes, how it will correlate with concept that run basically does the job until is not cancelled?

To give more context I can provide an example: I have service (say A) which connects to external service and register the running process. Then I start B/C/etc which rely that process is already registered and do not wait for it. Seems in the new version of lifecycle I need to start service A (and similar ones) beyond lifecycle service group.

1 Like

Great question! During the development of the new lib we also took a look at exactly that use-case.

From my experience with using the new service lifecycle and structured concurrency, I saw that this dependency concept is something that can be achieved in different ways. Importantly here is how are the dependent services (B/C) relying on the other service (A) running, i.e. are B/C using A directly by having it injected in their inits or are they relying on some "output" that A produces.

If B/C are getting A injected and using it to make requests then this is something that A should be able to handle on its own with async methods, e.g. A can suspend all requests until its run() method is called or it could throw an error to make sure that nothing is using it before its running. Another thing is that service A can expose its state which the other services can observe to know when they are ready to start, e.g.:

actor ServiceA: Service {
  enum State {
    case initial
    case running
    case shutdown
  }

  var state: AsyncStream<State>
}

On the other hand, if service A is producing an output that the other services want to consume to operate then you could model this by vending an AsyncSequence of the output that is injected into the dependent services which consume the sequence. An example of this is service A is producing a certificate that the other services need to use to rotate the certificate used for communicating to some external application.

Overall, we saw that we were able to solve all of these inter-service dependencies by employing one of the above approaches. If none of those work, then it would be great if you could share a more detailed example that we could discuss.

2 Likes

Thanks for advice @FranzBusch, I need to see how I can use suggested approaches to adjust my services to new lifecycle API.