Asyncio Support Arrives in Hazelcast Python Client 5.7.0

Introduction

If you’ve ever tried to use Hazelcast inside a FastAPI service, an aiohttp application, or any other asyncio-based Python code, you know the workarounds: wrapping blocking calls in thread pool executors, juggling Future.result() calls, or carefully keeping Hazelcast operations off the event loop entirely. Those workarounds are no longer necessary. With the release of Python Client 5.7.0 GA, the Hazelcast Python Client ships a native asyncio API — the most-requested feature by customers and the community in the client’s history. You can now await Hazelcast operations directly, use them in TaskGroups, and integrate cleanly with the asyncio ecosystem: FastAPI, uvloop, LangChain, and everything else built on Python’s standard async framework.

Asyncio API for Hazelcast Python Client

The asyncio API works on Linux, macOS, and Windows.

It is installed with the rest of the Hazelcast Python Client:

pip install hazelcast-python-client

Note that, with this release, we updated the minimum required Python version to 3.11.

Check out our Getting Started tutorial, and API Reference.

Design Decisions

We aimed to keep the surprises to a minimum while designing the asyncio API. If you are already familiar with asyncio and Hazelcast Python Client, most of the new API will be straightforward.

The public API for the asyncio client is in the hazelcast.asyncio module while private implementation is in the hazelcast.internal module. The entry point to the asyncio API is the hazelcast.asyncio.HazelcastClient class. In contrast with the current API, creating an instance of HazelcastClient doesn’t connect it to the cluster. Since connecting to the cluster is a blocking process, it is best handled asynchronously. For that purpose, we added the create_and_start method to the HazelcastClient which is the preferred way of creating a HazelcastClient instance and connecting it to the cluster.

from hazelcast.asyncio import HazelcastClient

async def amain():
   client = await HazelcastClient.create_and_start()
   # ... work with the client

The create_and_start method supports passing configuration using the hazelcast.config.Config objects or keyword arguments:

from hazelcast.asyncio import HazelcastClient
from hazelcast.config import Config


async def amain():
   cfg = Config()
   cfg.cluster_members = ["localhost:5701"]
   client1 = await HazelcastClient.create_and_start(cfg)
  
   # or, equivalently
   client2 = await HazelcastClient.create_and_start(
       cluster_members=["localhost:5701"]
   )

All client and data structure proxy operations are non-blocking, and must be used with await, or run in an asyncio task. For example, to retrieve an IMap proxy from a started client:

map = await client.get_map("sample-map")
value = await map.get("key")

The data structure proxy classes in the current API, such as hazelcast.proxy.map.Map, have a blocking() method which returns a blocking variant of the proxy. That method is not necessary, and not supported with the asyncio API as the await keyword supports that use case nicely.

For example, the following code uses the blocking Map variant in the current API:

from hazelcast import HazelcastClient
# ...


client = HazelcastClient()
# retrieve the Map proxy and create its blocking variant
map = client.get_map("my-map").blocking()
value = map.get("value")

It can trivially be converted to the asyncio API as follows:

from hazelcast.asyncio import HazelcastClient
# ...


client = await HazelcastClient.create_and_start()
map = await client.get_map("my-map")
value = await map.get("value")

Consider the following code which uses the current non-asyncio API. The map.get method returns a Future object immediately, without waiting for the operation to complete. That allows running many operations concurrently:

future1 = map.get("value1")
future2 = map.get("value2")
# the operations above run concurrently


# calling result blocks until the corresponding future is resolved
value1 = future1.result()
value2 = future2.result()
if value1 < value2:
   print("value1 is smaller than value2")
else:
   print("value1 is equal or greater than value2")

That, of course, can be easily ported to the asyncio API. Instead of awaiting an operation, create a task and await it later.

task1 = asyncio.create_task(map.get("value1"))
task2 = asyncio.create_task(map.get("value2"))
# the operations above run concurrently


# using await blocks until the corresponding task is done
value1 = await task1
value2 = await task2
if value1 < value2:
   print("value1 is smaller than value2")
else:
   print("value1 is equal or greater than value2")

Just like the current API, the asyncio API has types attached for all public methods. Asyncio API specific classes are imported from hazelcast.asyncio.

from hazelcast.asyncio import Map


async def populate_map(map: Map[str, str]) -> None:
   for i in range(10):
       await map.set(f"key-{i}", f"value-{i}")

Usage Example

Here is a sample Hazelcast client code that sets some values on an IMap, and then prints its size:

from hazelcast import HazelcastClient


# create the client
client = HazelcastClient()


# retrieve the IMap proxy
map_name = "my-map"
my_map = client.get_map(map_name)


# populate the map
for i in range(10):
   my_map.set(f"key-{i}", f"value-{i}").result()


# get map size
size = my_map.size().result()
print(f"size of {map_name} is: {size}")


# release client resources
client.shutdown()

This code can be easily converted to asyncio by following steps:

  1. Use HazelcastClient from the asyncio module,
  2. Instead of calling the constructor of HazelcastClient, call its create_and_start method,
  3. Instead of calling result(), use await on HazelcastClient and Map objects.

Additionally, the code must be written in an async function since await can only be used in async functions. Putting all of these together, the code sample for asyncio client becomes:

import asyncio


from hazelcast.asyncio import HazelcastClient


async def amain():
   # create the client
   client = await HazelcastClient.create_and_start()


   # retrieve the IMap proxy
   map_name = "my-map"
   my_map = await client.get_map(map_name)


   # populate the map
   for i in range(10):
       await my_map.set(f"key-{i}", f"value-{i}")


   # get map size
   size = await my_map.size()
   print(f"size of {map_name} is: {size}")


   # release client resources
   await client.shutdown()




asyncio.run(amain())

Once the code is modified to use the asyncio API, full asyncio functionality is available. For instance, you can schedule Map operation tasks to run concurrently and use a TaskGroup to handle them robustly.

my_map = await client.get_map(map_name)
async with asyncio.TaskGroup() as tg:
   # populate the map
   for i in range(10):
       tg.create_task(my_map.set(f"key-{i}", f"value-{i}"))

Caveats and Warnings

As with any asyncio code, never block the event loop for more than a few milliseconds. Common culprits are synchronous I/O, file system access, and heavy computation. Blocking may happen when doing a network operation without using asyncio, file system operations, or running a long loop or heavy computation. Use asyncio variants where they exist, yield control in long loops. In any case, you can run the blocking code in another thread.

Python asyncio client is not thread-safe, just like many other asyncio libraries. It runs on the asyncio event loop attached to the current thread. Mixing threaded code and asyncio code is not ideal, but if you have to, you can create one client per thread. Or, you can call the asyncio client across threads using asyncio.run_coroutine_threadsafe.

When an asyncio task that uses the Hazelcast Python Client is canceled manually, or due to a timeout, it is canceled only on the client-side. An operation may continue running on the server-side, even though it is canceled on the client-side.

Conclusions

The updated Hazelcast Python Client is a first-class citizen of the asyncio ecosystem — no thread pools, no workarounds, full feature parity with the existing API. Try it out and tell us what you think: open an issue or discussion on GitHub, or find us in Community Slack. If you hit a use case the asyncio API doesn’t cover, we want to hear about it.

You can find more about the Hazelcast Python Client in the links below: