Skip to content

[TaskGroup] reorder resumeWaiting->run and reenable async_taskgroup_discarding_dontLeak #67787

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

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion stdlib/public/Concurrency/TaskGroup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1182,8 +1182,8 @@ void AccumulatingTaskGroup::offer(AsyncTask *completedTask, AsyncContext *contex

// ==== a) has waiting task, so let us complete it right away
if (assumed.hasWaitingTask()) {
unlock();
resumeWaitingTask(completedTask, assumed, hadErrorResult);
unlock(); // TODO: remove fragment lock, and use status for synchronization
return;
} else {
// ==== b) enqueue completion ------------------------------------------------
Expand Down
226 changes: 148 additions & 78 deletions test/Concurrency/Runtime/async_taskgroup_discarding_dontLeak.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library) | %FileCheck %s --dump-input=always
// TODO: move to target-run-simple-leaks-swift once CI is using at least Xcode 14.3

// rdar://109998145 - Temporarily disable this test
// REQUIRES: rdar109998145

// REQUIRES: concurrency
// REQUIRES: executable_test
// REQUIRES: concurrency_runtime
Expand All @@ -16,6 +13,37 @@

import _Concurrency

actor SimpleCountDownLatch {
let from: Int
var count: Int

var continuation: CheckedContinuation<Void, Never>?

init(from: Int) {
self.from = from
self.count = from
}

func hit() {
defer { count -= 1 }
if count == 0 {
fatalError("Counted down more times than expected! (From: \(from))")
} else if count == 1 {
continuation?.resume()
}
}

func wait() async {
guard self.count > 0 else {
return // we're done
}

return await withCheckedContinuation { cc in
self.continuation = cc
}
}
}

final class PrintDeinit {
let id: String
init(id: String) {
Expand Down Expand Up @@ -60,96 +88,138 @@ final class SomeClass: @unchecked Sendable {

// NOTE: Not as StdlibUnittest/TestSuite since these types of tests are unreasonably slow to load/debug.

@main struct Main {
static func main() async {
_ = try? await withThrowingDiscardingTaskGroup() { group in
group.addTask {
throw Boom(id: "race-boom-class")
}
func testTwo() async {
let latch = SimpleCountDownLatch(from: 2)

_ = try? await withThrowingDiscardingTaskGroup() { group in
group.addTask {
await latch.hit()
throw Boom(id: "race-boom")
}
group.addTask {
await latch.hit()
SomeClass(id: "race-boom-class") // will be discarded
}

return 12
}

// since values may deinit in any order, we just assert their count basically
// CHECK-DAG: deinit, id: race-boom
// CHECK-DAG: deinit, id: race-boom
await latch.wait()
print("done") // CHECK: done
}

func manyOk() async {
let latch = SimpleCountDownLatch(from: 6)

_ = try? await withThrowingDiscardingTaskGroup() { group in
for i in 0..<6 {
group.addTask {
SomeClass(id: "race-boom-class") // will be discarded
await latch.hit()
_ = SomeClass(id: "many-ok") // will be discarded
}
// since values may deinit in any order, we just assert their count basically
// CHECK-DAG: deinit, id: race-boom-class
// CHECK-DAG: deinit, id: race-boom-class

return 12
}

// many ok
_ = try? await withThrowingDiscardingTaskGroup() { group in
return 12
}
// since values may deinit in any order, we just assert their count basically
// CHECK-DAG: deinit, id: many-ok
// CHECK-DAG: deinit, id: many-ok
// CHECK-DAG: deinit, id: many-ok
// CHECK-DAG: deinit, id: many-ok
// CHECK-DAG: deinit, id: many-ok
// CHECK-DAG: deinit, id: many-ok

await latch.wait()
print("done") // CHECK: done
}

func manyThrows() async {
let latch = SimpleCountDownLatch(from: 6)

do {
let value: Void = try await withThrowingDiscardingTaskGroup() { group in
for i in 0..<6 {
group.addTask {
SomeClass(id: "many-ok") // will be discarded
await latch.hit()
throw BoomClass(id: "many-error") // will be rethrown
}
// since values may deinit in any order, we just assert their count basically
// CHECK-DAG: deinit, id: many-ok
// CHECK-DAG: deinit, id: many-ok
// CHECK-DAG: deinit, id: many-ok
// CHECK-DAG: deinit, id: many-ok
// CHECK-DAG: deinit, id: many-ok
// CHECK-DAG: deinit, id: many-ok
}

return 12
// since values may deinit in any order, we just assert their count basically
// CHECK-DAG: deinit, id: many-error
// CHECK-DAG: deinit, id: many-error
// CHECK-DAG: deinit, id: many-error
// CHECK-DAG: deinit, id: many-error
// CHECK-DAG: deinit, id: many-error
// CHECK-DAG: deinit, id: many-error

12 // must be ignored
}
preconditionFailure("Should throw")
} catch {
precondition("\(error)" == "main.BoomClass", "error was: \(error)")
}

// many throws
do {
let value = try await withThrowingDiscardingTaskGroup() { group in
for i in 0..<6 {
group.addTask {
throw BoomClass(id: "many-error") // will be rethrown
}
}
await latch.wait()
print("done") // CHECK: done
}

// since values may deinit in any order, we just assert their count basically
// CHECK-DAG: deinit, id: many-error
// CHECK-DAG: deinit, id: many-error
// CHECK-DAG: deinit, id: many-error
// CHECK-DAG: deinit, id: many-error
// CHECK-DAG: deinit, id: many-error
// CHECK-DAG: deinit, id: many-error
func manyValuesThrows() async {
let latch = SimpleCountDownLatch(from: 6)

12 // must be ignored
}
preconditionFailure("Should throw")
} catch {
precondition("\(error)" == "main.BoomClass", "error was: \(error)")
// many errors, many values
_ = try? await withThrowingDiscardingTaskGroup() { group in
group.addTask {
await latch.hit()
_ = SomeClass(id: "mixed-ok") // will be discarded
}
group.addTask {
await latch.hit()
_ = SomeClass(id: "mixed-ok") // will be discarded
}
group.addTask {
await latch.hit()
_ = SomeClass(id: "mixed-ok") // will be discarded
}
group.addTask {
await latch.hit()
throw Boom(id: "mixed-error")
}
group.addTask {
await latch.hit()
throw Boom(id: "mixed-error")
}
group.addTask {
await latch.hit()
throw Boom(id: "mixed-error")
}

// many errors, many values
_ = try? await withThrowingDiscardingTaskGroup() { group in
group.addTask {
SomeClass(id: "mixed-ok") // will be discarded
}
group.addTask {
SomeClass(id: "mixed-ok") // will be discarded
}
group.addTask {
SomeClass(id: "mixed-ok") // will be discarded
}
group.addTask {
throw Boom(id: "mixed-error")
}
group.addTask {
throw Boom(id: "mixed-error")
}
group.addTask {
throw Boom(id: "mixed-error")
}

// since values may deinit in any order, we just assert their count basically
// three ok's
// CHECK-DAG: deinit, id: mixed
// CHECK-DAG: deinit, id: mixed
// CHECK-DAG: deinit, id: mixed
// three errors
// CHECK-DAG: deinit, id: mixed
// CHECK-DAG: deinit, id: mixed
// CHECK-DAG: deinit, id: mixed

return 12
}
return 12
}

// since values may deinit in any order, we just assert their count basically
// three ok's
// CHECK-DAG: deinit, id: mixed
// CHECK-DAG: deinit, id: mixed
// CHECK-DAG: deinit, id: mixed
// three errors
// CHECK-DAG: deinit, id: mixed
// CHECK-DAG: deinit, id: mixed
// CHECK-DAG: deinit, id: mixed

await latch.wait()
print("done") // CHECK: done
}

@main struct Main {
static func main() async {
await testTwo()
await manyOk()
await manyThrows()
await manyValuesThrows()
}
}