Section 15/161 menit
15. Real Use Cases
15. Real Use Cases
Use Case 1: Distributed Tracing System
Skenario: Backend service yang butuh propagasi trace ID ke seluruh async call chain tanpa mengubah signature semua fungsi.
swift
// MARK: - Distributed Tracing dengan TaskLocal
import Foundation
import os
// Trace context yang di-propagate otomatis
enum TraceContext {
@TaskLocal static var traceID: String = UUID().uuidString
@TaskLocal static var spanID: String = UUID().uuidString
@TaskLocal static var parentSpanID: String? = nil
@TaskLocal static var serviceName: String = "unknown"
}
// Span: unit kerja yang bisa di-trace
@discardableResult
func withSpan<T>(
_ name: String,
attributes: [String: String] = [:],
operation: () async throws -> T
) async rethrows -> T {
let spanID = UUID().uuidString
let startTime = ContinuousClock().now
// Push span context ke TaskLocal untuk child tasks
return try await TraceContext.$spanID.withValue(spanID) {
try await TraceContext.$parentSpanID.withValue(TraceContext.spanID) {
defer {
let elapsed = ContinuousClock().now - startTime
exportSpan(Span(
traceID: TraceContext.traceID,
spanID: spanID,
parentSpanID: TraceContext.parentSpanID,
name: name,
duration: elapsed,
attributes: attributes,
service: TraceContext.serviceName
))
}
return try await operation()
}
}
}
struct Span {
let traceID: String
let spanID: String
let parentSpanID: String?
let name: String
let duration: ContinuousClock.Instant.Duration
let attributes: [String: String]
let service: String
}
func exportSpan(_ span: Span) {
// Kirim ke Jaeger, Zipkin, atau OpenTelemetry collector
os_log("Span: %{public}s [%{public}s] duration=%.2fms",
span.name, span.traceID, span.duration.components.seconds * 1000)
}
// MARK: - Penggunaan: tracing tersebar tanpa ubah signature
actor OrderService {
func processOrder(_ order: Order) async throws -> OrderResult {
return try await withSpan("ProcessOrder", attributes: ["orderID": order.id]) {
// traceID dan spanID otomatis tersedia di semua sub-operations
let validated = try await withSpan("ValidateOrder") {
try await validate(order)
}
let inventory = try await withSpan("CheckInventory") {
try await checkInventory(for: validated)
}
let result = try await withSpan("ChargePayment") {
try await charge(order: validated, inventory: inventory)
}
return result
}
}
private func validate(_ order: Order) async throws -> Order {
// TraceContext.traceID tersedia di sini tanpa parameter!
let log = "Validating order \(order.id) trace=\(TraceContext.traceID)"
_ = log
return order
}
private func checkInventory(for order: Order) async throws -> InventorySnapshot {
return InventorySnapshot()
}
private func charge(order: Order, inventory: InventorySnapshot) async throws -> OrderResult {
return OrderResult()
}
}
struct Order { let id: String }
struct InventorySnapshot {}
struct OrderResult {}
Use Case 2: Audio Engine dengan Custom Executor
Skenario: Digital Audio Workstation (DAW) app yang harus memproses audio di dedicated real-time thread dengan priority tertinggi, tanpa preemption.
swift
// MARK: - Real-time Audio Actor dengan Custom Executor
import AVFoundation
// Executor khusus untuk real-time audio processing
// Menggunakan dedicated thread dengan realtime priority
final class AudioRealTimeExecutor: SerialExecutor, @unchecked Sendable {
private let thread: Thread
private let runLoop: RunLoop
private let semaphore = DispatchSemaphore(value: 0)
private var pendingJobs: [(UnownedJob, UnownedSerialExecutor)] = []
private let lock = NSLock()
private var isRunning = true
init() {
var capturedRunLoop: RunLoop?
let sem = DispatchSemaphore(value: 0)
thread = Thread {
capturedRunLoop = RunLoop.current
sem.signal()
// Keep thread alive
while true {
RunLoop.current.run(mode: .default, before: .distantFuture)
}
}
// Set real-time priority (Audio workload)
thread.threadPriority = 1.0
thread.qualityOfService = .userInteractive
thread.start()
sem.wait()
runLoop = capturedRunLoop!
}
func enqueue(_ job: consuming ExecutorJob) {
let unownedJob = UnownedJob(job)
let executor = asUnownedSerialExecutor()
runLoop.perform { unownedJob.runSynchronously(on: executor) }
}
func asUnownedSerialExecutor() -> UnownedSerialExecutor {
UnownedSerialExecutor(ordinary: self)
}
}
// Actor audio yang berjalan di dedicated real-time thread
actor AudioEngine {
private static let audioExecutor = AudioRealTimeExecutor()
nonisolated var unownedExecutor: UnownedSerialExecutor {
AudioEngine.audioExecutor.asUnownedSerialExecutor()
}
private var isRunning = false
private var processingGraph: [AudioNode] = []
private var sampleRate: Double = 44100
private var bufferSize: Int = 512
func start() {
isRunning = true
// Semua akses ke processing graph terjamin di audio thread
}
func stop() {
isRunning = false
}
func addNode(_ node: AudioNode) {
// Thread-safe karena actor isolation
processingGraph.append(node)
}
// Dipanggil dari AVAudioEngine callback — harus sangat cepat
func processBuffer(_ inputBuffer: AudioBuffer, outputBuffer: inout AudioBuffer) {
guard isRunning else { return }
// Process signal graph
var signal = inputBuffer
for node in processingGraph {
signal = node.process(signal, sampleRate: sampleRate)
}
outputBuffer = signal
}
func updateParameter(_ param: AudioParameter, value: Float) {
// Parameter update thread-safe via actor
processingGraph
.filter { $0.supportsParameter(param) }
.forEach { $0.setParameter(param, value: value) }
}
}
// Protocol untuk audio processing nodes
protocol AudioNode: AnyObject {
func process(_ buffer: AudioBuffer, sampleRate: Double) -> AudioBuffer
func supportsParameter(_ param: AudioParameter) -> Bool
func setParameter(_ param: AudioParameter, value: Float)
}
struct AudioParameter: Hashable { let name: String }
// MARK: - SwiftUI Integration
@MainActor
@Observable
final class AudioEngineViewModel {
private let engine = AudioEngine()
var isPlaying = false
var masterVolume: Float = 1.0 {
didSet {
Task {
await engine.updateParameter(.init(name: "masterVolume"), value: masterVolume)
}
}
}
func startPlayback() async {
await engine.start()
isPlaying = true
}
func stopPlayback() async {
await engine.stop()
isPlaying = false
}
}
Use Case 3: Typed Pagination dengan Custom AsyncSequence
Skenario: E-commerce app yang butuh scroll infinite dengan prefetch — data di-load secara streaming, tidak semua sekaligus.
swift
// MARK: - Typed Paginated AsyncSequence
struct PaginationConfig {
let pageSize: Int
let prefetchThreshold: Int // mulai fetch sebelum habis
}
struct ProductPage {
let products: [Product]
let totalCount: Int
let nextCursor: String?
}
struct Product: Identifiable, Sendable {
let id: String
let name: String
let price: Double
}
// AsyncSequence yang handle pagination + prefetching
struct ProductFeed: AsyncSequence {
typealias Element = Product
let config: PaginationConfig
let fetchPage: @Sendable (String?) async throws -> ProductPage
func makeAsyncIterator() -> ProductFeedIterator {
ProductFeedIterator(config: config, fetchPage: fetchPage)
}
}
struct ProductFeedIterator: AsyncIteratorProtocol {
private var buffer: [Product] = []
private var nextCursor: String? = nil
private var hasMore = true
private var isFetching = false
private var prefetchTask: Task<ProductPage, Error>?
private let config: PaginationConfig
private let fetchPage: @Sendable (String?) async throws -> ProductPage
private var isFirstFetch = true
init(config: PaginationConfig, fetchPage: @Sendable (String?) async throws -> ProductPage) {
self.config = config
self.fetchPage = fetchPage
}
mutating func next() async throws -> Product? {
// Kembalikan dari buffer jika ada
if !buffer.isEmpty {
let product = buffer.removeFirst()
// Trigger prefetch ketika buffer hampir habis
if buffer.count <= config.prefetchThreshold && hasMore && prefetchTask == nil {
let cursor = nextCursor
let fetch = fetchPage
prefetchTask = Task { try await fetch(cursor) }
}
return product
}
// Buffer kosong
guard hasMore else { return nil }
try Task.checkCancellation()
// Tunggu prefetch task jika ada, atau fetch baru
let page: ProductPage
if let prefetch = prefetchTask {
page = try await prefetch.value
prefetchTask = nil
} else {
page = try await fetchPage(nextCursor)
}
nextCursor = page.nextCursor
hasMore = page.nextCursor != nil
buffer = Array(page.products.dropFirst())
return page.products.first
}
}
// MARK: - SwiftUI Usage
@MainActor
@Observable
final class ProductListViewModel {
var products: [Product] = []
var isLoading = false
var error: String?
private var feedIterator: ProductFeedIterator?
private var loadMoreTask: Task<Void, Never>?
func startLoading() {
let feed = ProductFeed(
config: PaginationConfig(pageSize: 20, prefetchThreshold: 5)
) { cursor in
try await ProductAPI.fetchProducts(cursor: cursor, pageSize: 20)
}
feedIterator = feed.makeAsyncIterator()
loadMore()
}
func loadMore() {
guard !isLoading else { return }
isLoading = true
loadMoreTask = Task { [weak self] in
guard var iterator = self?.feedIterator else { return }
var newProducts: [Product] = []
// Load satu halaman worth of products
do {
for _ in 0..<20 {
guard let product = try await iterator.next() else { break }
newProducts.append(product)
}
self?.feedIterator = iterator
self?.products.append(contentsOf: newProducts)
} catch {
self?.error = error.localizedDescription
}
self?.isLoading = false
}
}
}
enum ProductAPI {
static func fetchProducts(cursor: String?, pageSize: Int) async throws -> ProductPage {
// Simulate API call
try await Task.sleep(for: .milliseconds(500))
let products = (0..<pageSize).map { i in
Product(id: UUID().uuidString, name: "Product \(i)", price: Double(i) * 9.99)
}
return ProductPage(
products: products,
totalCount: 1000,
nextCursor: products.count == pageSize ? UUID().uuidString : nil
)
}
}