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

jetbrains.exodus.log.replication.S3DataWriter.kt Maven / Gradle / Ivy

/**
 * Copyright 2010 - 2020 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
 *
 * http://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.log.replication

import jetbrains.exodus.ExodusException
import jetbrains.exodus.core.dataStructures.persistent.read
import jetbrains.exodus.core.dataStructures.persistent.write
import jetbrains.exodus.io.AbstractDataWriter
import jetbrains.exodus.io.Block
import jetbrains.exodus.io.RemoveBlockType
import jetbrains.exodus.log.Log
import jetbrains.exodus.log.LogTip
import mu.KLogging
import org.reactivestreams.Subscriber
import org.reactivestreams.Subscription
import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration
import software.amazon.awssdk.core.async.AsyncRequestBody
import software.amazon.awssdk.core.sync.RequestBody
import software.amazon.awssdk.services.s3.S3AsyncClient
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.CopyObjectRequest
import software.amazon.awssdk.services.s3.model.PutObjectRequest
import java.io.ByteArrayInputStream
import java.nio.ByteBuffer

class S3DataWriter(private val s3Sync: S3Client,
                   override val s3: S3AsyncClient,
                   override val bucket: String,
                   override val requestOverrideConfig: AwsRequestOverrideConfiguration? = null,
                   private val log: Log? = null
) : S3DataReaderOrWriter, AbstractDataWriter() {

    companion object : KLogging()

    private var block: S3FolderBlock? = null

    override val logTip: LogTip? get() = log?.tip

    override fun syncImpl() {}

    override fun write(bytes: ByteArray, off: Int, len: Int): Block {
        with(block ?: throw ExodusException("Can't write, S3DataWriter is closed")) {
            val subBlockAddress = address + length()
            val subBlockSize = len.toLong()
            val key = "${getPartialFolderPrefix(address)}${getPartialFileName(subBlockAddress)}"
            logger.info { "Put file of $key, length: $len" }
            try {
                s3Sync.putObject(PutObjectRequest.builder()
                        .bucket(bucket)
                        .overrideConfiguration(requestOverrideConfig)
                        .key(key)
                        .contentLength(subBlockSize)
                        .build(), RequestBody.fromInputStream(
                        ByteArrayInputStream(bytes, off, len), subBlockSize)
                )
                val blocksCopy = blocks.clone.apply {
                    write {
                        put(subBlockAddress, S3SubBlock(s3factory, subBlockAddress, subBlockSize, address))
                    }
                }
                return S3FolderBlock(s3factory, address, size + subBlockSize, blocksCopy).apply { block = this }
            } catch (e: Exception) {
                val msg = "failed to update '$key' in S3"
                logger.error(msg, e)
                throw ExodusException(msg, e)
            }
        }
    }

    override fun openOrCreateBlockImpl(address: Long, length: Long): Block {
        return newS3FolderBlock(this, address).apply {
            if (length() > length) {
                truncateBlock(address, length)
            }
            block = this
        }
    }

    override fun removeBlock(blockAddress: Long, rbt: RemoveBlockType) {
        val keysToDelete = ArrayList()
        fileBlocks.firstOrNull { it.address == blockAddress }?.apply {
            keysToDelete.add(key)
        }
        if (keysToDelete.isEmpty()) {
            folderBlocks.firstOrNull { it.address == blockAddress }?.apply {
                blocks.read {
                    forEach { keysToDelete.add(it.value.key) }
                }
            }
        }
        if (rbt == RemoveBlockType.Rename) {
            keysToDelete.forEach {
                val newName = it.replace(".xd", ".del")
                logger.debug { "renaming block $it to $newName" }
                try {
                    s3.copyObject(CopyObjectRequest.builder()
                            .bucket(bucket)
                            .overrideConfiguration(requestOverrideConfig)
                            .copySource("$bucket/$it")
                            .key(newName)
                            .build()).get()
                } catch (e: Exception) {
                    val msg = "failed to copy '$it' in S3"
                    logger.error(msg, e)
                    throw ExodusException(msg, e)
                }
            }
        }
        try {
            keysToDelete.deleteS3Objects(s3, bucket, requestOverrideConfig)
        } catch (e: Exception) {
            val msg = "failed to delete files '${keysToDelete.joinToString()}' in S3"
            logger.error(msg, e)
            throw ExodusException(msg, e)
        }
    }

    override fun truncateBlock(blockAddress: Long, length: Long) {
        fileBlocks.firstOrNull { it.address == blockAddress }?.truncate(length)
        // TODO: XD-743
        /*folderBlocks.filter { it.address == blockAddress }.forEach { folder ->
            folder.blocks.let {
                val last = it.findLast { it.address - folder.address < length }
                last?.truncate(length - (last.address - folder.address))
                it.asSequence()
                        .filter { it.address - folder.address >= length }
                        .map { it.key }
                        .windowed(size = deletePackSize, step = deletePackSize, partialWindows = true).forEach {
                            it.deleteS3Objects(s3, bucket, requestOverrideConfig)
                        }
            }
        }*/
    }

    override fun lock(timeout: Long) = true

    override fun release() = true

    override fun lockInfo(): String? = null

    override fun closeImpl() {
        block = null
    }

    override fun clearImpl() {
        listObjects(s3, listObjectsBuilder(bucket, requestOverrideConfig)).filter {
            it.key().isValidAddress || it.key().isValidSubFolder
        }.map {
            it.key()
        }.windowed(size = deletePackSize, step = deletePackSize, partialWindows = true).forEach {
            it.deleteS3Objects(s3, bucket, requestOverrideConfig)
        }
    }

    private fun BasicS3Block.truncate(length: Long) {
        logger.debug { "truncating block at $key to $length" }
        try {
            val array = ByteArray(length.toInt())
            read(array, 0, 0, length.toInt())
            s3.putObject(PutObjectRequest.builder()
                    .bucket(bucket)
                    .overrideConfiguration(requestOverrideConfig)
                    .key(key)
                    .contentLength(length)
                    .build(), object : AsyncRequestBody {

                override fun contentLength() = length

                override fun subscribe(subscriber: Subscriber) {
                    subscriber.onSubscribe(
                            object : Subscription {
                                override fun request(n: Long) {
                                    if (n > 0) {
                                        subscriber.onNext(ByteBuffer.wrap(array))
                                        subscriber.onComplete()
                                    }
                                }

                                override fun cancel() {}
                            }
                    )
                }
            }).get()
        } catch (e: Exception) {
            val msg = "failed to update $key"
            logger.error(msg, e)
            throw ExodusException(msg, e)
        }
    }

    private fun failIntegrity(): Nothing {
        throw IllegalStateException("Concurrency breach")
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy