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.
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.| API | Contract |
|---|---|
partition(name): FlowerClient | A 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. |
ControlOptions | Optional signal and requestId. A UUID is generated if omitted; set your own so you can retry safely. |
PartitionWaitOptions | Optional 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 tohost:portlists, 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.
| Setting | Effect |
|---|---|
FLOWER_ROUTE_CACHE_MS | How 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_BYTES | If the final change set is bigger than this, send a full copy instead. Unset by default. |
FLOWER_PARTITION_BASE_MAX_BYTES | Maximum 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.
| API | Contract |
|---|---|
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. |
TransactionCall | One 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_ABORTEDis 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_GROUPSand the shared peer secret.
Full protocol: transactions and recovery.