PowerSolver 2.0 — Administrator Guide
How to deploy, configure, and operate PowerSolver in production. Aimed at sysadmins and DevOps.
PowerSolver 2.0 is live. It runs on the same hostnames, ports, container names and redis sidecar the 1.x stack used.
This guide is the steady-state reference. The cutover, burn-in and 1.x archive procedures are complete and archived under _graveyard\PowerSolver\documentation\.
1. Architecture
┌─────────────────────────────────────────────────────────────────┐
│ PowerSolver 2.0 stack (per-host) │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ powersolver-api │◄───┤ powersolver-redis│ │
│ │ :8001 │ │ :6379 │ │
│ │ Spring Boot 4 │ │ redis:7-alpine │ │
│ │ Timefold 2.0.0 │ │ audit / history │ │
│ │ Java 21 │ └──────────────────┘ │
│ └────────┬─────────┘ │
│ │ │
│ ┌────────▼─────────┐ │
│ │ powersolver- │ │
│ │ wizard │ │
│ │ :8080 │ │
│ │ nginx:alpine │ │
│ │ + static HTML │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Support products (separate compose stacks, joined via pdp-network):
poweradmin-api :8010 — wizard fields, system config
powerai-api :8060 — LLM-driven spec generation
powerorchestrator :8050 — PDP pipeline orchestration
Reverse proxy (nginx) terminates TLS and routes:
| Public hostname | → container | Notes |
|---|---|---|
api.planningpowertools.com | powersolver-api:8001 | Solver REST surface |
wizard.planningpowertools.com | powersolver-wizard:80 (host port 8080) | Static UI |
admin-api.planningpowertools.com | poweradmin-api:8010 | Unchanged from 1.x |
powerai-api.planningpowertools.com | powerai-api:8060 | Unchanged |
orchestrator-api.planningpowertools.com | power-orchestrator:8050 | Unchanged |
2. Container layout
| File | Purpose |
|---|---|
powersolver-api/docker-compose.yml | Solver + redis sidecar |
powersolver-api/Dockerfile | Two-stage build: Maven JDK 21 → JRE 21 runtime |
powersolver-wizard/docker-compose.yml | nginx:alpine static container |
powersolver-wizard/Dockerfile | Copies static assets into the image |
Or alternatively, use the centralised deployment toolkit at D:\PowerProjects\documentation\installation\ (preferred for production — handles image transfer, network creation, db backup):
| Asset | Purpose |
|---|---|
project-registry.json | Build context + ports + redis for every product |
server-compose/docker-compose-powersolver.yml | Production compose (no build: block; loads pre-built image) |
build-and-export.ps1 | Local build → .tar |
transfer-images.ps1 | SCP .tar to server |
deploy-remote.sh | Server-side load + restart + DB backup |
deploy-all.ps1 | Orchestrates the full chain |
server-compose/docker-compose-powersolver.yml
services:
powersolver-api:
image: powersolver-api:latest
container_name: powersolver-api
ports:
- "8001:8001"
environment:
- JAVA_OPTS=-Xmx2048m -Xms512m
restart: unless-stopped
depends_on: [redis]
networks: [pdp-network]
redis:
image: redis:7-alpine
container_name: powersolver-redis
ports: ["6379:6379"]
volumes: ["powersolver-redis-data:/data"]
restart: unless-stopped
networks: [pdp-network]
volumes:
powersolver-redis-data:
name: powersolver-redis-data
networks:
pdp-network:
external: true
3. Environment variables
powersolver-api:
| Variable | Default | Meaning |
|---|---|---|
JAVA_OPTS | -Xmx2048m -Xms512m | JVM heap. Container-aware — does NOT need -XX:+UseContainerSupport (JDK 21 default). |
JAVA_TOOL_OPTIONS | unset | Alternative way to set JVM flags; useful for -XX:MaxRAMPercentage=75. |
SERVER_PORT | 8001 | Spring's HTTP listen port. Should stay 8001 (matches docker-compose port mapping). |
POWERSOLVER_ORCHESTRATOR_URL | unset | If set, emits solve-completion events to PowerOrchestrator. Empty = standalone. |
powersolver-wizard: no env vars (pure static). The wizard reads its runtime config from PowerAdmin's /api/system-config endpoint — see Wizard configuration below.
4. Redis sidecar
powersolver-redis runs alongside the solver, exposes 6379, persists data to the named volume powersolver-redis-data.
The 2.0 solver does NOT use redis for job state (it's synchronous — no async-job machinery). Redis is kept for:
- Job history / audit — operators can write each completed solve to redis for retention (current implementation: not wired, but the volume is in place).
- Future async-job machinery — if a later release reintroduces a queue, the sidecar is ready.
- Parity with 1.x operability — the 1.x ops surface relied on this redis for job-status inspection; keeping it preserves muscle memory.
Don't remove it without explicit sign-off. Removing it would silently delete the historical job data the volume holds.
Inspecting the volume
docker run --rm -it -v powersolver-redis-data:/data redis:7-alpine redis-cli -p 6379 KEYS '*'
docker run --rm -it -v powersolver-redis-data:/data redis:7-alpine redis-cli -p 6379 GET '<key>'
Backup
The redis volume is automatically preserved across docker compose down / up -d (a named volume; down doesn't touch it). For external backup, snapshot the volume:
docker run --rm -v powersolver-redis-data:/data -v $(pwd):/backup alpine \
tar czf /backup/redis-backup-$(date +%F).tgz -C /data .
5. Wizard configuration
The wizard reads its config from PowerAdmin (https://admin-api.planningpowertools.com/api/system-config). Critical keys:
| Key | Default | Purpose |
|---|---|---|
system.solver.api_base_url | https://api.planningpowertools.com | Where the wizard POSTs solves. |
system.wizard.max_entities | 1000 | Soft cap on Step 2 row count. |
system.wizard.max_planning_variables | 4 | Max variables in Step 3. |
system.wizard.max_values_per_variable | 500 | Max value-range items per variable. |
system.wizard.max_constraints | 100 | Max Step-4 entries. |
system.wizard.default_solver_timeout | 120 | Step 6 default. |
The wizard's ConfigService falls back to hardcoded defaults if PowerAdmin is unreachable. To change a production setting, update the row in PowerAdmin's system_config table and restart the wizard container.
6. Network architecture
All PowerSolver-stack containers join the shared pdp-network Docker network. This lets PowerOrchestrator reach http://powersolver-api:8001/api/solve by container DNS — no hostname needed inside the cluster.
The deploy-remote.sh script creates pdp-network automatically before any container starts. If you bring up containers manually:
docker network create pdp-network # one-time
docker compose up -d # joins the existing network
7. Logs
powersolver-api logs to stdout (default Spring Boot). Aggregate via:
docker logs -f powersolver-api
journalctl -u docker -f # if docker-compose runs as a systemd unit
docker compose logs -f --tail=100 powersolver-api
Useful log levels are pre-configured in application.yml:
| Logger | Level |
|---|---|
root | INFO |
ai.timefold | INFO (solver progress lines) |
com.powersolver | DEBUG (our own classes) |
To raise/lower verbosity at runtime, set LOGGING_LEVEL_<package> env vars and restart the container.
8. Health and monitoring
| Endpoint | Returns |
|---|---|
GET /api/health | {"status":"UP","version":"2.0.0","timestamp":"…"} |
GET /api/version | engine version, Timefold version, build time — check it after every deployment |
The other endpoints (POST /api/validate, /api/check, /api/solve) are described in the API Reference. /api/check ("Can this work?") never solves, except its optional quick plan (?trialSeconds=N, at most 60 s), which counts as a solve for the concurrency limit below.
The container's HEALTHCHECK directive calls /api/health every 30 seconds. docker ps shows (healthy) next to the container name when the last 3 probes succeeded.
For HTTP-level monitoring, point your uptime tool at https://api.planningpowertools.com/api/health (expect HTTP 200 + "status":"UP").
For ongoing workload monitoring, the rubric that carried the launch is still the right one to watch: container restarts, 5xx rate, p95 latency, corpus pass-rate, perf budget, quality gate, and P1/P2 incident count.
9. Common operations
Restart the solver
cd /opt/PowerProjects/PowerSolver/powersolver-api
docker compose restart powersolver-api
# OR full recreate (picks up new image after rebuild):
docker compose build && docker compose up -d
Restart with new code
cd /opt/PowerProjects/PowerSolver/powersolver-api
git pull
docker compose build
docker compose up -d
# Redis is unaffected; in-flight requests get HTTP 502 briefly.
Roll back
If a deployment misbehaves within the rollback window, git checkout the previous tag and rebuild:
cd /opt/PowerProjects/PowerSolver/powersolver-api
git checkout <previous-tag>
docker compose build
docker compose up -d
Redis volume persists across this — historical audit data unaffected.
Inspect the schema-hash cache
PowerSolver caches generated domain classes by schema hash. Cache evictions only happen on container restart. To force a cache flush:
docker compose restart powersolver-api
10. Limits and performance tuning
Limits (powersolver-api/src/main/resources/application.yml, powersolver.limits.*; overridable by environment, e.g. POWERSOLVER_LIMITS_MAXCONCURRENTSOLVES=6 — Spring drops the dashes):
| Limit | Default | Beyond it |
|---|---|---|
max-items | 5,000 per collection | 400 TOO_LARGE |
max-timeout-seconds | 600 | 400 BAD_CONFIG |
max-concurrent-solves | 4 | 429 BUSY, Retry-After: 30 (the wizard says "The planner is busy with other plans") |
A solve answers on the same HTTP request: the reverse proxy must allow at least the maximum time limit (proxy_read_timeout 660s, see installation/HETZNER_GUIDE.md). A solve runs on one thread (no moveThreadCount is set): size the server for max-concurrent-solves cores plus one.
| Symptom | Lever |
|---|---|
| Solver fails to find feasibility on large problems | Increase config.timeoutSeconds in the request JSON. |
| OOM / GC pressure | Raise JAVA_OPTS=-Xmx. Default 2GB; the perf reference (50 tasks × 3 hosts) runs comfortably under that. |
| Slow move-eval throughput | The supplier-shadow hot loop is reflection-cached per Class. If you see ~500 mv/s on simple problems, redeploy — that's the pre-cache regime indicating a bad build. After cache (current main), expect 1500–2600 mv/s on small problems, ~650 mv/s on the 50-task stress case. |
| First request takes 5–10s while later ones are fast | First-time per-schema class generation (~3–5s) + JIT warm-up. Subsequent same-schema requests hit the process-global cache. |
11. Integration with sister products
PowerAdmin
Provides sign-in (users, tenant), the wizard's form fields per problem type (wizard_fields, by time_grain assignment or sequence, including time-slot fields) and system config.
PowerAI
Generates problem JSON from the wizard's plain-language intake (step 0) and gives advice; it never computes the plan.
PowerOrchestrator
Runs pipelines with a planning stage. Before solving it calls /api/check (falls back to /api/validate on an older engine): an impossible verdict stops the pipeline with the reasons; "at risk" is recorded as an audit event. Its UI shows the Results page and the same messages as the wizard: the shared files and texts are copied by powersolver-wizard/tools/sync-shared.mjs (including powerorchestrator-api/app/engine/solver_feedback_en.json), so deploy PowerSolver and PowerOrchestrator together after a change to those files.
PowerTesting
End-to-end test framework. Health checks, contract conformance, network connectivity, full pipeline runs. See D:\PowerProjects\PowerTesting\.
12. Troubleshooting
Container won't start
docker logs powersolver-api
Common causes:
- Port 8001 already bound. Check
netstat -ano | findstr :8001. - JVM OOM at startup. Raise
JAVA_OPTS=-Xmx2048m(default). - Redis sidecar isn't reachable. Container starts but health checks fail.
docker psforpowersolver-redis.
/api/health returns 503
The Spring Boot context started but a sub-component is unhealthy. docker logs will show which one (typically: cannot resolve redis hostname powersolver-redis — fix by ensuring both containers are on pdp-network).
/api/solve returns 400
The problem was refused by validation: the body lists each issue with a code, a path and a message. The wizard shows them in plain words with the step to correct. Frequent causes: a rule names a property no item has, a value of the wrong kind, an unknown id, a list above 5,000 rows, a time limit above 600 s. A derivedShadow expression references X with no known root prefix comes from a caller that drops this. prefixes.
/api/solve returns 429
All solve slots are busy (max-concurrent-solves). Retry after 30 s, or raise the limit if the server has the cores.
/api/solve returns 500
Solver internal error. Capture the request JSON + the trace field of the response into an incident report. Likely root causes:
- ByteBuddy class generation failure (rare; logged with stack trace).
- Solver hit a Timefold edge case. Usually fixable by raising or lowering
timeoutSecondsor simplifying the problem.
Slow first request after deploy
Expected — DomainAssembler is generating + caching the domain classes for this schema. Subsequent requests with the same schema are fast.
13. Where to go next
- Developer Guide — extending the solver
- API Reference — request/response details