Code Monkey home page Code Monkey logo

oak's Introduction

Oak

Oak (Off-heap Allocated Keys) is a scalable, concurrent, in-memory Key-Value (KV) map.

OakMap is a concurrent Key-Value Map that keeps all keys and values off-heap. This allows storing more data (up to 3 times more data) compare to using the standard JVM heap management, albeit using the same memory footprint. OakMap implements the industry-standard Java8 ConcurrentNavigableMap API. It provides strong (atomic) semantics for read, write, and read-modify-write operations, as well as (non-atomic) range query (scan) operations, both forward and backward. OakMap is optimized for big keys and values, in particular, for incremental maintenance of objects (update in-place). It is faster and scales better with additional CPU cores than the popular Java ConcurrentNavigableMap ConcurrentSkipListMap.

Why OakMap?

  1. OakMap provides great performance: it employs fine-grain synchronization, and thus scales well with numbers of threads; it also achieves cache-friendliness by avoiding memory fragmentation (see performance evaluation).
  2. OakMap takes keys and the data off-heap, and thus allows working with a huge heap (RAM) -- even more than 50G -- without JVM GC overheads.
    • To support off-heap, OakMap has embedded, efficient, epoch-based memory management that mostly eliminates JVM GC overheads.
  3. OakMap provides a rich API for atomic accesses to data. For example, OakMap supports atomic compute() -- in place computations on existing keys -- whereas the current Java ConcurrentSkipListMap implementation does not guarantee the atomicity of compute(). OakMap’s update operations (such as put and compute) take user-provided lambda functions for easy integration in diverse use cases.
  4. Descending Scans: OakMap expedites descending scans without additional complexity. In our experiments, OakMap’s descending scans are 4.8x faster than ConcurrentSkipListMap’s, and perform similarly to their ascending counterparts (see performance evaluation).

Table of Contents

Background

Design Points

  • OakMap consists of an on-heap index to off-heap keys and values. OakMap's index is structured as a list of contiguous chunks of memory; this speeds up searches through the index due to access locality and cache-friendliness. Read more about OakMap design.
  • OakMap's keys and values are copied and stored in self-managed off-heap byte arrays.

Design Requirements

To efficiently manage its content, OakMap requires that the user define two auxiliary tools: an OakSerializer and an OakComparator; both are passed during construction.

  1. OakSerializer: Both keys and values need to provide a (1) serializer, (2) deserializer, and (3) serialized size calculator. All three are parts of OakSerializer.
    • For boosting performance, OakMap allocates space for a given key/value and then uses the given serializer to write the key/value directly to the allocated space. OakMap uses the appropriate size calculator to deduce the amount of space to be allocated. Note that both keys and values are variable-sized.
  2. OakComparator: To compare the internally-kept serialized keys with the deserialized key provided by the API, OakMap requires a (key) comparator. The comparator compares two keys, each of which may be provided either as a deserialized object or as a serialized one, determining whether they are equal, and if not, which is bigger.

Install

OakMap is a library. After downloading Oak, compile it using mvn install package to compile and install. Then update your project's pom.xml file dependencies, as follows:

  <dependency>
      <groupId>oak</groupId>
      <artifactId>oak</artifactId>
      <version>1.0-SNAPSHOT</version>
  </dependency>

Finally, import the relevant classes and use OakMap according to the description below.

Builder

To build OakMap, the user should first create the builder, and then use it to construct OakMap:

OakMapBuilder<K,V> builder = ... // create a builder; details provided below
OakMap<K,V> oak = builder.build();

OakMap requires multiple parameters to be defined for the builder, as shall be explained below. When constructing off-heap OakMap, the memory capacity (per OakMap instance) needs to be specified. OakMap allocates the off-heap memory with the requested capacity at construction time and later manages this memory.

OakSerializer

As explained above, OakMap<K, V> is given key K and value V, which are requested to come with a serializer, deserializer, and size calculator. OakMap user needs to implement the following interface that can be found in the Oak project.

public interface OakSerializer<T> {
  // serializes the data
  void serialize(T data, OakScopedWriteBuffer serializedData);

  // deserializes the data
  T deserialize(OakScopedReadBuffer serializedData);

  // returns the number of bytes needed for serializing the given data
  int calculateSize(T data);
}

Note 1: Oak use dedicated objects to access off-heap memory: OakScopedReadBuffer and OakScopedWriteBuffer. See Oak Buffers for more information.

Note 2: OakScopedReadBuffer and OakScopedWriteBuffer should not be stored for future use. They are valid only in the context of these methods (serialize()/deserialize()). Using these buffers outside their intended context may yield unpredicted results.

For example, the implementation of key serializer for an application that use integer as keys might look like this:

public class MyAppKeySerializer implements OakSerializer<Integer> {
  void serialize(Integer key, OakScopedWriteBuffer serializedKey) {
    // We store the value at the first position of the off-heap buffer.
    serializedKey.putInt(0, value);
  }
    
  Integer deserialize(OakScopedReadBuffer serializedKey) {
    return serializedKey.getInt(0);
  }
    
  int calculateSize(Integer key) {
    // We only store one integer
    return Integer.BYTES;
  }
}

public class MayAppValueSerializer implements OakSerializer<V>
{...}

Minimal Key

OakMap requires a minimal key that can represent negative infinity according to the user-defined comparison among the keys. The requested minimal key is of type K and is considered by the given comparator to be smaller than every other key (serialized or not). The minimal key is passed as a parameter during builder creation.

Comparator

After a Key-Value pair is inserted into OakMap, it is kept in a serialized (buffered) state. However, OakMap's API gets the input key as an object, the serialization of which is deferred until it proves to be required. Thus, while searching through the map, OakMap might compare between two keys in their Object and Serialized modes. OakMap provides the following interface for such a comparator:

public interface OakComparator<K> {
  int compareKeys(K key1, K key2);

  int compareSerializedKeys(OakScopedReadBuffer serializedKey1, OakScopedReadBuffer serializedKey2);

  int compareSerializedKeyAndKey(OakScopedReadBuffer serializedKey1, K key2);
}

Note 1: Oak use dedicated objects to access off-heap memory: OakScopedReadBuffer and OakScopedWriteBuffer. See Oak Buffers for more information.

Note 2: OakScopedReadBuffer and OakScopedWriteBuffer should not be stored for future use. They are valid only in the context of these methods (compareKeys()/compareSerializedKeys()/compareSerializedKeyAndKey()). Using these buffers outside their intended context may yield unpredicted results.

For example, the implementation of a key comparator for an application that uses integer as keys might look like this:

public class MyAppKeyComparator implements OakComparator<Integer>
{
  int compareKeys(Integer key1, Integer key2) {
    return Integer.compare(key1, key2);
  }

  int compareSerializedKeys(OakReadBuffer serializedKey1, OakReadBuffer serializedKey2) {
    return Integer.compare(serializedKey1.getInt(0), serializedKey2.getInt(0)); 
  }

  int compareSerializedKeyAndKey(OakReadBuffer serializedKey1, Integer key2) {
    return Integer.compare(serializedKey1.getInt(0), key2);
  }
}

Builder

We provide an example of how to create OakMapBuilder and OakMap. For a more comprehensive code example please refer to the Usage section.

OakMapBuilder<K,V> builder = new OakMapBuilder()
            .setKeySerializer(new MyAppKeySerializer())
            .setValueSerializer(new MyAppValueSerializer())
            .setMinKey(...)
            .setKeysComparator(new MyAppKeyComparator())
            .setMemoryCapacity(...);

OakMap<K,V> oak = builder.build();

API

OakMap Methods

OakMap's API implements the ConcurrentNavigableMap interface. For improved performance, it offers additional non-standard zero-copy API methods that are discussed below.

You are welcome to take a look at the OakMap's full API. For a more comprehensive code example please refer to the usage section.

Oak Buffers

Oak uses dedicated buffer objects to access off-heap memory. These buffers cannot be instantiated by the user and are always supplied to the user by Oak. Their interfaces are:

Buffer                    Access      Usage
------------------------  ----------  ------------------------------
OakBuffer                 read-only   base class for all the buffers
├── OakScopedReadBuffer   read-only   attached to a specific scope
├── OakScopedWriteBuffer  read/write  attached to a specific scope
└── OakUnscopedBuffer     read-only   can be used in any scope

These buffers may represent either a key or a value. They mimic the standard interface of Java's ByteBuffer, for example, int getInt(int index), char getChar(int index), capacity(), etc.

The scoped buffers (OakScopedReadBuffer and OakScopedWriteBuffer) are attached to the scope of the callback method they were first introduced to the user. The behavior of these buffers outside their attached scope is undefined. Such a callback method might be the application's serializer and comparator, or a lambda function that can read/store/update the data. This access reduces unnecessary copies and deserialization of the underlying data. In their intended context, the user does not need to worry about concurrent accesses and memory management. Using these buffers outside their intended context may yield unpredicted results, e.g., reading non-consistent data and/or irrelevant data.

The un-scoped buffer (OakUnscopedBuffer) is detached from any specific scope, i.e., it may be stored for future use. The zero-copy methods of OakMap return this buffer to avoid copying the data and instead the user can access the underlying memory buffer directly (lazy evaluation). While the scoped buffers' data accesses are synchronized, when using OakUnscopedBuffer, the same memory might be access by concurrent update operations. Thus, the reader may encounter different values -- and even value deletions -- when accessing OakUnscopedBuffer multiple times. Specifically, when trying to access a deleted mapping via an OakUnscopedBuffer, ConcurrentModificationException will be thrown. This is of course normal behavior for a concurrent map that avoids copying. To allow complex, multi-value atomic operations on the data, OakUnscopedBuffer provides a transform() method that allows the user to apply a transformation function atomically on a read-only, scoped version of the buffer (OakScopedReadBuffer). See the Data Retrieval for more information.

For performance and backward compatibility with applications that are already based on the use of ByteBuffer, Oak's buffers also implement a dedicated unsafe interface OakUnsafeDirectBuffer. This interface allows high-performance access to the underlying data of Oak. To achieve that, it sacrifices safety, so it should be used only if you know what you are doing. Misuse of this interface might result in corrupted data, a crash or a deadlock.

Specifically, the developer should be concerned with two issues:

  1. Concurrency: using this interface inside the context of serialize(), compute(), compare()andtransform()is thread-safe. In other contexts (e.g.,get()` output), the developer should ensure that there is no concurrent access to this data. Failing to ensure that might result in corrupted data.
  2. Data boundaries: when using this interface, Oak will not alert the developer regarding any out of boundary access. Thus, the developer should use getOffset() and getLength() to obtain the data boundaries and carefully access the data. Writing data out of these boundaries might result in corrupted data, a crash, or a deadlock.

To use this interface, the developer should cast Oak's buffer (OakScopedReadBuffer or OakScopedWriteBuffer) to this interface, similarly to how Java's internal DirectBuffer is used. For example:

int foo(OakScopedReadBuffer b) {
  OakUnsafeDirectBuffer ub = (OakUnsafeDirectBuffer) b;
  ByteBuffer bb = ub.getByteBuffer();
  return bb.getInt(ub.getOffset());
}

Note 1: in the above example, the following will throw a ReadOnlyBufferException because the buffer mode is read-only:

bb.putInt(ub.getOffset(), someInteger);

Note 2: the user should never change the buffer's state, namely the position and limit (bb.limit(i) or bb.position(i)). Changing the buffer's state will make some data inaccessible to the user in the future.

Data Retrieval

  1. For best performance of data retrieval, OakMap supplies a ZeroCopyMap interface of the map: ZeroCopyMap<K, V> zc()

    The ZeroCopyMap interface provides the following four methods for data retrieval, whose result is presented as an OakUnscopedBuffer:

    • OakUnscopedBuffer get(K key)
    • Collection<OakUnscopedBuffer> values()
    • Set<Map.Entry<OakUnscopedBuffer, OakUnscopedBuffer>> entrySet()
    • Set<OakUnscopedBuffer> keySet()
    • Set<OakUnscopedBuffer> keyStreamSet()
    • Collection<OakUnscopedBuffer> valuesStream()
    • Set<Map.Entry<OakUnscopedBuffer, OakUnscopedBuffer>> entryStreamSet()

    Note that in addition to the ConcurrentNavigableMap style sets, we introduce a new type of stream sets. When a stream-set is iterated it gives a "stream" view of the elements, meaning only one element can be observed at a time. It is preferred to use the stream iterators when possible as they instantiate significantly fewer objects, which improve performance.

  2. Without ZeroCopyMap, OakMap's data can be directly retrieved via the following four methods:

    • V get(Object key)
    • Collection<V> values()
    • Set<Map.Entry<K, V>> entrySet()
    • NavigableSet<K> keySet()

    However, these direct methods return keys and/or values as Objects by applying deserialization (copy). This is costly, and we strongly advise to use ZeroCopyMap to operate directly on the internal data representation.

  3. For examples of direct data manipulations, please refer to the usage section.

Data Ingestion

  1. Data can be ingested via the standard ConcurrentNavigableMap API.
  2. For improved performance, data can be also ingested and updated via the following five methods provided by the ZeroCopyMap interface:
    • void put(K key, V value)
    • boolean putIfAbsent(K key, V value)
    • void remove(K key)
    • boolean computeIfPresent(K key, Consumer<OakScopedWriteBuffer> computer)
    • boolean putIfAbsentComputeIfPresent(K key, V value, Consumer<OakScopedWriteBuffer> computer)
  3. In contrast to the ConcurrentNavigableMap API, the zero-copy method void put(K key, V value) does not return the value previously associated with the key, if key existed. Likewise, void remove(K key) does not return a boolean indicating whether key was actually deleted, if key existed.
  4. boolean computeIfPresent(K key, Consumer<OakScopedWriteBuffer> computer) gets the user-defined computer function. The computer is invoked in case the key exists. The computer is provided with a mutable OakScopedWriteBuffer, representing the serialized value associated with the key. The computer's effect is atomic, meaning either all updates are seen by concurrent readers, or none are. The compute() functionality offers the OakMap user an efficient zero-copy update-in-place, which allows OakMap users to focus on business logic without dealing with the hard problems that data layout and concurrency control present.
  5. Additionally, OakMap supports an atomic boolean putIfAbsentComputeIfPresent(K key, V value, Consumer<OakScopedWriteBuffer> computer) interface, (which is not part of ConcurrentNavigableMap). This API looks for a key. If the key does not exist, it adds a new Serialized key --> Serialized value mapping. Otherwise, the value associated with the key is updated with computer(old value). This interface works concurrently with other updates and requires only one search traversal. This interface returns true if a new key was added, false otherwise.

Memory Management

As explained above, when constructing off-heap OakMap, the memory capacity (per OakMap instance) needs to be specified. OakMap allocates the off-heap memory with the requested capacity at construction time, and later manages this memory. This memory (the entire given capacity) needs to be released later, thus OakMap implements AutoClosable. Be sure to use it within try-statement or better invoke OakMap.close() method when OakMap is no longer in use.

Please pay attention that multiple Oak sub-maps can reference the same underlying memory of OakMap. The memory will be released only when the last of those sub-maps are closed. However, note that each sub-map is in particular an OakMap and thus AutoCloseable and needs to be closed (explicitly or implicitly). Again, close() can be invoked on different objects referring to the same underlying memory, but the final release will happen only once.

Usage

An Integer to Integer build example can be seen in Code Examples. Here we illustrate individual operations.

Code Examples

We show some examples of Oak ZeroCopyMap interface usage below. These examples assume OakMap<Integer, Integer> oak is defined and constructed as described in the Builder section.

Simple Put and Get
oak.put(10,100);
Integer i = oak.get(10);
Remove
oak.zc().remove(11);
Get OakUnscopedBuffer
OakUnscopedBuffer buffer = oak.zc().get(10);
if(buffer != null) {
    try {
        int get = buffer.getInt(0);
    } catch (ConcurrentModificationException e) {
    }
}
Scan & Copy
Integer[] targetBuffer = new Integer[oak.size()]; // might not be correct with multiple threads
Iterator<Integer> iter = oak.values().iterator();
int i = 0;
while (iter.hasNext()) {
    targetBuffer[i++] = iter.next();
}
Compute
Consumer<OakScopedWriteBuffer> func = buf -> {
    Integer cnt = buf.getInt(0);   // read integer from position 0
    buf.putInt(0, (cnt+1));        // accumulate counter, position back to 0
};
oak.zc().computeIfPresent(10, func);
Conditional Compute
Consumer<OakScopedWriteBuffer> func = buf -> {
    if (buf.getInt(0) == 0) {     // check integer at position 0
        buf.putInt(1);             // position in the buffer is promoted
        buf.putInt(1);
    }
};
oak.zc().computeIfPresent(10, func);
Simple Iterator
Iterator<Integer> iterator = oak.keySet().iterator();
while (iter.hasNext()) {
    Integer i = iter.next();
}
Simple Descending Iterator
try (OakMap<Integer, Integer> oakDesc = oak.descendingMap()) {
    Iterator<Integer, Integer>> iter = oakDesc.entrySet().iterator();
    while (iter.hasNext()) {
        Map.Entry<Integer, Integer> e = iter.next();
    }
}
Simple Range Iterator
Integer from = (Integer)4;
Integer to = (Integer)6;

try (OakMap sub = oak.subMap(from, false, to, true)) {
    Iterator<Integer>  iter = sub.values().iterator();
    while (iter.hasNext()) {
        Integer i = iter.next();
    }
}
Transformations
Function<OakScopedReadBuffer, String> intToStrings = e -> String.valueOf(e.getInt(0));

Iterator<String> iter = oak.zc().values().stream().map(v -> v.transform(intToStrings)).iterator();
while (iter.hasNext()) {
    String s = iter.next();
}
Unsafe buffer access
Function<OakScopedReadBuffer, String> intToStringsDirect = b -> {
  OakUnsafeDirectBuffer ub = (OakUnsafeDirectBuffer) b;
  ByteBuffer bb = ub.getByteBuffer();
  return bb.getInt(ub.getOffset());
};

Iterator<String> iter = oak.zc().values().stream().map(v -> v.transform(intToStringsDirect)).iterator();
while (iter.hasNext()) {
    String s = iter.next();
}
Unsafe direct buffer access (address)

Oak support accessing its keys/values using direct memory address. DirectUtils can be used to access the memory address data.

Function<OakScopedReadBuffer, String> intToStringsDirect = b -> {
  OakUnsafeDirectBuffer ub = (OakUnsafeDirectBuffer) b;
  return DirectUtils.getInt(ub.getAddress());
};

Iterator<String> iter = oak.zc().values().stream().map(v -> v.transform(intToStringsDirect)).iterator();
while (iter.hasNext()) {
    String s = iter.next();
}

Note: in the above example, the following will not throw any exception even if the buffer mode is read-only:

DirectUtils.putInt(ub.getAddress(), someInteger);

Contribute

Please refer to the contributing file for information about how to get involved. We welcome issues, questions, and pull requests.

License

This project is licensed under the terms of the Apache 2.0 open source license.

oak's People

Contributors

boris-kap avatar dependabot[bot] avatar eranmeir avatar li0nr avatar liran-funaro avatar monishappusamy avatar nnrepos avatar orhayat avatar r-andrew-dev avatar sanastas avatar teaey avatar thiyanesh avatar yoavz1997 avatar yonigottesman avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

oak's Issues

Executers instead of Threads

Change all tests to use Executers instead of Threads. In order to propagate raised exceptions to the tests. Currently we might miss some exceptions.

@liran-funaro can you please add more details to this issue?

For more explanations feel free to add questions in this issue.

Expose standard Serializers

Currently serializers code is in the test/ source directory, therefore they don't get packaged int the jar - would be great to:

  1. move serializers to the main source directory
  2. Add documentation that these serializers exist and examples of how to use them

OakBuffer benchmark

OakBuffer hierarchical structure involves multiple classes. To take care of the future changes in the OakBuffers structure, we need to measure their performance with each change. Let's start by measuring the performance.

Add OakBuffer usage benchmark for performance evaluation. It should be quit exhaustive usage (for example matrix multiplication). The new benchmark should evaluate the performance of some OakBuffers variants.

@liran-funaro can you please add more details to this issue?

For more explanations feel free to add questions in this issue.

Oak memory leak when closing

When a Block is released the "allocated" field is not set back to zero, so once a block is returned it cannot be used again ever. after some oak allocation iterations where no block is free (even if they really are) the jvm throws outofmemory exception.

Fix:
in Block.reset() call allocated.set(0);

Relying on Reentrancy

V replace(K key, V value, Function<ByteBuffer, V> valueDeserializeTransformer) {
Chunk<K, V> c = findChunk(key); // find chunk matching key
Chunk.LookUp lookUp = c.lookUp(key);
if (lookUp == null || lookUp.handle == null)
return null;
Function<ByteBuffer, V> replaceTransform = bb -> {
// mutatingTransform guarantees that this is write-synchronous handle is not deleted
V v = valueDeserializeTransformer.apply(bb);
lookUp.handle.put(value, valueSerializer, memoryManager);
return v;
};
// will return null if handle was deleted between prior lookup and the next call
return (V) lookUp.handle.mutatingTransform(replaceTransform);
}

<T> T mutatingTransform(Function<ByteBuffer, T> transformer) {
T result;
try {
writeLock.lock();
if (isDeleted()) {
// finally clause will handle unlock
return null;
}
result = transformer.apply(getSlicedByteBuffer());
} finally {
writeLock.unlock();
}
return result;
}

In the replace functions, the write lock is acquired once when calling Handle::mutatingFunction and another time when calling Handle::put inside.
It works in the production code because the lock is reentrant, however, this assumption is limiting.

Possible deadlock when working with OakWBufferImpl

As it seems a user obtains an OakWBufferImpl when calling a compute().
Before returning this buffer to the user a writelock is taken.
However, OakWBuffer offers a transform() operation which acquires a readlock.
This cannot be done since a writelock is already held.

A redundant/illegal read

currSrcHandleIndex = srcChunk.getEntryField(srcEntryIdx, OFFSET_HANDLE_INDEX);

srcEntryIdx = srcChunk.getEntryField(srcEntryIdx, OFFSET_NEXT);
currSrcHandleIndex = srcChunk.getEntryField(srcEntryIdx, OFFSET_HANDLE_INDEX);
if (srcEntryIdx != NONE) continue;

In line 708 we read the next entry in the list, and then the handle index.
If the read entry is not NONE, we again read the same handle index in line 694.
If the entry is NONE, however, we read the handle index of an illegal entry, resulting in an invalid handle index which something causes the copying of additional entries.

A simple fix seems to be to comment out line 709.

Double-Checked Locking

if (instance == null) {
synchronized (BlocksPool.class) { // can be easily changed to lock-free
if (instance == null) {
instance = new BlocksPool();

Double-Checked Locking is widely cited and used as an efficient method for implementing lazy initialization in a multithreaded environment.
Unfortunately, it will not work reliably in a platform independent way when implemented in Java, without additional synchronization.

Oak iterator remove AutoCloseable

In InternalOakMap.Iter next() function we do:

        public T next() {
            try {
                memoryManager.startOperation();
                return internalNext();
            } finally {
                memoryManager.stopOperation();
            }
        }

So why do we need start and stop operation in object constructor and close function?
There are 2 problems with this:

  1. as long as an iterator exists buffers that are freed after it was created will never be really freed.
  2. When the iterator is statically assigned to an Iterator variable then the close is never called causing memory leaks.

Test IteratorModificationTest::concurrentModificationTest sometimes fail

@Test
public void concurrentModificationTest() throws InterruptedException {
CountDownLatch deleteLatch = new CountDownLatch(1);
CountDownLatch scanLatch = new CountDownLatch(1);
AtomicBoolean passed = new AtomicBoolean(false);
Thread scanThread = new Thread(() -> {
Iterator<Map.Entry<String, String>> iterator = oak.entrySet().iterator();
assertTrue(iterator.hasNext());
deleteLatch.countDown();
try {
scanLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
iterator.next();
} catch (ConcurrentModificationException e) {
passed.set(true);
}
});
Thread deleteThread = new Thread(() -> {
try {
deleteLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < ELEMENTS; i++) {
String key = String.format("%0$" + KEY_SIZE + "s", String.valueOf(i));
oak.zc().remove(key);
}
scanLatch.countDown();
});
scanThread.start();
deleteThread.start();
scanThread.join();
deleteThread.join();
assertTrue(passed.get());
}

In line 211, the next() sometimes returns null instead of throwing a ConcurrentModificationException.

In the picture attached that scenario can be seen.

image

Ideas for Oak new usage and improvement

Looking for ideas for Oak usage/improvement. Any idea (hopefully concrete) were else Oak can be used is welcome. Also, please let us know: where do you think is the most important for Oak to improve and why.

To participate in this issue it is enough to learn about Oak and to leave here a comment with your thoughts! Easy!

OakHash API

Add design and implementation of new OakHash API. API only, without the underlying HashMap. Probably the merge can go into a separate branch.

Similar to OakMap, OakHash should be extending ConcurrentHashMap, but must also support Zero-Copy API methods. Most likely new ZeroCopyHash should extand or be similar to ZeroCopyMap. And OakHash should return ZeroCopyHash, having all interfaces.

For more explanations feel free to add questions in this issue.

Bug found!

Find and Solve a bug we haven't spotted in existing issues yet!

If while working on Hackathon you encountered a bug, and more than that have a way to solve it, please tell us here!
This is a collective issue where can be more than one contributor/reporter.

Duplicate return values from InternalOakMap.getThreadIndex()

In the current implementation 2 threads may return the same value of
(Thread.currentThread().getId() % Chunk.MAX_THREADS)
Causing everything to break.
Fix:
Each thread will get assigned an index on demand.
getThreadIndex becomes non static, and each thread that enters get its index from a map that maps threadId to index. first time it enters its index is taken from an atomic counter.

The rebalancer copies deleted handles

When the rebalancer scans the source chuck, it skips entries with Handle index of -1, meaning that deleted Handles are copied to the newly created chunk.
If a rebalancer runs concurrently to the remove which caused the deletion of a Handle, this remove will not change the respective entry to have a Handle index of -1.
In such a case the remove would call removeRebalance, and do the main loop again.
The remove then returns without changing the Handle index to -1 because Chunk::lookup treats the entry as removed without ever completing its removal.

autoclosable API

Many functions in oak api return a new resource that is supposed to be closed.
This is not documented and not clear from the user point of view.
For example Oak::subMap() returns a new oakmap that is supposed to be closed

containsKey call AbstractMap.containsKey

OakMap.containsKey currently calls AbstractMap.containsKey which scans the entire map - it's possible to overcome by using zc().get(key) != null , however it will make more sense if OakMap did that on its own

Create a parallel version to support JDK14/15 (mostly for off-heap memory management)

Base all of the off-heap memory management on new Java14/15 VarHandle API, without unsafe usage. Other than memory management, the rest of the code should be easily transferred to JDK14. The reason for the change is to allow much wider using for Oak.

Implementing this issue requires a significant code writing: new (alternative) Block, BlockPool, MemoryAllocator, MemoryManager etc.

For more explanations feel free to add questions in this issue.

configurable BlocksPool

It would be nice to allow some of the properties of BlocksPool to be configurable.
Either via a configuration file and/or programmatically.

  1. The block size
  2. The number of blocks to be pre-allocated (even allow zero pre-allocation)
  3. The number of blocks to allocate each time a new block is needed
  4. Minimal and maximal number of unused blocks to preserve until releasing them back to the OS (excess blocks)

Specifically, it would be nice that the preallocated blocks would be a different setting than the number of new blocks to allocate, and that the number of excess blocks could be set via a ratio and/or via a specific number.
Also, the excess number of blocks should have a high and low threshold to prevent releasing blocks one at a time.
It might also help to set the number of excess blocks with proportion to the total number of blocks.

Question - Is it safe to remove(...) entries while iterating over an OakMap

Java's collections all don't allow modifying one while iterating it, resulting in a ConcurrentModificationException. If I want to remove some entries from an OakMap while iterating through them, what is the proper way of achieving it?

For instance, I created a head map, tried to call clear() on it, and failed with UnsupportedOperationException. Iterator.remove is not implemented for those returned from an OakMap. And glancing the source code, I'm not quite sure if I can safely invoke remove on an OakMap, while iterating over its content.

Currently I'm iterating over the entries and calling remove immediately after I'm done with each entry. Is it ok to do so?

Simple example application

Add a simple example application that uses Oak. It is really open-ended issue. We would love to see any variation of usage and of course this can be an umbrella to many contributors to participate!

For more explanations feel free to add questions in this issue.

Improve Oak test coverage

Add more tests to increase test coverage. Run jacoco maven plugin, fix the results, add maven constant rule for test coverage. This is an important step for safe contribution in our community.

@liran-funaro can you please add more details to this issue?

For more explanations feel free to add questions in this issue.

Needed retries in put and putIfAbsent

lookUp.handle.put(value, valueSerializer, memoryManager);

return Result.withValue(lookUp.handle.transform(transformer));

return Result.withValue(c.getHandle(prevEi).transform(transformer));

In all the lines above, the operation on the handle may return false, meaning that the value is deleted and operation should restart.
In particular, in line 309 if the operation returns false, it means that the value was deleted, and thus the put should restart in order to actually take effect.
In lines 383 and 421, we the value exists, and in the non-ZC API we need to return the previous value.
If the value managed to be deleted before we read it, the result would be null which does not match with the putIfAbsent API.

oak crash on macos M1 chip

env

os : macosx aarch64
jdk: OpenJDK 64-Bit Server VM Zulu11.48+21-CA (11.0.11+9-LTS, mixed mode, tiered, compressed oops, g1 gc, bsd-aarch64)

oak version

        <dependency>
            <groupId>com.yahoo.oak</groupId>
            <artifactId>oak</artifactId>
            <version>0.2.3.1</version>
        </dependency>

reproduce code

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.nio.ByteBuffer;

import org.junit.Assert;
import org.junit.Test;

import com.yahoo.oak.OakMap;
import com.yahoo.oak.OakMapBuilder;
import com.yahoo.oak.common.integer.OakIntComparator;
import com.yahoo.oak.common.integer.OakIntSerializer;
import com.yahoo.oak.common.string.OakStringSerializer;

public class XOakMapTest {
	
	/**
	 * 
	 */
	@Test
	public void test1() {
		//
		final OakMap<Integer, String> m;
		m = oakmap1(256); Assert.assertTrue(m.size() == 0);
		m.put(1, "123456789"); Assert.assertTrue(m.size() == 1);
		String v1 = m.remove(1); assertEquals (v1, "123456789");
		
		//
		m.put(2, "654321"); Assert.assertTrue(m.size() == 1);
		m.put(2, "654321"); Assert.assertTrue(m.size() == 1);
		String v2 = m.get(2); Assert.assertEquals(v2, "654321");
		m.close();
	}
	
	/**
	 * 
	 */
	private static OakMap<Integer, String> oakmap1(int n) {
		final int block = 1024 * 1024;
		final OakMapBuilder<Integer, String> builder;
		final OakIntComparator c = new OakIntComparator();
		final OakIntSerializer ks = new OakIntSerializer();
		final OakStringSerializer vs = new OakStringSerializer();
		builder = new OakMapBuilder<>(c, ks, vs, Integer.MIN_VALUE);
		builder.setPreferredBlockSize(block); return builder.build();
	}
}

hs_err_pid74815.log

#
# A fatal error has been detected by the Java Runtime Environment:
#
#  SIGBUS (0xa) at pc=0x00000001056ffb8c, pid=74815, tid=9731
#
# JRE version: OpenJDK Runtime Environment Zulu11.48+21-CA (11.0.11+9) (build 11.0.11+9-LTS)
# Java VM: OpenJDK 64-Bit Server VM Zulu11.48+21-CA (11.0.11+9-LTS, mixed mode, tiered, compressed oops, g1 gc, bsd-aarch64)
# Problematic frame:
# V  [libjvm.dylib+0x6ffb8c]  Unsafe_CompareAndSetLong(JNIEnv_*, _jobject*, _jobject*, long, long, long)+0x110
#
# No core dump will be written. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again
#
# If you would like to submit a bug report, please visit:
#   http://www.azulsystems.com/support/
#

more details see attachment
hs_err_pid74815.log

it works fine in windows 10, linux, macos x86_64

Documentation Review and Additions

Review the documentation (readme and wiki) of the repository and add an improvement. There is a lot what can be added - feel free to contact us (via questions here in the comments) to learn more about Oak Design.

Iterators in Oak

Currently the iterators in Oak may return NULL (as value or entry) upon key that is concurrently deleted or not fully inserted. For example, look on class EntryTransformIterator method internalNext(). I am fixing this problem partially under branch OAK--MEMORY_ALLOC--V01. Need to check it fully.

Exception when closing multiple instances of Oak simultaneously

I got the following exception when running 16 (independent) instances of Oak concurrently on 16 different threads.

java.lang.NullPointerException
at com.oath.oak.NativeAllocator.BlocksPool.returnBlock(BlocksPool.java:105)
at com.oath.oak.NativeAllocator.OakNativeMemoryAllocator.close(OakNativeMemoryAllocator.java:215)
at com.oath.oak.NovaManager.close(NovaManager.java:31)
at com.oath.oak.InternalOakMap.close(InternalOakMap.java:107)
at com.oath.oak.OakMap.close(OakMap.java:597)
at org.apache.druid.segment.incremental.OakIncrementalIndex$OakFactsHolder.close(OakIncrementalIndex.java:537)
at org.apache.druid.segment.incremental.OakIncrementalIndex.close(OakIncrementalIndex.java:145)
at org.apache.druid.benchmark.indexing.ConcurrentFullScaleIngestionBenchmark.add(ConcurrentFullScaleIngestionBenchmark.java:234)
at org.apache.druid.benchmark.indexing.generated.ConcurrentFullScaleIngestionBenchmark_add_jmhTest.add_AverageTime(ConcurrentFullScaleIngestionBenchmark_add_jmhTest.java:181)
at sun.reflect.GeneratedMethodAccessor12.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.openjdk.jmh.runner.BenchmarkHandler$BenchmarkTask.call(BenchmarkHandler.java:453)
at org.openjdk.jmh.runner.BenchmarkHandler$BenchmarkTask.call(BenchmarkHandler.java:437)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)

Elimination of all ByteBuffer Usage (for Java 8)

Base all of the off-heap memory management on unsafe allocation and access, without ByteBuffer intermediate layer (in addition to existing memory management). The reason for the change is the possibility to gain better throughput without spending CPU cycles and memory on ByteBuffer internals.

That means a new memory manager to be based on using addresses instead of ByteBuffers and allocating the address from unsafe:
Unsafe().allocateMemory(size);

That requires a significant code writing: new (alternative) Block, BlockPool, MemoryAllocator, MemoryManager etc.

For more explanations feel free to add questions in this issue.

clear() throws java.lang.UnsupportedOperationException: remove

Create a map and call .clear():

java.lang.UnsupportedOperationException: remove

	at java.base/java.util.Iterator.remove(Iterator.java:102)
	at java.base/java.util.AbstractCollection.clear(AbstractCollection.java:431)
	at java.base/java.util.AbstractMap.clear(AbstractMap.java:297)

Delete-Allocate Memory Consumption Benchmark

OakMap requires less memory than other ConcurrentNavigableMap, to emphasize this we would like to have a benchmark presenting memory consumption. This should also help us to compare different variants of memory management (MM).

Add a benchmark that is going to measure the memory allocation for high churn of deletions and allocations, to show the privilege of Oak MM

For more explanations feel free to add questions in this issue.

Enforce setting of serializer and comparator

Would be great if you can add a check whether the comparator is null to the OakBuilder.build() method - it's stated in the documentation, but still good to have it to save developer time

Add checkstyle to the tests

Currently, Oak code standards are enforced by the maven code-style plugin.
The code-style rules are detailed in codestyle/checkstyle.xml.
However, these rules are currently not enforced on the tests and benchmarks.

Please change the pom.xml file to enforce code style check on all modules and fix all the existing code-style issues.

Make BlocksPool not a singleton

This class is a singleton so keeps a reference to all blocks/ByteBuffers at all time so off heap memory is never released even if oak is closed and not referenced anymore

Add more common serializers

Oak requests its users to provide serializers for their objects representing keys and values.

Oak has internal common package including sub-packages for common serializers for common objects: IntBuffer, Integer and String. It would be good to have more of such. What kind of objects? It is up to implementer decision.

@liran-funaro can you please add more details to this issue?

For more explanations feel free to add questions in this issue.

OakMapBuilder must init mandatory fields

If i create a new builder like this:
OakMapBuilder builder = new OakMapBuilder() .setKeySerializer(stringSer) .setValueSerializer(stringSer) .setComparator(comparator)

I get an exception:
java.lang.NullPointerException at com.oath.oak.Insert$BenchmarkState$1.calculateSize(Insert.java:75) at com.oath.oak.Insert$BenchmarkState$1.calculateSize(Insert.java:52) at com.oath.oak.InternalOakMap.<init>(InternalOakMap.java:66) at com.oath.oak.OakMap.<init>(OakMap.java:74) at com.oath.oak.OakMapBuilder.build(OakMapBuilder.java:95)
If this field is mandatory, it should be part of the OakMapBuilder constructor.

oak map replace method throw AssertionError

java version

openjdk 11

oak version

        <dependency>
            <groupId>com.yahoo.oak</groupId>
            <artifactId>oak</artifactId>
            <version>0.2.3.1</version>
        </dependency>

reproduce code

package cn.nextop.gadget.fma;

import static org.junit.Assert.assertEquals;

import java.util.HashMap;
import java.util.Map;
import java.util.Random;

import org.junit.Assert;
import org.junit.Test;

import com.yahoo.oak.OakMap;
import com.yahoo.oak.OakMapBuilder;
import com.yahoo.oak.common.integer.OakIntComparator;
import com.yahoo.oak.common.integer.OakIntSerializer;
import com.yahoo.oak.common.string.OakStringSerializer;

import io.netty.util.internal.ThreadLocalRandom;

/**
 * 
 * @author Jingqi Xu
 */
public class XOakMapTest {
	
	/**
	 * 
	 */
	@Test
	public void test1() {
		//
		final int size = 1024;
		final Random random = ThreadLocalRandom.current();
		final OakMap<Integer, String> m1 = oakmap3(size);
		final Map<Integer, String> m2 = new HashMap<>(size);
		
		//
		for(int i = 0; i < 100000; i++) {
			final int k = random.nextInt(size);
			final String v = String.valueOf(k);
			switch (random.nextInt(12)) {
			case 0:
				Assert.assertEquals(m2.get(k), m1.get(k)); break;
			case 2:
				final String w2 = m2.put (k, v);
				Assert.assertEquals (w2, m1.put(k, v));
				Assert.assertEquals(m2.size(), m1.size()); break;
			case 3:
				m2.put(k, v); m1.zc().put(k, v);
				Assert.assertEquals(m2.size(), m1.size()); break;
				
			case 4:
				final String w4 = m2.putIfAbsent(k, v);
				assertEquals(w4, m1.putIfAbsent(k, v));
				Assert.assertEquals(m2.size(), m1.size()); break;
			case 5:
				boolean w5 = m2.putIfAbsent(k, v) == null;
				assertEquals(w5, m1.zc().putIfAbsent(k, v));
				Assert.assertEquals(m2.size(), m1.size()); break;
				
			case 6:
				final String w6 = m2.remove(k);
				Assert.assertEquals (w6, m1.remove(k));
				Assert.assertEquals(m2.size(), m1.size()); break;
			case 7:
				final boolean w7 = m2.remove(k) != null;
				Assert.assertEquals(w7, m1.zc().remove(k));
				Assert.assertEquals(m2.size(), m1.size()); break;
				
			case 8:
				final String w8 = m1.replace(k, v);
				assertEquals(w8, m2.replace(k, v));
				Assert.assertEquals(m2.size(), m1.size()); break;
			}
		}
		m1.close();
	}
	
	private static OakMap<Integer, String> oakmap3(int n) {
		final int block = 1024 * 1024;
		final OakMapBuilder<Integer, String> builder;
		final OakIntComparator c = new OakIntComparator();
		final OakIntSerializer ks = new OakIntSerializer();
		final OakStringSerializer vs = new OakStringSerializer();
		builder = new OakMapBuilder<>(c, ks, vs, Integer.MIN_VALUE);
		return builder.setPreferredBlockSize(block).build();
	}
}

exception stack

java.lang.AssertionError
	at com.yahoo.oak.ValueUtilsImpl.lockWrite(ValueUtilsImpl.java:249)
	at com.yahoo.oak.ValueUtilsImpl.exchange(ValueUtilsImpl.java:156)
	at com.yahoo.oak.InternalOakMap.replace(InternalOakMap.java:939)
	at com.yahoo.oak.OakMap.replace(OakMap.java:230)
	at cn.nextop.gadget.fma.XOakMapTest.test1(XOakMapTest.java:83)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
	at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
	at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:221)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)

OffHeapOakTest is failing

JUnit does not fail the test when exceptions (including assertion errors), occur in spawned threads. The test does have exceptions, which should be addressed:

Exception in thread "Thread-4" java.util.ConcurrentModificationException
	at com.oath.oak.InternalOakMap$Iter.initAfterRebalance(InternalOakMap.java:917)
	at com.oath.oak.InternalOakMap$Iter.advance(InternalOakMap.java:938)
	at com.oath.oak.InternalOakMap$EntryTransformIterator.next(InternalOakMap.java:1164)
	at com.oath.oak.OffHeapOakTest$RunThreads.run(OffHeapOakTest.java:119)
	at java.lang.Thread.run(Thread.java:748)

Single threaded OakMap

Add a flag to Oak, such that if turned on, OakMap is optimized for a single thread performance.

A flag should be added to OakBuilder to signal of creating an OakMap to be used by single thread. The flag should propagate to the internal Oak usage of CAS, where the later should be wrapped and based on the flag either CAS or simple write should be used.

The issue is not very complex but still requires understanding of OakMap design and internal classes structure. In addition to the running and correct version of single threaded Oak, the outcome of the issue should also be Oak benchmarks showing performance of single- versus multi-threaded OakMap.

For more explanations feel free to add questions in this issue.

Add delete-allocate stress test

Add a test for existing and known bug of accessing a deleted slice from released chunk (causing infinite loop). This will require long and massive concurrent put and delete usage including some delays in the middle. This will help to check the fix easier and never fail into this problem again.

For more explanations feel free to add questions in this issue.

off-heap memory used by oakMap can not be released

I use oakMap instead of concurrentSkipListMap in my project, while I find something strange.
When I no longer use an oakMap, java heap memory used by this oakMap is released, but off-heap memory does not seems to be released.

I use NMT to track the use of off-heap memory, and I find something strange.
image

image
ggggggggg

The heap memory occupied by current process is very small, but through the linux system command "top" found that it occupies huge memory. And I analyze the memory distribution in heap and find that heap memory used by the current process is very small. By the way, therer is no OakMap object in heap memory.
image
image

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.