a8b09fb5创建于 2025年5月14日历史提交
/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2022-2024. All rights reserved.
 */

/**
 * @file
 * The file declares the CallServerInterceptor class.
 */
package httpclient4cj

import std.fs

class DiskLruCache {
    let fileSystem: FileSystem
    var directoryPath: Path
    var valueCount: Int64
    var maxSize: Int64
    var size: Int64 = 0
    var initialized: Bool = false
    let lock: Mutex = Mutex()
    var closed: Bool = false

    static let LEGAL_KEY_PATTERN: String = "[a-z0-9_-]{1,120}"
    let lruEntries: HashMap<String, Entry> = HashMap<String, Entry>()
    var journalWriter: Option<Sink> = Option<Sink>.None
    var redundantOpCount: Int64 = 0
    var hasJournalErrors: Bool = false
    var mostRecentTrimFailed: Bool = false
    var mostRecentRebuildFailed: Bool = false
    var nextSequenceNumber = 0

    init(fileSystem: FileSystem, directoryPath: Path, valueCount: Int64, maxSize: Int64) {
        this.fileSystem = fileSystem
        this.directoryPath = directoryPath
        this.valueCount = valueCount
        this.maxSize = maxSize
    }

    static func create(
        fileSystem: FileSystem,
        directoryPath: Path,
        valueCount: Int64,
        maxSize: Int64
    ): DiskLruCache {
        if (maxSize <= 0) {
            throw IllegalArgumentException("maxSize <= 0")
        }

        if (valueCount <= 0) {
            throw IllegalArgumentException("valueCount <= 0")
        }

        return DiskLruCache(fileSystem, directoryPath, valueCount, maxSize)
    }

    public func remove(key: String): Bool {
        synchronized(lock) {
            initialize()
            checkNotClosed()
            validateKey(key)

            match (lruEntries.get(key)) {
                case Some(e) =>
                    let removed = removeEntry(e)

                    if (removed && size <= maxSize) {
                        mostRecentTrimFailed = false
                    }

                    return removed
                case None => return false
            }
        }
    }

    public func get(key: String): Option<Snapshot> {
        synchronized(lock) {
            initialize()
            checkNotClosed()
            validateKey(key)
            match (lruEntries.get(key)) {
                case None => return None
                case Some(entry) => if (!entry.readable) {
                    return None
                }
            }

            match (lruEntries[key].snapshot()) {
                case None => return Option<Snapshot>.None
                case Some(v) =>
                    redundantOpCount++
                    let jw = journalWriter.getOrThrow()
                    jw.write(READ.toUtf8Array())
                    jw.write(" ".toUtf8Array())
                    jw.write(key.toUtf8Array())
                    jw.write("\n".toUtf8Array())
                    if (journalRebuildRequired()) {
                        cleanUp()
                    }
                    return v
            }
        }
    }

    public func edit(key: String): Option<Editor> {
        return edit(key, ANY_SEQUENCE_NUMBER)
    }

    func edit(key: String, expectedSequenceNumber: Int64): Option<Editor> {
        synchronized(lock) {
            initialize()
            checkNotClosed()
            validateKey(key)
            var entry: Option<Entry> = lruEntries.get(key)
            if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (!has(entry) || entry.getOrThrow().sequenceNumber !=
                expectedSequenceNumber)) {
                return None
            }

            if (has(entry) && has(entry.getOrThrow().currentEditor)) {
                return None
            }

            if (mostRecentTrimFailed || mostRecentRebuildFailed) {
                cleanUp()
                return None
            }
            let jw = journalWriter.getOrThrow()
            jw.write(DIRTY.toUtf8Array())
            jw.write(" ".toUtf8Array())
            jw.write(key.toUtf8Array())
            jw.write("\n".toUtf8Array())
            jw.flush()

            if (hasJournalErrors) {
                return None
            }

            if (!has(entry)) {
                let e = Entry(key, this)
                lruEntries.add(key, e)
                entry = e
            }

            let editor = Editor(entry.getOrThrow(), this)
            entry.getOrThrow().currentEditor = editor
            return editor
        }
    }

    func cleanUp(): Unit {
        synchronized(lock) {
            if (!initialized || closed) {
                return
                // Nothing to do
            }

            try {
                trimToSize()
            } catch (e: Exception) {
                mostRecentTrimFailed = true
            }

            try {
                if (journalRebuildRequired()) {
                    rebuildJournal()
                    redundantOpCount = 0
                }
            } catch (e: IOException) {
                mostRecentRebuildFailed = true
                journalWriter = RealBufferSink(BlackHole())
            }
        }
    }

    func journalRebuildRequired() {
        let redundantOpCompactThreshold = 2000
        return redundantOpCount >= redundantOpCompactThreshold && redundantOpCount >= lruEntries.size
    }

    func removeEntry(entry: Entry): Bool {
        match (entry.currentEditor) {
            case Some(ce) => ce.detach()
            case None => ()
        }

        for (i in 0..valueCount) {
            fileSystem.delete(entry.cleanFiles[i])
            size -= entry.lengths[i]
            entry.lengths[i] = 0
        }

        redundantOpCount++
        let jw = journalWriter.getOrThrow()
        jw.write(REMOVE.toUtf8Array())
        jw.write(" ".toUtf8Array())
        jw.write(entry.key.toUtf8Array())
        jw.write("\n".toUtf8Array())
        lruEntries.remove(entry.key)

        if (journalRebuildRequired()) {
            cleanUp()
        }

        return true
    }

    private func validateKey(_: String): Unit {
        /* let matcher: Matcher = Regex(LEGAL_KEY_PATTERN).matcher(key)
           match (matcher.find()) {
           case None =>
               throw IllegalArgumentException("keys must match regex [a-z0-9_-]{1,120}: ${key}")
           case Some(data) =>
               ()
           }*/
    }

    private func checkNotClosed(): Unit {
        synchronized(lock) {
            if (isClosed()) {
                throw IllegalStateException("cache is closed")
            }
        }
    }

    public func isClosed(): Bool {
        synchronized(lock) {
            return closed
        }
    }

    func trimToSize(): Unit {
        while (size > maxSize) {
            let toEvict: Entry = lruEntries.values().iterator().next().getOrThrow()
            removeEntry(toEvict)
        }

        mostRecentTrimFailed = false
    }

    public func initialize(): Unit {
        synchronized(lock) {
            if (initialized) {
                return
                // Already initialized.
            }

            // If a bkp file exists, use it instead.
            if (fileSystem.exists(JOURNAL_FILE_BACKUP)) {
                if (fileSystem.exists(JOURNAL_FILE_BACKUP)) {
                    fileSystem.delete(JOURNAL_FILE_BACKUP)
                } else {
                    fileSystem.rename(JOURNAL_FILE_BACKUP, JOURNAL_FILE)
                }
            }

            // Prefer to pick up where we left off.
            if (fileSystem.exists(JOURNAL_FILE)) {
                try {
                    readJournal()
                    processJournal()
                    initialized = true
                    return
                } catch (e: FSException | HttpException | EOFException) {}

                try {
                    delete()
                } finally {
                    closed = false
                }
            }

            rebuildJournal()
            initialized = true
        }
    }

    public func delete() {
        fileSystem.deleteContents(directoryPath)
    }

    public func close(): Unit {
        if (closed) {
            return
        }
        if (initialized) {
            for (entry in lruEntries.values() |> collectArray) {
                match (entry.currentEditor) {
                    case None => ()
                    case Some(ce) => ce.abort()
                }
            }

            trimToSize()
            journalWriter.getOrThrow().close()
            journalWriter = None
            fileSystem.close()
        }
        closed = true
    }

    private func processJournal(): Unit {
        fileSystem.delete(JOURNAL_FILE_TEMP)
        let arr = ArrayList<String>()
        for (key in lruEntries.keys()) {
            let entry = lruEntries[key]
            match (entry.currentEditor) {
                case None => for (i in 0..valueCount) {
                    size += entry.lengths[i]
                }
                case Some(_) =>
                    entry.currentEditor = None
                    for (i in 0..valueCount) {
                        fileSystem.delete(entry.cleanFiles[i])
                        fileSystem.delete(entry.dirtyFiles[i])
                    }
                    arr.add(key)
            }
        }
        lruEntries.remove(all: arr)
    }

    private func readJournal() {
        try (source = fileSystem.source(JOURNAL_FILE)) {
            let magic = source.readUtf8Line()
            let version = source.readUtf8Line()
            let valueCountString = source.readUtf8Line()
            let blank = source.readUtf8Line()

            if (!MAGIC.equals(magic) || !VERSION_1.equals(version) || !"${valueCount}".equals(valueCountString) ||
                !"".equals(blank)) {
                throw HttpException("unexpected journal header: [${magic},${version},${valueCountString},${blank}")
            }

            var lineCount = 0
            let content = StringBuilder("${magic}\n${version}\n${valueCountString}\n\n")
            while (true) {
                try {
                    let s = source.readUtf8Line()
                    content.append(s)
                    content.append("\n")
                    readJournalLine(s)
                    lineCount++
                } catch (endOfJournal: EOFException) {
                    break
                }
            }
            redundantOpCount = lineCount - lruEntries.size
            if (!source.exhausted()) {
                rebuildJournal()
            } else {
                journalWriter = newJournalWriter()
            }
        }
    }

    private func newJournalWriter(): RealBufferSink {
        return RealBufferSink(fileSystem.appendingSink(JOURNAL_FILE))
    }

    func rebuildJournal(): Unit {
        match (journalWriter) {
            case Some(rj) => rj.close()
            case None => ()
        }

        synchronized(lock) {
            match (journalWriter) {
                case Some(jw) => jw.close()
                case None => ()
            }

            let writer = RealBufferSink(fileSystem.sink(JOURNAL_FILE_TEMP))

            try {
                writer.write(MAGIC.toUtf8Array())
                writer.write("\n".toUtf8Array())
                writer.write(VERSION_1.toUtf8Array())
                writer.write("\n".toUtf8Array())
                writer.write("${valueCount}".toUtf8Array())
                writer.write("\n".toUtf8Array())
                writer.write("\n".toUtf8Array())
                for (entry in lruEntries.values()) {
                    match (entry.currentEditor) {
                        case Some(_) =>
                            writer.write((DIRTY + " ").toUtf8Array())
                            writer.write(entry.key.toUtf8Array())
                            writer.write("\n".toUtf8Array())
                        case None =>
                            writer.write((CLEAN + " ").toUtf8Array())
                            writer.write(entry.key.toUtf8Array())
                            entry.writeLengths(writer)
                            writer.write("\n".toUtf8Array())
                    }
                }
            } finally {
                writer.close()
            }

            if (fileSystem.exists(JOURNAL_FILE)) {
                fileSystem.rename(JOURNAL_FILE, JOURNAL_FILE_BACKUP)
            }

            fileSystem.rename(JOURNAL_FILE_TEMP, JOURNAL_FILE)
            fileSystem.delete(JOURNAL_FILE_BACKUP)
            journalWriter = newJournalWriter()
            hasJournalErrors = false
            mostRecentRebuildFailed = false
        }
    }

    private func readJournalLine(line: String): Unit {
        let firstSpace = line.indexOf(' ').getOrThrow({=> HttpException("unexpected journal line: ${line}")})
        let keyBegin = firstSpace + 1
        let key: String
        let secondSpace = match (line.indexOf(' ', keyBegin)) {
            case Some(index) =>
                key = line[(keyBegin..index)]
                index
            case None =>
                key = line.substring(keyBegin, line.size - keyBegin)
                if (firstSpace == REMOVE.size && line.startsWith(REMOVE)) {
                    lruEntries.remove(key)
                    return
                }

                -1
        }

        var entry: Entry = match (lruEntries.get(key)) {
            case Some(e) => e
            case None =>
                let e = Entry(key, this)
                lruEntries.add(key, e)
                e
        }
        if (secondSpace != -1 && firstSpace == CLEAN.size && line.startsWith(CLEAN)) {
            let parts: Array<String> = line.substring(secondSpace + 1).split(" ")
            entry.readable = true
            entry.currentEditor = None
            entry.setLengths(parts)
        } else if (secondSpace == -1 && firstSpace == DIRTY.size && line.startsWith(DIRTY)) {
            entry.currentEditor = Editor(entry, this)
        } else if (secondSpace == -1 && firstSpace == READ.size && line.startsWith(READ)) {
            // This work was already done by calling lruEntries.get().
        } else {
            throw HttpException("unexpected journal line: ${line}")
        }
    }

    func flush(): Unit {
        synchronized(lock) {
            if (!initialized) {
                return
            }

            checkNotClosed()
            trimToSize()
            journalWriter.getOrThrow().flush()
        }
    }

    func evictAll() {
        synchronized(lock) {
            initialize()
            let arr: ArrayList<Entry> = ArrayList<Entry>()

            //Copying for safe iteration. resolve ConcurrentModificationException
            for ((_, v) in lruEntries) {
                arr.add(v)
            }

            for (i in 0..arr.size) {
                removeEntry(arr[i])
            }
        }

        mostRecentTrimFailed = false
    }

    func snapshots(): Iterator<Snapshot> {
        synchronized(lock) {
            initialize()
            return SnapshotIterator(this)
        }
    }

    func getSize() {
        synchronized(lock) {
            initialize()
            return size
        }
    }

    func getMaxSize() {
        synchronized(lock) {
            return maxSize
        }
    }
}

class SnapshotIterator <: Iterator<Snapshot> {
    let delegate: Iterator<Entry>
    let diskLruCache: DiskLruCache
    init(diskLruCache: DiskLruCache) {
        this.diskLruCache = diskLruCache
        this.delegate = (diskLruCache.lruEntries.values() |> collectArray).iterator()
    }

    public func next(): Option<Snapshot> {
        synchronized(diskLruCache.lock) {
            if (diskLruCache.closed) {
                return Option<Snapshot>.None
            }

            while (true) {
                let snapshot: Option<Snapshot> = match (delegate.next()) {
                    case None => break
                    case Some(entry) =>
                        if (!entry.readable) {
                            continue
                        }
                        entry.snapshot()
                }

                match (snapshot) {
                    case None => continue
                    case Some(s) => return s
                }
            }
        }
        return Option<Snapshot>.None
    }

    // public func iterator(): Iterator<Snapshot> {
    //     return this
    // }
}

class Editor {
    let entry: Entry
    let written: Option<Array<Bool>>
    private var done: Bool = false
    let diskLruCache: DiskLruCache
    let lock: Mutex = Mutex()

    init(entry: Entry, diskLruCache: DiskLruCache) {
        this.entry = entry
        this.diskLruCache = diskLruCache
        this.written = if (!entry.readable) {
            Array<Bool>(diskLruCache.valueCount, repeat: false)
        } else {
            Option<Array<Bool>>.None
        }
    }

    func detach(): Unit {
        if (refEq(entry.currentEditor.getOrThrow(), this)) {
            for (i in 0..diskLruCache.valueCount) {
                try {
                    diskLruCache.fileSystem.delete(entry.dirtyFiles[i])
                } catch (e: IOException) {
                    // This file is potentially leaked. Not much we can do about that
                }
            }
            entry.currentEditor = None
        }
    }

    public func newSink(index: Int64): Sink {
        synchronized(diskLruCache.lock) {
            if (done) {
                throw IllegalStateException("has done")
            }

            if (!refEq(entry.currentEditor.getOrThrow(), this)) {
                return BlackHole()
            }

            if (!entry.readable) {
                written.getOrThrow()[index] = true
            }

            let dirtyFile = entry.dirtyFiles[index]
            let sink: Sink
            try {
                sink = diskLruCache.fileSystem.sink(dirtyFile)
            } catch (e: FSException) {
                return BlackHole()
            }

            return FaultHidingSink(sink, this)
        }
    }

    func completeEdit(editor: Editor, success: Bool): Unit {
        synchronized(lock) {
            let entry = editor.entry
            if (!refEq(entry.currentEditor.getOrThrow(), editor)) {
                throw IllegalStateException("editor is others")
            }

            // If this edit is creating the entry for the first time, every index must have a value.
            if (success && !entry.readable) {
                for (i in 0..diskLruCache.valueCount) {
                    if (!editor.written.getOrThrow()[i]) {
                        editor.abort()
                        throw IllegalStateException("Newly created entry didn't create value for index ${i}")
                    }
                    if (!diskLruCache.fileSystem.exists(entry.dirtyFiles[i])) {
                        editor.abort()
                        return
                    }
                }
            }

            for (i in 0..diskLruCache.valueCount) {
                let dirty = entry.dirtyFiles[i]
                if (success) {
                    if (diskLruCache.fileSystem.exists(dirty)) {
                        let clean = entry.cleanFiles[i]
                        diskLruCache.fileSystem.rename(dirty, clean)
                        let oldLength = entry.lengths[i]
                        let newLength = diskLruCache.fileSystem.size(clean)
                        entry.lengths[i] = newLength
                        diskLruCache.size = diskLruCache.size - oldLength + newLength
                    }
                } else {
                    diskLruCache.fileSystem.delete(dirty)
                }
            }

            diskLruCache.redundantOpCount++
            entry.currentEditor = None
            let jw = diskLruCache.journalWriter.getOrThrow()
            if (entry.readable || success) {
                entry.readable = true
                jw.write(CLEAN.toUtf8Array())
                jw.write(" ".toUtf8Array())
                jw.write(entry.key.toUtf8Array())
                entry.writeLengths(jw)
                jw.write("\n".toUtf8Array())
                if (success) {
                    entry.sequenceNumber = diskLruCache.nextSequenceNumber
                    diskLruCache.nextSequenceNumber++
                }
            } else {
                diskLruCache.lruEntries.remove(entry.key)
                jw.write(REMOVE.toUtf8Array())
                jw.write(" ".toUtf8Array())
                jw.write(entry.key.toUtf8Array())
                jw.write("\n".toUtf8Array())
            }

            jw.flush()

            if (diskLruCache.size > diskLruCache.maxSize || diskLruCache.journalRebuildRequired()) {
                diskLruCache.cleanUp()
            }
        }
    }

    public func abort(): Unit {
        synchronized(diskLruCache.lock) {
            if (done) {
                throw IllegalStateException("abort has done")
            }

            if (refEq(entry.currentEditor.getOrThrow(), this)) {
                completeEdit(this, false)
            }

            done = true
        }
    }

    public func commit() {
        synchronized(diskLruCache.lock) {
            if (done) {
                throw IllegalStateException("can not commit editor now, because it has been done")
            }

            if (refEq(this, entry.currentEditor.getOrThrow())) {
                completeEdit(this, true)
            }

            done = true
        }
    }
}

class Entry {
    let key: String
    var currentEditor: Option<Editor> = None
    let lengths: Array<Int64>
    let cleanFiles: Array<String>
    let dirtyFiles: Array<String>
    var readable: Bool = false
    var sequenceNumber: Int64 = 0
    let diskLruCache: DiskLruCache

    init(key: String, diskLruCache: DiskLruCache) {
        this.key = key
        this.diskLruCache = diskLruCache
        lengths = Array<Int64>(diskLruCache.valueCount, repeat: 0)
        cleanFiles = Array<String>(diskLruCache.valueCount, repeat: "")
        dirtyFiles = Array<String>(diskLruCache.valueCount, repeat: "")
        var sb = StringBuilder("${key}.")
        let truncateTo = sb.size
        for (i in 0..diskLruCache.valueCount) {
            sb.append(i)
            cleanFiles[i] = "${sb}"
            sb.append(".tmp")
            dirtyFiles[i] = "${sb}"
            sb = StringBuilder("${sb}".substring(0, truncateTo))
        }
    }

    func snapshot(): Option<Snapshot> {
        let cl = lengths.clone()
        let arr: ArrayList<OkBuffer> = ArrayList<OkBuffer>()
        try {
            for (i in 0..diskLruCache.valueCount) {
                arr.add(diskLruCache.fileSystem.source(cleanFiles[i]))
            }
            return Snapshot(key, sequenceNumber, arr.toArray(), cl, diskLruCache)
        } catch (e: FSException) {
            for (i in 0..diskLruCache.valueCount) {
                if (i < arr.size) {
                    Util.closeQuietly(arr[i])
                } else {
                    break
                }
            }

            diskLruCache.removeEntry(this)
        }
        return None
    }

    func setLengths(strings: Array<String>) {
        if (strings.size != diskLruCache.valueCount) {
            throw HttpException("unexpected journal line: ${strings}")
        }

        try {
            for (i in 0..strings.size) {
                lengths[i] = Int64.parse(strings[i])
            }
        } catch (e: Exception) {
            throw HttpException("unexpected journal line: ${strings}")
        }
    }

    func writeLengths(writer: Sink): Unit {
        for (i in 0..lengths.size) {
            writer.write(" ".toUtf8Array())
            writer.write("${lengths[i]}".toUtf8Array())
        }
    }
}

class Snapshot <: Resource {
    private let key: String
    private let sequenceNumber: Int64
    private let sources: Array<OkBuffer>
    private let lengths: Array<Int64>
    private let diskLruCache: DiskLruCache

    init(
        key: String,
        sequenceNumber: Int64,
        sources: Array<OkBuffer>,
        lengths: Array<Int64>,
        diskLruCache: DiskLruCache
    ) {
        this.diskLruCache = diskLruCache
        this.key = key
        this.sequenceNumber = sequenceNumber
        this.sources = sources
        this.lengths = lengths
    }

    public func getSource(index: Int64): OkBuffer {
        return sources[index]
    }

    public func edit(): Option<Editor> {
        return diskLruCache.edit(key, sequenceNumber)
    }

    public func isClosed() {
        return sources.size == 0
    }

    public func close() {
        for (i in 0..sources.size) {
            Util.closeQuietly(sources[i])
        }
    }
}

class FileSystem {
    var directoryPath: Path
    let closeFuncs = ArrayList<() -> Unit>()

    init(directoryPath: Path) {
        this.directoryPath = directoryPath
    }

    public func source(name: String): OkBuffer {
        let file = File(directoryPath.join(name), OpenMode.ReadWrite)
        closeFuncs.add({=> file.close()})
        return OkBuffer(file)
    }

    public func sink(name: String): Sink {
        let file = File.create(directoryPath.join(name))
        closeFuncs.add({=> file.close()})
        return FileSink(file)
    }

    public func appendingSink(name: String): Sink {
        let file = File(directoryPath.join(name), OpenMode.Append)
        closeFuncs.add({=> file.close()})
        return FileSink(file)
    }

    func close(): Unit {
        for (f in closeFuncs) {
            f()
        }
        closeFuncs.clear()
    }

    public func delete(name: String): Unit {
        if (exists(name)) {
            fs.remove(directoryPath.join(name))
        }
    }

    public func exists(name: String): Bool {
        fs.exists(directoryPath.join(name))
    }

    public func rename(f: String, t: String): Unit {
        fs.rename(directoryPath.join(f), to: directoryPath.join(t), overwrite: true)
    }

    public func deleteContents(directoryPath: Path): Unit {
        deleteRecursive(directoryPath)
    }

    private func deleteRecursive(directoryPath: Path): Unit {
        Directory.walk(directoryPath) { i =>
            if (i.isRegular()) {
                fs.remove(i.path)
            } else if (i.isDirectory()) {
                deleteRecursive(i.path)
            }
            return true
        }
    }

    public func size(name: String): Int64 {
        return FileInfo(directoryPath.join(name)).size
    }
}