All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jetbrains.exodus.io.FileDataWriter.kt Maven / Gradle / Ivy

There is a newer version: 9.8.0.76914
Show newest version
/**
 * Copyright 2010 - 2022 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package jetbrains.exodus.io

import jetbrains.exodus.ExodusException
import jetbrains.exodus.OutOfDiskSpaceException
import jetbrains.exodus.io.FileDataReader.FileBlock
import jetbrains.exodus.kotlin.notNull
import jetbrains.exodus.log.LogUtil
import jetbrains.exodus.system.JVMConstants
import jetbrains.exodus.util.SharedRandomAccessFile
import jetbrains.exodus.util.UnsafeHolder
import mu.KLogging
import java.io.File
import java.io.IOException
import java.io.RandomAccessFile
import java.nio.channels.ClosedChannelException
import java.nio.channels.FileChannel

open class FileDataWriter @JvmOverloads constructor(private val reader: FileDataReader, lockId: String? = null) : AbstractDataWriter() {

    private var dirChannel: FileChannel? = null
    private val lockingManager: LockingManager
    private var file: RandomAccessFile? = null
    private var block: FileBlock? = null
    private var useNio = false

    init {
        var channel: FileChannel? = null
        if (!JVMConstants.IS_ANDROID) {
            try {
                channel = FileChannel.open(reader.dir.toPath())
                // try to force as XD-698 requires
                channel.force(false)
            } catch (e: IOException) {
                channel = null
                warnCantFsyncDirectory()
            }
        }
        dirChannel = channel
        lockingManager = LockingManager(reader.dir, lockId)
    }

    override fun write(b: ByteArray, off: Int, len: Int): Block {
        try {
            ensureFile("Can't write, FileDataWriter is closed").write(b, off, len)
        } catch (ioe: IOException) {
            if (lockingManager.usableSpace < len) {
                throw OutOfDiskSpaceException(ioe)
            }
            throw ioe
        }
        return block ?: throw ExodusException("Can't write, FileDataWriter is closed")
    }

    override fun lock(timeout: Long): Boolean {
        return lockingManager.lock(timeout)
    }

    override fun release(): Boolean {
        return lockingManager.release()
    }

    override fun lockInfo(): String? {
        return lockingManager.lockInfo()
    }

    override fun syncImpl() {
        file?.let { forceSync(it) }
    }

    override fun closeImpl() {
        ensureFile("Can't close already closed FileDataWriter").close()
        file = null
        dirChannel?.close()
        dirChannel = null
        block = null
    }

    override fun clearImpl() {
        for (file in LogUtil.listFiles(reader.dir)) {
            if (!file.canWrite()) {
                setWritable(file)
            }
            if (file.exists() && !file.delete()) {
                throw ExodusException("Failed to delete $file")
            }
        }
    }

    override fun openOrCreateBlockImpl(address: Long, length: Long) =
        FileBlock(address, reader).also { openOrCreateFile(it, length) }


    override fun syncDirectory() {
        dirChannel?.let { channel ->
            try {
                channel.asUninterruptible().force(false)
            } catch (e: IOException) {
                // just warn as XD-698 requires
                warnCantFsyncDirectory()
            }
        }
    }

    override fun removeBlock(blockAddress: Long, rbt: RemoveBlockType) {
        val file = FileBlock(blockAddress, reader)
        removeFileFromFileCache(file)
        setWritable(file)
        val deleted = if (rbt == RemoveBlockType.Delete) file.delete() else renameFile(file)
        if (!deleted) {
            throw ExodusException("Failed to delete ${file.absolutePath}")
        } else {
            logger.info { "Deleted file ${file.absolutePath}" }
        }
    }

    override fun truncateBlock(blockAddress: Long, length: Long) {
        val file = FileBlock(blockAddress, reader)
        removeFileFromFileCache(file)
        setWritable(file)
        SharedRandomAccessFile(file, "rw").use { f -> f.setLength(length) }
        logger.info { "Truncated file ${file.absolutePath} to length = $length" }
    }

    private fun warnCantFsyncDirectory() {
        this.dirChannel = null
        warnIfWindows
    }

    private fun removeFileFromFileCache(file: File) {
        SharedOpenFilesCache.getInstance().removeFile(file)
        if (useNio) {
            SharedMappedFilesCache.getInstance().removeFileBuffer(file)
        }
    }

    private fun openOrCreateFile(block: FileBlock, length: Long): RandomAccessFile {
        val result = RandomAccessFile(block.apply {
            if (!canWrite()) {
                setWritable(this)
            }
        }, "rw")
        result.seek(length)
        if (length != result.length()) {
            result.setLength(length)
            forceSync(result)
        }
        file = result
        this.block = block
        return result
    }

    private fun ensureFile(errorMsg: String): RandomAccessFile {
        file?.let { file ->
            if (file.channel.isOpen) {
                return file
            }
            return openOrCreateFile(block.notNull, file.length()).also { file.close() }
        }
        throw ExodusException(errorMsg)
    }

    companion object : KLogging() {

        private const val DELETED_FILE_EXTENSION = ".del"
        private val warnIfWindows by lazy {
            logger.warn("Can't open directory channel. Log directory fsync won't be performed.")
        }
        private val setUninterruptibleMethod =
            if (JVMConstants.IS_JAVA9_OR_HIGHER) {
                UnsafeHolder.doPrivileged {
                    try {
                        Class.forName("sun.nio.ch.FileChannelImpl").getDeclaredMethod("setUninterruptible").apply {
                            isAccessible = true
                            logger.info { "Uninterruptible file channel will be used" }
                        }
                    } catch (t: Throwable) {
                        logger.info(t) { "Interruptible file channel will be used" }
                            null
                        }
                    }
                } else {
                    null
                }

        private fun FileChannel.asUninterruptible(): FileChannel {
            setUninterruptibleMethod?.invoke(this)
            return this
        }

        fun renameFile(file: File): Boolean {
            val name = file.name
            return file.renameTo(File(file.parent,
                    name.substring(0, name.indexOf(LogUtil.LOG_FILE_EXTENSION)) + DELETED_FILE_EXTENSION))
        }

        private fun forceSync(file: RandomAccessFile) {
            try {
                file.channel.asUninterruptible().force(false)
            } catch (e: ClosedChannelException) {
                // ignore
            } catch (ioe: IOException) {
                if (file.channel.isOpen) {
                    throw ioe
                }
            }
        }

        private fun setWritable(file: File) {
            if (file.exists() && !file.setWritable(true)) {
                throw ExodusException("Failed to set writable " + file.absolutePath)
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy