Spaces:
Sleeping
Sleeping
File size: 4,227 Bytes
6ee917b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | import Foundation
import NIOConcurrencyHelpers
import Vapor
protocol GenericFileManagable {
func wasDeleted() -> Bool
static func open(from: OmFileManagerReadable) throws -> Self?
}
/// Cache file handles, background close checks
/// If a file path is missing, this information is cached and checked in the background
/// This could be later extended to use file system events
/// Maybe upgraded to an actor as well. Currently uses pthread locks
struct GenericFileManager<File: GenericFileManagable> {
/// A file might exist and is open, or it is missing
enum OmFileState {
case exists(file: File, opened: Timestamp)
case missing(path: String, opened: Timestamp)
}
/// Non existing files are set to nil
private let cached = NIOLockedValueBox<[Int: OmFileState]>(.init())
private let statistics = NIOLockedValueBox<(count: Double, elapsed: Double, max: Double)>((0,0,0))
/// Called every 2 conds from a life cycle handler on any available thread
@Sendable func backgroundTask(application: Application) {
let logger = application.logger
var (count, elapsed, max) = statistics.withLockedValue({$0})
let start = DispatchTime.now()
let stats = self.secondlyCallback()
let dt = Double((DispatchTime.now().uptimeNanoseconds - start.uptimeNanoseconds)) / 1_000_000_000
if dt > max {
max = dt
}
elapsed += dt
count += 1
if count >= 10 {
if (stats.open > 0) {
logger.info("OmFileManager checked \(stats.open) open files and \(stats.missing) missing files. Time average=\((elapsed/count).asSecondsPrettyPrint) max=\(max.asSecondsPrettyPrint)")
}
count = 0
elapsed = 0
max = 0
}
statistics.withLockedValue({ $0 = (count, elapsed, max) })
}
/// Called every couple of seconds to check for any file modifications
func secondlyCallback() -> (open: Int, missing: Int, ejected: Int) {
// Could be later used to expose some metrics
var countExisting = 0
var countMissing = 0
var countEjected = 0
let copy = cached.withLockedValue {
return $0
}
// Close file handles after 1 hour
let ejectionTime = Timestamp.now().subtract(hours: 1)
for e in copy {
switch e.value {
case .exists(file: let file, opened: let opened):
// Remove file from cache, if it was deleted
if opened < ejectionTime || file.wasDeleted() {
cached.withLockedValue({
$0.removeValue(forKey: e.key)
countEjected += 1
})
}
countExisting += 1
case .missing(path: let path, opened: let opened):
// Remove file from cache, if it is now available, so the next open, will make it available
if opened < ejectionTime || FileManager.default.fileExists(atPath: path) {
cached.withLockedValue({
let _ = $0.removeValue(forKey: e.key)
countEjected += 1
})
}
countMissing += 1
}
}
return (countExisting, countMissing, countEjected)
}
/// Get cached file or return nil, if the files does not exist
public func get(_ file: OmFileManagerReadable) throws -> File? {
let key = file.hashValue
return try cached.withLockedValue { cached -> File? in
if let file = cached[key] {
switch file {
case .exists(file: let file, opened: _):
return file
case .missing(path: _, opened: _):
return nil
}
}
guard let file = try File.open(from: file) else {
cached[key] = .missing(path: file.getFilePath(), opened: .now())
return nil
}
cached[key] = .exists(file: file, opened: .now())
return file
}
}
}
|