28 comments

[ 2.8 ms ] story [ 77.2 ms ] thread
And I'd be happy to get your feedback on how to improve on it.
It's a bit unclear on why you're using JSON to pass things between main code and the task itself. I can imagine serializing/deserializing that takes quite a bit of CPU time compared to if you just passed native data structures, or at least something binary instead of a format meant for human introspection.
indeed I used Json without giving much thinking to it. Moving to byte arrays improved the performance. Thx!
ThreadPool and Queues are my personal preference here
It might be worth looking at a generic Callable with your own generic response type, maybe something like Either where Left is an error and Right is your response type.

I'd wrap an executor into a provider. Make it accept a WorkItem and the method accepting WorkItem would construct the Callable.

I would suggest looking into an optimal way of doing this in, say, Scala and try to replicate some of those learnings in Java. Mind you - I didn't say "write it in Scala". I did say "try in a different language" and take the learnings from there. Scala Future is a good thing to look at.

You didn't want to couple your task to the Runnable interface, so instead you coupled it to an entire HTTP server? And in order to avoid the complexity of calling CompleteableFuture.runAsync, you're using an HTTP client? Could you explain in more detail how you think this is a simplification, because I don't get it?

ETA: I'm genuinely baffled by the proposed solution, and I struggled to phrase my comment in a way that didn't come off as confrontational. So please accept my apologies if it still appears overly negative; it's entirely possible I've not understood what your goals and constraints are.

I think I don't completely understand from the blog your aversion to using standard concurrency tools like Executors.
Why not simply:

    var input = new HashMap<Integer, String>();
    var results = new ConcurrentHashMap<Integer, Document>();
    var futures = input.entrySet().stream().map(entry ->
        CompletableFuture.runAsync(() -> {
            var sentiment = getSentiment(entry.getValue());
            results.put(entry.getKey(), new Document(entry.getKey(), sentiment));
        })
    );
    CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)).join();
Let us know what the speedup is :)
I think this is crazy, coz now you have HTTP stuff leaking into your source code. It's just clearly a different abstraction, a worse mess that implementing Callable .

You can implement Callable in a lambda now, so a wrapper can be conjured up easily when you need it. https://www.concretepage.com/java/jdk-8/java-8-runnable-and-... (I agree implementing Callable in the business logic was naff)

If I am writing an HTTP server then I do indeed expect "HTTP stuff" to "leak" into my source code.
1. HTTP stuff should be an adapter for transport agnostic business logic. You want to be able to support gRPC or whatever later without disturbing the internal domain functionality. 2. this article is about using HTTP servers to provide general purpose concurrency handling in Java, so your point is off topic. Of course if you are writing a HTTP server you need HTTP server code, thats kinda obvious but not what we are talking about. We are talking about: if you need concurrency, should you bring up a HTTP server?
> HTTP stuff should[1] be an adapter for transport agnostic business logic.

There's no reason to assign methodology or context to a software process. It's useful or it is not, for qualities of utility that are unique to use cases. With such soft terms as "adapter" and "business logic" you can rationalize anything anyway, so I'm not sure that's a compelling statement on it's face. Across various languages, this methodology has been used since the 90s. It's "an oldie but goodie".

[1]"should" is a weasel word, that typically is a stand-in for "I believe this". It's unlikely that I subscribe to your specific belief.

You are throwing the past two (three?) decades of research and development in software engineering if you insist on tightly coupling code structure to implementation and rejecting design patterns.
That's the weirdest thing about the post though. They're using an http server as a Thread Pool/ExecutorService? To avoid using Runable and Callable?

It doesn't seem like they we're writing an HTTP Service.

Now we have N+1 problems or how to microservice at microscale. The article really misses a "must have" these days piece - Kubernetes service deployment yaml for the server side task.
Kubernetes is hardly a "must have", there are plenty of other ways to deploy things, especially if you're in/have a small to medium size team/infrastructure. People reach for Kubernetes way too early nowadays.
I think OP took a satirical take to it.

That's why the blog post is about using a private, local HTTP API to make a task asynchronous. I mean you're already on the same machine, there must be easier way to create a background process instead of going to your network card and back...

At this moment, you might just use NodeJS with multi running instances to achieve the async+concurrency natively
I prefer Guava's ListenableFuture for java concurrency. No reason to use http or json for in-process work.
I prefer some extensions to Netty EventLoop and EventLoopGroup classes. You get very clear semantics on the pool of threads you create. Running additional calls in async manner lets you add tasks back to the same thread in an orderable way. They implement ExecutorService so existing code won’t be impacted. As far as Runnable semantics, use CompletableFuture or lambda enclosures and very little of the reuse issues appear.
I had somehow not understood that you ran the api yourself. So at first it seemed reasonable to use the HttpClient to fetch someting that was available over HTTP somewhere. But then I realized that you hosted the api yourself. To me it seems very inefficient to introduce both server and client to analyze some text.
If your tasks are CPU-bound, and you are ready to move outside your process, why not consider your deployment options? If you are deploying on AWS, AWS Lambdas were invented for exactly this kind of processing.
Just use parallel streams?

This makes absolutely no sense.

Architectural and personal preferences aside, consider what you had to put in to compensate for the stuff you took out.

Payload serialisation and deserialization.

An http client

An http server

Not only this has nothing to do with the domain of the problem you are solving, it is very likely be less performant then direct in-memory method calls.

Perfect way to shoot yourself in the foot