/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.nifi.distributed.cache.server.map;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import org.wali.MinimalLockingWriteAheadLog;
import org.wali.SerDe;
import org.wali.UpdateType;
import org.wali.WriteAheadRepository;
public class PersistentMapCache implements MapCache {
private final MapCache wrapped;
private final WriteAheadRepository<MapWaliRecord> wali;
private final AtomicLong modifications = new AtomicLong(0L);
public PersistentMapCache(final String serviceIdentifier, final File persistencePath, final MapCache cacheToWrap) throws IOException {
wali = new MinimalLockingWriteAheadLog<>(persistencePath.toPath(), 1, new Serde(), null);
wrapped = cacheToWrap;
}
synchronized void restore() throws IOException {
final Collection<MapWaliRecord> recovered = wali.recoverRecords();
for (final MapWaliRecord record : recovered) {
if (record.getUpdateType() == UpdateType.CREATE) {
wrapped.putIfAbsent(record.getKey(), record.getValue());
}
}
}
@Override
public MapPutResult putIfAbsent(final ByteBuffer key, final ByteBuffer value) throws IOException {
final MapPutResult putResult = wrapped.putIfAbsent(key, value);
putWriteAheadLog(key, value, putResult);
return putResult;
}
@Override
public MapPutResult put(final ByteBuffer key, final ByteBuffer value) throws IOException {
final MapPutResult putResult = wrapped.put(key, value);
putWriteAheadLog(key, value, putResult);
return putResult;
}
protected void putWriteAheadLog(ByteBuffer key, ByteBuffer value, MapPutResult putResult) throws IOException {
if ( putResult.isSuccessful() ) {
// The put was successful.
final MapWaliRecord record = new MapWaliRecord(UpdateType.CREATE, key, value);
final List<MapWaliRecord> records = new ArrayList<>();
records.add(record);
final MapCacheRecord evicted = putResult.getEvicted();
if ( evicted != null ) {
records.add(new MapWaliRecord(UpdateType.DELETE, evicted.getKey(), evicted.getValue()));
}
wali.update(records, false);
final long modCount = modifications.getAndIncrement();
if ( modCount > 0 && modCount % 100000 == 0 ) {
wali.checkpoint();
}
}
}
@Override
public boolean containsKey(final ByteBuffer key) throws IOException {
return wrapped.containsKey(key);
}
@Override
public ByteBuffer get(final ByteBuffer key) throws IOException {
return wrapped.get(key);
}
@Override
public MapCacheRecord fetch(ByteBuffer key) throws IOException {
return wrapped.fetch(key);
}
@Override
public MapPutResult replace(MapCacheRecord record) throws IOException {
final MapPutResult putResult = wrapped.replace(record);
putWriteAheadLog(record.getKey(), record.getValue(), putResult);
return putResult;
}
@Override
public ByteBuffer remove(final ByteBuffer key) throws IOException {
final ByteBuffer removeResult = wrapped.remove(key);
if (removeResult != null) {
final MapWaliRecord record = new MapWaliRecord(UpdateType.DELETE, key, removeResult);
final List<MapWaliRecord> records = new ArrayList<>(1);
records.add(record);
wali.update(records, false);
final long modCount = modifications.getAndIncrement();
if (modCount > 0 && modCount % 1000 == 0) {
wali.checkpoint();
}
}
return removeResult;
}
@Override
public Map<ByteBuffer, ByteBuffer> removeByPattern(final String regex) throws IOException {
final Map<ByteBuffer, ByteBuffer> removeResult = wrapped.removeByPattern(regex);
if (removeResult != null) {
final List<MapWaliRecord> records = new ArrayList<>(removeResult.size());
for(Map.Entry<ByteBuffer, ByteBuffer> entry : removeResult.entrySet()) {
final MapWaliRecord record = new MapWaliRecord(UpdateType.DELETE, entry.getKey(), entry.getValue());
records.add(record);
wali.update(records, false);
final long modCount = modifications.getAndIncrement();
if (modCount > 0 && modCount % 1000 == 0) {
wali.checkpoint();
}
}
}
return removeResult;
}
@Override
public void shutdown() throws IOException {
wali.shutdown();
}
private static class MapWaliRecord {
private final UpdateType updateType;
private final ByteBuffer key;
private final ByteBuffer value;
public MapWaliRecord(final UpdateType updateType, final ByteBuffer key, final ByteBuffer value) {
this.updateType = updateType;
this.key = key;
this.value = value;
}
public UpdateType getUpdateType() {
return updateType;
}
public ByteBuffer getKey() {
return key;
}
public ByteBuffer getValue() {
return value;
}
}
private static class Serde implements SerDe<MapWaliRecord> {
@Override
public void serializeEdit(final MapWaliRecord previousRecordState, final MapWaliRecord newRecordState, final java.io.DataOutputStream out) throws IOException {
final UpdateType updateType = newRecordState.getUpdateType();
if (updateType == UpdateType.DELETE) {
out.write(0);
} else {
out.write(1);
}
final byte[] key = newRecordState.getKey().array();
final byte[] value = newRecordState.getValue().array();
out.writeInt(key.length);
out.write(key);
out.writeInt(value.length);
out.write(value);
}
@Override
public void serializeRecord(final MapWaliRecord record, final java.io.DataOutputStream out) throws IOException {
serializeEdit(null, record, out);
}
@Override
public MapWaliRecord deserializeEdit(final DataInputStream in, final Map<Object, MapWaliRecord> currentRecordStates, final int version) throws IOException {
final int updateTypeValue = in.read();
if (updateTypeValue < 0) {
throw new EOFException();
}
final UpdateType updateType = (updateTypeValue == 0 ? UpdateType.DELETE : UpdateType.CREATE);
final int keySize = in.readInt();
final byte[] key = new byte[keySize];
in.readFully(key);
final int valueSize = in.readInt();
final byte[] value = new byte[valueSize];
in.readFully(value);
return new MapWaliRecord(updateType, ByteBuffer.wrap(key), ByteBuffer.wrap(value));
}
@Override
public MapWaliRecord deserializeRecord(final DataInputStream in, final int version) throws IOException {
return deserializeEdit(in, new HashMap<Object, MapWaliRecord>(), version);
}
@Override
public Object getRecordIdentifier(final MapWaliRecord record) {
return record.getKey();
}
@Override
public UpdateType getUpdateType(final MapWaliRecord record) {
return record.getUpdateType();
}
@Override
public String getLocation(final MapWaliRecord record) {
return null;
}
@Override
public int getVersion() {
return 1;
}
}
}