/*
* Copyright 2016 The Simple File Server Authors
*
* 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 org.sfs.block;
import com.google.common.base.Preconditions;
import com.google.common.math.LongMath;
import org.sfs.math.Rounding;
import java.util.Comparator;
import java.util.Map;
import java.util.NavigableMap;
import java.util.NavigableSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicLong;
public class RecyclingAllocator {
private final Object mutex = new Object();
private final NavigableMap<Long, Range> byPosition = new TreeMap<>();
private final NavigableMap<Long, NavigableSet<Range>> bySize = new TreeMap<>();
private final Comparator<Range> byPositionComparator = (o1, o2) -> Long.compare(o1.getFirst(), o2.getFirst());
private final AtomicLong bytesFree = new AtomicLong(0);
private final int blockSize;
public RecyclingAllocator(int blockSize) {
Preconditions.checkArgument(blockSize >= 1, "BlockSize must be >= 1");
this.blockSize = blockSize;
Range toMerge = new Range(0, computeLast(0, Rounding.down(Long.MAX_VALUE, blockSize)));
free0(toMerge);
}
public long allocFromLastRange(long length) {
synchronized (mutex) {
Map.Entry<Long, Range> lastEntry = byPosition.pollLastEntry();
Preconditions.checkNotNull(lastEntry);
Range range = lastEntry.getValue();
Preconditions.checkNotNull(lastEntry);
long blockCount = range.getBlockCount();
NavigableSet<Range> sortedByPosition = bySize.get(blockCount);
Preconditions.checkNotNull(sortedByPosition);
Preconditions.checkState(sortedByPosition.remove(range));
if (sortedByPosition.isEmpty()) {
Preconditions.checkState(sortedByPosition == bySize.remove(blockCount));
}
Preconditions.checkState(range == byPosition.remove(range.getFirst()));
bytesFree.addAndGet(-blockCount);
Preconditions.checkNotNull(range);
long position = range.getFirst();
Range[] updated = range.remove(position, computeLast(position, length));
for (Range update : updated) {
putRange(update);
}
return position;
}
}
public long allocNextAvailable(long length) {
Range match = null;
synchronized (mutex) {
Map.Entry<Long, NavigableSet<Range>> entry = bySize.ceilingEntry(length);
if (entry != null) {
long numberOfBlocks = entry.getKey();
NavigableSet<Range> sortedByPosition = entry.getValue();
Preconditions.checkNotNull(sortedByPosition);
match = sortedByPosition.pollFirst();
Preconditions.checkNotNull(match);
if (sortedByPosition.isEmpty()) {
Preconditions.checkState(sortedByPosition == bySize.remove(numberOfBlocks));
}
Preconditions.checkState(match == byPosition.remove(match.getFirst()));
bytesFree.addAndGet(-numberOfBlocks);
}
Preconditions.checkNotNull(match);
long position = match.getFirst();
Range[] updated = match.remove(position, computeLast(position, length));
for (Range update : updated) {
putRange(update);
}
return position;
}
}
public long alloc(long position, long length) {
checkRange(position, length);
Range toRemove = new Range(position, computeLast(position, length));
return alloc0(toRemove);
}
protected long alloc0(Range toRemove) {
long position = toRemove.getFirst();
Range match = null;
synchronized (mutex) {
Map.Entry<Long, Range> floorEntry = byPosition.floorEntry(position);
if (floorEntry != null) {
Range range = floorEntry.getValue();
if (range.encloses(toRemove)) {
match = range;
removeRange(range);
}
}
if (match != null) {
Range[] updated = match.remove(toRemove);
for (Range update : updated) {
putRange(update);
}
return position;
} else {
return -1;
}
}
}
private void removeRange(Range range) {
long blockCount = range.getBlockCount();
NavigableSet<Range> sortedByPosition = bySize.get(blockCount);
Preconditions.checkNotNull(sortedByPosition);
Preconditions.checkState(sortedByPosition.remove(range));
if (sortedByPosition.isEmpty()) {
Preconditions.checkState(sortedByPosition == bySize.remove(blockCount));
}
Preconditions.checkState(range == byPosition.remove(range.getFirst()));
bytesFree.addAndGet(-blockCount);
}
private void putRange(Range range) {
long blockCount = range.getBlockCount();
NavigableSet<Range> sortedBySize = bySize.get(blockCount);
if (sortedBySize == null) {
sortedBySize = newSortedByPosition();
Preconditions.checkState(bySize.put(blockCount, sortedBySize) == null);
}
Preconditions.checkState(sortedBySize.add(range));
Preconditions.checkState(byPosition.put(range.getFirst(), range) == null);
bytesFree.addAndGet(blockCount);
}
private NavigableSet<Range> newSortedByPosition() {
return new TreeSet<>(byPositionComparator);
}
public void free(long position, long length) {
checkRange(position, length);
Range toMerge = new Range(position, computeLast(position, length));
free0(toMerge);
}
protected void free0(Range toMerge) {
long first = toMerge.getFirst();
synchronized (mutex) {
Map.Entry<Long, Range> floorEntry = byPosition.floorEntry(first);
Map.Entry<Long, Range> ceilingEntry = byPosition.ceilingEntry(first);
if (floorEntry != null) {
Range floorRange = floorEntry.getValue();
if (floorRange.intersects(toMerge) || floorRange.adjacent(toMerge)) {
removeRange(floorRange);
toMerge = floorRange.merge(toMerge);
}
}
if (ceilingEntry != null) {
Range ceilingRange = ceilingEntry.getValue();
if (ceilingRange.intersects(toMerge) || ceilingRange.adjacent(toMerge)) {
removeRange(ceilingRange);
toMerge = ceilingRange.merge(toMerge);
}
}
putRange(toMerge);
}
}
protected void checkRange(long position, long length) {
Preconditions.checkArgument(position % blockSize == 0, "Position is not a multiple of the block size, Was %s", position);
Preconditions.checkArgument(position >= 0, "Position must be >= 0, Was %s", position);
Preconditions.checkArgument(length >= 0, "Length must be >= 0, Was %s", length);
}
public Iterable<Range> freeRanges() {
return byPosition.values();
}
public long greatestFreePosition() {
return byPosition.lastEntry().getKey();
}
public int getNumberOfFreeRanges() {
return byPosition.size();
}
public long getBytesFree(long useableSpace) {
long free = bytesFree.get();
synchronized (mutex) {
for (Range range : byPosition.descendingMap().values()) {
long first = range.getFirst();
if (first >= useableSpace) {
free -= range.getBlockCount();
} else {
free -= range.getBlockCount();
free += (useableSpace - first);
break;
}
}
}
return free;
}
protected long computeLast(long first, long length) {
return LongMath.checkedAdd(first, Rounding.up(length, blockSize)) - 1;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (Range freeRange : freeRanges()) {
sb.append("Range: ");
sb.append(freeRange);
sb.append('\n');
}
return sb.toString();
}
}