|
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