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
        }
    }
}