Skip to content

Commit cd2c95c

Browse files
committed
Expand EPaxos visualizer with financial learning lab
1 parent 1672291 commit cd2c95c

14 files changed

Lines changed: 2125 additions & 240 deletions

File tree

EPAXOS.MD

Lines changed: 259 additions & 0 deletions
Large diffs are not rendered by default.

visualizer/cmd/wasm/main.go

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,14 @@ type request struct {
2121
}
2222

2323
type response struct {
24-
OK bool `json:"ok"`
25-
Scenarios []sim.ScenarioMeta `json:"scenarios,omitempty"`
26-
Trace *sim.ScenarioTrace `json:"trace,omitempty"`
27-
Frame *sim.Frame `json:"frame,omitempty"`
28-
CanBack *bool `json:"canBack,omitempty"`
29-
CanForward *bool `json:"canForward,omitempty"`
30-
Error *errorResponse `json:"error,omitempty"`
24+
OK bool `json:"ok"`
25+
Scenarios []sim.ScenarioMeta `json:"scenarios,omitempty"`
26+
Throughput []sim.ThroughputPoint `json:"throughput,omitempty"`
27+
Trace *sim.ScenarioTrace `json:"trace,omitempty"`
28+
Frame *sim.Frame `json:"frame,omitempty"`
29+
CanBack *bool `json:"canBack,omitempty"`
30+
CanForward *bool `json:"canForward,omitempty"`
31+
Error *errorResponse `json:"error,omitempty"`
3132
}
3233

3334
type errorResponse struct {
@@ -36,9 +37,13 @@ type errorResponse struct {
3637
}
3738

3839
type bridge struct {
39-
lab *sim.Session
40-
labCursor int
41-
labLength int
40+
lab *sim.Session
41+
labCursor int
42+
labLength int
43+
finance *sim.Session
44+
financeCursor int
45+
financeLength int
46+
throughput []sim.ThroughputPoint
4247
}
4348

4449
var dispatchFunction js.Func
@@ -89,6 +94,18 @@ func (b *bridge) handle(req request) response {
8994
return failure(sim.CodeInvalidRequest, "Catalog does not accept additional fields.")
9095
}
9196
return response{OK: true, Scenarios: sim.Catalog()}
97+
case "performance":
98+
if req.Scenario != "" || req.Size != 0 || req.Index != 0 || req.Action != (sim.Action{}) {
99+
return failure(sim.CodeInvalidRequest, "Performance does not accept additional fields.")
100+
}
101+
if b.throughput == nil {
102+
profile, err := sim.FaultThroughputProfile()
103+
if err != nil {
104+
return fromError(err)
105+
}
106+
b.throughput = profile
107+
}
108+
return response{OK: true, Throughput: b.throughput}
92109
case "scenario":
93110
if req.Scenario == "" || req.Size != 0 || req.Index != 0 || req.Action != (sim.Action{}) {
94111
return failure(sim.CodeInvalidRequest, "Choose one guided scenario.")
@@ -135,6 +152,43 @@ func (b *bridge) handle(req request) response {
135152
}
136153
b.labCursor = frame.Index
137154
return frameResponse(frame, b.labCursor > 0, b.labCursor < b.labLength)
155+
case "finance.reset":
156+
if req.Scenario != "" || req.Size != 0 || req.Index != 0 || req.Action != (sim.Action{}) {
157+
return failure(sim.CodeInvalidRequest, "Financial reset does not accept additional fields.")
158+
}
159+
session, err := sim.NewFinancialSession()
160+
if err != nil {
161+
return fromError(err)
162+
}
163+
frame, err := session.Seek(0)
164+
if err != nil {
165+
return fromError(err)
166+
}
167+
b.finance = session
168+
b.financeCursor = 0
169+
b.financeLength = 0
170+
return frameResponse(frame, false, false)
171+
case "finance.action":
172+
if req.Scenario != "" || req.Size != 0 || req.Index != 0 || b.finance == nil {
173+
return failure(sim.CodeInvalidRequest, "Reset the financial simulation before dispatching an action.")
174+
}
175+
frame, err := b.finance.Dispatch(req.Action)
176+
if err != nil {
177+
return fromError(err)
178+
}
179+
b.financeCursor = frame.Index
180+
b.financeLength = frame.Index
181+
return frameResponse(frame, b.financeCursor > 0, false)
182+
case "finance.seek":
183+
if req.Scenario != "" || req.Size != 0 || req.Action != (sim.Action{}) || b.finance == nil {
184+
return failure(sim.CodeInvalidRequest, "Reset the financial simulation before seeking its history.")
185+
}
186+
frame, err := b.finance.Seek(req.Index)
187+
if err != nil {
188+
return fromError(err)
189+
}
190+
b.financeCursor = frame.Index
191+
return frameResponse(frame, b.financeCursor > 0, b.financeCursor < b.financeLength)
138192
default:
139193
return failure(sim.CodeInvalidRequest, "That operation is not supported.")
140194
}

visualizer/internal/sim/finance.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package sim
2+
3+
import (
4+
"encoding/binary"
5+
"fmt"
6+
"sort"
7+
"strconv"
8+
9+
"gosuda.org/moreconsensus/epaxos"
10+
)
11+
12+
type financialAccount struct {
13+
id string
14+
name string
15+
balance int64
16+
home [2]uint64
17+
}
18+
19+
var financialAccounts = [...]financialAccount{
20+
{id: "northwind", name: "Northwind Treasury", balance: 25_000_000, home: [2]uint64{1, 2}},
21+
{id: "contoso", name: "Contoso Clearing", balance: 12_000_000, home: [2]uint64{2, 3}},
22+
{id: "globex", name: "Globex Settlement", balance: 18_500_000, home: [2]uint64{3, 4}},
23+
{id: "initech", name: "Initech Payroll", balance: 8_000_000, home: [2]uint64{4, 5}},
24+
{id: "umbrella", name: "Umbrella Reserve", balance: 40_000_000, home: [2]uint64{5, 1}},
25+
{id: "alpine", name: "Alpine Merchant", balance: 9_000_000, home: [2]uint64{2, 1}},
26+
{id: "fabrikam", name: "Fabrikam Escrow", balance: 15_000_000, home: [2]uint64{3, 2}},
27+
{id: "soylent", name: "Soylent Operations", balance: 11_250_000, home: [2]uint64{4, 3}},
28+
{id: "hooli", name: "Hooli Benefits", balance: 6_750_000, home: [2]uint64{5, 4}},
29+
{id: "vehement", name: "Vehement Custody", balance: 22_000_000, home: [2]uint64{1, 5}},
30+
}
31+
32+
var financialRegions = [...]string{
33+
"",
34+
"Virginia · AZ-A",
35+
"Virginia · AZ-B",
36+
"Virginia · AZ-C",
37+
"Virginia · AZ-D",
38+
"Virginia · AZ-E",
39+
}
40+
41+
var financialRTT = [...][6]uint64{
42+
{},
43+
{0, 0, 2, 7, 11, 3},
44+
{0, 2, 0, 3, 8, 7},
45+
{0, 7, 3, 0, 2, 9},
46+
{0, 11, 8, 2, 0, 4},
47+
{0, 3, 7, 9, 4, 0},
48+
}
49+
50+
func initializeFinancialMachine(m *machine) {
51+
for from := 1; from <= m.size; from++ {
52+
copy(m.rtt[from], financialRTT[from][:m.size+1])
53+
if m.booted[from] {
54+
seedFinancialApplication(&m.apps[from])
55+
}
56+
}
57+
}
58+
59+
func seedFinancialApplication(app *application) {
60+
if len(app.state) != 0 {
61+
return
62+
}
63+
for _, account := range financialAccounts {
64+
app.state[accountStateKey(account.id)] = strconv.FormatInt(account.balance, 10)
65+
}
66+
}
67+
68+
func accountStateKey(id string) string {
69+
return "acct_" + id
70+
}
71+
72+
func accountByID(id string) (financialAccount, bool) {
73+
for _, account := range financialAccounts {
74+
if account.id == id {
75+
return account, true
76+
}
77+
}
78+
return financialAccount{}, false
79+
}
80+
81+
func financialAccountViews() []AccountView {
82+
views := make([]AccountView, 0, len(financialAccounts))
83+
for _, account := range financialAccounts {
84+
views = append(views, AccountView{ID: account.id, Name: account.name, Home: []uint64{account.home[0], account.home[1]}})
85+
}
86+
return views
87+
}
88+
89+
func (m *machine) routeTransfer(account financialAccount) (uint64, bool) {
90+
var selected uint64
91+
for _, candidate := range account.home {
92+
if !m.booted[candidate] || m.paused[candidate] || m.crashed[candidate] {
93+
continue
94+
}
95+
if selected == 0 || m.coordinated[candidate] < m.coordinated[selected] {
96+
selected = candidate
97+
}
98+
}
99+
return selected, selected != 0
100+
}
101+
102+
func (m *machine) proposeTransfer(action Action, events *[]Event) error {
103+
sequence := uint64(len(m.commands) + 1)
104+
id := epaxos.CommandID{Client: 1, Sequence: sequence}
105+
cycle := make([]byte, 8)
106+
binary.BigEndian.PutUint64(cycle, sequence)
107+
resources := []string{
108+
"acct/" + action.From,
109+
"acct/" + action.To,
110+
fmt.Sprintf("dedup/1/%d", sequence),
111+
fmt.Sprintf("txn/1/%d", sequence),
112+
}
113+
sort.Strings(resources)
114+
points := make([][]byte, len(resources))
115+
for i := range resources {
116+
points[i] = []byte(resources[i])
117+
}
118+
summary := fmt.Sprintf("TRANSFER %s → %s · %s", action.From, action.To, formatCents(action.Amount))
119+
command := epaxos.Command{
120+
ID: id,
121+
Payload: []byte(fmt.Sprintf("TRANSFER %s %s %d", action.From, action.To, action.Amount)),
122+
Footprint: epaxos.Footprint{Points: points},
123+
CycleKey: cycle,
124+
}
125+
entry := commandEntry{
126+
id: id,
127+
view: CommandView{
128+
ID: commandIDString(id),
129+
Operation: "TRANSFER",
130+
From: action.From,
131+
To: action.To,
132+
Amount: action.Amount,
133+
Summary: summary,
134+
Resources: append([]string(nil), resources...),
135+
Order: sequence,
136+
},
137+
cycle: append([]byte(nil), cycle...),
138+
}
139+
m.commands = append(m.commands, entry)
140+
ref, err := m.nodes[action.Replica].Propose(command)
141+
if err != nil {
142+
return err
143+
}
144+
m.coordinated[action.Replica]++
145+
m.commands[len(m.commands)-1].view.Ref = ref.String()
146+
view := m.commands[len(m.commands)-1].view
147+
*events = append(*events, Event{
148+
Kind: "proposed",
149+
Replica: action.Replica,
150+
Command: &view,
151+
Detail: fmt.Sprintf("The locality router sent %s to R%d as %s.", summary, action.Replica, ref),
152+
})
153+
return m.drainReady(events)
154+
}
155+
156+
func (m *machine) applyTransfer(replica int, entry commandEntry) (string, error) {
157+
app := &m.apps[replica]
158+
fromKey := accountStateKey(entry.view.From)
159+
toKey := accountStateKey(entry.view.To)
160+
fromBalance, err := strconv.ParseInt(app.state[fromKey], 10, 64)
161+
if err != nil {
162+
return "", fmt.Errorf("decode source balance %q: %w", fromKey, err)
163+
}
164+
toBalance, err := strconv.ParseInt(app.state[toKey], 10, 64)
165+
if err != nil {
166+
return "", fmt.Errorf("decode destination balance %q: %w", toKey, err)
167+
}
168+
if fromBalance < entry.view.Amount {
169+
return fmt.Sprintf("DECLINED · %s has insufficient funds", entry.view.From), nil
170+
}
171+
app.state[fromKey] = strconv.FormatInt(fromBalance-entry.view.Amount, 10)
172+
app.state[toKey] = strconv.FormatInt(toBalance+entry.view.Amount, 10)
173+
return entry.view.Summary, nil
174+
}
175+
176+
func formatCents(cents int64) string {
177+
return fmt.Sprintf("$%d.%02d", cents/100, cents%100)
178+
}

0 commit comments

Comments
 (0)