|
11 | 11 | //===----------------------------------------------------------------------===//
|
12 | 12 |
|
13 | 13 | @_exported import Crypto
|
| 14 | +import Helpers |
14 | 15 | import struct Logging.Logger
|
15 | 16 | @_exported import struct SystemPackage.FilePath
|
16 |
| -import Helpers |
17 | 17 |
|
18 | 18 | public func withEngine(
|
19 |
| - _ fileSystem: any FileSystem, |
20 |
| - _ logger: Logger, |
21 |
| - cacheLocation: SQLite.Location, |
22 |
| - _ body: @Sendable (Engine) async throws -> Void |
| 19 | + _ fileSystem: any FileSystem, |
| 20 | + _ logger: Logger, |
| 21 | + cacheLocation: SQLite.Location, |
| 22 | + _ body: @Sendable (Engine) async throws -> () |
23 | 23 | ) async throws {
|
24 |
| - let engine = Engine( |
25 |
| - fileSystem, |
26 |
| - logger, |
27 |
| - cacheLocation: cacheLocation |
28 |
| - ) |
29 |
| - |
30 |
| - try await withAsyncThrowing { |
31 |
| - try await body(engine) |
32 |
| - } defer: { |
33 |
| - try await engine.shutDown() |
34 |
| - } |
| 24 | + let engine = Engine( |
| 25 | + fileSystem, |
| 26 | + logger, |
| 27 | + cacheLocation: cacheLocation |
| 28 | + ) |
| 29 | + |
| 30 | + try await withAsyncThrowing { |
| 31 | + try await body(engine) |
| 32 | + } defer: { |
| 33 | + try await engine.shutDown() |
| 34 | + } |
35 | 35 | }
|
36 | 36 |
|
37 | 37 | /// Cacheable computations engine. Currently the engine makes an assumption that computations produce same results for
|
38 | 38 | /// the same query values and write results to a single file path.
|
39 | 39 | public actor Engine {
|
40 |
| - private(set) var cacheHits = 0 |
41 |
| - private(set) var cacheMisses = 0 |
42 |
| - |
43 |
| - public let fileSystem: any FileSystem |
44 |
| - public let logger: Logger |
45 |
| - private let resultsCache: SQLiteBackedCache |
46 |
| - private var isShutDown = false |
47 |
| - |
48 |
| - /// Creates a new instance of the ``QueryEngine`` actor. Requires an explicit call |
49 |
| - /// to ``QueryEngine//shutdown`` before the instance is deinitialized. The recommended approach to resource |
50 |
| - /// management is to place `engine.shutDown()` when the engine is no longer used, but is not deinitialized yet. |
51 |
| - /// - Parameter fileSystem: Implementation of a file system this engine should use. |
52 |
| - /// - Parameter cacheLocation: Location of cache storage used by the engine. |
53 |
| - /// - Parameter logger: Logger to use during queries execution. |
54 |
| - init( |
55 |
| - _ fileSystem: any FileSystem, |
56 |
| - _ logger: Logger, |
57 |
| - cacheLocation: SQLite.Location |
58 |
| - ) { |
59 |
| - self.fileSystem = fileSystem |
60 |
| - self.logger = logger |
61 |
| - self.resultsCache = SQLiteBackedCache(tableName: "cache_table", location: cacheLocation, logger) |
62 |
| - } |
| 40 | + private(set) var cacheHits = 0 |
| 41 | + private(set) var cacheMisses = 0 |
| 42 | + |
| 43 | + public let fileSystem: any FileSystem |
| 44 | + public let logger: Logger |
| 45 | + private let resultsCache: SQLiteBackedCache |
| 46 | + private var isShutDown = false |
| 47 | + |
| 48 | + /// Creates a new instance of the ``QueryEngine`` actor. Requires an explicit call |
| 49 | + /// to ``QueryEngine//shutdown`` before the instance is deinitialized. The recommended approach to resource |
| 50 | + /// management is to place `engine.shutDown()` when the engine is no longer used, but is not deinitialized yet. |
| 51 | + /// - Parameter fileSystem: Implementation of a file system this engine should use. |
| 52 | + /// - Parameter cacheLocation: Location of cache storage used by the engine. |
| 53 | + /// - Parameter logger: Logger to use during queries execution. |
| 54 | + init( |
| 55 | + _ fileSystem: any FileSystem, |
| 56 | + _ logger: Logger, |
| 57 | + cacheLocation: SQLite.Location |
| 58 | + ) { |
| 59 | + self.fileSystem = fileSystem |
| 60 | + self.logger = logger |
| 61 | + self.resultsCache = SQLiteBackedCache(tableName: "cache_table", location: cacheLocation, logger) |
| 62 | + } |
| 63 | + |
| 64 | + public func shutDown() async throws { |
| 65 | + precondition(!self.isShutDown, "`QueryEngine/shutDown` should be called only once") |
| 66 | + try self.resultsCache.close() |
| 67 | + |
| 68 | + self.isShutDown = true |
| 69 | + } |
| 70 | + |
| 71 | + deinit { |
| 72 | + let isShutDown = self.isShutDown |
| 73 | + precondition( |
| 74 | + isShutDown, |
| 75 | + "`QueryEngine/shutDown` should be called explicitly on instances of `Engine` before deinitialization" |
| 76 | + ) |
| 77 | + } |
| 78 | + |
| 79 | + /// Executes a given query if no cached result of it is available. Otherwise fetches the result from engine's cache. |
| 80 | + /// - Parameter query: A query value to execute. |
| 81 | + /// - Returns: A file path to query's result recorded in a file. |
| 82 | + public subscript(_ query: some Query) -> FileCacheRecord { |
| 83 | + get async throws { |
| 84 | + let hashEncoder = HashEncoder<SHA512>() |
| 85 | + try hashEncoder.encode(query.cacheKey) |
| 86 | + let key = hashEncoder.finalize() |
| 87 | + |
| 88 | + if let fileRecord = try resultsCache.get(key, as: FileCacheRecord.self) { |
| 89 | + let fileHash = try await self.fileSystem.withOpenReadableFile(fileRecord.path) { |
| 90 | + var hashFunction = SHA512() |
| 91 | + try await $0.hash(with: &hashFunction) |
| 92 | + return hashFunction.finalize().description |
| 93 | + } |
| 94 | + |
| 95 | + if fileHash == fileRecord.hash { |
| 96 | + self.cacheHits += 1 |
| 97 | + return fileRecord |
| 98 | + } |
| 99 | + } |
63 | 100 |
|
64 |
| - public func shutDown() async throws { |
65 |
| - precondition(!self.isShutDown, "`QueryEngine/shutDown` should be called only once") |
66 |
| - try self.resultsCache.close() |
| 101 | + self.cacheMisses += 1 |
| 102 | + let resultPath = try await query.run(engine: self) |
67 | 103 |
|
68 |
| - self.isShutDown = true |
69 |
| - } |
| 104 | + let resultHash = try await self.fileSystem.withOpenReadableFile(resultPath) { |
| 105 | + var hashFunction = SHA512() |
| 106 | + try await $0.hash(with: &hashFunction) |
| 107 | + return hashFunction.finalize().description |
| 108 | + } |
| 109 | + let result = FileCacheRecord(path: resultPath, hash: resultHash) |
70 | 110 |
|
71 |
| - deinit { |
72 |
| - let isShutDown = self.isShutDown |
73 |
| - precondition( |
74 |
| - isShutDown, |
75 |
| - "`QueryEngine/shutDown` should be called explicitly on instances of `Engine` before deinitialization" |
76 |
| - ) |
77 |
| - } |
| 111 | + // FIXME: update `SQLiteBackedCache` to store `resultHash` directly instead of relying on string conversions |
| 112 | + try self.resultsCache.set(key, to: result) |
78 | 113 |
|
79 |
| - /// Executes a given query if no cached result of it is available. Otherwise fetches the result from engine's cache. |
80 |
| - /// - Parameter query: A query value to execute. |
81 |
| - /// - Returns: A file path to query's result recorded in a file. |
82 |
| - public subscript(_ query: some Query) -> FileCacheRecord { |
83 |
| - get async throws { |
84 |
| - let hashEncoder = HashEncoder<SHA512>() |
85 |
| - try hashEncoder.encode(query.cacheKey) |
86 |
| - let key = hashEncoder.finalize() |
87 |
| - |
88 |
| - if let fileRecord = try resultsCache.get(key, as: FileCacheRecord.self) { |
89 |
| - |
90 |
| - let fileHash = try await self.fileSystem.withOpenReadableFile(fileRecord.path) { |
91 |
| - var hashFunction = SHA512() |
92 |
| - try await $0.hash(with: &hashFunction) |
93 |
| - return hashFunction.finalize().description |
94 |
| - } |
95 |
| - |
96 |
| - if fileHash == fileRecord.hash { |
97 |
| - self.cacheHits += 1 |
98 |
| - return fileRecord |
99 |
| - } |
100 |
| - } |
101 |
| - |
102 |
| - self.cacheMisses += 1 |
103 |
| - let resultPath = try await query.run(engine: self) |
104 |
| - |
105 |
| - let resultHash = try await self.fileSystem.withOpenReadableFile(resultPath) { |
106 |
| - var hashFunction = SHA512() |
107 |
| - try await $0.hash(with: &hashFunction) |
108 |
| - return hashFunction.finalize().description |
109 |
| - } |
110 |
| - let result = FileCacheRecord(path: resultPath, hash: resultHash) |
111 |
| - |
112 |
| - // FIXME: update `SQLiteBackedCache` to store `resultHash` directly instead of relying on string conversions |
113 |
| - try self.resultsCache.set(key, to: result) |
114 |
| - |
115 |
| - return result |
116 |
| - } |
| 114 | + return result |
117 | 115 | }
|
| 116 | + } |
118 | 117 | }
|
0 commit comments