Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
48d15b1
feat: Detect failing RPC method and use it for health checks
sanbotto Aug 13, 2026
af78020
Update gitignore
sanbotto Aug 13, 2026
089d5b2
Update to go 1.26 + use hardened image
sanbotto Aug 13, 2026
811ee01
Include native make commands
sanbotto Aug 13, 2026
5958357
fix(health): persist health status when block or sync probe calls har…
sanbotto Aug 13, 2026
f9ce588
fix(store): return a copy from mock GetEndpointStatus to avoid a data…
sanbotto Aug 13, 2026
6238852
fix(store): stop expiring custom probe state after 24h
sanbotto Aug 13, 2026
393924c
fix(health): bind custom probe methods to their chain type
sanbotto Aug 13, 2026
432e96f
fix(health): track prior health checks separately per protocol
sanbotto Aug 13, 2026
cc7303f
fix(server): make custom probe capture atomic and context-bounded
sanbotto Aug 14, 2026
8d3c1c2
fix(store): return a copy from mock GetCustomProbeState too
sanbotto Aug 14, 2026
ecece26
Set bash as shell
sanbotto Aug 14, 2026
a96520a
Configure locales and fix valkey-tools install
sanbotto Aug 14, 2026
d7d2f3b
Make sure GOOS and GOARCH aren't set for native builds
sanbotto Aug 14, 2026
1151318
Fix shell name
sanbotto Aug 14, 2026
2e35716
Improve the installation of valkey-tools
sanbotto Aug 14, 2026
24474d1
Update README
sanbotto Aug 14, 2026
fa35d38
Install gzip, required by locale-gen
sanbotto Aug 14, 2026
74e3f79
Add docstrings
sanbotto Aug 14, 2026
efb8044
Improve how GOOS and GOARCH are unset
sanbotto Aug 18, 2026
06cf6c9
Update Prometheus TSDB path
sanbotto Aug 18, 2026
dc4d96b
fix(health): stop using t.Fatalf inside test server goroutines
sanbotto Aug 18, 2026
1d39054
fix(health): serialize per-endpoint status writes to prevent lost upd…
sanbotto Aug 18, 2026
1f291e2
fix(health): only stamp LastXHealthCheck for protocols the endpoint has
sanbotto Aug 18, 2026
a6d2700
fix(health): don't fail health on a skipped-slot getBlock response
sanbotto Aug 18, 2026
dbb94fc
fix(server): skip custom probe capture when ephemeral checks are disa…
sanbotto Aug 18, 2026
4656916
fix(store): reject sub-second ttl in TryAcquireCustomProbeGate
sanbotto Aug 18, 2026
8aadf65
fix(health): read health-transition inputs inside the status lock
sanbotto Aug 18, 2026
3260460
fix: record the timestamp after the probes complete
sanbotto Aug 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,24 +1,39 @@
FROM golang:1.25-trixie
FROM dhi.io/golang:1.26-dev

# Run as root
USER root
Comment thread
sanbotto marked this conversation as resolved.

SHELL ["/bin/bash", "-c"]

# Install additional OS packages
RUN apt update && apt upgrade -y && export DEBIAN_FRONTEND=noninteractive \
&& apt -y install --no-install-recommends \
bsdutils \
curl \
git \
valkey-tools \
gzip \
libc-bin \
locales \
unzip \
&& ldconfig \
&& apt clean -y \
&& rm -rf /var/lib/apt/lists/*

# Generate and set the locale used by the shell
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen \
&& locale-gen

ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US:en
ENV LC_ALL=en_US.UTF-8

# Install Node.js (latest LTS)
RUN cd /tmp; curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \
&& apt install -y nodejs \
&& npm install -g npm@latest

# Install Claude Code
RUN curl -fsSL https://claude.ai/install.sh | bash \
RUN curl -fsSL https://claude.ai/install.sh | bash -s stable \
&& echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc \
&& echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bash_profile

Expand All @@ -27,5 +42,11 @@ RUN go install golang.org/x/tools/gopls@latest \
&& go install github.com/go-delve/delve/cmd/dlv@latest \
&& go install github.com/air-verse/air@latest

# Install Valkey's CLI
RUN apt update \
&& (getent group adm >/dev/null || groupadd -r -g 4 adm) \
&& apt install -y --no-install-recommends valkey-tools \
&& rm -rf /var/lib/apt/lists/*

# Set up the workspace
WORKDIR /workspace
16 changes: 12 additions & 4 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,21 @@
"go.goroot": "/usr/local/go",
"go.toolsEnvVars": {
"GO111MODULE": "on"
},
"terminal.integrated.defaultProfile.linux": "bash",
"terminal.integrated.profiles.linux": {
"bash": {
"path": "/bin/bash"
}
}
}
}
},
"forwardPorts": [8080],
"forwardPorts": [
8080
],
"remoteUser": "root",
"mounts": [
"source=~/.claude,target=/root/.claude,type=bind,consistency=cached"
]
"mounts": [
"source=~/.claude,target=/root/.claude,type=bind,consistency=cached"
]
}
8 changes: 4 additions & 4 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ services:
- "8081:8081"

valkey:
image: valkey/valkey:8.1-alpine
image: dhi.io/valkey:8.1-dev
ports:
- "6379:6379"
# If you want to enable TLS, uncomment the following line
Expand All @@ -38,20 +38,20 @@ services:
- valkey

prometheus:
image: prom/prometheus:latest
image: dhi.io/prometheus:3.13
Comment thread
coderabbitai[bot] marked this conversation as resolved.
ports:
- "9999:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--storage.tsdb.path=/var/prometheus"
- "--storage.tsdb.retention.time=24h"
- "--web.enable-lifecycle"
restart: unless-stopped

grafana:
image: grafana/grafana:latest
image: dhi.io/grafana:13-dev
ports:
- "3000:3000"
environment:
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/build-and-push-images.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
pull_request:
types: [opened, synchronize]
branches:
- '!misc/**'
- "!misc/**"
paths:
- "**.go"
- "**/Dockerfile"
Expand Down Expand Up @@ -47,7 +47,7 @@ jobs:
id: setup_go
uses: actions/setup-go@v6
with:
go-version: "1.25"
go-version: "1.26"

- name: Run tests
id: run_tests
Expand Down Expand Up @@ -108,7 +108,7 @@ jobs:
id: setup_go
uses: actions/setup-go@v6
with:
go-version: "1.25"
go-version: "1.26"

- name: Log in to GitHub Container Registry
id: login_ghcr
Expand Down Expand Up @@ -166,7 +166,7 @@ jobs:
id: setup_go
uses: actions/setup-go@v6
with:
go-version: "1.25"
go-version: "1.26"

- name: Log in to GitHub Container Registry
id: login_ghcr_lb
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,6 @@ configs/endpoints.json

# Local benchmarks
benchmarks/

# Misc
*.bak
22 changes: 19 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -108,23 +108,37 @@ k8s-delete:
kubectl delete -f k8s/health-checker.yaml
kubectl delete -f k8s/namespace.yaml

# Build health checker for the local host (no GOOS/GOARCH override)
.PHONY: build-hc-native
build-hc-native:
@echo "Building Health Checker (native)..."
mkdir -p bin
env -u GOOS -u GOARCH go build -o bin/aetherlay-hc ./services/health-checker/main.go

# Build load balancer for the local host (no GOOS/GOARCH override)
.PHONY: build-lb-native
build-lb-native:
@echo "Building RPC Load Balancer (native)..."
mkdir -p bin
env -u GOOS -u GOARCH go build -o bin/aetherlay-lb ./services/load-balancer/main.go

# Run both services in the background
.PHONY: run
run: build
run: build-hc-native build-lb-native
@echo "Starting both services..."
./bin/aetherlay-hc &
./bin/aetherlay-lb --metrics-port=9091 &
@echo "Both services started in the background. Use 'ps' to see them."

# Run health checker
.PHONY: run-hc
run-hc: build-hc
run-hc: build-hc-native
@echo "Running Health Checker..."
./bin/aetherlay-hc

# Run load balancer
.PHONY: run-lb
run-lb: build-lb
run-lb: build-lb-native
@echo "Running RPC Load Balancer..."
./bin/aetherlay-lb --metrics-port=9091

Expand Down Expand Up @@ -167,7 +181,9 @@ help:
@echo "Available targets:"
@echo " build - Build both services"
@echo " build-hc - Build health checker only"
@echo " build-hc-native - Build health checker for the local host (used by run/run-hc)"
@echo " build-lb - Build load balancer only"
@echo " build-lb-native - Build load balancer for the local host (used by run/run-lb)"
@echo " clean - Clean build artifacts"
@echo " dev-setup - Set up development environment"
@echo " docker-build - Build Docker images for both services"
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,15 @@ The service checks the health of an endpoint by sending these requests to it. Wh

In both cases, the sync/health-status call is treated as optional: if an endpoint doesn't implement it (a JSON-RPC "method not found" error), it's assumed healthy rather than being marked down over a missing optional method. You can also skip it for a specific endpoint with `"skip_sync_check": true`.

### Failing-Method Detection

The `eth_blockNumber`/`getSlot` and sync-status calls above only prove those specific methods work, they say nothing about a provider that's healthy overall but failing on a different method on which your traffic actually depends (e.g., `eth_getBlockByNumber` or `getBlock`). Aetherlay closes that gap automatically, with no configuration needed:

1. **Capture**: When a proxied request to an endpoint fails with a real 5xx, Aetherlay checks whether the failed request's JSON-RPC method is on a small allowlist of methods it knows how to safely re-test on its own with a read-only, proven-to-be-valid request (currently `eth_getBlockByNumber` for `evm` chains and `getBlock` for `solana`). This is keyed off the method name only; the client's original request body is never replayed, so a captured failure can never cause Aetherlay to resubmit a state-mutating call.
2. **Targeted re-testing**: While a method is captured for an endpoint, every health check that would otherwise mark it healthy also re-tests that exact method. The endpoint isn't considered healthy again until both the regular probe and the captured method's request succeed.
3. **Stability window**: The captured method stays the target for `ephemeral-checks-healthy-threshold * ephemeral-checks-interval` seconds (plus a small fixed overhead), so an endpoint failing on every method doesn't have its target constantly overwritten before any single method can accumulate enough consecutive passes to prove recovery.
4. **Automatic reset**: Once the endpoint passes the configured consecutive-success threshold, the captured method is cleared and health checks revert to the default probe.

### Chain Types

Each endpoint can declare a `chain_type`, which selects the JSON-RPC dialect used for health checks (and the rate-limit recovery probe):
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module aetherlay

go 1.25
go 1.26

require (
github.com/gorilla/mux v1.8.1
Expand All @@ -23,7 +23,7 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/sys v0.44.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
Expand Down
Loading
Loading