Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
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
8 changes: 0 additions & 8 deletions .github/workflows/job_test_api_local.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ jobs:
uses: ./.github/actions/setup-go
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Install Goose
run: go install github.com/pressly/goose/v3/cmd/goose@latest
- name: Build
run: pnpm turbo run build --filter=./apps/api...
env:
Expand All @@ -42,12 +40,6 @@ jobs:
env:
DRIZZLE_DATABASE_URL: "mysql://unkey:password@localhost:3306/unkey"
CI: 1
- name: Migrate ClickHouse
run: goose up
env:
GOOSE_DRIVER: clickhouse
GOOSE_DBSTRING: "tcp://default:[email protected]:9000"
GOOSE_MIGRATION_DIR: ./internal/clickhouse/schema
- name: Test
run: pnpm vitest run -c vitest.integration.ts --bail=1
working-directory: apps/api
Expand Down
6 changes: 0 additions & 6 deletions .github/workflows/job_test_unit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,6 @@ jobs:
run: |
sudo chmod 666 /var/run/docker.sock
docker version
- name: Install goose
run: |
wget -qO- https://github.com/pressly/goose/releases/download/v3.20.0/goose_linux_x86_64 > /tmp/goose
chmod +x /tmp/goose
sudo mv /tmp/goose /usr/local/bin/goose
goose --version
- name: Setup Node
uses: ./.github/actions/setup-node
with:
Expand Down
12 changes: 0 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,6 @@ down:
up: down build
docker compose -f ./deployment/docker-compose.yaml up -d

migrate-clickhouse:
@export GOOSE_DRIVER=clickhouse && \
export GOOSE_DBSTRING="tcp://default:[email protected]:9000" && \
export GOOSE_MIGRATION_DIR=./internal/clickhouse/schema && \
goose up

migrate-clickhouse-reset:
@export GOOSE_DRIVER=clickhouse && \
export GOOSE_DBSTRING="tcp://default:[email protected]:9000" && \
export GOOSE_MIGRATION_DIR=./internal/clickhouse/schema && \
goose down-to 0

integration: up
@cd apps/api && \
$(MAKE) seed && \
Expand Down
8 changes: 8 additions & 0 deletions deployment/Dockerfile.clickhouse
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM bitnami/clickhouse:25.6.4

# Copy ClickHouse schemas
COPY go/pkg/clickhouse/migrations /opt/clickhouse-schemas/

# Create initialization script that will execute our SQL files on first run
# (script is already made executable on host)
COPY deployment/init-clickhouse.sh /docker-entrypoint-initdb.d/init-clickhouse.sh
12 changes: 4 additions & 8 deletions deployment/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ services:
VAULT_MASTER_KEYS: "Ch9rZWtfMmdqMFBJdVhac1NSa0ZhNE5mOWlLSnBHenFPENTt7an5MRogENt9Si6wms4pQ2XIvqNSIgNpaBenJmXgcInhu6Nfv2U="
CLICKHOUSE_URL: "clickhouse://default:password@clickhouse:9000"
clickhouse:
image: bitnami/clickhouse:25.6.4
build:
context: ..
dockerfile: deployment/Dockerfile.clickhouse
container_name: clickhouse
environment:
CLICKHOUSE_ADMIN_USER: default
Expand Down Expand Up @@ -138,13 +140,7 @@ services:
retries: 10
start_period: 30s
interval: 5s
clickhouse_migrator:
container_name: clickhouse_migrator
build:
context: ../internal/clickhouse
dockerfile: ./Dockerfile
depends_on:
- clickhouse

s3:
container_name: s3
image: bitnami/minio:2025.4.3
Expand Down
18 changes: 18 additions & 0 deletions deployment/init-clickhouse.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/bash
set -e

echo "Initializing ClickHouse schemas..."

# Execute SQL files in order from our schemas directory

# Execute SQL files in numerical order
for sql_file in /opt/clickhouse-schemas/*.sql; do
echo "Executing: $sql_file"

if ! clickhouse-client --host localhost --user "$CLICKHOUSE_ADMIN_USER" --password "$CLICKHOUSE_ADMIN_PASSWORD" --queries-file "$sql_file"; then
echo "Error executing $sql_file - stopping initialization"
exit 1
fi
done

echo "ClickHouse schema initialization complete!"
41 changes: 18 additions & 23 deletions go/GO_DOCUMENTATION_GUIDELINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Create a `doc.go` file in each package with this structure:
//
// # Key Types
//
// The main entry point is [RateLimiter], which provides the [RateLimiter.Allow]
// The main entry point is [RateLimiter], which provides the [RateLimiter.Allow]
// method for checking rate limits. Configuration is handled through [Config].
//
// # Usage
Expand Down Expand Up @@ -99,7 +99,7 @@ Every exported function and method must be documented. Focus on:
Every public function must be thoroughly documented. Here's what comprehensive documentation looks like:

```go
// Allow determines whether the specified identifier can perform the requested number
// Allow determines whether the specified identifier can perform the requested number
// of operations within the configured rate limit window.
//
// This method implements distributed rate limiting with strong consistency guarantees
Expand All @@ -121,11 +121,6 @@ Every public function must be thoroughly documented. Here's what comprehensive d
// - Updates the rate limit counters atomically if the request is allowed
// - Implements fair queuing to prevent starvation under high load
//
// Performance:
// - Typical latency: <1ms for cached identifiers, <10ms for distributed coordination
// - Scales linearly with cluster size up to ~100 nodes
// - Memory usage: ~100 bytes per active identifier
//
// Returns:
// - (true, nil): Request is allowed and counters have been updated
// - (false, nil): Request is rate limited, no error condition
Expand Down Expand Up @@ -200,10 +195,10 @@ Document all exported types, focusing on:
type Config struct {
// Window is the time period over which operations are counted
Window time.Duration

// Limit is the maximum number of operations allowed within Window
Limit int64

// ClusterNodes lists all nodes participating in distributed rate limiting.
// Must include at least the local node.
ClusterNodes []string
Expand All @@ -225,7 +220,7 @@ type Cache[T any] interface {
// Get retrieves a value by key. Returns the value and whether it was found.
// A cache miss (found=false) is not an error.
Get(ctx context.Context, key string) (value T, found bool, err error)

// Set stores a value. The value will be replicated to other cache nodes
// asynchronously. Use SetSync if you need immediate consistency.
Set(ctx context.Context, key string, value T) error
Expand All @@ -241,7 +236,7 @@ var (
// ErrRateLimited is returned when an operation exceeds the configured rate limit.
// This is expected behavior, not a system error.
ErrRateLimited = errors.New("rate limit exceeded")

// ErrClusterUnavailable indicates that the required number of cluster nodes
// are not reachable. Operations may still succeed if configured to fail-open.
ErrClusterUnavailable = errors.New("insufficient cluster nodes available")
Expand All @@ -264,7 +259,7 @@ const (
// DefaultWindow is the standard rate limiting window for new limiters.
// Chosen as a balance between memory usage and granularity for most use cases.
DefaultWindow = time.Minute

// MaxBurstRatio determines how much bursting is allowed above the base rate.
// Set to 1.5 based on analysis of traffic patterns in production.
MaxBurstRatio = 1.5
Expand Down Expand Up @@ -301,7 +296,7 @@ func (r *RateLimiter) distributeTokens(ctx context.Context, required int64) (gra
if r.localTokens.Load() >= required {
// ... implementation
}

// Cluster coordination required
// We use Raft consensus here instead of eventual consistency because
// rate limiting must be strictly enforced for security and billing
Expand All @@ -321,25 +316,25 @@ func Example_basicUsage() {
Limit: 1000,
ClusterNodes: []string{"localhost:8080"},
}

limiter, err := New(cfg)
if err != nil {
log.Fatal(err)
}
defer limiter.Close()

// Check if user can make 5 API calls
allowed, err := limiter.Allow(context.Background(), "user:alice", 5)
if err != nil {
log.Printf("System error: %v", err)
return
}

if !allowed {
log.Println("Rate limit exceeded")
return
}

log.Println("Request allowed")
// Output: Request allowed
}
Expand Down Expand Up @@ -395,7 +390,7 @@ Follow these formatting and style conventions:
9. **Self-contained documentation** - provide all necessary information
10. **Cross-references** using Go's `[Reference]` format:
- `[OtherFunc]` for functions
- `[TypeName]` for structs/interfaces
- `[TypeName]` for structs/interfaces
- `[ConstantName]` for constants

## Best Practices and Anti-Patterns
Expand All @@ -409,11 +404,11 @@ Follow these formatting and style conventions:
//
// IMPORTANT: Do not call Allow() in a loop without backoff - this can
// overwhelm the system. Instead use:
//
//
// // Bad:
// for !limiter.Allow(ctx, id, 1) { /* busy wait */ }
//
// // Good:
//
// // Good:
// if allowed, err := limiter.Allow(ctx, id, 1); !allowed {
// return ErrRateLimited
// }
Expand Down Expand Up @@ -444,11 +439,11 @@ When deprecating APIs, provide clear migration paths:

```go
// Deprecated: Use NewRateLimiterV2 instead. This function will be removed in v2.0.
//
//
// Migration example:
// // Old:
// limiter := NewRateLimiter(100, time.Minute)
//
//
// // New:
// limiter := NewRateLimiterV2(Config{Limit: 100, Window: time.Minute})
func NewRateLimiter(limit int, window time.Duration) *RateLimiter
Expand Down
12 changes: 1 addition & 11 deletions go/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,7 @@ pull:

up: pull
@docker compose -f ../deployment/docker-compose.yaml up -d planetscale mysql redis clickhouse s3 otel
@echo "Starting ClickHouse migrations (will retry if ClickHouse isn't ready)..."
@for i in {1..10}; do \
echo "Migration attempt $$i..."; \
if docker compose -f ../deployment/docker-compose.yaml run --rm clickhouse_migrator; then \
echo "Migrations completed successfully!"; \
break; \
else \
echo "Migration failed, retrying in 5 seconds..."; \
sleep 5; \
fi; \
done


clean:
@docker compose -f ../deployment/docker-compose.yaml down --volumes
Expand Down
Binary file added go/clickhouse.test
Binary file not shown.
1 change: 1 addition & 0 deletions go/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.37.0
go.opentelemetry.io/otel/trace v1.37.0
golang.org/x/net v0.43.0
golang.org/x/perf v0.0.0-20250813145418-2f7363a06fe1
golang.org/x/text v0.28.0
google.golang.org/protobuf v1.36.8
)
Expand Down
2 changes: 2 additions & 0 deletions go/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/perf v0.0.0-20250813145418-2f7363a06fe1 h1:stGRioFgvBd3x8HoGVg9bb41lLTWLjBMFT/dMB7f4mQ=
golang.org/x/perf v0.0.0-20250813145418-2f7363a06fe1/go.mod h1:rjfRjhHXb3XNVh/9i5Jr2tXoTd0vOlZN5rzsM8cQE6k=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down
Loading