Section 4/161 menit
4. TaskLocal Values
4. TaskLocal Values
@TaskLocal adalah storage yang otomatis di-propagate ke semua child tasks dan melalui await boundaries — mirip thread-local storage tapi untuk task tree.
Deklarasi dan Penggunaan
swift
// MARK: - TaskLocal Declarations
enum Tracing {
@TaskLocal static var traceID: String = ""
@TaskLocal static var spanID: String = ""
@TaskLocal static var isDebugMode: Bool = false
}
enum UserContext {
@TaskLocal static var userID: String? = nil
@TaskLocal static var locale: Locale = .current
}
// Menggunakan TaskLocal: withValue membuat scope
func handleAPIRequest(traceID: String, userID: String) async throws -> Response {
try await Tracing.$traceID.withValue(traceID) {
try await UserContext.$userID.withValue(userID) {
try await processRequest()
}
}
}
// Di mana saja di call chain, bisa baca langsung
func processRequest() async throws -> Response {
let trace = Tracing.traceID // otomatis tersedia
let user = UserContext.userID // otomatis tersedia
logger.info("Processing request", metadata: [
"traceID": "\(trace)",
"userID": "\(user ?? "anonymous")"
])
return try await fetchDataFromDB()
}
func fetchDataFromDB() async throws -> Response {
// Masih bisa akses — di-propagate ke seluruh subtask tree
let trace = Tracing.traceID
return Response()
}
Propagasi ke Child Tasks
TaskLocal otomatis di-copy ke child tasks yang dibuat dengan Task {}, async let, dan TaskGroup:
swift
func demonstrateTaskLocalPropagation() async {
await Tracing.$traceID.withValue("req-123") {
print(Tracing.traceID) // "req-123"
// async let: child task juga dapat "req-123"
async let result1 = Task {
print(Tracing.traceID) // "req-123" — di-copy
}.value
// TaskGroup: semua child tasks dapat "req-123"
await withTaskGroup(of: Void.self) { group in
group.addTask {
print(Tracing.traceID) // "req-123"
}
}
// Unstructured Task: TIDAK propagate (berbeda lifecycle)
Task {
print(Tracing.traceID) // "req-123" — TAPI ini Task detached-like
// Structured Task detached: "" (default value)
}
await result1
}
print(Tracing.traceID) // "" — kembali ke default setelah scope withValue
}
Nesting TaskLocal
swift
// Nesting: nilai inner scope override nilai outer scope
await Tracing.$traceID.withValue("parent-trace") {
print(Tracing.traceID) // "parent-trace"
await Tracing.$traceID.withValue("child-trace") {
print(Tracing.traceID) // "child-trace"
}
print(Tracing.traceID) // "parent-trace" — dikembalikan otomatis
}
TaskLocal untuk Dependency Injection
swift
// Gunakan TaskLocal untuk DI di tests vs production
protocol DatabaseClient {
func fetchUser(id: String) async throws -> User
}
enum Dependencies {
@TaskLocal static var database: any DatabaseClient = ProductionDatabase()
@TaskLocal static var analytics: AnalyticsService = ProductionAnalytics()
}
// Production: default value dipakai
func fetchUserProfile(id: String) async throws -> UserProfile {
let user = try await Dependencies.database.fetchUser(id: id)
return UserProfile(user: user)
}
// Testing: inject mock via TaskLocal
@Test("Profile fetch mengembalikan data yang benar")
func testFetchUserProfile() async throws {
let mockDB = MockDatabase(users: [User(id: "1", name: "Test")])
try await Dependencies.$database.withValue(mockDB) {
let profile = try await fetchUserProfile(id: "1")
#expect(profile.name == "Test")
}
}
struct User { let id: String; let name: String }
struct UserProfile { let name: String; init(user: User) { name = user.name } }
class MockDatabase: DatabaseClient {
let users: [User]
init(users: [User]) { self.users = users }
func fetchUser(id: String) async throws -> User {
guard let user = users.first(where: { $0.id == id }) else { throw DBError.notFound }
return user
}
}
class ProductionDatabase: DatabaseClient {
func fetchUser(id: String) async throws -> User { fatalError("Not in test") }
}
class AnalyticsService {}
class ProductionAnalytics: AnalyticsService {}
enum DBError: Error { case notFound }
struct Response {}