Requirements & approach:- Maintain an approximate PageRank (PR) continuously under streaming edge inserts/deletes.- Use delta propagation: when an edge change affects a node's out-degree or edges, emit PR deltas to neighbors; accumulate until local convergence bound reached.- Periodically reconcile with a full batch recompute to remove drift.State to store (per node, durable key-value store like RocksDB / Redis / DynamoDB / HBase):- PR: current PageRank value- out_deg: current out-degree- in_neighbors (optional compacted list or maintained elsewhere)- pending_delta: accumulated unpropagated delta (float)- active_flag / last_update_tsHigh-level algorithm (event handling + worker loop):Pseudocode:python
# config
DAMPING=0.85
EPS_NODE=1e-6 # local convergence threshold per node
MAX_HOPS=20 # propagation bounding (TTL)
RECONCILE_PERIOD=3600 # seconds for batch recompute
def on_edge_update(src, dst, op):
# op = 'insert' or 'delete'
# update out_deg atomically for source
old_out = get_out_deg(src)
new_out = old_out + (1 if op=='insert' else -1)
set_out_deg(src, new_out)
# compute delta effect on src's outgoing contribution
# change in contribution = PR(src) * (1/old_out - 1/new_out)
pr_src = get_PR(src)
if old_out==0 and new_out==0:
return
old_contrib = pr_src / old_out if old_out>0 else 0
new_contrib = pr_src / new_out if new_out>0 else 0
delta = new_contrib - old_contrib
# schedule delta to affected neighbor(s)
schedule_delta(dst, delta, ttl=MAX_HOPS)
def schedule_delta(node, delta, ttl):
if abs(delta) < EPS_NODE: return
# accumulate pending delta and mark active
acc = atomic_add_pending(node, delta)
if abs(acc) >= EPS_NODE:
push_to_worker_queue(node, ttl)
# Worker: consumes active nodes, applies pending delta and propagates
def worker_process(node, ttl):
pending = atomic_swap_pending(node, 0)
if abs(pending) < EPS_NODE: return
pr = get_PR(node)
# update PR using push-style delta (personalization ignored)
new_pr = pr + pending
set_PR(node, new_pr)
set_last_update(node, now())
if ttl<=0: return
out_d = get_out_deg(node)
if out_d==0: return
# propagate portion to each neighbor: contribution = pending * DAMPING / out_d
contrib = pending * DAMPING / out_d
for nbr in get_out_neighbors(node):
schedule_delta(nbr, contrib, ttl-1)
# Periodic bounding & local convergence:
# - limit per-node work rate by a token bucket or max updates per window
# - stop propagating if contribution magnitude < EPS_NODE or hop exceeded
# - use active_flag TTL to avoid reprocessing hot nodes endlessly
# Periodic reconciliation job (batch):
def reconcile_batch():
# run full PageRank on current snapshot (e.g., Spark GraphX)
full_PR = run_batch_pagerank_snapshot()
for node, pr in full_PR.items():
set_PR(node, pr)
clear_pending(node)
set_last_reconcile(node, now())
Key design points / reasoning:- Delta-based (push) reduces work to local neighborhoods; storing pending_delta avoids many small updates and enables batching.- Local convergence: threshold EPS_NODE and TTL bound propagation hops; also limit per-node ops per second to bound compute on hot nodes.- Storage: keep PR and out_deg locally for quick delta computations; neighbor lists may be sharded or read from a graph service.- Reconciliation: periodic global batch recompute corrects accumulated approximation errors and topology drift; frequency depends on update rate and SLA.- Trade-offs: smaller EPS_NODE yields higher accuracy but more compute; TTL bounds propagation but can delay distant effects until batches reconcile.- Fault tolerance: persist pending_deltas and use idempotent atomic ops; use checkpointing for workers.