Skip to content

Commit

Permalink
Merge pull request #195 from swsnider/cache_fixer
Browse files Browse the repository at this point in the history
Allow FXKeys to have programmatic 'cache fixing' methods
  • Loading branch information
swsnider authored Oct 30, 2023
2 parents d692081 + 61504b6 commit 4866c2c
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 32 deletions.
87 changes: 56 additions & 31 deletions Sources/llbuild2/Core/Engine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,22 +101,24 @@ open class LLBTypedCachingFunction<K: LLBKey, V: LLBValue>: LLBFunction {
public init() {}

private func computeAndUpdate(key: K, _ fi: LLBFunctionInterface, _ ctx: Context) -> LLBFuture<LLBValue> {
return self.compute(key: key, fi, ctx).flatMap { (value: LLBValue) in
do {
return ctx.db.put(try value.asCASObject(), ctx).flatMap { resultID in
return fi.functionCache.update(key: key, value: resultID, ctx).map {
return value
}
}
} catch {
return ctx.group.next().makeFailedFuture(error)
}
}.map { (value: LLBValue) in
ctx.logger?.trace(" evaluated \(key.logDescription())")
return value
return ctx.group.any().makeFutureWithTask {
return try await self.computeAndUpdate(key: key, fi, ctx)
}
}

private func computeAndUpdate(key: K, _ fi: LLBFunctionInterface, _ ctx: Context) async throws -> LLBValue {
defer { ctx.logger?.trace(" evaluated \(key.logDescription())") }

let value = try await self.compute(key: key, fi, ctx).get()
guard self.validateCache(key: key, cached: value) else {
throw LLBError.inconsistentValue("\(String(describing: type(of: key))) evaluated to a value that does not pass its own validateCache() check!")
}

let resultID = try await ctx.db.put(try value.asCASObject(), ctx).get()
_ = try await fi.functionCache.update(key: key, value: resultID, ctx).get()
return value
}

private func unpack(_ object: LLBCASObject, _ fi: LLBFunctionInterface) throws -> V {
if
let type = V.self as? LLBPolymorphicSerializable.Type,
Expand All @@ -135,38 +137,60 @@ open class LLBTypedCachingFunction<K: LLBKey, V: LLBValue>: LLBFunction {

@_disfavoredOverload
public func compute(key: LLBKey, _ fi: LLBFunctionInterface, _ ctx: Context) -> LLBFuture<LLBValue> {
return ctx.group.any().makeFutureWithTask {
return try await self.compute(key: key, fi, ctx)
}
}

@_disfavoredOverload
public func compute(key: LLBKey, _ fi: LLBFunctionInterface, _ ctx: Context) async throws -> LLBValue {
guard let typedKey = key as? K else {
return ctx.group.next().makeFailedFuture(LLBError.unexpectedKeyType(String(describing: type(of: key))))
throw LLBError.unexpectedKeyType(String(describing: type(of: key)))
}

ctx.logger?.trace("evaluating \(key.logDescription())")

return fi.functionCache.get(key: key, ctx).flatMap { result -> LLBFuture<LLBValue> in
if let resultID = result {
return ctx.db.get(resultID, ctx).flatMap { objectOpt in
guard let object = objectOpt else {
return self.computeAndUpdate(key: typedKey, fi, ctx)
}
do {
let value: V = try self.unpack(object, fi)
ctx.logger?.trace(" cached \(key.logDescription())")
return ctx.group.next().makeSucceededFuture(value)
} catch {
guard self.recomputeOnCacheFailure else {
return ctx.group.next().makeFailedFuture(error)
}
return self.computeAndUpdate(key: typedKey, fi, ctx)
}
guard let resultID = try await fi.functionCache.get(key: key, ctx).get(), let object = try await ctx.db.get(resultID, ctx).get() else {
return try await self.computeAndUpdate(key: typedKey, fi, ctx).get()
}

do {
let value: V = try self.unpack(object, fi)
ctx.logger?.trace(" cached \(key.logDescription())")

guard validateCache(key: typedKey, cached: value) else {
guard let newValue = try await self.fixCached(key: typedKey, value: value, fi, ctx).get() else {
// Throw here to engage recomputeOnCacheFailure logic below.
throw LLBError.invalidValueType("failed to validate cache for \(String(describing: type(of: typedKey))), and fixCached() was not able to solve the problem")
}

let newResultID = try await ctx.db.put(try newValue.asCASObject(), ctx).get()
_ = try await fi.functionCache.update(key: typedKey, value: newResultID, ctx).get()
return newValue
}
return self.computeAndUpdate(key: typedKey, fi, ctx)

return value
} catch {
guard self.recomputeOnCacheFailure else {
throw error
}

return try await self.computeAndUpdate(key: typedKey, fi, ctx).get()
}
}

open func compute(key: K, _ fi: LLBFunctionInterface, _ ctx: Context) -> LLBFuture<V> {
// This is a developer error and not a runtime error, which is why fatalError is used.
fatalError("unimplemented: this method is expected to be overridden by subclasses.")
}

open func validateCache(key: K, cached: V) -> Bool {
return true
}

open func fixCached(key: K, value: V, _ fi: LLBFunctionInterface, _ ctx: Context) -> LLBFuture<V?> {
return ctx.group.next().makeSucceededFuture(nil)
}
}

public protocol LLBEngineDelegate {
Expand All @@ -181,6 +205,7 @@ public extension LLBEngineDelegate {
public enum LLBError: Error {
case invalidValueType(String)
case unexpectedKeyType(String)
case inconsistentValue(String)
}

internal struct Key {
Expand Down
29 changes: 29 additions & 0 deletions Sources/llbuild2fx/Key.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public protocol FXKey: Encodable, FXVersioning {
var hint: String? { get }

func computeValue(_ fi: FXFunctionInterface<Self>, _ ctx: Context) -> LLBFuture<ValueType>

func validateCache(cached: ValueType) -> Bool
func fixCached(value: ValueType, _ fi: FXFunctionInterface<Self>, _ ctx: Context) -> LLBFuture<ValueType?>
}

extension FXKey {
Expand All @@ -32,6 +35,14 @@ extension FXKey {
public static var recomputeOnCacheFailure: Bool { true }

public var hint: String? { nil }

public func validateCache(cached: ValueType) -> Bool {
return true
}

public func fixCached(value: ValueType, _ fi: FXFunctionInterface<Self>, _ ctx: Context) -> LLBFuture<ValueType?> {
return ctx.group.next().makeSucceededFuture(nil)
}
}


Expand Down Expand Up @@ -199,10 +210,22 @@ final class FXFunction<K: FXKey>: LLBTypedCachingFunction<InternalKey<K>, Intern
ctx.fxBuildEngineStats.remove(key: key.name)
}
}

override func validateCache(key: InternalKey<K>, cached: InternalValue<K.ValueType>) -> Bool {
return key.key.validateCache(cached: cached.value)
}

override func fixCached(key: InternalKey<K>, value: InternalValue<K.ValueType>, _ fi: LLBFunctionInterface, _ ctx: Context) -> LLBFuture<InternalValue<K.ValueType>?> {
let actualKey = key.key

let fxfi = FXFunctionInterface(actualKey, fi)
return actualKey.fixCached(value: value.value, fxfi, ctx).map { maybeFixed in maybeFixed.map { InternalValue($0, requestedCacheKeyPaths: fxfi.requestedCacheKeyPathsSnapshot) }}
}
}

public protocol AsyncFXKey: FXKey {
func computeValue(_ fi: FXFunctionInterface<Self>, _ ctx: Context) async throws -> ValueType
func fixCached(value: ValueType, _ fi: FXFunctionInterface<Self>, _ ctx: Context) async throws -> ValueType?
}

extension AsyncFXKey {
Expand All @@ -211,6 +234,12 @@ extension AsyncFXKey {
try await computeValue(fi, ctx)
}
}

public func fixCached(value: ValueType, _ fi: FXFunctionInterface<Self>, _ ctx: Context) -> LLBFuture<ValueType?> {
ctx.group.any().makeFutureWithTask {
try await fixCached(value: value, fi, ctx)
}
}
}

extension FXFunctionInterface {
Expand Down
79 changes: 78 additions & 1 deletion Tests/llbuild2fxTests/EngineTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import XCTest
import NIOCore
import TSFCAS
import TSFFutures
import llbuild2fx
import llbuild2
@testable import llbuild2fx


public struct SumInput: Codable {
Expand Down Expand Up @@ -94,6 +95,62 @@ public struct Sum: FXKey {
}


public struct AbsoluteSum: AsyncFXKey {
public typealias ValueType = SumAction.ValueType

public static let version = SumAction.version

public static let versionDependencies: [FXVersioning.Type] = [Sum.self]

public let values: [Int]

public init(values: [Int]) {
self.values = values
}

public func computeValue(_ fi: FXFunctionInterface<AbsoluteSum>, _ ctx: Context) async throws -> SumAction.ValueType {
let sum = try await fi.request(Sum(values: self.values), ctx).total
return SumOutput(total: sum < 0 ? sum * -1 : sum)
}

public func validateCache(cached: SumAction.ValueType) -> Bool {
return cached.total >= 0
}

public func fixCached(value: SumAction.ValueType, _ fi: FXFunctionInterface<AbsoluteSum>, _ ctx: Context) async throws -> SumAction.ValueType? {
if value.total < 0 {
return SumAction.ValueType(total: value.total * -1)
}

return nil
}
}

actor TestFunctionCache {
private var cache: [String: LLBDataID] = [:]

func get(key: LLBKey, props: FXKeyProperties, _ ctx: Context) async -> LLBDataID? {
return cache[props.cachePath]
}

func update(key: LLBKey, props: FXKeyProperties, value: LLBDataID, _ ctx: Context) async {
cache[props.cachePath] = value
}
}

extension TestFunctionCache: FXFunctionCache {
nonisolated func get(key: LLBKey, props: FXKeyProperties, _ ctx: Context) -> LLBFuture<LLBDataID?> {
return ctx.group.any().makeFutureWithTask {
return await self.get(key: key, props: props, ctx)
}
}

nonisolated func update(key: LLBKey, props: FXKeyProperties, value: LLBDataID, _ ctx: Context) -> LLBFuture<Void> {
return ctx.group.any().makeFutureWithTask {
_ = await self.update(key: key, props: props, value: value, ctx)
}
}
}

final class EngineTests: XCTestCase {
func testBasicMath() throws {
Expand All @@ -107,4 +164,24 @@ final class EngineTests: XCTestCase {
let result = try engine.build(key: Sum(values: [2, 3, 4]), ctx).wait()
XCTAssertEqual(result.total, 9)
}

func testWeirdMath() async throws {
var ctx = Context()
let group = LLBMakeDefaultDispatchGroup()
let db = LLBInMemoryCASDatabase(group: group)
let executor = FXLocalExecutor()
let functionCache = TestFunctionCache()
ctx.db = db
ctx.group = group

let internalKey = AbsoluteSum(values: [-2, -3, -4]).internalKey(ctx)
let cachedOutput = SumOutput(total: -9)
let cacheID = try await ctx.db.put(try cachedOutput.asCASObject(), ctx).get()
try await functionCache.update(key: internalKey, props: internalKey, value: cacheID, ctx).get()

let engine = FXBuildEngine(group: group, db: db, functionCache: functionCache, executor: executor)

let result = try await engine.build(key: AbsoluteSum(values: [-2, -3, -4]), ctx).get()
XCTAssertEqual(result.total, 9)
}
}

0 comments on commit 4866c2c

Please sign in to comment.