Reference · 08

Partitions and transactions.

Move tenants between Raft groups, and commit changes across groups at once.

Partitions and resizing

A named partition is a separate database (its own bundle, data, timers and retry receipts) that you can move between Raft groups. Several partitions share one group.

  • Choose partitions when you create a database. Data in the default database can't be turned into a partition later.
  • Named partitions don't support cross-partition reads or transactions yet.
Operator · existing, initialized groups
import { FlowerClient } from "@flower-js/sdk/client";

const cluster = new FlowerClient("http://catalog-1:7101", {
  adminToken: process.env.FLOWER_ADMIN_TOKEN,
});
await cluster.registerGroup({ id: "west", addresses: ["west-1:7101", "west-2:7101", "west-3:7101"] });
await cluster.registerGroup({ id: "east", addresses: ["east-1:7101", "east-2:7101", "east-3:7101"] });
await cluster.createPartition("tenant-a", "west", { requestId: "create-tenant-a" });
await cluster.waitForPartition("tenant-a");
const tenant = cluster.partition("tenant-a");
// Deploy a bundle through tenant.deploy(bundle), then call its exposed methods.
await cluster.resize(["west", "east"], { requestId: "grow-two-groups" });
console.log(await cluster.layout()); // Durable progress; resize continues after this client exits.
APIContract
partition(name): FlowerClientA client for /partitions/{name}, keeping the same transport and tokens. Works wherever the partition lives. Isolation only; it doesn't authenticate tenants.
layout(options?): Promise<ClusterLayout>Current groups, placements, moves and rebalance plan.
registerGroup(group, options?): Promise<ClusterGroup>Register a group that is already running. Safe to repeat. Doesn't start servers.
removeGroup(id, options?): Promise<{removed: string}>Unregister an empty, unused group (never the catalog). Doesn't stop servers or delete files.
createPartition(name, group, options?): Promise<PartitionPlacement>Create an empty partition. Wait until active, then deploy to it.
movePartition(name, destination, options?): Promise<PartitionMove>Start moving to another group. Returns before the move finishes; the move continues if the client goes away.
resize(groupIds, options?): Promise<RebalancePlan>Spread partitions evenly by count across these groups, one move at a time; groups left out are drained. Doesn't balance by size or load. Register new groups first; remove drained ones after. Rejected while another plan or move is running.
partitionStatus(name, options?): Promise<PartitionPlacement>Current phase, owner and epoch.
waitForPartition(name, options?): Promise<PartitionPlacement>Poll until active. Defaults: timeoutMs 30,000, intervalMs 100. Throws PARTITION_WAIT_TIMEOUT on timeout; abort only stops waiting.
ControlOptionsOptional signal and requestId. A UUID is generated if omitted; set your own so you can retry safely.
PartitionWaitOptionsOptional signal, timeoutMs and intervalMs.
ClusterGroup{id: string, addresses: string[]}, with distinct host:port entries. Addresses are seeds and can't be changed while the group is registered; keep at least one reachable.
PartitionPlacement{partition, epoch, owner: ClusterGroup, status: "creating" | "active" | "moving", operation, movement: PartitionMove | null}.
PartitionMove{operation, partition, source: ClusterGroup, destination: ClusterGroup, source_epoch, epoch, phase}. The epoch changes on each move; revision and receipts carry over.
PartitionMovePhase"copying" | "freezing" | "importing" | "activating" | "retiring" | "complete". Moves always finish; there is no cancel.
RebalanceMove{partition, source, destination, operation}, with group IDs.
RebalancePlan{operation, groups: ClusterGroup[], moves: RebalanceMove[], next: number, complete: boolean}. next is the index of the next move.
ClusterLayout{groups: ClusterGroup[], partitions: PartitionPlacement[], moves: PartitionMove[], rebalance: RebalancePlan | null}. Move history is kept indefinitely.

Running partitions

Configure every server with:

  • FLOWER_GROUP: this server's group.
  • FLOWER_CATALOG_GROUP: the group that stores placements.
  • FLOWER_GROUPS: JSON map of group IDs to host:port lists, including this group and the catalog.
  • The same peer secret everywhere, and a separate operator token.

Initialize each group on its own. Put applications in named partitions, not the catalog's default database.

During a move

  • The source keeps serving while it copies, then briefly freezes to send the final changes. Writes either land before the freeze or fail; none are lost.
  • The freeze can take longer for large partitions or slow networks. It is not guaranteed to be milliseconds.
  • If a group loses quorum after the freeze, the move pauses until it recovers.
  • Pending cross-group transactions delay the freeze.
  • Live watches end with an error when the owner changes. Reconnect through the partition client; there's no automatic replay.
  • Other partitions on the same group keep working but share CPU, disk and the Raft log.
SettingEffect
FLOWER_ROUTE_CACHE_MSHow long a server trusts a cached owner. Default 1,000 ms. If the catalog is down when an entry expires, requests to that partition fail.
FLOWER_PARTITION_TAIL_MAX_BYTESIf the final change set is bigger than this, send a full copy instead. Unset by default.
FLOWER_PARTITION_BASE_MAX_BYTESMaximum size of the initial copy. Unset by default.

Partition URLs are not a security boundary. For public traffic, put a trusted gateway in front that limits each caller to its partitions. Retry uncertain mutations with the same request ID and body.

Cross-group transactions

A transaction method calls methods on several groups or partitions and commits them all or none.

APIContract
transaction<Args, Value = Json>(name, plan): TransactionMethod<Args, Value>plan(args) returns a TransactionPlan. It can't read data. Expose it as an HTTP alias and call it with client.call.
TransactionCallOne of group:string or partition:string, plus method:string and optional args:Json. The method must be a query or mutation, not another transaction.
TransactionPlan<Value = Json>calls: readonly TransactionCall[] and optional value: Value. The call list is fixed up front; results can't choose later calls.
TransactionMethod<Args = Json, Value = Json>{kind: "transactionMethod", name, compute}. Create it with transaction; don't call compute yourself.
  • Check business rules inside the participant methods. Later calls to the same target see earlier ones. The result is {results, value?} in plan order.
  • While a transaction is prepared, each participating database blocks fresh reads, watches, writes and deploys, even for unrelated keys. Other partitions on the same group are unaffected.
  • Replica-local reads may not see the transaction atomically.
  • If the coordinator is unreachable, participants stay blocked until it recovers. Nothing times out on its own.
  • TRANSACTION_PREPARED: wait for recovery and retry with the same ID and content. TRANSACTION_ABORTED is final for that ID; fix the problem and use a new ID.
  • Completed transaction records are cleaned up only by operator action; see retry retention and transaction closure.
  • Every participant needs FLOWER_GROUP, FLOWER_GROUPS and the shared peer secret.

Full protocol: transactions and recovery.