So you want to build a NoSql Huh? Virtual Threads
A long time ago (10 years) in a galaxy far far away, I was 'internet famous'. Every once in a while they would quote me on highscalability.com. I was on my way.
Recently I was reading some science news, apparently they are looking for another planet on orbit outside Pluto. This reminds me of ancient aliens, where I first heard of the Nibiru Cataclysm. Omg Ed! Are you on drugs? What are you talking about?....Drum role.....Yes! I have my own NoSQL database Nibiru!
Nibiru a distributed NoSQL database inspired by the Column Family data store. Besides all the cool code, I even have a power point. My blog is back, and it can't be a one-and-done. What better way to get on track then rekindle the "So you want to build a NoSQL? Huh?" Blog.
I went over the code for Nibiru and I decided to convert some bits to virtual threads. Virtual threads were introduced in Java 21. The idea behind virtual threads is they are lighter than "platform threads" as they are not handcuffed to a system thread. When they are blocked by IO they can yield.
Nibiru is a distributed database. On the write path the "coordinator" is a piece of the code that decides where data should go and sends it there. On the read path it looks in all the places for data and merges the results.
Lets look at the PR:
//look here
try (ExecutorService service = Executors.newVirtualThreadPerTaskExecutor()) {
ExecutorCompletionService<Response> completionService = new ExecutorCompletionService<>(service);
List<RemoteMessageCallable> remote = new ArrayList<>();
List<Future<Response>> remoteFutures = new ArrayList<>();
for (final Destination destination : destinations) {
if (destination.equals(destinationLocal)) {
completionService.submit(new LocalActionCallable(action));
} else {
RemoteMessageCallable r = new RemoteMessageCallable(clientForDestination(destination), message, destination);
remote.add(r);
remoteFutures.add(completionService.submit(r));
}
}
long start = System.currentTimeMillis();
long deadline = start + timeoutInMs;
if (c.getLevel() == ConsistencyLevel.ALL) {
Response response = handleAll(start, deadline, completionService, destinations, merger, message);
if (hinter != null) {
maybeSendHints(remote, remoteFutures, start, deadline, hinter);
}
return response;
} else if (c.getLevel() == ConsistencyLevel.N) {
Response response = handleN(start, deadline, completionService, destinations, merger, message, c);
if (hinter != null) {
maybeSendHints(remote, remoteFutures, start, deadline, hinter);
}
return response;
} else {
return new Response().withProperty("exception", "unsupported consistency level");
}
The code didn't change much, but it is important to note virtual threads are used differently than platform threads. First, you no longer have to create an executor service and close it down.
public void case(){
try (ExecutorService service = Executors.newVirtualThreadPerTaskExecutor()) {
Future<Object> fut1 = service.submit(...);
}
One of the nice things about executor services is they can be bounded sizes. There is no limit on the lightweight threads one can create but eventually if you make to many you will run out of memory. A nice trick to limit the number of tasks in a similar way to a bounded queue is to use a semaphore:
Semaphore sem = new Semaphore(10);
...
Result foo() {
sem.acquire();
try {
return callLimitedService();
} finally {
sem.release();
}
}Since I took bounded queues of the coordinator, I likely will add the semaphore type thing back.
But anyway there you have it, the return of Nibiru!
Comments
Post a Comment