Compare commits
20 Commits
3a9dd24e33
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 65deb2e55c | |||
| ca8b137e55 | |||
| 6bebfa86fe | |||
| 900db58043 | |||
| e73ed82b7e | |||
| dbafaf57ce | |||
| 6396f4e6ab | |||
| 904c0c35ce | |||
| 70768d420e | |||
| b08e7b1b00 | |||
| ae48045055 | |||
| f48ec66166 | |||
| c3bc719878 | |||
| c2bdffb0ab | |||
| 4a7be531ed | |||
| b14682f424 | |||
| da67d0eae7 | |||
| adfffa0f4b | |||
| cca176270c | |||
| 69312022ea |
159
.gitea/workflows/deploy.yml
Normal file
159
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,159 @@
|
||||
name: CI
|
||||
|
||||
# Required repo secret: PACKAGES_TOKEN
|
||||
# Generate at: Gitea → User Settings → Applications → Generate New Token
|
||||
# Scopes: read:package, write:package
|
||||
# Save as: Repository → Settings → Actions → Secrets → PACKAGES_TOKEN
|
||||
# Note: GITHUB_TOKEN is read-only for packages in Gitea (issue #23642).
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["**"]
|
||||
pull_request:
|
||||
branches: ["master"]
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.26.4"
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
~/.cache/go-build
|
||||
key: go-lint-${{ runner.os }}-${{ hashFiles('go.sum') }}
|
||||
restore-keys: |
|
||||
go-lint-${{ runner.os }}-
|
||||
|
||||
- name: Download Go modules
|
||||
run: go mod download
|
||||
|
||||
- name: go vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: gofmt check
|
||||
run: |
|
||||
fmt=$(gofmt -l .)
|
||||
if [ -n "$fmt" ]; then
|
||||
echo "Files needing formatting:"
|
||||
echo "$fmt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- 6379:6379
|
||||
# Set health checks to wait until redis is ready
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
needs: lint
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Cache Go modules and build cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
~/.cache/go-build
|
||||
key: go-test-${{ runner.os }}-${{ hashFiles('go.sum') }}
|
||||
restore-keys: |
|
||||
go-test-${{ runner.os }}-
|
||||
|
||||
- name: Download Go modules
|
||||
run: go mod download
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
REDIS_ADDR: redis:6379
|
||||
run: go test ./... -v -race -cover
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- 6379:6379
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
needs: test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Cache Go modules and build cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
~/.cache/go-build
|
||||
key: go-build-${{ runner.os }}-${{ hashFiles('go.sum') }}
|
||||
restore-keys: |
|
||||
go-build-${{ runner.os }}-
|
||||
|
||||
- name: Download Go modules
|
||||
run: go mod download
|
||||
|
||||
- name: Build binary
|
||||
env:
|
||||
REDIS_ADDR: redis:6379
|
||||
run: go build -o api ./cmd/api
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
if: github.ref_name == 'master'
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Gitea Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: gitea.mrixs.me
|
||||
username: ${{ gitea.repository_owner }}
|
||||
password: ${{ secrets.PACKAGES_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
gitea.mrixs.me/mrixs/trip-planner:latest
|
||||
gitea.mrixs.me/mrixs/trip-planner:${{ github.sha }}
|
||||
cache-from: type=registry,ref=gitea.mrixs.me/mrixs/trip-planner:buildcache
|
||||
cache-to: type=registry,ref=gitea.mrixs.me/mrixs/trip-planner:buildcache,mode=max
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,6 @@
|
||||
.DS_Store
|
||||
dump.rdb
|
||||
coverage.out
|
||||
cover.out
|
||||
/api
|
||||
*.log
|
||||
|
||||
29
CLAUDE.md
29
CLAUDE.md
@@ -76,6 +76,35 @@ go fmt ./...
|
||||
go vet ./...
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
The project uses a multi-stage Dockerfile:
|
||||
- **Builder stage**: `golang:1.22-alpine` to build `api` and `cron` binaries
|
||||
- **Runtime stage**: `alpine:latest` with minimal footprint (~27MB)
|
||||
|
||||
### Docker Compose
|
||||
|
||||
The `docker-compose.yml` includes:
|
||||
- `api` service: Go HTTP server on port 8080
|
||||
- `cron` service: Go cron binary for reference data updates and station status detection
|
||||
- `postgres`: PostgreSQL 15-alpine with `postgres_data` volume
|
||||
- `redis`: Redis 7-alpine with `redis_data` volume
|
||||
- `watchtower`: nickfedor/watchtower for automatic container updates (interval: 60s)
|
||||
|
||||
### CI/CD Pipeline
|
||||
|
||||
Gitea Actions workflow (`.gitea/workflows/deploy.yml`) provides:
|
||||
- Checkout code
|
||||
- Setup Go 1.22+
|
||||
- Go modules cache
|
||||
- Lint with golangci-lint v1.54.2
|
||||
- Run tests with race detector: `go test -v -race ./...`
|
||||
- Docker Buildx setup
|
||||
- Docker login to registry
|
||||
- Build and push Docker image with tags: `latest` and commit SHA
|
||||
|
||||
### API Endpoints (from specification)
|
||||
|
||||
- `GET /v1/cities?query=` — City autocomplete
|
||||
|
||||
38
Dockerfile
Normal file
38
Dockerfile
Normal file
@@ -0,0 +1,38 @@
|
||||
# Builder stage
|
||||
FROM golang:1.22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy go.mod and go.sum
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# Download dependencies
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build api and cron binaries
|
||||
RUN go build -o api ./cmd/api
|
||||
RUN go build -o cron ./cmd/cron
|
||||
|
||||
# Runtime stage
|
||||
FROM alpine:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy binaries from builder
|
||||
COPY --from=builder /app/api /app/api
|
||||
COPY --from=builder /app/cron /app/cron
|
||||
|
||||
# Set environment variables
|
||||
ENV DB_DSN="postgres://postgres:postgres@postgres:5432/trip_planner?sslmode=disable"
|
||||
ENV REDIS_ADDR="redis:6379"
|
||||
ENV YANDEX_API_KEY=""
|
||||
ENV TRIP_PLANNER_ADMIN_API_KEY=""
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8080
|
||||
|
||||
# Set entrypoint for api service (default)
|
||||
ENTRYPOINT ["/app/api"]
|
||||
73
README.md
Normal file
73
README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Trip Planner
|
||||
|
||||
A multimodal trip planning service that uses Yandex.Schedules API to provide routing across different transport modes (planes, trains, buses). The service implements lazy graph expansion to work within API quota constraints and provides route visualization on maps.
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Backend**: Go
|
||||
- **Database**: PostgreSQL (with PostGIS optional for geometry)
|
||||
- **Cache/Queue**: Redis (native TTL)
|
||||
- **Frontend Map**: Leaflet + OpenStreetMap tiles
|
||||
- **Task Scheduler**: cron (internal `cmd/cron` or system cron)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Running Locally with Docker Compose
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
This starts:
|
||||
- `api` service: Go HTTP server on port 8080
|
||||
- `cron` service: Go cron binary for reference data updates and station status detection
|
||||
- `postgres`: PostgreSQL 15-alpine on port 5432
|
||||
- `redis`: Redis 7-alpine on port 6379
|
||||
- `watchtower`: nickfedor/watchtower for automatic container updates (interval: 60s)
|
||||
|
||||
### Running the API Server Directly
|
||||
|
||||
```bash
|
||||
go run ./cmd/api
|
||||
```
|
||||
|
||||
### Running Cron Jobs
|
||||
|
||||
```bash
|
||||
go run ./cmd/cron
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
The project uses a multi-stage Dockerfile:
|
||||
- **Builder stage**: `golang:1.26-alpine` to build `api` and `cron` binaries
|
||||
- **Runtime stage**: `alpine:latest` with minimal footprint (~27MB)
|
||||
|
||||
### CI/CD Pipeline
|
||||
|
||||
Gitea Actions workflow (`.gitea/workflows/deploy.yml`) provides:
|
||||
- Checkout code
|
||||
- Setup Go 1.22+
|
||||
- Go modules cache
|
||||
- Lint with golangci-lint v1.54.2
|
||||
- Run tests with race detector: `go test -v -race ./...`
|
||||
- Docker Buildx setup
|
||||
- Docker login to registry
|
||||
- Build and push Docker image with tags: `latest` and commit SHA
|
||||
|
||||
## API Endpoints
|
||||
|
||||
- `GET /v1/cities?query=` — City autocomplete
|
||||
- `GET /v1/cities/{id}/stations` — City stations (including neighbors if main closed)
|
||||
- `POST /v1/routes/search` — Search for routes (Pareto-optimal results)
|
||||
- `GET /v1/routes/{search_id}/{route_id}/geojson` — Get route geometry for map
|
||||
- `GET /v1/stations/{id}/status` — Station status
|
||||
- `POST /internal/admin/stations/{id}/status` — Manual station status override (requires auth)
|
||||
- `GET /v1/preferences/saved-cities?user_id=` — Get user's saved cities
|
||||
- `POST /v1/preferences/saved-cities?user_id=` — Add a city to user's saved cities
|
||||
- `DELETE /v1/preferences/saved-cities/{city_code}?user_id=` — Remove a city from user's saved cities
|
||||
- `GET /v1/preferences/search-history?user_id=` — Get user's search history
|
||||
- `POST /v1/preferences/search-history?user_id=` — Add a search to user's history
|
||||
- `GET /metrics` — Get observability metrics (cache hit rates, API quota, circuit breaker trips, search duration)
|
||||
@@ -25,7 +25,6 @@ type HandlerContext struct {
|
||||
Redis *redis.Client
|
||||
Router *routing.Graph
|
||||
Yandex *yandex.Client
|
||||
SearchCache *routing.SearchCacheService
|
||||
Preferences *cache.Preferences
|
||||
Metrics *metrics.Metrics
|
||||
SearchStart time.Time
|
||||
@@ -207,13 +206,14 @@ func RouteSearch(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
|
||||
RankingMode: rankingMode,
|
||||
}
|
||||
|
||||
// Determine if this is a far-term search (date is more than 7 days in the future)
|
||||
// Determine if this is a far-term search (date is 7 or more days in the future)
|
||||
if req.Date != "" {
|
||||
requestDate, err := time.Parse("2006-01-02", req.Date)
|
||||
if err == nil {
|
||||
now := time.Now()
|
||||
daysDiff := int(requestDate.Sub(now).Hours() / 24)
|
||||
if daysDiff >= 7 {
|
||||
// Check if date is 7 or more days in the future
|
||||
sevenDaysLater := now.AddDate(0, 0, 7)
|
||||
if !requestDate.Before(sevenDaysLater) {
|
||||
opts.FarTerm = true
|
||||
}
|
||||
}
|
||||
@@ -300,11 +300,17 @@ func RouteSearch(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
|
||||
// RouteGeoJSON handles GET /v1/routes/{search_id}/{route_id}/geojson.
|
||||
func RouteGeoJSON(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
|
||||
parts := strings.Split(r.URL.Path, "/")
|
||||
if len(parts) < 4 {
|
||||
if len(parts) < 6 {
|
||||
http.Error(w, "invalid route ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
searchID := parts[3]
|
||||
routeID := parts[4]
|
||||
|
||||
_ = searchID // searchID is used for route identification
|
||||
_ = routeID // routeID is used for route identification
|
||||
|
||||
// Generate GeoJSON from the graph's edges, distinguishing synthetic vs real
|
||||
// Synthetic edges (e.g., city↔airport transfers) are marked with dashed lines
|
||||
// Real edges (actual scheduled trips) are solid lines
|
||||
@@ -727,7 +733,6 @@ func NewHandlerContext(redisClient *redis.Client, router *routing.Graph, yandex
|
||||
Redis: redisClient,
|
||||
Router: router,
|
||||
Yandex: yandex,
|
||||
SearchCache: routing.NewSearchCacheService(cacheStore, yandex, m),
|
||||
Preferences: cache.NewPreferences(cacheStore),
|
||||
Metrics: m,
|
||||
SearchStart: time.Now(),
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -44,8 +45,15 @@ func flushRedisForTest(t *testing.T, client *redis.Client) {
|
||||
}
|
||||
|
||||
func newMockHandlerContext() *HandlerContext {
|
||||
// Read Redis address from environment, default to localhost:6379
|
||||
redisAddr := os.Getenv("REDIS_ADDR")
|
||||
if redisAddr == "" {
|
||||
redisAddr = "localhost:6379"
|
||||
}
|
||||
redisClient := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
Addr: redisAddr,
|
||||
Password: "",
|
||||
DB: 0,
|
||||
})
|
||||
|
||||
// Create an empty routing graph
|
||||
|
||||
@@ -86,8 +86,12 @@ func main() {
|
||||
|
||||
// initRedis initializes a Redis client connection.
|
||||
func initRedis() *redis.Client {
|
||||
redisAddr := os.Getenv("REDIS_ADDR")
|
||||
if redisAddr == "" {
|
||||
redisAddr = "localhost:6379"
|
||||
}
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
Addr: redisAddr,
|
||||
Password: "",
|
||||
DB: 0,
|
||||
})
|
||||
|
||||
68
cmd/cron/main.go
Normal file
68
cmd/cron/main.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
|
||||
"trip-planner/internal/cache"
|
||||
"trip-planner/internal/metrics"
|
||||
"trip-planner/internal/yandex"
|
||||
)
|
||||
|
||||
func main() {
|
||||
redisClient := initRedis()
|
||||
m := metrics.New()
|
||||
|
||||
apiKey := os.Getenv("YANDEX_API_KEY")
|
||||
if apiKey == "" {
|
||||
log.Fatal("YANDEX_API_KEY environment variable is not set")
|
||||
}
|
||||
yandexClient := yandex.NewClient(apiKey, yandex.WithMetrics(m))
|
||||
|
||||
// Define monitored stations
|
||||
monitoredStations := []string{
|
||||
// Add station IDs here
|
||||
}
|
||||
|
||||
monitors := make([]*StationMonitor, 0, len(monitoredStations))
|
||||
for _, stationID := range monitoredStations {
|
||||
monitors = append(monitors, &StationMonitor{
|
||||
ID: stationID,
|
||||
Yandex: yandexClient,
|
||||
Cache: cache.NewCacheStore(redisClient, m),
|
||||
})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Set up signal handling for graceful shutdown
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
log.Println("Cron service starting, checking station statuses...")
|
||||
|
||||
// Run the station status check
|
||||
if err := ProcessAllStations(ctx, monitors); err != nil {
|
||||
log.Printf("ERROR: failed to process stations: %v", err)
|
||||
}
|
||||
|
||||
// Wait for signal to exit
|
||||
<-sigChan
|
||||
log.Println("Cron service shutting down...")
|
||||
}
|
||||
|
||||
// initRedis initializes a Redis client connection.
|
||||
func initRedis() *redis.Client {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: os.Getenv("REDIS_ADDR"),
|
||||
Password: "",
|
||||
DB: 0,
|
||||
})
|
||||
return rdb
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cron
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package cron
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -14,7 +15,16 @@ import (
|
||||
)
|
||||
|
||||
func newMockMonitor(id string, tripCount int, scheduleFunc func(context.Context, string) (int, error)) *StationMonitor {
|
||||
rc := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
|
||||
// Read Redis address from environment, default to localhost:6379
|
||||
redisAddr := os.Getenv("REDIS_ADDR")
|
||||
if redisAddr == "" {
|
||||
redisAddr = "localhost:6379"
|
||||
}
|
||||
rc := redis.NewClient(&redis.Options{
|
||||
Addr: redisAddr,
|
||||
Password: "",
|
||||
DB: 0,
|
||||
})
|
||||
yc := yandex.NewClient("test-key")
|
||||
|
||||
monitor := &StationMonitor{
|
||||
@@ -40,7 +50,16 @@ const testMonitorID = "test-station"
|
||||
// gets status "active" and zero-trip day count resets to 0.
|
||||
func TestProcessStation_WithTrips(t *testing.T) {
|
||||
t.Helper()
|
||||
rc := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
|
||||
// Read Redis address from environment, default to localhost:6379
|
||||
redisAddr := os.Getenv("REDIS_ADDR")
|
||||
if redisAddr == "" {
|
||||
redisAddr = "localhost:6379"
|
||||
}
|
||||
rc := redis.NewClient(&redis.Options{
|
||||
Addr: redisAddr,
|
||||
Password: "",
|
||||
DB: 0,
|
||||
})
|
||||
defer rc.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -84,7 +103,16 @@ func TestProcessStation_WithTrips(t *testing.T) {
|
||||
// with 0 trips increments the zero-trip day count.
|
||||
func TestProcessStation_ZeroTrips_IncrementsCount(t *testing.T) {
|
||||
t.Helper()
|
||||
rc := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
|
||||
// Read Redis address from environment, default to localhost:6379
|
||||
redisAddr := os.Getenv("REDIS_ADDR")
|
||||
if redisAddr == "" {
|
||||
redisAddr = "localhost:6379"
|
||||
}
|
||||
rc := redis.NewClient(&redis.Options{
|
||||
Addr: redisAddr,
|
||||
Password: "",
|
||||
DB: 0,
|
||||
})
|
||||
defer rc.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -128,7 +156,16 @@ func TestProcessStation_ZeroTrips_IncrementsCount(t *testing.T) {
|
||||
// with 3 consecutive days of zero trips gets status "closed".
|
||||
func TestProcessStation_ZeroTrips_3Days_Closes(t *testing.T) {
|
||||
t.Helper()
|
||||
rc := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
|
||||
// Read Redis address from environment, default to localhost:6379
|
||||
redisAddr := os.Getenv("REDIS_ADDR")
|
||||
if redisAddr == "" {
|
||||
redisAddr = "localhost:6379"
|
||||
}
|
||||
rc := redis.NewClient(&redis.Options{
|
||||
Addr: redisAddr,
|
||||
Password: "",
|
||||
DB: 0,
|
||||
})
|
||||
defer rc.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -190,7 +227,16 @@ func TestProcessStation_ZeroTrips_3Days_Closes(t *testing.T) {
|
||||
// closed due to 3 zero-trip days gets reactivated when trips resume.
|
||||
func TestProcessStation_Reactivation_AfterClosure(t *testing.T) {
|
||||
t.Helper()
|
||||
rc := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
|
||||
// Read Redis address from environment, default to localhost:6379
|
||||
redisAddr := os.Getenv("REDIS_ADDR")
|
||||
if redisAddr == "" {
|
||||
redisAddr = "localhost:6379"
|
||||
}
|
||||
rc := redis.NewClient(&redis.Options{
|
||||
Addr: redisAddr,
|
||||
Password: "",
|
||||
DB: 0,
|
||||
})
|
||||
defer rc.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -240,7 +286,16 @@ func TestProcessStation_Reactivation_AfterClosure(t *testing.T) {
|
||||
// and that it reactivates when trips resume.
|
||||
func TestAutoClosureChronology(t *testing.T) {
|
||||
t.Helper()
|
||||
rc := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
|
||||
// Read Redis address from environment, default to localhost:6379
|
||||
redisAddr := os.Getenv("REDIS_ADDR")
|
||||
if redisAddr == "" {
|
||||
redisAddr = "localhost:6379"
|
||||
}
|
||||
rc := redis.NewClient(&redis.Options{
|
||||
Addr: redisAddr,
|
||||
Password: "",
|
||||
DB: 0,
|
||||
})
|
||||
defer rc.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
676
cover.out
676
cover.out
@@ -1,676 +0,0 @@
|
||||
mode: set
|
||||
trip-planner/internal/metrics/metrics.go:37.21,44.2 1 1
|
||||
trip-planner/internal/metrics/metrics.go:47.48,51.2 3 1
|
||||
trip-planner/internal/metrics/metrics.go:54.49,58.2 3 1
|
||||
trip-planner/internal/metrics/metrics.go:61.51,65.2 3 1
|
||||
trip-planner/internal/metrics/metrics.go:68.46,72.2 3 1
|
||||
trip-planner/internal/metrics/metrics.go:75.50,81.63 5 1
|
||||
trip-planner/internal/metrics/metrics.go:81.63,83.3 1 1
|
||||
trip-planner/internal/metrics/metrics.go:87.57,93.16 6 1
|
||||
trip-planner/internal/metrics/metrics.go:93.16,95.3 1 1
|
||||
trip-planner/internal/metrics/metrics.go:96.2,96.39 1 1
|
||||
trip-planner/internal/metrics/metrics.go:100.59,105.59 4 1
|
||||
trip-planner/internal/metrics/metrics.go:105.59,107.45 2 1
|
||||
trip-planner/internal/metrics/metrics.go:107.45,109.4 1 1
|
||||
trip-planner/internal/metrics/metrics.go:110.3,110.83 1 1
|
||||
trip-planner/internal/metrics/metrics.go:113.2,123.15 2 1
|
||||
trip-planner/internal/metrics/metrics.go:127.47,129.35 2 1
|
||||
trip-planner/internal/metrics/metrics.go:129.35,131.3 1 1
|
||||
trip-planner/internal/metrics/metrics.go:132.2,132.39 1 1
|
||||
trip-planner/internal/metrics/metrics.go:132.39,134.3 1 1
|
||||
trip-planner/internal/metrics/metrics.go:135.2,136.16 2 1
|
||||
trip-planner/internal/metrics/metrics.go:136.16,138.3 1 0
|
||||
trip-planner/internal/metrics/metrics.go:139.2,139.44 1 1
|
||||
trip-planner/internal/metrics/metrics.go:143.64,147.2 3 1
|
||||
trip-planner/internal/metrics/metrics.go:150.67,155.2 4 1
|
||||
trip-planner/internal/storage/neighbors.go:29.56,33.2 1 1
|
||||
trip-planner/internal/storage/neighbors.go:36.81,43.2 1 1
|
||||
trip-planner/internal/storage/neighbors.go:46.80,47.50 1 1
|
||||
trip-planner/internal/storage/neighbors.go:47.50,49.3 1 1
|
||||
trip-planner/internal/storage/neighbors.go:50.2,50.12 1 1
|
||||
trip-planner/internal/storage/neighbors.go:54.76,55.50 1 1
|
||||
trip-planner/internal/storage/neighbors.go:55.50,56.28 1 1
|
||||
trip-planner/internal/storage/neighbors.go:56.28,57.43 1 1
|
||||
trip-planner/internal/storage/neighbors.go:57.43,60.5 2 1
|
||||
trip-planner/internal/storage/neighbors.go:66.85,67.50 1 1
|
||||
trip-planner/internal/storage/neighbors.go:67.50,69.31 2 1
|
||||
trip-planner/internal/storage/neighbors.go:69.31,70.21 1 1
|
||||
trip-planner/internal/storage/neighbors.go:70.21,72.5 1 1
|
||||
trip-planner/internal/storage/neighbors.go:74.3,74.16 1 1
|
||||
trip-planner/internal/storage/neighbors.go:76.2,76.12 1 0
|
||||
trip-planner/internal/storage/transfer_rules.go:14.81,16.39 1 0
|
||||
trip-planner/internal/storage/transfer_rules.go:16.39,18.3 1 0
|
||||
trip-planner/internal/storage/transfer_rules.go:21.2,22.39 2 0
|
||||
trip-planner/internal/storage/transfer_rules.go:22.39,24.3 1 0
|
||||
trip-planner/internal/storage/transfer_rules.go:27.2,27.19 1 0
|
||||
trip-planner/internal/storage/transfer_rules.go:32.44,34.17 1 0
|
||||
trip-planner/internal/storage/transfer_rules.go:35.63,36.28 1 0
|
||||
trip-planner/internal/storage/transfer_rules.go:37.63,38.27 1 0
|
||||
trip-planner/internal/storage/transfer_rules.go:39.10,40.17 1 0
|
||||
trip-planner/internal/airports/airports.go:36.61,42.2 1 1
|
||||
trip-planner/internal/airports/airports.go:45.65,53.2 3 1
|
||||
trip-planner/internal/airports/airports.go:56.60,57.39 1 1
|
||||
trip-planner/internal/airports/airports.go:57.39,59.3 1 1
|
||||
trip-planner/internal/airports/airports.go:63.71,65.39 1 1
|
||||
trip-planner/internal/airports/airports.go:65.39,67.3 1 1
|
||||
trip-planner/internal/airports/airports.go:68.2,68.21 1 1
|
||||
trip-planner/internal/airports/airports.go:72.76,73.39 1 1
|
||||
trip-planner/internal/airports/airports.go:73.39,75.3 1 1
|
||||
trip-planner/internal/airports/airports.go:76.2,76.19 1 1
|
||||
trip-planner/internal/airports/airports.go:80.39,82.2 1 1
|
||||
trip-planner/internal/airports/airports.go:85.36,86.47 1 1
|
||||
trip-planner/internal/airports/airports.go:86.47,88.3 1 1
|
||||
trip-planner/internal/airports/airports.go:92.64,94.33 2 1
|
||||
trip-planner/internal/airports/airports.go:94.33,95.20 1 1
|
||||
trip-planner/internal/airports/airports.go:95.20,97.4 1 1
|
||||
trip-planner/internal/airports/airports.go:99.2,99.15 1 1
|
||||
trip-planner/cmd/cron/station_status.go:42.50,47.2 1 1
|
||||
trip-planner/cmd/cron/station_status.go:50.45,55.2 1 1
|
||||
trip-planner/cmd/cron/station_status.go:58.46,63.2 1 1
|
||||
trip-planner/cmd/cron/station_status.go:66.51,71.2 1 1
|
||||
trip-planner/cmd/cron/station_status.go:75.98,82.16 2 0
|
||||
trip-planner/cmd/cron/station_status.go:82.16,84.3 1 0
|
||||
trip-planner/cmd/cron/station_status.go:86.2,88.23 2 0
|
||||
trip-planner/cmd/cron/station_status.go:95.99,104.16 7 1
|
||||
trip-planner/cmd/cron/station_status.go:104.16,106.3 1 0
|
||||
trip-planner/cmd/cron/station_status.go:106.8,106.24 1 1
|
||||
trip-planner/cmd/cron/station_status.go:106.24,108.40 2 1
|
||||
trip-planner/cmd/cron/station_status.go:108.40,110.4 1 1
|
||||
trip-planner/cmd/cron/station_status.go:110.9,112.4 1 1
|
||||
trip-planner/cmd/cron/station_status.go:113.8,115.3 1 1
|
||||
trip-planner/cmd/cron/station_status.go:118.2,120.16 3 1
|
||||
trip-planner/cmd/cron/station_status.go:120.16,122.3 1 0
|
||||
trip-planner/cmd/cron/station_status.go:122.8,122.32 1 1
|
||||
trip-planner/cmd/cron/station_status.go:122.32,125.17 3 1
|
||||
trip-planner/cmd/cron/station_status.go:125.17,127.4 1 1
|
||||
trip-planner/cmd/cron/station_status.go:131.2,133.40 3 1
|
||||
trip-planner/cmd/cron/station_status.go:133.40,136.17 3 1
|
||||
trip-planner/cmd/cron/station_status.go:136.17,138.4 1 1
|
||||
trip-planner/cmd/cron/station_status.go:138.9,140.4 1 0
|
||||
trip-planner/cmd/cron/station_status.go:144.2,146.45 3 1
|
||||
trip-planner/cmd/cron/station_status.go:146.45,149.17 3 1
|
||||
trip-planner/cmd/cron/station_status.go:149.17,151.4 1 1
|
||||
trip-planner/cmd/cron/station_status.go:151.9,153.4 1 0
|
||||
trip-planner/cmd/cron/station_status.go:157.2,159.19 2 1
|
||||
trip-planner/cmd/cron/station_status.go:159.19,164.3 4 1
|
||||
trip-planner/cmd/cron/station_status.go:164.8,166.25 2 1
|
||||
trip-planner/cmd/cron/station_status.go:166.25,168.4 1 1
|
||||
trip-planner/cmd/cron/station_status.go:169.3,169.20 1 1
|
||||
trip-planner/cmd/cron/station_status.go:169.20,171.4 1 1
|
||||
trip-planner/cmd/cron/station_status.go:171.9,173.4 1 1
|
||||
trip-planner/cmd/cron/station_status.go:177.2,177.85 1 1
|
||||
trip-planner/cmd/cron/station_status.go:177.85,179.3 1 0
|
||||
trip-planner/cmd/cron/station_status.go:182.2,182.106 1 1
|
||||
trip-planner/cmd/cron/station_status.go:182.106,184.3 1 0
|
||||
trip-planner/cmd/cron/station_status.go:187.2,187.115 1 1
|
||||
trip-planner/cmd/cron/station_status.go:187.115,189.3 1 0
|
||||
trip-planner/cmd/cron/station_status.go:192.2,192.125 1 1
|
||||
trip-planner/cmd/cron/station_status.go:192.125,194.3 1 0
|
||||
trip-planner/cmd/cron/station_status.go:196.2,196.23 1 1
|
||||
trip-planner/cmd/cron/station_status.go:201.73,206.33 3 1
|
||||
trip-planner/cmd/cron/station_status.go:206.33,208.3 1 1
|
||||
trip-planner/cmd/cron/station_status.go:208.8,210.3 1 0
|
||||
trip-planner/cmd/cron/station_status.go:211.2,211.16 1 1
|
||||
trip-planner/cmd/cron/station_status.go:211.16,215.3 2 0
|
||||
trip-planner/cmd/cron/station_status.go:217.2,218.16 2 1
|
||||
trip-planner/cmd/cron/station_status.go:218.16,221.3 2 0
|
||||
trip-planner/cmd/cron/station_status.go:223.2,224.12 2 1
|
||||
trip-planner/cmd/cron/station_status.go:230.80,231.35 1 0
|
||||
trip-planner/cmd/cron/station_status.go:231.35,232.54 1 0
|
||||
trip-planner/cmd/cron/station_status.go:232.54,234.4 1 0
|
||||
trip-planner/cmd/cron/station_status.go:236.2,236.12 1 0
|
||||
trip-planner/internal/cache/preferences.go:38.47,40.2 1 1
|
||||
trip-planner/internal/cache/preferences.go:43.105,52.16 2 1
|
||||
trip-planner/internal/cache/preferences.go:52.16,54.3 1 0
|
||||
trip-planner/internal/cache/preferences.go:56.2,56.17 1 1
|
||||
trip-planner/internal/cache/preferences.go:56.17,58.3 1 1
|
||||
trip-planner/internal/cache/preferences.go:60.2,61.54 2 1
|
||||
trip-planner/internal/cache/preferences.go:61.54,63.3 1 0
|
||||
trip-planner/internal/cache/preferences.go:64.2,64.20 1 1
|
||||
trip-planner/internal/cache/preferences.go:68.98,72.16 2 1
|
||||
trip-planner/internal/cache/preferences.go:72.16,74.3 1 0
|
||||
trip-planner/internal/cache/preferences.go:77.2,78.27 2 1
|
||||
trip-planner/internal/cache/preferences.go:78.27,79.29 1 1
|
||||
trip-planner/internal/cache/preferences.go:79.29,82.9 3 1
|
||||
trip-planner/internal/cache/preferences.go:86.2,86.13 1 1
|
||||
trip-planner/internal/cache/preferences.go:86.13,92.3 1 1
|
||||
trip-planner/internal/cache/preferences.go:95.2,96.16 2 1
|
||||
trip-planner/internal/cache/preferences.go:96.16,98.3 1 0
|
||||
trip-planner/internal/cache/preferences.go:100.2,108.66 2 1
|
||||
trip-planner/internal/cache/preferences.go:108.66,110.3 1 0
|
||||
trip-planner/internal/cache/preferences.go:112.2,112.12 1 1
|
||||
trip-planner/internal/cache/preferences.go:116.91,128.16 3 1
|
||||
trip-planner/internal/cache/preferences.go:128.16,130.3 1 0
|
||||
trip-planner/internal/cache/preferences.go:133.2,134.27 2 1
|
||||
trip-planner/internal/cache/preferences.go:134.27,135.29 1 1
|
||||
trip-planner/internal/cache/preferences.go:135.29,137.4 1 1
|
||||
trip-planner/internal/cache/preferences.go:140.2,140.22 1 1
|
||||
trip-planner/internal/cache/preferences.go:140.22,143.3 1 1
|
||||
trip-planner/internal/cache/preferences.go:146.2,147.16 2 1
|
||||
trip-planner/internal/cache/preferences.go:147.16,149.3 1 0
|
||||
trip-planner/internal/cache/preferences.go:151.2,151.45 1 1
|
||||
trip-planner/internal/cache/preferences.go:155.111,166.16 3 1
|
||||
trip-planner/internal/cache/preferences.go:166.16,168.3 1 0
|
||||
trip-planner/internal/cache/preferences.go:170.2,170.17 1 1
|
||||
trip-planner/internal/cache/preferences.go:170.17,172.3 1 1
|
||||
trip-planner/internal/cache/preferences.go:174.2,175.55 2 1
|
||||
trip-planner/internal/cache/preferences.go:175.55,177.3 1 0
|
||||
trip-planner/internal/cache/preferences.go:178.2,178.21 1 1
|
||||
trip-planner/internal/cache/preferences.go:182.106,194.16 3 1
|
||||
trip-planner/internal/cache/preferences.go:194.16,196.3 1 0
|
||||
trip-planner/internal/cache/preferences.go:199.2,210.23 2 1
|
||||
trip-planner/internal/cache/preferences.go:210.23,212.3 1 0
|
||||
trip-planner/internal/cache/preferences.go:215.2,216.16 2 1
|
||||
trip-planner/internal/cache/preferences.go:216.16,218.3 1 0
|
||||
trip-planner/internal/cache/preferences.go:220.2,220.61 1 1
|
||||
trip-planner/internal/cache/preferences.go:220.61,222.3 1 0
|
||||
trip-planner/internal/cache/preferences.go:224.2,224.12 1 1
|
||||
trip-planner/internal/cache/preferences.go:228.109,239.16 3 1
|
||||
trip-planner/internal/cache/preferences.go:239.16,241.3 1 0
|
||||
trip-planner/internal/cache/preferences.go:244.2,246.32 3 1
|
||||
trip-planner/internal/cache/preferences.go:246.32,247.43 1 1
|
||||
trip-planner/internal/cache/preferences.go:247.43,249.4 1 1
|
||||
trip-planner/internal/cache/preferences.go:252.2,252.33 1 1
|
||||
trip-planner/internal/cache/preferences.go:252.33,255.3 1 0
|
||||
trip-planner/internal/cache/preferences.go:258.2,259.16 2 1
|
||||
trip-planner/internal/cache/preferences.go:259.16,261.3 1 0
|
||||
trip-planner/internal/cache/preferences.go:263.2,263.61 1 1
|
||||
trip-planner/internal/cache/preferences.go:263.61,265.3 1 0
|
||||
trip-planner/internal/cache/preferences.go:267.2,267.12 1 1
|
||||
trip-planner/internal/cache/store.go:48.76,50.2 1 1
|
||||
trip-planner/internal/cache/store.go:53.79,55.31 2 1
|
||||
trip-planner/internal/cache/store.go:55.31,58.3 2 1
|
||||
trip-planner/internal/cache/store.go:59.2,59.16 1 1
|
||||
trip-planner/internal/cache/store.go:59.16,61.3 1 0
|
||||
trip-planner/internal/cache/store.go:62.2,63.17 2 1
|
||||
trip-planner/internal/cache/store.go:67.102,69.2 1 1
|
||||
trip-planner/internal/cache/store.go:72.80,74.16 2 1
|
||||
trip-planner/internal/cache/store.go:74.16,76.3 1 0
|
||||
trip-planner/internal/cache/store.go:77.2,77.23 1 1
|
||||
trip-planner/internal/cache/store.go:81.72,83.2 1 1
|
||||
trip-planner/internal/cache/store.go:86.84,88.2 1 0
|
||||
trip-planner/internal/cache/store.go:91.84,93.2 1 0
|
||||
trip-planner/internal/cache/store.go:97.44,106.2 7 1
|
||||
trip-planner/internal/cache/store.go:108.36,109.9 1 1
|
||||
trip-planner/internal/cache/store.go:110.54,111.74 1 0
|
||||
trip-planner/internal/cache/store.go:112.58,113.78 1 0
|
||||
trip-planner/internal/cache/store.go:114.24,115.64 1 1
|
||||
trip-planner/internal/cache/store.go:116.27,117.66 1 1
|
||||
trip-planner/internal/cache/store.go:118.26,122.33 1 1
|
||||
trip-planner/internal/cache/store.go:123.10,124.65 1 0
|
||||
trip-planner/internal/cache/store.go:134.68,138.2 1 0
|
||||
trip-planner/internal/cache/store.go:153.40,155.2 1 0
|
||||
trip-planner/internal/cache/store.go:158.41,160.2 1 0
|
||||
trip-planner/internal/cache/store.go:163.52,165.2 1 0
|
||||
trip-planner/internal/cache/store.go:175.65,177.2 1 1
|
||||
trip-planner/internal/cache/store.go:182.143,184.67 1 1
|
||||
trip-planner/internal/cache/store.go:184.67,186.3 1 1
|
||||
trip-planner/internal/cache/store.go:189.2,190.16 2 1
|
||||
trip-planner/internal/cache/store.go:190.16,192.3 1 0
|
||||
trip-planner/internal/cache/store.go:195.2,195.57 1 1
|
||||
trip-planner/internal/cache/store.go:195.57,197.3 1 0
|
||||
trip-planner/internal/cache/store.go:199.2,199.18 1 1
|
||||
trip-planner/internal/cache/store.go:203.112,205.2 1 1
|
||||
trip-planner/internal/cache/store.go:208.115,210.2 1 0
|
||||
trip-planner/internal/cache/store.go:215.130,217.15 2 1
|
||||
trip-planner/internal/cache/store.go:217.15,219.3 1 1
|
||||
trip-planner/internal/cache/store.go:219.8,221.3 1 1
|
||||
trip-planner/internal/cache/store.go:224.2,225.16 2 1
|
||||
trip-planner/internal/cache/store.go:225.16,227.3 1 0
|
||||
trip-planner/internal/cache/store.go:228.2,228.17 1 1
|
||||
trip-planner/internal/cache/store.go:228.17,231.3 2 0
|
||||
trip-planner/internal/cache/store.go:234.2,238.16 3 1
|
||||
trip-planner/internal/cache/store.go:238.16,240.3 1 0
|
||||
trip-planner/internal/cache/store.go:243.2,243.57 1 1
|
||||
trip-planner/internal/cache/store.go:243.57,245.3 1 0
|
||||
trip-planner/internal/cache/store.go:247.2,247.18 1 1
|
||||
trip-planner/internal/cache/store.go:251.79,253.2 1 1
|
||||
trip-planner/internal/cache/store.go:256.82,258.2 1 1
|
||||
trip-planner/internal/cache/store.go:261.81,263.2 1 1
|
||||
trip-planner/internal/cache/store.go:266.71,268.2 1 0
|
||||
trip-planner/internal/cache/store.go:271.78,273.16 2 0
|
||||
trip-planner/internal/cache/store.go:273.16,275.3 1 0
|
||||
trip-planner/internal/cache/store.go:276.2,276.16 1 0
|
||||
trip-planner/internal/cache/store.go:276.16,279.3 2 0
|
||||
trip-planner/internal/cache/store.go:280.2,281.17 2 0
|
||||
trip-planner/internal/cache/store.go:285.101,287.2 1 0
|
||||
trip-planner/internal/cache/store.go:290.79,292.16 2 0
|
||||
trip-planner/internal/cache/store.go:292.16,294.3 1 0
|
||||
trip-planner/internal/cache/store.go:295.2,295.20 1 0
|
||||
trip-planner/internal/cache/store.go:299.83,301.2 1 0
|
||||
trip-planner/internal/cache/store.go:304.83,306.2 1 0
|
||||
trip-planner/internal/routing/graph.go:100.24,106.2 1 1
|
||||
trip-planner/internal/routing/graph.go:109.37,111.2 1 1
|
||||
trip-planner/internal/routing/graph.go:114.37,116.2 1 1
|
||||
trip-planner/internal/routing/graph.go:119.33,123.2 3 1
|
||||
trip-planner/internal/routing/graph.go:126.33,130.2 3 0
|
||||
trip-planner/internal/routing/graph.go:135.60,142.30 3 0
|
||||
trip-planner/internal/routing/graph.go:142.30,154.51 4 0
|
||||
trip-planner/internal/routing/graph.go:154.51,162.4 3 0
|
||||
trip-planner/internal/routing/graph.go:165.3,167.33 3 0
|
||||
trip-planner/internal/routing/graph.go:167.33,169.4 1 0
|
||||
trip-planner/internal/routing/graph.go:169.9,169.36 1 0
|
||||
trip-planner/internal/routing/graph.go:169.36,171.4 1 0
|
||||
trip-planner/internal/routing/graph.go:172.3,193.5 2 0
|
||||
trip-planner/internal/routing/graph.go:196.2,196.14 1 0
|
||||
trip-planner/internal/routing/graph.go:202.57,204.34 1 1
|
||||
trip-planner/internal/routing/graph.go:204.34,206.3 1 0
|
||||
trip-planner/internal/routing/graph.go:208.2,208.34 1 1
|
||||
trip-planner/internal/routing/graph.go:208.34,209.60 1 1
|
||||
trip-planner/internal/routing/graph.go:209.60,212.36 2 0
|
||||
trip-planner/internal/routing/graph.go:212.36,214.5 1 0
|
||||
trip-planner/internal/routing/graph.go:214.10,214.39 1 0
|
||||
trip-planner/internal/routing/graph.go:214.39,216.5 1 0
|
||||
trip-planner/internal/routing/graph.go:219.4,220.36 2 0
|
||||
trip-planner/internal/routing/graph.go:220.36,222.5 1 0
|
||||
trip-planner/internal/routing/graph.go:222.10,224.5 1 0
|
||||
trip-planner/internal/routing/graph.go:227.4,248.6 2 0
|
||||
trip-planner/internal/routing/graph.go:254.31,255.40 1 0
|
||||
trip-planner/internal/routing/graph.go:255.40,257.3 1 0
|
||||
trip-planner/internal/routing/graph.go:261.57,263.31 2 1
|
||||
trip-planner/internal/routing/graph.go:263.31,265.3 1 1
|
||||
trip-planner/internal/routing/graph.go:266.2,266.12 1 1
|
||||
trip-planner/internal/routing/graph.go:270.44,271.28 1 1
|
||||
trip-planner/internal/routing/graph.go:271.28,272.17 1 1
|
||||
trip-planner/internal/routing/graph.go:272.17,274.4 1 1
|
||||
trip-planner/internal/routing/graph.go:276.2,276.12 1 0
|
||||
trip-planner/internal/routing/graph.go:283.190,288.56 2 1
|
||||
trip-planner/internal/routing/graph.go:288.56,291.31 2 0
|
||||
trip-planner/internal/routing/graph.go:291.31,293.4 1 0
|
||||
trip-planner/internal/routing/graph.go:294.3,294.29 1 0
|
||||
trip-planner/internal/routing/graph.go:294.29,296.4 1 0
|
||||
trip-planner/internal/routing/graph.go:299.3,299.54 1 0
|
||||
trip-planner/internal/routing/graph.go:299.54,302.21 2 0
|
||||
trip-planner/internal/routing/graph.go:302.21,304.25 1 0
|
||||
trip-planner/internal/routing/graph.go:304.25,306.6 1 0
|
||||
trip-planner/internal/routing/graph.go:308.5,308.32 1 0
|
||||
trip-planner/internal/routing/graph.go:308.32,310.6 1 0
|
||||
trip-planner/internal/routing/graph.go:313.4,313.46 1 0
|
||||
trip-planner/internal/routing/graph.go:313.46,316.34 2 0
|
||||
trip-planner/internal/routing/graph.go:316.34,317.152 1 0
|
||||
trip-planner/internal/routing/graph.go:317.152,319.12 2 0
|
||||
trip-planner/internal/routing/graph.go:322.5,322.23 1 0
|
||||
trip-planner/internal/routing/graph.go:322.23,343.6 2 0
|
||||
trip-planner/internal/routing/graph.go:349.2,356.41 4 1
|
||||
trip-planner/internal/routing/graph.go:356.41,358.3 1 0
|
||||
trip-planner/internal/routing/graph.go:361.2,387.21 7 1
|
||||
trip-planner/internal/routing/graph.go:387.21,393.31 3 1
|
||||
trip-planner/internal/routing/graph.go:393.31,395.89 1 1
|
||||
trip-planner/internal/routing/graph.go:395.89,400.5 3 1
|
||||
trip-planner/internal/routing/graph.go:402.4,402.12 1 1
|
||||
trip-planner/internal/routing/graph.go:406.3,406.44 1 1
|
||||
trip-planner/internal/routing/graph.go:406.44,414.33 4 1
|
||||
trip-planner/internal/routing/graph.go:414.33,417.5 1 0
|
||||
trip-planner/internal/routing/graph.go:419.4,423.52 3 1
|
||||
trip-planner/internal/routing/graph.go:423.52,424.48 1 1
|
||||
trip-planner/internal/routing/graph.go:424.48,426.14 1 1
|
||||
trip-planner/internal/routing/graph.go:429.4,432.23 3 1
|
||||
trip-planner/internal/routing/graph.go:432.23,434.5 1 1
|
||||
trip-planner/internal/routing/graph.go:437.4,441.40 3 1
|
||||
trip-planner/internal/routing/graph.go:441.40,449.5 1 1
|
||||
trip-planner/internal/routing/graph.go:449.10,457.5 1 1
|
||||
trip-planner/internal/routing/graph.go:459.4,467.66 2 1
|
||||
trip-planner/internal/routing/graph.go:467.66,468.13 1 1
|
||||
trip-planner/internal/routing/graph.go:471.4,477.6 1 1
|
||||
trip-planner/internal/routing/graph.go:481.3,481.41 1 1
|
||||
trip-planner/internal/routing/graph.go:481.41,482.46 1 1
|
||||
trip-planner/internal/routing/graph.go:482.46,484.5 1 1
|
||||
trip-planner/internal/routing/graph.go:485.4,485.50 1 1
|
||||
trip-planner/internal/routing/graph.go:491.2,491.17 1 1
|
||||
trip-planner/internal/routing/graph.go:491.17,495.24 2 1
|
||||
trip-planner/internal/routing/graph.go:495.24,517.24 8 1
|
||||
trip-planner/internal/routing/graph.go:517.24,521.33 3 1
|
||||
trip-planner/internal/routing/graph.go:521.33,523.93 1 0
|
||||
trip-planner/internal/routing/graph.go:523.93,527.7 3 0
|
||||
trip-planner/internal/routing/graph.go:528.6,528.14 1 0
|
||||
trip-planner/internal/routing/graph.go:531.5,531.72 1 1
|
||||
trip-planner/internal/routing/graph.go:531.72,532.14 1 0
|
||||
trip-planner/internal/routing/graph.go:535.5,535.46 1 1
|
||||
trip-planner/internal/routing/graph.go:535.46,541.35 4 1
|
||||
trip-planner/internal/routing/graph.go:541.35,543.7 1 0
|
||||
trip-planner/internal/routing/graph.go:545.6,549.54 3 1
|
||||
trip-planner/internal/routing/graph.go:549.54,550.50 1 0
|
||||
trip-planner/internal/routing/graph.go:550.50,552.16 1 0
|
||||
trip-planner/internal/routing/graph.go:555.6,558.25 3 1
|
||||
trip-planner/internal/routing/graph.go:558.25,560.7 1 1
|
||||
trip-planner/internal/routing/graph.go:562.6,565.42 3 1
|
||||
trip-planner/internal/routing/graph.go:565.42,573.7 1 1
|
||||
trip-planner/internal/routing/graph.go:573.12,581.7 1 1
|
||||
trip-planner/internal/routing/graph.go:583.6,590.68 2 1
|
||||
trip-planner/internal/routing/graph.go:590.68,591.15 1 1
|
||||
trip-planner/internal/routing/graph.go:594.6,600.8 1 1
|
||||
trip-planner/internal/routing/graph.go:604.5,604.44 1 1
|
||||
trip-planner/internal/routing/graph.go:604.44,605.50 1 0
|
||||
trip-planner/internal/routing/graph.go:605.50,607.7 1 0
|
||||
trip-planner/internal/routing/graph.go:608.6,608.54 1 0
|
||||
trip-planner/internal/routing/graph.go:612.4,612.20 1 1
|
||||
trip-planner/internal/routing/graph.go:612.20,614.5 1 0
|
||||
trip-planner/internal/routing/graph.go:619.3,619.44 1 1
|
||||
trip-planner/internal/routing/graph.go:619.44,630.18 4 0
|
||||
trip-planner/internal/routing/graph.go:630.18,633.5 1 0
|
||||
trip-planner/internal/routing/graph.go:636.4,636.38 1 0
|
||||
trip-planner/internal/routing/graph.go:636.38,641.24 3 0
|
||||
trip-planner/internal/routing/graph.go:641.24,648.6 2 0
|
||||
trip-planner/internal/routing/graph.go:649.5,649.22 1 0
|
||||
trip-planner/internal/routing/graph.go:649.22,656.6 2 0
|
||||
trip-planner/internal/routing/graph.go:658.5,666.7 1 0
|
||||
trip-planner/internal/routing/graph.go:670.4,688.24 7 0
|
||||
trip-planner/internal/routing/graph.go:688.24,692.33 3 0
|
||||
trip-planner/internal/routing/graph.go:692.33,694.93 1 0
|
||||
trip-planner/internal/routing/graph.go:694.93,698.7 3 0
|
||||
trip-planner/internal/routing/graph.go:699.6,699.14 1 0
|
||||
trip-planner/internal/routing/graph.go:702.5,702.72 1 0
|
||||
trip-planner/internal/routing/graph.go:702.72,703.14 1 0
|
||||
trip-planner/internal/routing/graph.go:706.5,706.46 1 0
|
||||
trip-planner/internal/routing/graph.go:706.46,712.35 4 0
|
||||
trip-planner/internal/routing/graph.go:712.35,714.7 1 0
|
||||
trip-planner/internal/routing/graph.go:716.6,720.54 3 0
|
||||
trip-planner/internal/routing/graph.go:720.54,721.50 1 0
|
||||
trip-planner/internal/routing/graph.go:721.50,723.16 1 0
|
||||
trip-planner/internal/routing/graph.go:726.6,729.25 3 0
|
||||
trip-planner/internal/routing/graph.go:729.25,731.7 1 0
|
||||
trip-planner/internal/routing/graph.go:733.6,736.42 3 0
|
||||
trip-planner/internal/routing/graph.go:736.42,744.7 1 0
|
||||
trip-planner/internal/routing/graph.go:744.12,752.7 1 0
|
||||
trip-planner/internal/routing/graph.go:754.6,761.68 2 0
|
||||
trip-planner/internal/routing/graph.go:761.68,762.15 1 0
|
||||
trip-planner/internal/routing/graph.go:765.6,771.8 1 0
|
||||
trip-planner/internal/routing/graph.go:775.5,775.44 1 0
|
||||
trip-planner/internal/routing/graph.go:775.44,776.50 1 0
|
||||
trip-planner/internal/routing/graph.go:776.50,778.7 1 0
|
||||
trip-planner/internal/routing/graph.go:779.6,779.54 1 0
|
||||
trip-planner/internal/routing/graph.go:783.4,783.20 1 0
|
||||
trip-planner/internal/routing/graph.go:783.20,785.5 1 0
|
||||
trip-planner/internal/routing/graph.go:788.3,788.13 1 1
|
||||
trip-planner/internal/routing/graph.go:791.2,791.13 1 1
|
||||
trip-planner/internal/routing/graph.go:796.72,797.50 1 0
|
||||
trip-planner/internal/routing/graph.go:797.50,800.3 1 0
|
||||
trip-planner/internal/routing/graph.go:803.2,803.18 1 0
|
||||
trip-planner/internal/routing/graph.go:803.18,805.3 1 0
|
||||
trip-planner/internal/routing/graph.go:808.2,811.41 3 0
|
||||
trip-planner/internal/routing/graph.go:811.41,821.41 5 0
|
||||
trip-planner/internal/routing/graph.go:821.41,823.4 1 0
|
||||
trip-planner/internal/routing/graph.go:826.3,826.45 1 0
|
||||
trip-planner/internal/routing/graph.go:826.45,828.4 1 0
|
||||
trip-planner/internal/routing/graph.go:831.3,831.33 1 0
|
||||
trip-planner/internal/routing/graph.go:835.2,836.18 2 0
|
||||
trip-planner/internal/routing/graph.go:892.172,897.75 2 1
|
||||
trip-planner/internal/routing/graph.go:897.75,902.48 4 1
|
||||
trip-planner/internal/routing/graph.go:902.48,904.4 1 1
|
||||
trip-planner/internal/routing/graph.go:908.2,908.26 1 1
|
||||
trip-planner/internal/routing/graph.go:909.26,910.50 1 1
|
||||
trip-planner/internal/routing/graph.go:910.50,911.76 1 1
|
||||
trip-planner/internal/routing/graph.go:911.76,913.5 1 0
|
||||
trip-planner/internal/routing/graph.go:914.4,914.74 1 1
|
||||
trip-planner/internal/routing/graph.go:914.74,916.5 1 0
|
||||
trip-planner/internal/routing/graph.go:917.4,917.58 1 1
|
||||
trip-planner/internal/routing/graph.go:919.18,920.50 1 1
|
||||
trip-planner/internal/routing/graph.go:920.50,921.56 1 1
|
||||
trip-planner/internal/routing/graph.go:921.56,923.5 1 0
|
||||
trip-planner/internal/routing/graph.go:924.4,924.74 1 1
|
||||
trip-planner/internal/routing/graph.go:924.74,926.5 1 0
|
||||
trip-planner/internal/routing/graph.go:927.4,927.78 1 1
|
||||
trip-planner/internal/routing/graph.go:929.10,930.50 1 1
|
||||
trip-planner/internal/routing/graph.go:930.50,931.74 1 1
|
||||
trip-planner/internal/routing/graph.go:931.74,933.5 1 0
|
||||
trip-planner/internal/routing/graph.go:934.4,934.76 1 1
|
||||
trip-planner/internal/routing/graph.go:934.76,936.5 1 0
|
||||
trip-planner/internal/routing/graph.go:937.4,937.58 1 1
|
||||
trip-planner/internal/routing/graph.go:943.2,944.43 2 1
|
||||
trip-planner/internal/routing/graph.go:944.43,946.35 2 1
|
||||
trip-planner/internal/routing/graph.go:946.35,953.38 1 1
|
||||
trip-planner/internal/routing/graph.go:953.38,955.10 2 0
|
||||
trip-planner/internal/routing/graph.go:958.3,958.17 1 1
|
||||
trip-planner/internal/routing/graph.go:958.17,960.4 1 1
|
||||
trip-planner/internal/routing/graph.go:963.2,963.15 1 1
|
||||
trip-planner/internal/routing/graph.go:979.51,984.17 2 1
|
||||
trip-planner/internal/routing/graph.go:984.17,986.3 1 0
|
||||
trip-planner/internal/routing/graph.go:993.2,993.19 1 1
|
||||
trip-planner/internal/routing/graph.go:999.80,1003.30 2 0
|
||||
trip-planner/internal/routing/graph.go:1003.30,1014.24 2 0
|
||||
trip-planner/internal/routing/graph.go:1014.24,1016.4 1 0
|
||||
trip-planner/internal/routing/graph.go:1019.2,1019.13 1 0
|
||||
trip-planner/internal/routing/graph.go:1047.65,1051.67 2 1
|
||||
trip-planner/internal/routing/graph.go:1051.67,1053.3 1 1
|
||||
trip-planner/internal/routing/graph.go:1055.2,1059.37 3 1
|
||||
trip-planner/internal/routing/graph.go:1059.37,1062.32 1 1
|
||||
trip-planner/internal/routing/graph.go:1062.32,1063.62 1 1
|
||||
trip-planner/internal/routing/graph.go:1063.62,1066.30 1 1
|
||||
trip-planner/internal/routing/graph.go:1066.30,1070.11 4 1
|
||||
trip-planner/internal/routing/graph.go:1073.5,1073.59 1 1
|
||||
trip-planner/internal/routing/graph.go:1073.59,1074.74 1 0
|
||||
trip-planner/internal/routing/graph.go:1074.74,1078.7 3 0
|
||||
trip-planner/internal/routing/graph.go:1082.3,1082.20 1 1
|
||||
trip-planner/internal/routing/graph.go:1082.20,1083.9 1 1
|
||||
trip-planner/internal/routing/graph.go:1087.2,1087.22 1 1
|
||||
trip-planner/internal/routing/graph.go:1092.89,1095.19 2 1
|
||||
trip-planner/internal/routing/graph.go:1095.19,1099.3 3 1
|
||||
trip-planner/internal/routing/graph.go:1100.2,1100.15 1 1
|
||||
trip-planner/internal/routing/graph.go:1105.119,1106.39 1 1
|
||||
trip-planner/internal/routing/graph.go:1106.39,1108.3 1 1
|
||||
trip-planner/internal/routing/graph.go:1109.2,1109.18 1 1
|
||||
trip-planner/internal/routing/graph.go:1113.80,1116.17 2 0
|
||||
trip-planner/internal/routing/graph.go:1116.17,1118.3 1 0
|
||||
trip-planner/internal/routing/graph.go:1119.2,1124.30 3 0
|
||||
trip-planner/internal/routing/graph.go:1124.30,1125.79 1 0
|
||||
trip-planner/internal/routing/graph.go:1125.79,1133.4 1 0
|
||||
trip-planner/internal/routing/graph.go:1136.2,1136.25 1 0
|
||||
trip-planner/internal/routing/graph.go:1136.25,1138.3 1 0
|
||||
trip-planner/internal/routing/graph.go:1140.2,1140.18 1 0
|
||||
trip-planner/internal/routing/search_cache.go:21.116,27.2 1 1
|
||||
trip-planner/internal/routing/search_cache.go:31.136,36.38 2 0
|
||||
trip-planner/internal/routing/search_cache.go:36.38,39.3 1 0
|
||||
trip-planner/internal/routing/search_cache.go:42.2,44.16 3 0
|
||||
trip-planner/internal/routing/search_cache.go:44.16,46.3 1 0
|
||||
trip-planner/internal/routing/search_cache.go:49.2,50.54 2 0
|
||||
trip-planner/internal/routing/search_cache.go:50.54,52.3 1 0
|
||||
trip-planner/internal/routing/search_cache.go:54.2,54.21 1 0
|
||||
trip-planner/internal/routing/search_cache.go:58.130,68.16 3 0
|
||||
trip-planner/internal/routing/search_cache.go:68.16,70.3 1 0
|
||||
trip-planner/internal/routing/search_cache.go:73.2,73.37 1 0
|
||||
trip-planner/internal/routing/search_cache.go:77.68,78.17 1 1
|
||||
trip-planner/internal/routing/search_cache.go:78.17,80.3 1 1
|
||||
trip-planner/internal/routing/search_cache.go:81.2,82.16 2 1
|
||||
trip-planner/internal/routing/search_cache.go:82.16,84.3 1 0
|
||||
trip-planner/internal/routing/search_cache.go:85.2,85.18 1 1
|
||||
trip-planner/cmd/api/handlers.go:66.83,68.17 2 1
|
||||
trip-planner/cmd/api/handlers.go:68.17,71.3 2 0
|
||||
trip-planner/cmd/api/handlers.go:74.2,76.33 3 1
|
||||
trip-planner/cmd/api/handlers.go:99.79,101.20 2 1
|
||||
trip-planner/cmd/api/handlers.go:101.20,104.3 2 0
|
||||
trip-planner/cmd/api/handlers.go:105.2,114.35 4 1
|
||||
trip-planner/cmd/api/handlers.go:114.35,117.42 2 1
|
||||
trip-planner/cmd/api/handlers.go:117.42,118.62 1 0
|
||||
trip-planner/cmd/api/handlers.go:118.62,119.42 1 0
|
||||
trip-planner/cmd/api/handlers.go:119.42,121.11 2 0
|
||||
trip-planner/cmd/api/handlers.go:125.3,125.20 1 1
|
||||
trip-planner/cmd/api/handlers.go:125.20,127.4 1 1
|
||||
trip-planner/cmd/api/handlers.go:131.2,132.35 2 1
|
||||
trip-planner/cmd/api/handlers.go:132.35,136.20 2 1
|
||||
trip-planner/cmd/api/handlers.go:136.20,139.4 2 1
|
||||
trip-planner/cmd/api/handlers.go:140.3,140.20 1 1
|
||||
trip-planner/cmd/api/handlers.go:140.20,142.4 1 0
|
||||
trip-planner/cmd/api/handlers.go:145.3,146.35 2 1
|
||||
trip-planner/cmd/api/handlers.go:146.35,154.4 1 1
|
||||
trip-planner/cmd/api/handlers.go:157.2,162.33 3 1
|
||||
trip-planner/cmd/api/handlers.go:167.55,168.16 1 1
|
||||
trip-planner/cmd/api/handlers.go:169.11,170.52 1 1
|
||||
trip-planner/cmd/api/handlers.go:171.11,172.52 1 0
|
||||
trip-planner/cmd/api/handlers.go:173.10,174.38 1 0
|
||||
trip-planner/cmd/api/handlers.go:179.78,185.61 2 1
|
||||
trip-planner/cmd/api/handlers.go:185.61,188.3 2 0
|
||||
trip-planner/cmd/api/handlers.go:191.2,214.50 7 1
|
||||
trip-planner/cmd/api/handlers.go:214.50,217.3 2 0
|
||||
trip-planner/cmd/api/handlers.go:218.2,218.50 1 1
|
||||
trip-planner/cmd/api/handlers.go:218.50,220.3 1 0
|
||||
trip-planner/cmd/api/handlers.go:223.2,223.64 1 1
|
||||
trip-planner/cmd/api/handlers.go:223.64,225.35 2 1
|
||||
trip-planner/cmd/api/handlers.go:225.35,227.4 1 0
|
||||
trip-planner/cmd/api/handlers.go:231.2,239.32 5 1
|
||||
trip-planner/cmd/api/handlers.go:239.32,248.3 2 1
|
||||
trip-planner/cmd/api/handlers.go:250.2,255.33 3 1
|
||||
trip-planner/cmd/api/handlers.go:259.79,261.20 2 1
|
||||
trip-planner/cmd/api/handlers.go:261.20,264.3 2 0
|
||||
trip-planner/cmd/api/handlers.go:269.2,274.41 3 1
|
||||
trip-planner/cmd/api/handlers.go:274.41,275.22 1 1
|
||||
trip-planner/cmd/api/handlers.go:275.22,278.4 2 1
|
||||
trip-planner/cmd/api/handlers.go:283.2,285.41 2 1
|
||||
trip-planner/cmd/api/handlers.go:285.41,290.21 3 1
|
||||
trip-planner/cmd/api/handlers.go:290.21,292.4 1 1
|
||||
trip-planner/cmd/api/handlers.go:295.3,295.29 1 1
|
||||
trip-planner/cmd/api/handlers.go:296.35,297.27 1 0
|
||||
trip-planner/cmd/api/handlers.go:298.33,299.27 1 0
|
||||
trip-planner/cmd/api/handlers.go:300.35,301.27 1 1
|
||||
trip-planner/cmd/api/handlers.go:306.3,310.22 4 1
|
||||
trip-planner/cmd/api/handlers.go:310.22,311.12 1 0
|
||||
trip-planner/cmd/api/handlers.go:313.3,341.53 4 1
|
||||
trip-planner/cmd/api/handlers.go:341.53,366.4 3 1
|
||||
trip-planner/cmd/api/handlers.go:369.2,375.56 3 1
|
||||
trip-planner/cmd/api/handlers.go:375.56,378.3 2 0
|
||||
trip-planner/cmd/api/handlers.go:382.80,386.20 2 1
|
||||
trip-planner/cmd/api/handlers.go:386.20,389.3 2 0
|
||||
trip-planner/cmd/api/handlers.go:390.2,397.20 4 1
|
||||
trip-planner/cmd/api/handlers.go:397.20,398.42 1 0
|
||||
trip-planner/cmd/api/handlers.go:398.42,399.60 1 0
|
||||
trip-planner/cmd/api/handlers.go:399.60,400.42 1 0
|
||||
trip-planner/cmd/api/handlers.go:400.42,402.11 2 0
|
||||
trip-planner/cmd/api/handlers.go:408.2,409.19 2 1
|
||||
trip-planner/cmd/api/handlers.go:409.19,411.3 1 1
|
||||
trip-planner/cmd/api/handlers.go:413.2,414.20 2 1
|
||||
trip-planner/cmd/api/handlers.go:414.20,416.3 1 0
|
||||
trip-planner/cmd/api/handlers.go:418.2,426.33 3 1
|
||||
trip-planner/cmd/api/handlers.go:431.81,434.26 2 1
|
||||
trip-planner/cmd/api/handlers.go:434.26,438.3 2 0
|
||||
trip-planner/cmd/api/handlers.go:439.2,440.65 2 1
|
||||
trip-planner/cmd/api/handlers.go:440.65,443.3 2 1
|
||||
trip-planner/cmd/api/handlers.go:444.2,444.13 1 1
|
||||
trip-planner/cmd/api/handlers.go:449.85,451.26 1 1
|
||||
trip-planner/cmd/api/handlers.go:451.26,453.3 1 1
|
||||
trip-planner/cmd/api/handlers.go:456.2,458.20 2 1
|
||||
trip-planner/cmd/api/handlers.go:458.20,461.3 2 0
|
||||
trip-planner/cmd/api/handlers.go:462.2,469.61 3 1
|
||||
trip-planner/cmd/api/handlers.go:469.61,472.3 2 1
|
||||
trip-planner/cmd/api/handlers.go:475.2,479.32 2 1
|
||||
trip-planner/cmd/api/handlers.go:479.32,482.3 2 1
|
||||
trip-planner/cmd/api/handlers.go:485.2,485.28 1 1
|
||||
trip-planner/cmd/api/handlers.go:485.28,488.3 2 1
|
||||
trip-planner/cmd/api/handlers.go:492.2,502.33 4 1
|
||||
trip-planner/cmd/api/handlers.go:507.58,514.2 3 1
|
||||
trip-planner/cmd/api/handlers.go:524.81,526.18 2 1
|
||||
trip-planner/cmd/api/handlers.go:526.18,528.3 1 0
|
||||
trip-planner/cmd/api/handlers.go:530.2,531.16 2 1
|
||||
trip-planner/cmd/api/handlers.go:531.16,534.3 2 0
|
||||
trip-planner/cmd/api/handlers.go:536.2,540.4 2 1
|
||||
trip-planner/cmd/api/handlers.go:544.79,546.18 2 1
|
||||
trip-planner/cmd/api/handlers.go:546.18,548.3 1 0
|
||||
trip-planner/cmd/api/handlers.go:550.2,554.61 2 1
|
||||
trip-planner/cmd/api/handlers.go:554.61,557.3 2 0
|
||||
trip-planner/cmd/api/handlers.go:559.2,559.97 1 1
|
||||
trip-planner/cmd/api/handlers.go:559.97,562.3 2 0
|
||||
trip-planner/cmd/api/handlers.go:564.2,567.4 2 1
|
||||
trip-planner/cmd/api/handlers.go:571.82,573.18 2 1
|
||||
trip-planner/cmd/api/handlers.go:573.18,575.3 1 0
|
||||
trip-planner/cmd/api/handlers.go:577.2,579.20 2 1
|
||||
trip-planner/cmd/api/handlers.go:579.20,582.3 2 0
|
||||
trip-planner/cmd/api/handlers.go:583.2,585.86 2 1
|
||||
trip-planner/cmd/api/handlers.go:585.86,588.3 2 0
|
||||
trip-planner/cmd/api/handlers.go:590.2,593.4 2 1
|
||||
trip-planner/cmd/api/handlers.go:597.83,599.18 2 1
|
||||
trip-planner/cmd/api/handlers.go:599.18,601.3 1 0
|
||||
trip-planner/cmd/api/handlers.go:603.2,604.16 2 1
|
||||
trip-planner/cmd/api/handlers.go:604.16,607.3 2 0
|
||||
trip-planner/cmd/api/handlers.go:609.2,613.4 2 1
|
||||
trip-planner/cmd/api/handlers.go:617.83,619.18 2 1
|
||||
trip-planner/cmd/api/handlers.go:619.18,621.3 1 0
|
||||
trip-planner/cmd/api/handlers.go:623.2,628.61 2 1
|
||||
trip-planner/cmd/api/handlers.go:628.61,631.3 2 0
|
||||
trip-planner/cmd/api/handlers.go:634.2,634.76 1 1
|
||||
trip-planner/cmd/api/handlers.go:634.76,637.3 2 0
|
||||
trip-planner/cmd/api/handlers.go:639.2,639.113 1 1
|
||||
trip-planner/cmd/api/handlers.go:639.113,642.3 2 0
|
||||
trip-planner/cmd/api/handlers.go:644.2,647.4 2 1
|
||||
trip-planner/cmd/api/handlers.go:651.81,654.2 2 0
|
||||
trip-planner/cmd/api/handlers.go:657.133,669.2 2 1
|
||||
trip-planner/cmd/api/main.go:17.13,23.18 5 0
|
||||
trip-planner/cmd/api/main.go:23.18,25.3 1 0
|
||||
trip-planner/cmd/api/main.go:26.2,43.46 12 0
|
||||
trip-planner/cmd/api/main.go:47.32,54.2 2 0
|
||||
trip-planner/cmd/api/main.go:58.73,63.2 2 0
|
||||
trip-planner/cmd/api/main.go:67.122,68.54 1 0
|
||||
trip-planner/cmd/api/main.go:68.54,70.3 1 0
|
||||
trip-planner/internal/yandex/client.go:67.58,83.30 2 1
|
||||
trip-planner/internal/yandex/client.go:83.30,85.3 1 1
|
||||
trip-planner/internal/yandex/client.go:87.2,87.10 1 1
|
||||
trip-planner/internal/yandex/client.go:94.55,95.25 1 1
|
||||
trip-planner/internal/yandex/client.go:95.25,97.3 1 1
|
||||
trip-planner/internal/yandex/client.go:101.62,102.25 1 1
|
||||
trip-planner/internal/yandex/client.go:102.25,104.3 1 1
|
||||
trip-planner/internal/yandex/client.go:108.97,109.25 1 1
|
||||
trip-planner/internal/yandex/client.go:109.25,116.3 1 1
|
||||
trip-planner/internal/yandex/client.go:120.45,121.25 1 0
|
||||
trip-planner/internal/yandex/client.go:121.25,123.3 1 0
|
||||
trip-planner/internal/yandex/client.go:127.107,129.48 1 0
|
||||
trip-planner/internal/yandex/client.go:129.48,131.3 1 0
|
||||
trip-planner/internal/yandex/client.go:134.2,140.67 4 0
|
||||
trip-planner/internal/yandex/client.go:140.67,142.32 1 0
|
||||
trip-planner/internal/yandex/client.go:142.32,145.4 2 0
|
||||
trip-planner/internal/yandex/client.go:147.3,148.17 2 0
|
||||
trip-planner/internal/yandex/client.go:148.17,151.4 2 0
|
||||
trip-planner/internal/yandex/client.go:154.3,154.29 1 0
|
||||
trip-planner/internal/yandex/client.go:154.29,157.4 2 0
|
||||
trip-planner/internal/yandex/client.go:159.3,161.41 2 0
|
||||
trip-planner/internal/yandex/client.go:161.41,163.28 2 0
|
||||
trip-planner/internal/yandex/client.go:163.28,165.5 1 0
|
||||
trip-planner/internal/yandex/client.go:166.4,166.23 1 0
|
||||
trip-planner/internal/yandex/client.go:170.2,172.17 3 0
|
||||
trip-planner/internal/yandex/client.go:176.85,178.16 2 0
|
||||
trip-planner/internal/yandex/client.go:178.16,180.3 1 0
|
||||
trip-planner/internal/yandex/client.go:182.2,185.20 2 0
|
||||
trip-planner/internal/yandex/client.go:185.20,187.3 1 0
|
||||
trip-planner/internal/yandex/client.go:189.2,190.16 2 0
|
||||
trip-planner/internal/yandex/client.go:190.16,192.3 1 0
|
||||
trip-planner/internal/yandex/client.go:194.2,194.28 1 0
|
||||
trip-planner/internal/yandex/client.go:194.28,198.3 3 0
|
||||
trip-planner/internal/yandex/client.go:200.2,201.65 2 0
|
||||
trip-planner/internal/yandex/client.go:201.65,203.3 1 0
|
||||
trip-planner/internal/yandex/client.go:205.2,205.19 1 0
|
||||
trip-planner/internal/yandex/client.go:262.35,264.2 1 1
|
||||
trip-planner/internal/yandex/client.go:266.54,268.2 1 1
|
||||
trip-planner/internal/yandex/client.go:271.39,272.16 1 1
|
||||
trip-planner/internal/yandex/client.go:272.16,274.3 1 1
|
||||
trip-planner/internal/yandex/client.go:276.2,277.8 2 1
|
||||
trip-planner/internal/yandex/client.go:277.8,279.3 1 0
|
||||
trip-planner/internal/yandex/client.go:281.2,285.56 2 1
|
||||
trip-planner/internal/yandex/client.go:289.60,292.26 3 1
|
||||
trip-planner/internal/yandex/client.go:292.26,294.3 1 1
|
||||
trip-planner/internal/yandex/client.go:295.2,296.10 2 1
|
||||
trip-planner/internal/yandex/client.go:301.60,308.2 1 1
|
||||
trip-planner/internal/yandex/client.go:310.40,317.19 5 1
|
||||
trip-planner/internal/yandex/client.go:317.19,320.3 2 1
|
||||
trip-planner/internal/yandex/client.go:322.2,322.117 1 1
|
||||
trip-planner/internal/yandex/client.go:325.46,327.28 2 1
|
||||
trip-planner/internal/yandex/client.go:327.28,329.42 2 1
|
||||
trip-planner/internal/yandex/client.go:329.42,331.4 1 0
|
||||
trip-planner/internal/yandex/client.go:331.9,333.4 1 1
|
||||
trip-planner/internal/yandex/client.go:334.3,334.22 1 1
|
||||
trip-planner/internal/yandex/client.go:341.42,347.2 1 1
|
||||
trip-planner/internal/yandex/client.go:349.49,356.2 6 1
|
||||
trip-planner/internal/yandex/client.go:358.40,362.18 3 1
|
||||
trip-planner/internal/yandex/client.go:363.14,364.14 1 1
|
||||
trip-planner/internal/yandex/client.go:365.12,367.45 1 1
|
||||
trip-planner/internal/yandex/client.go:367.45,371.4 3 1
|
||||
trip-planner/internal/yandex/client.go:372.3,372.15 1 1
|
||||
trip-planner/internal/yandex/client.go:373.16,374.14 1 0
|
||||
trip-planner/internal/yandex/client.go:376.2,376.14 1 0
|
||||
trip-planner/internal/yandex/client.go:379.43,383.18 3 1
|
||||
trip-planner/internal/yandex/client.go:384.14,384.14 0 0
|
||||
trip-planner/internal/yandex/client.go:386.16,388.24 2 1
|
||||
trip-planner/internal/yandex/client.go:388.24,391.4 2 1
|
||||
trip-planner/internal/yandex/client.go:392.12,392.12 0 0
|
||||
trip-planner/internal/yandex/client.go:397.43,401.18 3 1
|
||||
trip-planner/internal/yandex/client.go:402.14,404.38 2 1
|
||||
trip-planner/internal/yandex/client.go:404.38,407.4 2 1
|
||||
trip-planner/internal/yandex/client.go:408.16,410.28 2 0
|
||||
trip-planner/internal/yandex/client.go:411.12,411.12 0 1
|
||||
trip-planner/internal/yandex/client.go:418.55,421.2 2 0
|
||||
trip-planner/internal/yandex/client.go:423.28,426.2 1 0
|
||||
@@ -16,10 +16,25 @@ services:
|
||||
- postgres
|
||||
command: ["/api"]
|
||||
|
||||
cron:
|
||||
build: .
|
||||
environment:
|
||||
- REDIS_ADDR=redis:6379
|
||||
- POSTGRES_DATABASE=trip_planner
|
||||
- POSTGRES_USER=trip_planner
|
||||
- POSTGRES_PASSWORD=trip_planner
|
||||
- POSTGRES_HOST=postgres
|
||||
depends_on:
|
||||
- redis
|
||||
- postgres
|
||||
command: ["/cron"]
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
@@ -32,5 +47,12 @@ services:
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
watchtower:
|
||||
image: nickfedor/watchtower:latest
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
command: --interval 60 --cleanup travel-api
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
@@ -0,0 +1,116 @@
|
||||
# 2026-08-18-implement-deployment-infrastructure
|
||||
|
||||
## Overview
|
||||
|
||||
- Clear description of the feature/change being implemented: Implement the complete deployment infrastructure for the trip-planner service including Dockerfile, updated docker-compose.yml with cron and watchtower services, and Gitea Actions CI/CD workflow.
|
||||
- Problem it solves and key benefits: The project has the core routing logic, API endpoints, caching, and frontend fully implemented, but lacks the deployment infrastructure specified in `docs/deployment.md`. This plan adds the missing Dockerfile, completes docker-compose.yml with cron and watchtower services, and creates the Gitea Actions CI/CD workflow for automated build and deployment.
|
||||
- How it integrates with existing system: The deployment infrastructure wraps the existing Go backend (API and cron services) in Docker containers and provides automated CI/CD pipeline for building and pushing images to a registry, with watchtower handling automatic updates on the production server.
|
||||
|
||||
## Context (from discovery)
|
||||
|
||||
- Files/components involved:
|
||||
- `Dockerfile` (to be created)
|
||||
- `docker-compose.yml` (to be updated with cron and watchtower services, redis_data volume)
|
||||
- `.gitea/workflows/deploy.yml` (to be created)
|
||||
- Existing: `/cmd/api/main.go`, `/cmd/cron/station_status.go`, `go.mod`, `go.sum`
|
||||
- Related patterns found: Multi-stage Docker build for Go applications, Gitea Actions workflow with checkout, setup Go, lint & test, build binaries, Docker build & push, watchtower for CD.
|
||||
- Dependencies identified: Go 1.22+, PostgreSQL 15-alpine, Redis 7-alpine, nickfedor/watchtower image.
|
||||
|
||||
## Development Approach
|
||||
|
||||
- **Testing approach**: TDD (tests first) - Define test scenarios and verify deployment files work correctly
|
||||
- Complete each task fully before moving to the next
|
||||
- Make small, focused changes
|
||||
- **CRITICAL: every task MUST include verification** for deployment files
|
||||
- verification is not optional - they are a required part of the checklist
|
||||
- verify Dockerfile syntax and multi-stage structure
|
||||
- verify docker-compose.yml syntax and service definitions
|
||||
- verify Gitea Actions workflow YAML syntax
|
||||
- include both success and error scenarios in verification
|
||||
- **CRITICAL: all verifications must pass before starting next task** - no exceptions
|
||||
- **CRITICAL: update this plan file when scope changes during implementation**
|
||||
- Keep plan in sync with actual work done
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- **Verification tests**: required for every task (see Development Approach above)
|
||||
- **Syntax validation**: verify YAML files with yaml lint, verify Dockerfile with docker build --dry-run
|
||||
- **Configuration validation**: verify docker-compose.yml with `docker-compose config`, verify Gitea Actions workflow syntax
|
||||
|
||||
## Progress Tracking
|
||||
|
||||
- Mark completed items with `[x]` immediately when done
|
||||
- Add newly discovered tasks with ➕ prefix
|
||||
- Document issues/blockers with ⚠️ prefix
|
||||
- Update plan if implementation deviates from original scope
|
||||
- Keep plan in sync with actual work done
|
||||
|
||||
## What Goes Where
|
||||
|
||||
- **Implementation Steps** (`[ ]` checkboxes): tasks achievable within this codebase - creating Dockerfile, updating docker-compose.yml, creating Gitea Actions workflow
|
||||
- **Post-Completion** (no checkboxes): items requiring external action - manual testing on production server, registry configuration, third-party service integrations to verify
|
||||
- **Checkbox placement**: Checkboxes belong only in Task sections (`### Task N:`). Do not put checkboxes in Success criteria, Overview, or Context — they cause extra loop iterations.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Task 1: Create Multi-stage Dockerfile
|
||||
- [x] create Dockerfile with builder stage (Go 1.26+, build api and cron binaries)
|
||||
- [x] create Dockerfile with runtime stage (alpine, copy binaries, set entrypoint)
|
||||
- [x] verify Dockerfile syntax with `docker build` (successful build)
|
||||
- [x] verify multi-stage structure produces minimal runtime image (27.1MB)
|
||||
|
||||
### Task 2: Update docker-compose.yml
|
||||
- [x] add `cron` service (Go cron binary, depends_on: postgres, redis)
|
||||
- [x] add `watchtower` service (nickfedor/watchtower image, volume for docker.sock, command for cleanup)
|
||||
- [x] add `redis_data` volume to volumes section
|
||||
- [x] verify docker-compose.yml syntax with `docker-compose config`
|
||||
|
||||
### Task 3: Create Gitea Actions CI/CD Workflow
|
||||
- [x] create `.gitea/workflows/deploy.yml` file
|
||||
- [x] add checkout step (actions/checkout@v3)
|
||||
- [x] add setup Go step (actions/setup-go@v4, go-version: '1.22')
|
||||
- [x] add Go modules cache step (actions/cache@v3)
|
||||
- [x] add lint & test step (golangci-lint, go test -v -race ./...)
|
||||
- [x] add Docker Buildx setup step (docker/setup-buildx-action@v2)
|
||||
- [x] add Docker login step (docker/login-action@v2)
|
||||
- [x] add Docker build & push step (docker/build-push-action@v4, tags: latest and {{.CommitID}})
|
||||
- [x] verify Gitea Actions workflow YAML syntax
|
||||
|
||||
### Task 4: Verify acceptance criteria
|
||||
- [x] verify Dockerfile matches multi-stage specification
|
||||
- [x] verify docker-compose.yml has api, cron, postgres, redis, watchtower services
|
||||
- [x] verify Gitea Actions workflow has all required steps (checkout, setup Go, lint & test, build binaries, Docker build & push)
|
||||
- [x] verify all YAML files are valid syntax
|
||||
- [x] verify docker-compose.yml volumes section includes postgres_data and redis_data
|
||||
|
||||
## Technical Details
|
||||
|
||||
- **Dockerfile structure**:
|
||||
- Builder stage: `golang:1.22-alpine` as builder, copy go.mod/go.sum, `go mod download`, copy source, `go build -o api cmd/api/main.go`, `go build -o cron cmd/cron/main.go`
|
||||
- Runtime stage: `alpine:latest` or `scratch`, copy binaries from builder, set environment variables, expose port 8080, set entrypoint
|
||||
|
||||
- **docker-compose.yml services**:
|
||||
- `api`: build from Dockerfile, ports: 8080:8080, environment: DB_DSN, REDIS_ADDR, YANDEX_API_KEY, depends_on: postgres, redis
|
||||
- `cron`: build from Dockerfile, environment: same as api, depends_on: postgres, redis
|
||||
- `postgres` (postgres): postgres:15-alpine, environment: POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB, volumes: postgres_data
|
||||
- `redis`: redis:7-alpine, volumes: redis_data
|
||||
- `watchtower`: nickfedor/watchtower, volumes: /var/run/docker.sock:/var/run/docker.sock, command: --interval 60 --cleanup travel-api
|
||||
|
||||
- **.gitea/workflows/deploy.yml structure**:
|
||||
- on: push to master branch
|
||||
- jobs: build-and-deploy on ubuntu-latest
|
||||
- steps: Checkout Code, Set up Go, Go Modules Cache, Run Tests, Set up Docker Buildx, Login to Docker Registry, Build and Push Docker Image
|
||||
|
||||
## Post-Completion
|
||||
|
||||
*Items requiring manual intervention or external systems - no checkboxes, informational only*
|
||||
|
||||
**Manual verification** (if applicable):
|
||||
- Test docker-compose.yml on local Docker environment
|
||||
- Test Gitea Actions workflow in a test repository
|
||||
- Verify watchtower auto-update behavior on production server
|
||||
|
||||
**External system updates** (if applicable):
|
||||
- Configure Docker Registry credentials in Gitea Secrets (DOCKER_USERNAME, DOCKER_PASSWORD)
|
||||
- Configure YANDEX_API_KEY and TRIP_PLANNER_ADMIN_API_KEY in Docker Compose .env file
|
||||
- Verify PostgreSQL and Redis persistence volumes are properly mounted
|
||||
4
go.mod
4
go.mod
@@ -2,9 +2,9 @@ module trip-planner
|
||||
|
||||
go 1.26.4
|
||||
|
||||
require github.com/go-redis/redis/v8 v8.11.5
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||
github.com/jmoiron/sqlx v1.4.0 // indirect
|
||||
)
|
||||
|
||||
24
go.sum
24
go.sum
@@ -1,12 +1,24 @@
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
|
||||
40
internal/cache/store_test.go
vendored
40
internal/cache/store_test.go
vendored
@@ -12,9 +12,11 @@ import (
|
||||
// TestCacheGetSet tests basic Get and Set operations.
|
||||
func TestCacheGetSet(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := NewRedisClient(redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
}), metrics.New())
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "127.0.0.1:6379",
|
||||
})
|
||||
client := NewRedisClient(rdb, metrics.New())
|
||||
defer rdb.Close()
|
||||
|
||||
// Test Set
|
||||
key := &CacheKey{Kind: "city", Code: "c146"}
|
||||
@@ -88,9 +90,11 @@ func TestCacheKeyString(t *testing.T) {
|
||||
// TestCacheAsideGetOrSet tests the cache-aside GetOrSetFuncPattern.
|
||||
func TestCacheAsideGetOrSet(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := NewRedisClient(redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
}), metrics.New())
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "127.0.0.1:6379",
|
||||
})
|
||||
client := NewRedisClient(rdb, metrics.New())
|
||||
defer rdb.Close()
|
||||
|
||||
fetchCallCount := 0
|
||||
fetchFunc := func() ([]byte, error) {
|
||||
@@ -128,9 +132,11 @@ func TestCacheAsideGetOrSet(t *testing.T) {
|
||||
// TestCacheAsideGetCity tests GetCity with cache.
|
||||
func TestCacheAsideGetCity(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := NewRedisClient(redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
}), metrics.New())
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "127.0.0.1:6379",
|
||||
})
|
||||
client := NewRedisClient(rdb, metrics.New())
|
||||
defer rdb.Close()
|
||||
|
||||
fetchCallCount := 0
|
||||
fetchFunc := func() ([]byte, error) {
|
||||
@@ -167,9 +173,11 @@ func TestCacheAsideGetCity(t *testing.T) {
|
||||
// TestCacheAsideGetSearch tests GetSearch with near-term and far-term TTL.
|
||||
func TestCacheAsideGetSearch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := NewRedisClient(redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
}), metrics.New())
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "127.0.0.1:6379",
|
||||
})
|
||||
client := NewRedisClient(rdb, metrics.New())
|
||||
defer rdb.Close()
|
||||
|
||||
fetchNearTerm := func() ([]byte, error) {
|
||||
return []byte(`{"near_term":true}`), nil
|
||||
@@ -205,9 +213,11 @@ func TestCacheAsideGetSearch(t *testing.T) {
|
||||
// TestCacheInvalidate tests invalidation operations.
|
||||
func TestCacheInvalidate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := NewRedisClient(redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
}), metrics.New())
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "127.0.0.1:6379",
|
||||
})
|
||||
client := NewRedisClient(rdb, metrics.New())
|
||||
defer rdb.Close()
|
||||
|
||||
// Set up some keys
|
||||
cityKey := &CacheKey{Kind: "city", Code: "c146"}
|
||||
|
||||
@@ -557,21 +557,22 @@ func (g *Graph) FindRoute(originID, destID string, opts SearchOptions, closedSta
|
||||
|
||||
newDurationWithMCT := newDuration + transferTime
|
||||
|
||||
// Check if we've visited this node with fewer transfers
|
||||
visKey := nextNode.ID
|
||||
if existingTransfers, ok := visited[visKey]; ok {
|
||||
if current.transfers+1 > existingTransfers {
|
||||
// Already visited this node with fewer transfers, skip
|
||||
continue
|
||||
}
|
||||
}
|
||||
visited[visKey] = current.transfers + 1
|
||||
|
||||
// Calculate new transfers before checking visited
|
||||
newTransfers := current.transfers
|
||||
if edge.IsTransfer {
|
||||
newTransfers++
|
||||
}
|
||||
|
||||
// Check if we've visited this node with fewer transfers
|
||||
visKey := nextNode.ID
|
||||
if existingTransfers, ok := visited[visKey]; ok {
|
||||
if newTransfers > existingTransfers {
|
||||
// Already visited this node with fewer transfers, skip
|
||||
continue
|
||||
}
|
||||
}
|
||||
visited[visKey] = newTransfers
|
||||
|
||||
newLegs := make([]RouteLeg, len(current.itinerary.Legs)+1)
|
||||
copy(newLegs, current.itinerary.Legs)
|
||||
|
||||
@@ -733,21 +734,22 @@ func (g *Graph) FindRoute(originID, destID string, opts SearchOptions, closedSta
|
||||
|
||||
newDurationWithMCT := newDuration + transferTime
|
||||
|
||||
// Check if we've visited this node with fewer transfers
|
||||
visKey := nextNode.ID
|
||||
if existingTransfers, ok := visited[visKey]; ok {
|
||||
if current.transfers+1 > existingTransfers {
|
||||
// Already visited this node with fewer transfers, skip
|
||||
continue
|
||||
}
|
||||
}
|
||||
visited[visKey] = current.transfers + 1
|
||||
|
||||
// Calculate new transfers before checking visited
|
||||
newTransfers := current.transfers
|
||||
if edge.IsTransfer {
|
||||
newTransfers++
|
||||
}
|
||||
|
||||
// Check if we've visited this node with fewer transfers
|
||||
visKey := nextNode.ID
|
||||
if existingTransfers, ok := visited[visKey]; ok {
|
||||
if newTransfers > existingTransfers {
|
||||
// Already visited this node with fewer transfers, skip
|
||||
continue
|
||||
}
|
||||
}
|
||||
visited[visKey] = newTransfers
|
||||
|
||||
newLegs := make([]RouteLeg, len(current.itinerary.Legs)+1)
|
||||
copy(newLegs, current.itinerary.Legs)
|
||||
|
||||
@@ -1106,7 +1108,7 @@ func (g *Graph) checkRouteForChanges(itinerary *Itinerary) bool {
|
||||
}
|
||||
// Check for significant delay (more than 2x normal duration)
|
||||
if edge.Duration > leg.Duration*2 && leg.Duration > 0 {
|
||||
if !needsReSearch || itinerary.ReSearchReason == string(reasonNone) {
|
||||
if !needsReSearch {
|
||||
needsReSearch = true
|
||||
itinerary.NeedsReSearch = true
|
||||
itinerary.ReSearchReason = string(reasonMajorDelay)
|
||||
@@ -1129,8 +1131,8 @@ func (g *Graph) rescheduleRoute(originID, destID string, opts SearchOptions, clo
|
||||
result := g.FindRoute(originID, destID, opts, closedStations, neighbors, yclient...)
|
||||
if result != nil {
|
||||
result.LastChecked = time.Now().Unix()
|
||||
result.NeedsReSearch = false
|
||||
result.ReSearchReason = string(reasonNone)
|
||||
// Keep NeedsReSearch and ReSearchReason from the original itinerary to indicate
|
||||
// that a re-search was triggered due to changes
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1139,7 +1141,13 @@ func (g *Graph) rescheduleRoute(originID, destID string, opts SearchOptions, clo
|
||||
// This is the main entry point for flight change notification logic.
|
||||
func (g *Graph) CheckAndRescheduleRoute(itinerary *Itinerary, originID, destID string, opts SearchOptions, closedStations map[string]bool, neighbors map[string][]storage.StationNeighbor, yclient ...*yandex.Client) *Itinerary {
|
||||
if g.checkRouteForChanges(itinerary) {
|
||||
return g.rescheduleRoute(originID, destID, opts, closedStations, neighbors, yclient...)
|
||||
result := g.rescheduleRoute(originID, destID, opts, closedStations, neighbors, yclient...)
|
||||
if result != nil {
|
||||
// Preserve the NeedsReSearch and ReSearchReason from the original itinerary
|
||||
result.NeedsReSearch = itinerary.NeedsReSearch
|
||||
result.ReSearchReason = itinerary.ReSearchReason
|
||||
}
|
||||
return result
|
||||
}
|
||||
return itinerary
|
||||
}
|
||||
|
||||
@@ -398,10 +398,10 @@ func TestRouteReSearchOnChange(t *testing.T) {
|
||||
// We need to do this after the check runs, so let's verify the initial state first.
|
||||
|
||||
// Verify that initial state has NeedsReSearch false (no changes simulated yet)
|
||||
if !itinerary.NeedsReSearch {
|
||||
t.Log("PASS: Initial NeedsReSearch is false (no changes simulated)")
|
||||
if itinerary.NeedsReSearch {
|
||||
t.Errorf("expected initial NeedsReSearch to be false, got true")
|
||||
} else {
|
||||
t.Log("INFO: Initial NeedsReSearch is already true")
|
||||
t.Log("PASS: Initial NeedsReSearch is false (no changes simulated)")
|
||||
}
|
||||
|
||||
// Now simulate cancellation by setting edge s1->s2 duration to > 86400 (1 day = cancellation)
|
||||
@@ -413,6 +413,9 @@ func TestRouteReSearchOnChange(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Reset LastChecked to force re-check (bypass the 1-hour cache)
|
||||
itinerary.LastChecked = time.Now().Unix() - 7200
|
||||
|
||||
// Re-check for changes after simulating cancellation
|
||||
checked2 := graph.CheckAndRescheduleRoute(itinerary, "s1", "s3", SearchOptions{MaxTransfers: 5}, nil, nil)
|
||||
t.Logf("After cancellation - NeedsReSearch: %v, ReSearchReason: %s", checked2.NeedsReSearch, checked2.ReSearchReason)
|
||||
@@ -440,12 +443,20 @@ func TestRouteReSearchOnChange(t *testing.T) {
|
||||
ReSearchReason: "",
|
||||
}
|
||||
|
||||
// For major delay, the check uses: edge.Duration > leg.Cost*2 && leg.Cost > 0
|
||||
// With Cost=500, threshold would be 1000. Setting duration to 2000 should trigger.
|
||||
// Reset the s1->s2 edge duration to normal value before testing major delay
|
||||
for _, edge := range graph.edges {
|
||||
if edge.From.ID == "s1" && edge.To.ID == "s2" {
|
||||
edge.Duration = 3600 // Reset to normal duration
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// For major delay, the check uses: edge.Duration > leg.Duration*2 && leg.Duration > 0
|
||||
// With leg.Duration=3600, threshold would be 7200. Setting duration to 8000 should trigger.
|
||||
for _, edge := range graph.edges {
|
||||
if edge.From.ID == "s2" && edge.To.ID == "s3" {
|
||||
edge.Duration = 2000 // > 500*2 = 1000, should trigger major delay
|
||||
t.Logf("Set s2->s3 edge duration to %d (simulating major delay, threshold=1000)", edge.Duration)
|
||||
edge.Duration = 8000 // > 3600*2 = 7200, should trigger major delay
|
||||
t.Logf("Set s2->s3 edge duration to %d (simulating major delay, threshold=7200)", edge.Duration)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
package routing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"trip-planner/internal/cache"
|
||||
"trip-planner/internal/metrics"
|
||||
"trip-planner/internal/yandex"
|
||||
)
|
||||
|
||||
// SearchCacheService handles caching and on-demand Yandex /search calls.
|
||||
type SearchCacheService struct {
|
||||
cache *cache.CacheAside
|
||||
yclient *yandex.Client
|
||||
metrics *metrics.Metrics
|
||||
}
|
||||
|
||||
// NewSearchCacheService creates a new search cache service.
|
||||
func NewSearchCacheService(cacheStore cache.Cache, yclient *yandex.Client, m *metrics.Metrics) *SearchCacheService {
|
||||
return &SearchCacheService{
|
||||
cache: cache.NewCacheAside(cacheStore, m),
|
||||
yclient: yclient,
|
||||
metrics: m,
|
||||
}
|
||||
}
|
||||
|
||||
// SearchWithCache performs a route search with caching support.
|
||||
// It uses the cache-aside pattern: try cache first, then Yandex API, then write back to cache.
|
||||
func (s *SearchCacheService) SearchWithCache(ctx context.Context, from, to, date string, opts SearchOptions) (*yandex.Response, error) {
|
||||
// Generate cache key including far-term flag to distinguish near-term vs far-term searches
|
||||
farTermFlag := "near"
|
||||
if opts.FarTerm {
|
||||
farTermFlag = "far"
|
||||
}
|
||||
searchKey := cache.GetSearchKeyWithFarTerm(from, to, date, farTermFlag)
|
||||
|
||||
// Try to get from cache first
|
||||
fetchFunc := func() ([]byte, error) {
|
||||
// If we reach here, it's a cache miss - perform on-demand Yandex /search call
|
||||
return s.performYandexSearch(ctx, from, to, date, opts)
|
||||
}
|
||||
|
||||
// Get or set from cache with appropriate TTL based on far-term flag
|
||||
isFarTerm := opts.FarTerm
|
||||
data, err := s.cache.GetSearch(ctx, searchKey, fetchFunc, isFarTerm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("search cache get/set: %w", err)
|
||||
}
|
||||
|
||||
// Parse the yandex.Response from cached data
|
||||
var result yandex.Response
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse yandex response from cache: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// performYandexSearch makes the actual Yandex /search API call.
|
||||
func (s *SearchCacheService) performYandexSearch(ctx context.Context, from, to, date string, opts SearchOptions) ([]byte, error) {
|
||||
// Build query parameters for Yandex /search endpoint
|
||||
query := map[string]string{
|
||||
"from": from,
|
||||
"to": to,
|
||||
"date": date,
|
||||
}
|
||||
|
||||
// Execute the Yandex API request
|
||||
resp, err := s.yclient.Do(ctx, "GET", "/v3.0/search/", query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("yandex search failed: %w", err)
|
||||
}
|
||||
|
||||
// Convert response to bytes for caching
|
||||
return convertResponseToBytes(resp)
|
||||
}
|
||||
|
||||
// convertResponseToBytes converts Yandex API response to bytes for caching.
|
||||
func convertResponseToBytes(resp *yandex.Response) ([]byte, error) {
|
||||
if resp == nil {
|
||||
return nil, fmt.Errorf("nil response")
|
||||
}
|
||||
data, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal response: %w", err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package routing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"trip-planner/internal/cache"
|
||||
"trip-planner/internal/metrics"
|
||||
"trip-planner/internal/yandex"
|
||||
)
|
||||
|
||||
// mockCacheStoreForSearch is a mock implementation of Cache for testing search cache
|
||||
type mockCacheStoreForSearch struct {
|
||||
data map[string][]byte
|
||||
}
|
||||
|
||||
func (m *mockCacheStoreForSearch) Get(ctx context.Context, key *cache.CacheKey) ([]byte, error) {
|
||||
keyStr := key.Kind + ":" + key.Code
|
||||
if data, ok := m.data[keyStr]; ok {
|
||||
return data, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockCacheStoreForSearch) Set(ctx context.Context, key *cache.CacheKey, value []byte, ttl time.Duration) error {
|
||||
keyStr := key.Kind + ":" + key.Code
|
||||
m.data[keyStr] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockCacheStoreForSearch) Exists(ctx context.Context, key *cache.CacheKey) (bool, error) {
|
||||
keyStr := key.Kind + ":" + key.Code
|
||||
_, ok := m.data[keyStr]
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
func (m *mockCacheStoreForSearch) Delete(ctx context.Context, key *cache.CacheKey) error {
|
||||
keyStr := key.Kind + ":" + key.Code
|
||||
delete(m.data, keyStr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockCacheStoreForSearch) Increment(ctx context.Context, key *cache.CacheKey) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (m *mockCacheStoreForSearch) Decrement(ctx context.Context, key *cache.CacheKey) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func TestNewSearchCacheService(t *testing.T) {
|
||||
mockStore := &mockCacheStoreForSearch{data: make(map[string][]byte)}
|
||||
metrics := metrics.New()
|
||||
yclient := yandex.NewClient("test-key")
|
||||
|
||||
svc := NewSearchCacheService(mockStore, yclient, metrics)
|
||||
if svc == nil {
|
||||
t.Error("expected SearchCacheService to be created")
|
||||
}
|
||||
if svc.cache == nil {
|
||||
t.Error("expected cache to be initialized")
|
||||
}
|
||||
if svc.yclient == nil {
|
||||
t.Error("expected yclient to be initialized")
|
||||
}
|
||||
if svc.metrics == nil {
|
||||
t.Error("expected metrics to be initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertResponseToBytes(t *testing.T) {
|
||||
// Test with nil response
|
||||
_, err := convertResponseToBytes(nil)
|
||||
if err == nil {
|
||||
t.Error("expected error for nil response")
|
||||
}
|
||||
|
||||
// Test with valid response
|
||||
resp := &yandex.Response{
|
||||
Segments: []yandex.Segment{},
|
||||
}
|
||||
data, err := convertResponseToBytes(resp)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got %v", err)
|
||||
}
|
||||
if data == nil {
|
||||
t.Error("expected non-nil data")
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -197,10 +196,9 @@ func (c *Client) executeRequest(ctx context.Context, url string) (*Response, err
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
io.ReadAll(resp.Body) // Drain body to allow connection reuse
|
||||
resp.Body.Close()
|
||||
return nil, newAPIError(resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
@@ -279,10 +277,10 @@ func isRetryableError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
// Check for HTTP status codes that are retryable (5xx errors)
|
||||
// Check for HTTP status codes that are retryable (5xx errors and 429)
|
||||
apiErr, ok := err.(*APIError)
|
||||
if ok {
|
||||
return apiErr.Code >= 500 && apiErr.Code < 600
|
||||
return (apiErr.Code >= 500 && apiErr.Code < 600) || apiErr.Code == 429
|
||||
}
|
||||
// Check for network errors
|
||||
errStr := err.Error()
|
||||
|
||||
Reference in New Issue
Block a user