Data Flow — Write Operation
This page traces the complete lifecycle of a single UPLOAD operation through the system. Every read and write follows a similar pattern, but writes are the most complex because they involve the TO-Multicast protocol.
Sequence Diagram
Phase-by-Phase Breakdown
Phase 1 — Login (Authentication)
- The client sends
UserCredentials(username + plaintext password) over mTLS to the AuthService - The AuthService hashes the password with the user's stored salt using PBKDF2 (260,000 iterations)
- If the hash matches, the AuthService generates a UUID session token and returns it
- The client stores this token locally and sends it with every subsequent operation
Phase 2 — Write Operation (Client → Node)
- The client constructs a
FileOperationusing the static factory:FileOperation.upload(username, filename, data) - Two critical security fields are auto-populated:
timestamp:System.currentTimeMillis()— for replay window enforcementnonce:UUID.randomUUID().toString()— for replay uniqueness
- The client calls
node.handleClientOperation(op, token)over mTLS RMI
Phase 3 — Authentication Check (Node → Auth)
- Before touching the file system, the node calls
authService.validateToken(token) - The AuthService looks up the token in its internal map
- If valid, returns the username associated with the token
- If invalid or expired, the node immediately returns an
OperationResultwithsuccess=false
This check happens before any file I/O — an attacker can't even list files without a valid token.
Phase 4 — Totally Ordered Multicast
This is the heart of the distributed consistency guarantee. Here's what happens:
-
Node 0 (the receiving node) calls
clock.tick()to increment its Lamport clock. The new timestampts=1is attached to theClockMessage. -
Node 0 calls
peer.receiveMulticast(msg)on every node, including itself. -
Each receiving node:
- Calls
clock.update(msg.timestamp)→clock = max(local, msg.ts) + 1 - Enqueues the message in a
PriorityQueuesorted by(timestamp, senderId) - Broadcasts
sendAck(fromNodeId, messageId, newClockValue)to all nodes
- Calls
-
The ACKs serve two purposes:
- They tell the originator "I received this message"
- Their timestamps prove "I have nothing earlier in flight"
Phase 5 — Delivery
A background thread (the delivery loop, running every 50 ms) polls the queue:
- Look at the head of the priority queue
- Has this message received ACKs from all nodes?
- Is every ACK's timestamp strictly greater than the message's timestamp?
- If yes: dequeue the message, execute the file operation, complete the
CompletableFuture - If no: wait for the next loop iteration
:::tip Why "strictly greater"? If an ACK arrives with timestamp = msg.timestamp, that means the sending node's clock hasn't advanced past this message — it might still be processing an earlier message from a different sender. The strict inequality guarantees that no earlier-timestamped message can still be in transit from that node. :::
Phase 6 — Return to Client
The originating node's handleClientOperation was blocked on a CompletableFuture.get(10, SECONDS). When the delivery loop executes the message, it completes the future, and the result is serialized back to the client over mTLS.
Read Operations
Read operations (DOWNLOAD, SEARCH, LIST) are much simpler:
- Client calls
handleClientOperation(readOp, token) - Node validates the token with AuthService
- Node reads from the local file system or performs the search
- Node returns the result immediately — no multicast needed
Reads are never broadcast. They go to a single node. For linearisable reads, the client is directed to the current Raft leader.
Delete and Rename
Both DELETE and RENAME follow the same TO-Multicast flow as UPLOAD. They are write operations because they modify the file system state, and that state must be consistent across all replicas.