package io.eguan.vvr.persistence.repository; /* * #%L * Project eguan * %% * Copyright (C) 2012 - 2017 Oodrive * %% * 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. * #L% */ import io.eguan.hash.ByteBufferDigest; import io.eguan.hash.HashAlgorithm; import io.eguan.ibs.Ibs; import io.eguan.ibs.IbsIOException; import io.eguan.net.MsgServerRemoteStatus; import io.eguan.net.MsgServerTimeoutException; import io.eguan.nrs.NrsFile; import io.eguan.nrs.NrsFileHeader; import io.eguan.proto.Common.OpCode; import io.eguan.proto.Common.Type; import io.eguan.proto.Common.Uuid; import io.eguan.proto.vvr.VvrRemote; import io.eguan.proto.vvr.VvrRemote.RemoteOperation; import io.eguan.utils.SimpleIdentifierProvider; import io.eguan.utils.UuidT; import io.eguan.vvr.remote.VvrRemoteUtils; import io.eguan.vvr.repository.core.api.AbstractDeviceImplHelper; import io.eguan.vvr.repository.core.api.BlockKeyLookupEx; import io.eguan.vvr.repository.core.api.Device; import io.eguan.vvr.repository.core.api.FutureSnapshot; import io.eguan.vvr.repository.core.api.FutureVoid; import io.eguan.vvr.repository.core.api.Snapshot; import java.io.IOException; import java.net.ConnectException; import java.security.NoSuchAlgorithmException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Collection; import java.util.Date; import java.util.Iterator; import java.util.Objects; import java.util.UUID; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.annotation.Nonnegative; import javax.annotation.Nonnull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Strings; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; /** * {@link NrsFile} based device implementation relying essentially on superclass methods. * <p> * * @author oodrive * @author llambert * @author ebredzinski * @author jmcaba * @author pwehrle * */ public final class NrsDevice extends NrsVvrItem implements Device { private static final Logger LOGGER = LoggerFactory.getLogger(NrsDevice.class); final class NrsDeviceImplHelper extends AbstractDeviceImplHelper { NrsDeviceImplHelper() { super(); } /** * Create a new {@link ReadWriteHandle} for the device. */ @Override public final ReadWriteHandle newReadWriteHandle(final Ibs ibs, final HashAlgorithm hashAlgorithm, final boolean readOnly, final int blockSize) { // Enable NrsFileBlock device RW handler if (NRS_BLOCK_FILE_ENABLED) { return new NrsBlkDeviceReadWriteHandleImpl(this, hashAlgorithm, readOnly, blockSize); } // Default implementation return super.newReadWriteHandle(ibs, hashAlgorithm, readOnly, blockSize); } @Override protected final long getSize() { return NrsDevice.this.getSize(); } @Override protected final BlockKeyLookupEx lookupBlockKeyEx(final long blockIndex, final boolean recursive) throws IOException { if (!NrsDevice.this.isActive()) { throw new IllegalStateException("Device is deactivated"); } return (BlockKeyLookupEx) readHash(blockIndex, recursive, true); } @Override protected final void writeBlockKey(final long blockIndex, final byte[] key) throws IOException { if (!NrsDevice.this.isActive()) { throw new IllegalStateException("Device is deactivated"); } writeBlockHash(blockIndex, key); } @Override protected final void resetBlockKey(final long blockIndex) throws IOException { if (!NrsDevice.this.isActive()) { throw new IllegalStateException("Device is deactivated"); } resetBlockHash(blockIndex); } @Override protected final void trimBlockKey(final long blockIndex) throws IOException { if (!NrsDevice.this.isActive()) { throw new IllegalStateException("Device is deactivated"); } trimBlockHash(blockIndex); } @Override protected final Lock getIoLock() { return ioLock.readLock(); } @Override protected final void notifyIO(@Nonnull final RemoteOperation.Builder opBuilder) { try { // Send message getVvr().sendMessage(opBuilder, Type.IBS, OpCode.SET, true, null); } catch (final Throwable t) { LOGGER.warn("Failed to notify peers for device '" + getUuid() + "'", t); } } @Override protected final ByteString getRemoteBuffer(final byte[] key, final UUID srcNode) throws InterruptedException { // Get block request final RemoteOperation.Builder opBuilder = RemoteOperation.newBuilder(); // Fill IBS info final VvrRemote.Ibs.Builder nrsBuilder = VvrRemote.Ibs.newBuilder(); nrsBuilder.setKey(ByteString.copyFrom(key)); opBuilder.addIbs(nrsBuilder); // First, ask the node that create the NrsFile { try { final Collection<MsgServerRemoteStatus> reply = getVvr().sendMessage(opBuilder, Type.IBS, OpCode.GET, false, srcNode); if (reply == null) { // Stand alone mode: no remote buffer return null; } assert reply.size() == 1; final ByteString block = getRemoteBufferFromPeer(key, reply); // Block found? if (block != null) { return block; } } catch (ConnectException | MsgServerTimeoutException | IllegalArgumentException e) { LOGGER.debug("Device " + getUuid() + ": failed to get Ibs block from " + srcNode, e); } } // Read from any peer { try { final Collection<MsgServerRemoteStatus> reply = getVvr().sendMessage(opBuilder, Type.IBS, OpCode.GET, false, null); final ByteString block = getRemoteBufferFromPeer(key, reply); // Block found? if (block != null) { return block; } } catch (final MsgServerTimeoutException e) { LOGGER.warn("Device " + getUuid() + ": failed to get Ibs block", e); } catch (ConnectException | IllegalArgumentException e) { // Should not occur LOGGER.error("Device " + getUuid() + ": failed to get Ibs block", e); } } return null; } /** * Get the requested block from the given reply. * * @param reply * @return the block found or <code>null</code> */ private final ByteString getRemoteBufferFromPeer(final byte[] key, final Collection<MsgServerRemoteStatus> reply) { final Iterator<MsgServerRemoteStatus> ite = reply.iterator(); while (ite.hasNext()) { final MsgServerRemoteStatus status = ite.next(); final String exceptionName = status.getExceptionName(); if (exceptionName != null) { LOGGER.debug("Device " + getUuid() + ": failed to get Ibs block from " + status.getNodeId() + ", cause=" + exceptionName); } else { try { final ByteString msgReply = status.getReplyBytes(); final RemoteOperation opReply = RemoteOperation.parseFrom(msgReply, null); assert opReply.getIbsCount() == 1; final ByteString block = opReply.getIbs(0).getValue(); // Block found? if (block != null) { // Check block integrity if (checkRemoteBlock(key, block)) { return block; } } } catch (final InvalidProtocolBufferException e) { LOGGER.warn("Device " + getUuid() + ": failed to get Ibs block from " + status.getNodeId(), e); } } } return null; } /** * Checks if the block is correct. If true, try to add it in the local {@link Ibs}. * * @param key * @param block * @return <code>true</code> if the block is correct */ private final boolean checkRemoteBlock(final byte[] key, final ByteString block) { assert block.asReadOnlyByteBuffer().capacity() == getBlockSize(); try { if (ByteBufferDigest.match(block, key)) { // Can add this block in Ibs try { getVvr().getIbsInstance().put(key, block); } catch (RuntimeException | IbsIOException e) { LOGGER.warn("Device " + getUuid() + ": failed add the block in Ibs", e); } return true; } } catch (RuntimeException | NoSuchAlgorithmException e) { LOGGER.warn("Device " + getUuid() + ": failed to check block integrity", e); } return false; } /** * Gets the current {@link NrsFile}. * * @return the current {@link NrsFile}. */ final NrsFile getCurrentNrsFile() { return getNrsFilePath(); } } /** Enable NrsFileBlock proof of concept (temporary) */ private static final String NRS_BLOCK_FILE_ENABLED_PROP = "io.eguan.nrsblockfile.enable"; static boolean NRS_BLOCK_FILE_ENABLED = Boolean.getBoolean(NRS_BLOCK_FILE_ENABLED_PROP); /** * The device's active status. */ private volatile boolean active = false; /** Impl helper. Not <code>null</code> when activated */ private NrsDeviceImplHelper deviceImplHelper; /** * This instance's lock for reading/writing raw data. */ private final ReadWriteLock ioLock = new ReentrantReadWriteLock(); /** * Device constructor to be invoked by builders. * * @param builder * the configured builder */ private NrsDevice(final Builder builder) { super(builder); } @Override public final FutureVoid setSize(@Nonnegative final long size) { final UUID taskId = newNrsFile(size, false); return new NrsFutureVoid(getVvr(), taskId, getDeviceId()); } /** * Create a new {@link NrsFile} for the device. * * @param size * the new size * @param force * force the creation of the new file, even if the size does not change * @return the UUID of a task or <code>null</code> */ private final UUID newNrsFile(@Nonnegative final long size, final boolean force) { // Check size if (size <= 0) { throw new IllegalArgumentException(); } if (!force && size == getSize()) { // No change return null; } // Create payload final NrsRepository repository = getVvr(); final UuidT<NrsFile> parentFileUuid = getNrsFileId(); final UUID deviceUuid = getUuid(); final UuidT<NrsFile> futureSnapshotUuid = SimpleIdentifierProvider.newId(); final NrsFileHeader<NrsFile> deviceNrsFileHeader = repository.doCreateFutureNrsFileHeader(parentFileUuid, size, deviceUuid, futureSnapshotUuid); // Create and launch transaction final RemoteOperation.Builder opBuilder = RemoteOperation.newBuilder(); NrsRemoteUtils.setNrsDeviceNrsHeader(opBuilder, this, deviceNrsFileHeader); return repository.submitTransaction(opBuilder, Type.DEVICE, OpCode.SET); } /** * Create a new {@link NrsFile} for the device. * * @param nrsFileHeader * the NRS file header template, may be <code>null</code> */ final void newNrsFileLocal(@Nonnull final NrsFileHeader<NrsFile> nrsFileHeader) { // Freeze IOs while resizing ioLock.writeLock().lock(); try { final boolean wasOpened = isNrsFileLocked(); if (wasOpened) { // Close the file closeNrsFile(true); } // New NrsFile createNewNrsFile(nrsFileHeader); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Device " + getDeviceId() + " new file " + nrsFileHeader.getFileId() + " (size=" + nrsFileHeader.getSize() + ")"); } // Need to open new file? // Note: must not re-open the previous file on error (is read-only) if (wasOpened) { try { openNrsFile(); } catch (final IOException e) { throw new IllegalStateException(e); } } } finally { ioLock.writeLock().unlock(); } } /* * Forbid null and empty name. * * @see io.eguan.vvr.repository.core.api.AbstractUniqueVvrObject#setName(java.lang.String) */ @Override public final FutureVoid setName(final String name) { if (Strings.isNullOrEmpty(name)) { throw new IllegalArgumentException("Name is null or empty"); } return super.setName(name); } @Override public final boolean isActive() { return this.active; } @Override public final FutureVoid activate() { // TODO: add more checks and locking if (active) { throw new IllegalStateException("Active"); } if (isNrsFileLocked()) { throw new IllegalStateException("Opened"); } // Create a new NrsFile if the device was activated on another node // TODO creates new NrsFile even if the file is activated read-only!! final FutureVoid futureVoid; final NrsRepository repository = getVvr(); final UUID nodeUuid = repository.getNodeId(); if (!nodeUuid.equals(getNodeId())) { final UUID taskId = newNrsFile(getSize(), true); // TODO create a local task to open the new NrsFile in background futureVoid = new NrsFutureVoid(repository, taskId, getDeviceId()); try { futureVoid.get(); } catch (final Exception e) { throw new IllegalStateException(e); } } else { // No task futureVoid = new NrsFutureVoid(null, null, null); } this.active = true; this.deviceImplHelper = new NrsDeviceImplHelper(); try { openNrsFile(); } catch (final IOException e) { throw new IllegalStateException(e); } return futureVoid; } @Override public final FutureVoid deactivate() { // TODO: add more checks and locking // TODO create a local task to close the NrsFile in background closeNrsFile(); this.active = false; this.deviceImplHelper = null; return new NrsFutureVoid(null, null, null); } /** Date formatter to create the default name */ private static final DateFormat iso8601DateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mmZ"); @Override public final FutureSnapshot createSnapshot() { return createSnapshot(UUID.randomUUID()); } @Override public final FutureSnapshot createSnapshot(final String name) { return createSnapshot(name, null, UUID.randomUUID()); } @Override public final FutureSnapshot createSnapshot(final UUID uuid) { // Default name: device name+date ISO 8601 final String now; // format() is not thread safe synchronized (iso8601DateFormat) { now = iso8601DateFormat.format(new Date()); } return createSnapshot(getName() + " " + now, null, uuid); } @Override public final FutureSnapshot createSnapshot(final String name, final UUID uuid) { return createSnapshot(name, null, uuid); } @Override public final FutureSnapshot createSnapshot(final String name, final String description) { return createSnapshot(name, description, UUID.randomUUID()); } @Override public final FutureSnapshot createSnapshot(final String name, final String description, final UUID createdSnapshotId) { if (Strings.isNullOrEmpty(name)) { throw new IllegalArgumentException("name is null or empty"); } // Create payload final NrsRepository repository = getVvr(); // Create future NRS file header of the device final UuidT<NrsFile> futureFileUuid = SimpleIdentifierProvider.newId(); // Note: the parent fileId set is the current ID, but if there is another transaction pending // or in progress on this device, the parent may have changed when the transaction will be run final NrsFileHeader<NrsFile> deviceNrsFileHeader = repository.doCreateFutureNrsFileHeader(getNrsFileId(), getSize(), getUuid(), futureFileUuid); // Create and launch transaction final RemoteOperation.Builder opBuilder = RemoteOperation.newBuilder(); NrsRemoteUtils.addNrsDevice(opBuilder, deviceNrsFileHeader, createdSnapshotId, name, description); final UUID taskId = repository.submitTransaction(opBuilder, Type.SNAPSHOT, OpCode.CREATE); // Get the snapshot future return new NrsFutureSnapshot(createdSnapshotId, repository, taskId); } /** * Create a new snapshot for this device. * * @param uuid * @param name * @param description * @param nrsFileHeader * the NRS file header template, may be <code>null</code> * @param notifyPeers * <code>true</code> if the peers must be notified * @return the new snapshot */ final Snapshot doCreateSnapshot(final UUID uuid, final String name, final String description, final NrsFileHeader<NrsFile> nrsFileHeader) { // Freeze IOs while taking a snapshot ioLock.writeLock().lock(); try { final boolean wasOpened = isNrsFileLocked(); if (wasOpened) { // Close the file closeNrsFile(true); } final NrsRepository repository = getVvr(); // Take snapshot final NrsSnapshot.BuilderCreate builder = new NrsSnapshot.BuilderCreate(); builder.uuid(uuid).name(name).description(description); builder.device(this); builder.vvr(repository); builder.metadataDirectory(repository.getSnapshotDir()); builder.deviceFutureNrsFileHeader(nrsFileHeader); final Snapshot snapshot = builder.create(); LOGGER.debug("Snapshot " + snapshot.getUuid() + " created"); // Need to open new file? // Note: must not re-open the previous file on error (is read-only) if (wasOpened) { try { openNrsFile(); } catch (final IOException e) { throw new IllegalStateException(e); } } return snapshot; } finally { ioLock.writeLock().unlock(); } } @Override public final NrsFutureDevice clone(final String name) { return clone(name, null, UUID.randomUUID()); } @Override public final NrsFutureDevice clone(final String name, final String description) { return clone(name, description, UUID.randomUUID()); } @Override public final NrsFutureDevice clone(final String name, final UUID createdDeviceUuid) { return clone(name, null, createdDeviceUuid); } @Override public final NrsFutureDevice clone(final String name, final String description, final UUID createdDeviceUuid) { if (Strings.isNullOrEmpty(name)) { throw new IllegalArgumentException("name is null"); } // Create payload final NrsRepository repository = getVvr(); // Create transaction final RemoteOperation.Builder opBuilder = RemoteOperation.newBuilder(); // Future device (clone) final UuidT<NrsFile> futureDeviceNrsUuid = SimpleIdentifierProvider.newId(); final NrsFileHeader<NrsFile> futureDeviceNrsFileHeader = repository.doCreateFutureNrsFileHeader(getNrsFileId(), getSize(), createdDeviceUuid, futureDeviceNrsUuid); NrsRemoteUtils.addNrsDevice(opBuilder, futureDeviceNrsFileHeader, getUuid(), name, description); // Create NrsFileHeader for original device (gets a new NrsFile) final UuidT<NrsFile> futureSnapshotUuid = SimpleIdentifierProvider.newId(); final NrsFileHeader<NrsFile> origDeviceNrsFileHeader = repository.doCreateFutureNrsFileHeader(getNrsFileId(), getSize(), getUuid(), futureSnapshotUuid); NrsRemoteUtils.addNrsFileHeaderMsg(opBuilder, origDeviceNrsFileHeader); // Launch transaction final UUID taskId = repository.submitTransaction(opBuilder, Type.DEVICE, OpCode.CLONE); // Get the device future return new NrsFutureDevice(createdDeviceUuid, repository, taskId); } final Device doCloneDevice(final UUID uuid, final String name, final String description, final @Nonnull NrsFileHeader<NrsFile> nrsFileHeader) { // Freeze IOs while cloning the device (and changing the NrsFile) ioLock.writeLock().lock(); try { final boolean wasOpened = isNrsFileLocked(); if (wasOpened) { // Close the file closeNrsFile(true); } final NrsRepository repository = getVvr(); final NrsDevice.Builder builder = repository.newDeviceBuilder(nrsFileHeader.getParentId(), name, description, getSize(), uuid); final NrsDevice device = builder.create(nrsFileHeader); // Need to open new file? // Note: must not re-open the previous file on error (is read-only) if (wasOpened) { try { openNrsFile(); } catch (final IOException e) { throw new IllegalStateException(e); } } return device; } finally { ioLock.writeLock().unlock(); } } @Override public final ReadWriteHandle open(final boolean readWrite) { // TODO check active final NrsRepository repository = getVvr(); return deviceImplHelper.newReadWriteHandle(repository.getIbsInstance(), repository.getHashAlgorithm(), !readWrite, getBlockSize()); } @Override public final FutureVoid delete() { final RemoteOperation.Builder opBuilder = RemoteOperation.newBuilder(); final Uuid deviceUuid = VvrRemoteUtils.newUuid(getUuid()); opBuilder.setUuid(deviceUuid); final NrsRepository repository = getVvr(); final UUID taskId = repository.submitTransaction(opBuilder, Type.DEVICE, OpCode.DELETE); return new NrsFutureVoid(repository, taskId, getDeviceId()); } final void deleteDevice() { if (active) { throw new IllegalStateException("Active"); } if (isNrsFileLocked()) { throw new IllegalStateException("Opened"); } getVvr().unregisterDevice(getUuid()); // Delete NrsFile deleteNrsFile(); // Delete persistence unpersist(); } /** * Member builder for device instances. * * */ public static final class Builder extends NrsVvrItem.Builder { @Override protected final UUID deviceID() { return uuid(); } private NrsFileHeader<NrsFile> header; public final NrsDevice.Builder header(@Nonnull final NrsFileHeader<NrsFile> header) { this.header = Objects.requireNonNull(header); return this; } /** * New UUID of the NrsFile. New for a create, the one from the header for the load. */ private UuidT<NrsFile> futureId; @Override protected final UuidT<NrsFile> futureID() { return futureId; } public final NrsDevice.Builder futureFileId(@Nonnull final UuidT<NrsFile> futureId) { this.futureId = Objects.requireNonNull(futureId); return this; } /** * Originating node. */ private UUID nodeID; @Override protected final UUID nodeID() { return nodeID; } public final NrsDevice.Builder nodeID(@Nonnull final UUID nodeID) { this.nodeID = Objects.requireNonNull(nodeID); return this; } public final NrsDevice build() { uuid(header.getDeviceId()); parentFile(header.getParentId()); futureFileId(header.getFileId()); size(header.getSize()); flags(header.getFlags()); return new NrsDevice(this); } public final NrsDevice create(final @Nonnull NrsFileHeader<NrsFile> nrsFileHeader) { // Check UUID conflict final NrsRepository repository = getVvr(); { final UUID uuid = deviceID(); if (repository.getDevice(uuid) != null) { throw new IllegalStateException("Failed to create device, duplicate uuid=" + uuid); } if (repository.getSnapshot(uuid) != null) { throw new IllegalStateException("Failed to create device, duplicate uuid=" + uuid + " (snapshot)"); } } // Create and set NrsFile final NrsFile nrsFile = createNrsFile(Objects.requireNonNull(nrsFileHeader)); sourceFile(nrsFile); final NrsDevice device = new NrsDevice(this); try { device.create(); repository.registerDevice(device); } catch (final IOException e) { // TODO delete NrsFile throw new IllegalStateException("Failed to create device", e); } return device; } } }