The Hasher interface
GoAkt hashes keys with a pluggable function defined in thehash package:
hash.DefaultHasher(), uses xxh3: a fast, allocation-free, non-cryptographic hash with excellent distribution. It is used automatically; you only implement Hasher when you need to replace it.
Two parts of the framework accept a custom Hasher:
Cluster mode
In cluster mode, GoAkt keeps a distributed registry of every actor, grain, and scheduled job. The registry is split into a fixed number of partitions (default 271, tunable withClusterConfig.WithPartitionCount). The hasher sits at the center of this machinery, in two places:
- Key to partition. Every registry key (actor name, grain identity, job ID) is hashed, and the hash code modulo the partition count selects the partition that stores the record:
partition = HashCode(key) % partitionCount. - Partition to node. Partitions are assigned to cluster members through a consistent-hash ring built with the same hasher: each partition ID is itself hashed to position it on the ring. When nodes join or leave, only the affected partitions move.
Correlation with WithPartitionCount
The hasher andWithPartitionCount jointly define the placement function: the hasher produces the 64-bit hash code, and the partition count buckets it. Neither is meaningful in isolation, and tuning one affects the other.
ClusterConfig, next to each other, because neither means anything without the other.
- Use a prime count. Modulo folds the hash’s 64 bits into
partitionCountbuckets. A prime count avoids resonance between patterned hash outputs and the modulo, which matters even more when you substitute a weaker custom hasher for xxh3. The default 271 is prime; keep that property when changing it. - The count sets the rebalancing granularity. A partition is the unit of ownership and relocation: with more partitions, load spreads more evenly across members and topology changes move smaller chunks. Pick a count comfortably above the maximum number of nodes you expect, otherwise some nodes own several partitions while others sit idle.
- The count multiplies memory overhead. Each partition holding data allocates its own storage table (
ClusterConfig.WithTableSize, default 4 MB), so worst-case registry memory grows withpartitionCount x tableSize. Very large counts buy smoother balancing at the price of baseline memory. - Same consistency rule as the hasher. All nodes must use the same partition count. Because the mapping is
hash % count, changing the count remaps almost every key to a different partition, exactly like changing the hash function.
Benefits
- Deterministic, coordination-free lookups. Resolving an actor by name (
ActorOf,SpawnOn, grain activation) requires no directory service and no broadcast: the owner is a pure function of the key. - Even data and load distribution. A well-distributed hash spreads registry records, and therefore lookup and write traffic, uniformly across partitions and nodes.
- Bounded reshuffling. Consistent hashing over partitions means a topology change relocates only the partitions owned by the affected node instead of rehashing the whole keyspace.
Setting a custom hasher
The hasher is set on theClusterConfig, alongside the partition count:
actor.WithPartitionHasher is deprecated in favor of the cluster config setter. It keeps working for existing systems, and when both are set the cluster config hasher takes precedence.
A custom partition hasher must satisfy all of the following:
- Deterministic and stable. The same byte slice must produce the same hash code on every node, across process restarts, Go versions, and CPU architectures. Never use a per-process random seed (for example
hash/maphashwith a random seed). - Identical cluster-wide. Every node must be configured with the same hasher. Nodes with different hashers disagree on partition ownership, and registry reads and writes silently miss each other.
- Uniform. A skewed hash concentrates keys in a few partitions and turns the nodes owning them into hot spots.
- Fast. The hasher runs on every registry read and write; prefer allocation-free implementations.
Keep the default xxh3 hasher unless you have a measured reason not to. Typical reasons to replace it are matching the placement scheme of an external system that pre-computes the same hashes, or complying with an organization-wide mandated hash function.
Non-cluster (standalone) mode
In standalone mode there is no distributed registry and no partitions, so the partition hasher has no effect: theClusterConfig is only consulted when clustering is enabled.
Hashing still matters in one place: consistent-hash routers. A router configured with WithConsistentHashRouter(extractor) builds a local hash ring over its routees using the same hash.Hasher interface, replaceable with WithConsistentHashHasher(h). Messages carrying the same routing key always land on the same routee, which gives you:
- Sticky, per-key processing. All messages for one entity (an order, a session, a device) are handled by one routee, preserving per-key ordering.
- Cache locality. A routee accumulates state for the keys it owns instead of every routee holding everything.