mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-24 19:24:16 +00:00
Around 2026-08-20 the Bluesky relay marked every ATCR hold offline and stopped dialing. Nobody noticed for two weeks, and it surfaced only indirectly as "pull and push counts are up to 24 h stale". The stats were a symptom; the fleet was simply disconnected. It cannot recover on its own. Indigo's relay gives up on a host after 16 consecutive dial failures and returns from the redialer, and only a fresh requestCrawl revives it. The hold sends requestCrawl exactly once, at boot (server.go:400), with no ticker and no check that any relay is subscribed. So a dropped hold is invisible until its process restarts, silently. Documents the current mechanics, the failure mode, how to diagnose it with getHostStatus and a frozen repo rev, and how to recover. The automatic re-crawl is described as a deferred proposal and explicitly NOT implemented, by decision: a jittered ticker guarded on subscriber liveness, plus surfacing the subscriber count, since the deeper problem is that this was silent. Two things found while writing it, both recorded. The proposal needs plumbing that does not exist: EventBroadcaster has no exported subscriber count, and Subscriber does not retain the userAgent, so "is a relay listening" cannot currently be answered. And ResubscribeAllHosts selects only active hosts, so an offline host is not recovered even by a relay restart. Carries a replay warning.ca539b1fixed a panic on subscriber disconnect during firehose backfill, and the exposure condition is that a backfill goroutine exists at all, which Subscribe skips when the cursor is current. So a caught-up relay never triggered it and a hold whose relays are far behind is exposed on every reconnect. Verify a deployed hold containsca539b1before provoking a re-crawl;efabb677does not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PDqoCE1j3njokkZ9b1C5n9
467 lines
12 KiB
Markdown
467 lines
12 KiB
Markdown
# ATCR Troubleshooting Guide
|
|
|
|
This document provides troubleshooting guidance for common ATCR deployment and operational issues.
|
|
|
|
## OAuth Authentication Failures
|
|
|
|
### JWT Timestamp Validation Errors
|
|
|
|
**Symptom:**
|
|
```
|
|
error: invalid_client
|
|
error_description: Validation of "client_assertion" failed: "iat" claim timestamp check failed (it should be in the past)
|
|
```
|
|
|
|
**Root Cause:**
|
|
The AppView server's system clock is ahead of the PDS server's clock. When the AppView generates a JWT for OAuth client authentication (confidential client mode), the "iat" (issued at) claim appears to be in the future from the PDS's perspective.
|
|
|
|
**Diagnosis:**
|
|
|
|
1. Check AppView system time:
|
|
```bash
|
|
date -u
|
|
timedatectl status
|
|
```
|
|
|
|
2. Check if NTP is active and synchronized:
|
|
```bash
|
|
timedatectl show-timesync --all
|
|
```
|
|
|
|
3. Compare AppView time with PDS time (if accessible):
|
|
```bash
|
|
# On AppView
|
|
date +%s
|
|
|
|
# On PDS (or via HTTP headers)
|
|
curl -I https://your-pds.example.com | grep -i date
|
|
```
|
|
|
|
4. Check AppView logs for clock information (logged at startup):
|
|
```bash
|
|
docker logs atcr-appview 2>&1 | grep "Configured confidential OAuth client"
|
|
```
|
|
|
|
Example log output (the startup message logs `key_id` and `key_path` only; system time is not included):
|
|
```
|
|
level=INFO msg="Configured confidential OAuth client" key_id=did:key:z... key_path=/var/lib/atcr/oauth/client.key
|
|
```
|
|
|
|
To verify clock skew separately, compare the output of `date +%s` on the AppView host with the `Date:` header returned by the PDS (see step 3 above).
|
|
|
|
**Solution:**
|
|
|
|
1. **Enable NTP synchronization** (recommended):
|
|
|
|
On most Linux systems using systemd:
|
|
```bash
|
|
# Enable and start systemd-timesyncd
|
|
sudo timedatectl set-ntp true
|
|
|
|
# Verify NTP is active
|
|
timedatectl status
|
|
```
|
|
|
|
Expected output:
|
|
```
|
|
System clock synchronized: yes
|
|
NTP service: active
|
|
```
|
|
|
|
2. **Alternative: Use chrony** (if systemd-timesyncd is not available):
|
|
```bash
|
|
# Install chrony
|
|
sudo apt-get install chrony # Debian/Ubuntu
|
|
sudo yum install chrony # RHEL/CentOS
|
|
|
|
# Enable and start chronyd
|
|
sudo systemctl enable chronyd
|
|
sudo systemctl start chronyd
|
|
|
|
# Check sync status
|
|
chronyc tracking
|
|
```
|
|
|
|
3. **Force immediate sync**:
|
|
```bash
|
|
# systemd-timesyncd
|
|
sudo systemctl restart systemd-timesyncd
|
|
|
|
# Or with chrony
|
|
sudo chronyc makestep
|
|
```
|
|
|
|
4. **In Docker/Kubernetes environments:**
|
|
|
|
The container inherits the host's system clock, so fix NTP on the **host** machine:
|
|
```bash
|
|
# On Docker host
|
|
sudo timedatectl set-ntp true
|
|
|
|
# Restart AppView container to pick up correct time
|
|
docker restart atcr-appview
|
|
```
|
|
|
|
5. **Verify clock skew is resolved**:
|
|
```bash
|
|
# Should show clock offset < 1 second
|
|
timedatectl timesync-status
|
|
```
|
|
|
|
**Acceptable Clock Skew:**
|
|
- Most OAuth implementations tolerate ±30-60 seconds of clock skew
|
|
- DPoP proof validation is typically stricter (±10 seconds)
|
|
- Aim for < 1 second skew for reliable operation
|
|
|
|
**Prevention:**
|
|
- Configure NTP synchronization in your infrastructure-as-code (Terraform, Ansible, etc.)
|
|
- Monitor clock skew in production (e.g., Prometheus node_exporter includes clock metrics)
|
|
- Use managed container platforms (ECS, GKE, AKS) that handle NTP automatically
|
|
|
|
---
|
|
|
|
### DPoP Nonce Mismatch Errors
|
|
|
|
**Symptom:**
|
|
```
|
|
error: use_dpop_nonce
|
|
error_description: DPoP "nonce" mismatch
|
|
```
|
|
|
|
Repeated multiple times, potentially followed by:
|
|
```
|
|
error: server_error
|
|
error_description: Server error
|
|
```
|
|
|
|
**Root Cause:**
|
|
DPoP (Demonstrating Proof-of-Possession) requires a server-provided nonce for replay protection. These errors typically occur when:
|
|
1. Multiple concurrent requests create a DPoP nonce race condition
|
|
2. Clock skew causes DPoP proof timestamps to fail validation
|
|
3. PDS session state becomes corrupted after repeated failures
|
|
|
|
**Diagnosis:**
|
|
|
|
1. Check if errors occur during concurrent operations:
|
|
```bash
|
|
# During docker push with multiple layers
|
|
docker logs atcr-appview 2>&1 | grep "use_dpop_nonce" | wc -l
|
|
```
|
|
|
|
2. Check for clock skew (see section above):
|
|
```bash
|
|
timedatectl status
|
|
```
|
|
|
|
3. Look for session lock acquisition in logs (requires debug-level logging):
|
|
```bash
|
|
ATCR_LOG_LEVEL=debug docker logs atcr-appview 2>&1 | grep "Acquired session lock"
|
|
```
|
|
|
|
**Solution:**
|
|
|
|
1. **If caused by clock skew**: Fix NTP synchronization (see section above)
|
|
|
|
2. **If caused by session corruption**:
|
|
```bash
|
|
# The AppView will automatically delete corrupted sessions
|
|
# User just needs to re-authenticate
|
|
docker login atcr.io
|
|
```
|
|
|
|
3. **If persistent despite clock sync**:
|
|
- Check PDS health and logs (may be a PDS-side issue)
|
|
- Verify network connectivity between AppView and PDS
|
|
- Check if PDS supports latest OAuth/DPoP specifications
|
|
|
|
**What ATCR does automatically:**
|
|
- Per-DID locking prevents concurrent DPoP nonce races
|
|
- Indigo library automatically retries with fresh nonces
|
|
- Sessions are auto-deleted after repeated failures
|
|
- Service token cache prevents excessive PDS requests
|
|
|
|
**Prevention:**
|
|
- Ensure reliable NTP synchronization
|
|
- Use a stable, well-maintained PDS implementation
|
|
- Monitor AppView error rates for DPoP-related issues
|
|
|
|
---
|
|
|
|
### OAuth Session Not Found
|
|
|
|
**Symptom:**
|
|
```
|
|
error: failed to get OAuth session: no session found for DID
|
|
```
|
|
|
|
**Root Cause:**
|
|
- User has never authenticated via OAuth
|
|
- OAuth session was deleted due to corruption or expiry
|
|
- Database migration cleared sessions
|
|
|
|
**Solution:**
|
|
|
|
1. User re-authenticates via OAuth flow:
|
|
```bash
|
|
docker login atcr.io
|
|
# Or for web UI: visit https://atcr.io/login
|
|
```
|
|
|
|
2. If using app passwords (legacy), verify token is cached:
|
|
```bash
|
|
# Check if app-password token exists
|
|
docker logout atcr.io
|
|
docker login atcr.io -u your.handle -p your-app-password
|
|
```
|
|
|
|
---
|
|
|
|
## AppView Deployment Issues
|
|
|
|
### Client Metadata URL Not Accessible
|
|
|
|
**Symptom:**
|
|
```
|
|
error: unauthorized_client
|
|
error_description: Client metadata endpoint returned 404
|
|
```
|
|
|
|
**Root Cause:**
|
|
PDS cannot fetch OAuth client metadata from `{ATCR_SERVER_BASE_URL}/client-metadata.json`
|
|
|
|
**Diagnosis:**
|
|
|
|
1. Verify client metadata endpoint is accessible:
|
|
```bash
|
|
curl https://your-atcr-instance.com/client-metadata.json
|
|
```
|
|
|
|
2. Check AppView logs for startup errors:
|
|
```bash
|
|
docker logs atcr-appview 2>&1 | grep "client-metadata"
|
|
```
|
|
|
|
3. Verify `ATCR_SERVER_BASE_URL` is set correctly:
|
|
```bash
|
|
echo $ATCR_SERVER_BASE_URL
|
|
```
|
|
|
|
**Solution:**
|
|
|
|
1. Ensure `ATCR_SERVER_BASE_URL` matches your public URL:
|
|
```bash
|
|
export ATCR_SERVER_BASE_URL=https://atcr.example.com
|
|
```
|
|
|
|
2. Verify reverse proxy (nginx, Caddy, etc.) routes `/.well-known/*` and `/client-metadata.json`:
|
|
```nginx
|
|
location / {
|
|
proxy_pass http://localhost:5000;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
```
|
|
|
|
3. Check firewall rules allow inbound HTTPS:
|
|
```bash
|
|
sudo ufw status
|
|
sudo iptables -L -n | grep 443
|
|
```
|
|
|
|
---
|
|
|
|
## Hold Service Issues
|
|
|
|
### Blob Storage Connectivity
|
|
|
|
**Symptom:**
|
|
```
|
|
error: failed to upload blob: connection refused
|
|
```
|
|
|
|
**Diagnosis:**
|
|
|
|
1. Check hold service logs:
|
|
```bash
|
|
docker logs atcr-hold 2>&1 | grep -i error
|
|
```
|
|
|
|
2. Verify S3 credentials are correct:
|
|
```bash
|
|
# Test S3 access
|
|
aws s3 ls s3://your-bucket --endpoint-url=$S3_ENDPOINT
|
|
```
|
|
|
|
3. Check hold configuration:
|
|
```bash
|
|
env | grep -E "(S3_|AWS_|STORAGE_)"
|
|
```
|
|
|
|
**Solution:**
|
|
|
|
1. Verify environment variables in hold service:
|
|
```bash
|
|
export AWS_ACCESS_KEY_ID=your-key
|
|
export AWS_SECRET_ACCESS_KEY=your-secret
|
|
export S3_BUCKET=your-bucket
|
|
export S3_ENDPOINT=https://s3.us-west-2.amazonaws.com
|
|
```
|
|
|
|
2. Test S3 connectivity from hold container:
|
|
```bash
|
|
docker exec atcr-hold curl -v $S3_ENDPOINT
|
|
```
|
|
|
|
3. Check S3 bucket permissions (requires PutObject, GetObject, DeleteObject)
|
|
|
|
---
|
|
|
|
### Hold Dropped by the Relay
|
|
|
|
**Symptom:**
|
|
|
|
Push and pull counts on the AppView go stale (up to 24 hours behind), or the hold's
|
|
records stop appearing anywhere on the network, while the hold itself serves pushes and
|
|
pulls normally and logs no errors at all.
|
|
|
|
**Diagnosis:**
|
|
|
|
```bash
|
|
curl -s "https://relay1.us-west.bsky.network/xrpc/com.atproto.sync.getHostStatus?hostname=hold.example.com"
|
|
```
|
|
|
|
`"status":"offline"` means the relay gave up dialing your hold and, because the hold only
|
|
sends `requestCrawl` at boot, it will never retry on its own.
|
|
|
|
**Solution:**
|
|
|
|
Request a crawl from the hold admin panel's Relays tab, or restart the hold.
|
|
|
|
**Before doing either**, confirm the deployed build contains commit `ca539b1`. Older
|
|
builds can crash during the firehose replay that a reconnecting relay triggers.
|
|
|
|
Full write-up, including the 2026-08-20 fleet outage, the diagnosis procedure, and a
|
|
deferred proposal for automatic recovery: [hold.md, "Relay Subscription"](hold.md#relay-subscription).
|
|
|
|
---
|
|
|
|
## Performance Issues
|
|
|
|
### High Database Lock Contention
|
|
|
|
**Symptom:**
|
|
Slow Docker push/pull operations, high CPU usage on AppView
|
|
|
|
**Diagnosis:**
|
|
|
|
1. Check SQLite database size:
|
|
```bash
|
|
ls -lh /var/lib/atcr/ui.db
|
|
```
|
|
|
|
2. Look for long-running queries:
|
|
```bash
|
|
docker logs atcr-appview 2>&1 | grep "database is locked"
|
|
```
|
|
|
|
**Solution:**
|
|
|
|
1. For production, enable libSQL embedded-replica mode to offload reads to a remote sync target (Turso cloud or self-hosted libsql-server):
|
|
```bash
|
|
export ATCR_UI_LIBSQL_SYNC_URL=libsql://your-db.turso.io
|
|
export ATCR_UI_LIBSQL_AUTH_TOKEN=your-auth-token
|
|
export ATCR_UI_LIBSQL_SYNC_INTERVAL=60s # optional, default 60s
|
|
```
|
|
|
|
Or in YAML (`config-appview.yaml`):
|
|
```yaml
|
|
ui:
|
|
libsql_sync_url: libsql://your-db.turso.io
|
|
libsql_auth_token: your-auth-token
|
|
libsql_sync_interval: 60s
|
|
```
|
|
|
|
PostgreSQL is not supported; the AppView uses SQLite/libSQL only.
|
|
|
|
2. Vacuum the database to reclaim space:
|
|
```bash
|
|
sqlite3 /var/lib/atcr/ui.db "VACUUM;"
|
|
```
|
|
|
|
---
|
|
|
|
## Logging and Debugging
|
|
|
|
### Enable Debug Logging
|
|
|
|
Set log level to debug for detailed troubleshooting:
|
|
|
|
```bash
|
|
export ATCR_LOG_LEVEL=debug
|
|
docker restart atcr-appview
|
|
```
|
|
|
|
### Useful Log Queries
|
|
|
|
**OAuth token exchange errors:**
|
|
```bash
|
|
docker logs atcr-appview 2>&1 | grep "OAuth callback failed"
|
|
```
|
|
|
|
**Service token request failures:**
|
|
```bash
|
|
docker logs atcr-appview 2>&1 | grep "OAuth authentication failed during service token request"
|
|
```
|
|
|
|
**Clock diagnostics (confidential OAuth client startup):**
|
|
```bash
|
|
docker logs atcr-appview 2>&1 | grep "Configured confidential OAuth client"
|
|
```
|
|
|
|
**DPoP nonce issues:**
|
|
```bash
|
|
docker logs atcr-appview 2>&1 | grep -E "(use_dpop_nonce|DPoP)"
|
|
```
|
|
|
|
### Health Checks
|
|
|
|
**AppView health:**
|
|
```bash
|
|
curl http://localhost:5000/v2/
|
|
# Should return: {"errors":[{"code":"UNAUTHORIZED",...}]}
|
|
```
|
|
|
|
**Hold service health:**
|
|
```bash
|
|
curl http://localhost:8080/.well-known/did.json
|
|
# Should return DID document
|
|
```
|
|
|
|
---
|
|
|
|
## Getting Help
|
|
|
|
If issues persist after following this guide:
|
|
|
|
1. **Check Issues**: https://tangled.org/evan.jarrett.net/at-container-registry/issues
|
|
2. **Collect logs**: Include output from `docker logs` for AppView and Hold services
|
|
3. **Include diagnostics**:
|
|
- `timedatectl status` output
|
|
- AppView version: `docker exec atcr-appview cat /VERSION` (if available)
|
|
- PDS version and implementation (Bluesky PDS, other)
|
|
4. **File an issue** with reproducible steps
|
|
|
|
---
|
|
|
|
## Common Error Reference
|
|
|
|
| Error Code | Component | Common Cause | Fix |
|
|
|------------|-----------|--------------|-----|
|
|
| `invalid_client` (iat timestamp) | OAuth | Clock skew | Enable NTP sync |
|
|
| `use_dpop_nonce` | OAuth/DPoP | Concurrent requests or clock skew | Fix NTP, wait for auto-retry |
|
|
| `server_error` (500) | PDS | PDS internal error | Check PDS logs |
|
|
| `invalid_grant` | OAuth | Expired auth code | Retry OAuth flow |
|
|
| `unauthorized_client` | OAuth | Client metadata unreachable | Check ATCR_SERVER_BASE_URL and firewall |
|
|
| `RecordNotFound` | ATProto | Manifest doesn't exist | Verify repository name |
|
|
| Connection refused | Hold/S3 | Network/credentials | Check S3 config and connectivity |
|