Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
b8194f5
Initial plan
Copilot Mar 17, 2026
bc3c983
feat: add next-page prefetch cache for paginated SELECT queries
Copilot Mar 17, 2026
eb9ac07
refactor: improve thread naming and reduce duplication in executeQuer…
Copilot Mar 17, 2026
563e3f8
feat: materialise CLOB/NCLOB return columns in prefetch cache as String
Copilot Mar 17, 2026
77195b9
fix: datasource-isolated cache keys + background cleanup job for next…
Copilot Mar 17, 2026
268cdaa
fix: single shared static cleanup executor guarantees one background …
Copilot Mar 17, 2026
e953c30
feat(test): Postgres pagination cache integration test with BYTEA LOB…
Copilot Mar 17, 2026
73a4d18
fix(paging): use virtual thread for CLEANUP_EXECUTOR thread factory
Copilot Mar 17, 2026
a2a9726
feat(paging): per-datasource prefetchWaitTimeoutMs configuration
Copilot Mar 17, 2026
1d3d878
fix(jdbc): skip remote close() when ResultSet has no server-side UUID…
Copilot Mar 17, 2026
59ca642
fix(paging): fix Sonar issues — resource leaks, dead code, unused imp…
Copilot Mar 17, 2026
8c71a39
fix(paging): fix remaining Sonar issues — duplicate conditions, regex…
Copilot Mar 17, 2026
0293f8f
test: parameterize detection tests, add assertion, fix duplicate @Aft…
Copilot Mar 17, 2026
fab19cd
test: address review comments on test classes (round 2)
Copilot Mar 17, 2026
76f8de7
docs: add prefetch cache feature docs and refactor buildNextPageSql t…
Copilot Mar 17, 2026
27a02cd
feat: add per-datasource cache enabled flag with tests and docs update
Copilot Mar 18, 2026
37a84bf
refactor: per-datasource cache enabled is client-side property (ojp.n…
Copilot Mar 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,15 @@ jobs:
-e JAVA_TOOL_OPTIONS="-Dojp.server.port=10593 -Dojp.prometheus.port=9163 -Dojp.server.slowQuerySegregation.enabled=true -Dojp.sql.enhancer.enabled=true -Dojp.sql.enhancer.mode=OPTIMIZE -Dojp.sql.enhancer.dialect=POSTGRESQL" \
rrobetti/ojp:0.4.1-SNAPSHOT
# Start third OJP server WITH next-page prefetch cache enabled
# Pagination-cache integration tests run against this server (port 10594)
- name: Start OJP Server container (prefetch cache on port 10594)
run: |
docker run -d --name ojp-server-prefetch-cache \
--network host \
-e JAVA_TOOL_OPTIONS="-Dojp.server.port=10594 -Dojp.prometheus.port=9164 -Dojp.server.slowQuerySegregation.enabled=true -Dojp.server.nextPageCache.enabled=true -Dojp.server.nextPageCache.ttlSeconds=60 -Dojp.server.nextPageCache.prefetchWaitTimeoutMs=5000" \
rrobetti/ojp:0.4.1-SNAPSHOT
- name: Wait for ojp-server to start
run: sleep 10

Expand All @@ -235,7 +244,7 @@ jobs:

# Run PostgreSQL-specific tests with -DenablePostgresTests flag
- name: Test (ojp-jdbc-driver) with PostgreSQL enabled
run: mvn test -pl ojp-jdbc-driver -Dgpg.skip=true -DenablePostgresTests=true
run: mvn test -pl ojp-jdbc-driver -Dgpg.skip=true -DenablePostgresTests=true -DenablePostgresPrefetchCacheTests=true

# ===================================================================
# SQL Enhancer Integration Test
Expand Down Expand Up @@ -267,6 +276,9 @@ jobs:
echo ""
echo "=== OJP Server (with SQL enhancer) log ==="
docker logs ojp-server-enhancer 2>&1 || echo "ojp-server-enhancer container not found"
echo ""
echo "=== OJP Server (with prefetch cache) log ==="
docker logs ojp-server-prefetch-cache 2>&1 || echo "ojp-server-prefetch-cache container not found"
# ===========================================================================
# JOB 3: MySQL Integration Tests
Expand Down
8 changes: 8 additions & 0 deletions documents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ Located in [connection-pool/](connection-pool/):
Located in [analysis/](analysis/):
- [Transaction Isolation Handling](analysis/TRANSACTION_ISOLATION_HANDLING.md) - Complete technical documentation on transaction isolation reset behavior

## Features

Located in [features/](features/):
- [Next-Page Prefetch Cache](features/NEXT_PAGE_PREFETCH_CACHE.md) - Transparent background pre-fetching of the next query page to eliminate round-trip latency in paginated result sets
- [SQL Enhancer Engine Quickstart](features/SQL_ENHANCER_ENGINE_QUICKSTART.md) - SQL optimisation using Apache Calcite (experimental)
- [SQL Enhancer Configuration Examples](features/SQL_ENHANCER_CONFIGURATION_EXAMPLES.md) - Configuration examples for the SQL enhancer

## Database Setup Guides

Located in [environment-setup/](environment-setup/):
Expand Down Expand Up @@ -158,6 +165,7 @@ documents/
├── contributor-badges/ # Recognition program
├── designs/ # Design documents
├── environment-setup/ # Database setup guides
├── features/ # Feature guides and documentation
├── fixed-issues/ # Issue fix documentation
├── guides/ # Developer guides
├── images/ # Diagrams and images
Expand Down
65 changes: 65 additions & 0 deletions documents/configuration/ojp-server-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,71 @@ For full integration examples including Docker Compose setups, see the **[Teleme
| `ojp.server.slowQuerySegregation.slowSlotTimeout` | `OJP_SERVER_SLOWQUERYSEGREGATION_SLOWSLOTTIMEOUT` | long | 120000 | Timeout for acquiring slow operation slots (ms) | 0.2.0-beta |
| `ojp.server.slowQuerySegregation.fastSlotTimeout` | `OJP_SERVER_SLOWQUERYSEGREGATION_FASTSLOTTIMEOUT` | long | 60000 | Timeout for acquiring fast operation slots (ms) | 0.2.0-beta |

### Next-Page Prefetch Cache Settings

The prefetch cache transparently pre-executes the **next page query** in the background while the current page is being sent to the client. When the client requests the next page, the rows are served from memory instead of hitting the database again, significantly reducing perceived latency for paginated result sets.

The cache detects SQL pagination clauses automatically (`LIMIT/OFFSET`, `OFFSET … FETCH`, `FETCH FIRST … ROWS ONLY`, MySQL `LIMIT m, n`, and standalone `LIMIT n`). No client changes are needed — the feature is entirely transparent.

| Property | Environment Variable | Type | Default | Description | Since |
|---|---|---|---|---|---|
| `ojp.server.nextPageCache.enabled` | `OJP_SERVER_NEXTPAGECACHE_ENABLED` | boolean | false | Enable/disable the next-page prefetch cache globally | 0.4.1 |
| `ojp.server.nextPageCache.ttlSeconds` | `OJP_SERVER_NEXTPAGECACHE_TTLSECONDS` | long | 60 | Maximum time (seconds) a cached page is kept before being discarded | 0.4.1 |
| `ojp.server.nextPageCache.maxEntries` | `OJP_SERVER_NEXTPAGECACHE_MAXENTRIES` | int | 100 | Maximum number of cache entries across all datasources | 0.4.1 |
| `ojp.server.nextPageCache.prefetchWaitTimeoutMs` | `OJP_SERVER_NEXTPAGECACHE_PREFETCHWAITTIMEOUTMS` | long | 5000 | Maximum time (ms) to wait for a prefetch to complete before falling back to a live query | 0.4.1 |
| `ojp.server.nextPageCache.cleanupIntervalSeconds` | `OJP_SERVER_NEXTPAGECACHE_CLEANUPINTERVALSECONDS` | long | 60 | Interval (seconds) at which the background cleanup thread evicts expired entries | 0.4.1 |
| `ojp.server.nextPageCache.datasource.<name>.prefetchWaitTimeoutMs` | *(no env-var equivalent)* | long | *(global default)* | Per-datasource override for `prefetchWaitTimeoutMs`; `<name>` matches `ojp.datasource.name` on the client | 0.4.1 |

> **Per-datasource `enabled` is a client-side setting.**
> Each datasource in the client application can independently opt in or out of the prefetch cache
> by setting `ojp.nextPageCache.enabled=false` in its `ojp.properties`:
> ```properties
> # ojp.properties — client application
> # Disable the prefetch cache for the "random-access" datasource
> random-access.ojp.nextPageCache.enabled=false
> ```

#### Next-Page Prefetch Cache Configuration Examples

**Enable the cache with default settings:**
```bash
java -Duser.timezone=UTC \
-Dojp.server.nextPageCache.enabled=true \
-jar ojp-server.jar
```

**Enable with custom TTL and wait timeout:**
```bash
java -Duser.timezone=UTC \
-Dojp.server.nextPageCache.enabled=true \
-Dojp.server.nextPageCache.ttlSeconds=30 \
-Dojp.server.nextPageCache.prefetchWaitTimeoutMs=2000 \
-jar ojp-server.jar
```

**Per-datasource wait timeout override (server-side):**
```bash
# Give the "analytics" datasource more time to prefetch large pages
java -Duser.timezone=UTC \
-Dojp.server.nextPageCache.enabled=true \
-Dojp.server.nextPageCache.prefetchWaitTimeoutMs=2000 \
-D"ojp.server.nextPageCache.datasource.analytics.prefetchWaitTimeoutMs=10000" \
-jar ojp-server.jar
```

**Via environment variables:**
```bash
export OJP_SERVER_NEXTPAGECACHE_ENABLED=true
export OJP_SERVER_NEXTPAGECACHE_TTLSECONDS=60
export OJP_SERVER_NEXTPAGECACHE_PREFETCHWAITTIMEOUTMS=5000
export OJP_SERVER_NEXTPAGECACHE_CLEANUPINTERVALSECONDS=60
java -Duser.timezone=UTC -jar ojp-server.jar
```

> **ℹ️ Cache isolation**: Entries are keyed by `datasourceId + normalizedSQL`, so two datasources executing the same query never share cached data.

> **ℹ️ Background cleanup**: A single shared virtual thread (`ojp-prefetch-cache-cleanup`) runs the eviction scan at the configured interval. No additional threads are created regardless of how many datasources are active.

### SQL Enhancer and Schema Loader Settings

> **⚠️ EXPERIMENTAL FEATURE - NOT RECOMMENDED FOR PRODUCTION**
Expand Down
114 changes: 112 additions & 2 deletions documents/ebook/part2-chapter6-server-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,116 @@ graph LR
E --> C
```

## 6.8 Next-Page Prefetch Cache

For applications that page through query results — common in reporting, data exports, and list views — OJP can dramatically reduce latency by pre-executing the **next page query in the background** while the current page is being delivered to the client. When the client then requests the next page, the rows are served directly from memory instead of making a round-trip to the database.

### How It Works

OJP automatically detects SQL pagination clauses in the queries your application already writes. There are no client-side changes needed — the feature is fully transparent. Supported pagination patterns include:

| SQL Pattern | Example |
|---|---|
| `LIMIT n OFFSET m` | `SELECT * FROM orders LIMIT 100 OFFSET 200` |
| `OFFSET m ROWS FETCH NEXT n ROWS ONLY` | SQL Server, Oracle |
| `FETCH FIRST n ROWS ONLY` | DB2, Oracle |
| `LIMIT m, n` | MySQL shorthand |
| Standalone `LIMIT n` | First-page query without OFFSET |

```mermaid
flowchart TD
A([Client requests page N]) --> B{Cache hit?}
B -- Yes --> C[Serve rows from memory]
B -- No --> D[Execute page N SQL against DB]
D --> E[Stream rows to client]
E --> F{SQL is paginated?}
F -- No --> Z([Done])
F -- Yes --> G[Rewrite SQL for page N+1]
G --> H[Start background virtual thread]
H --> I[(Execute page N+1 query against DB)]
I --> J[Materialise all rows in memory]
J --> K[Store in cache keyed by datasource + SQL]
C --> L[Remove entry from cache]
L --> F
```

The cache key combines the datasource identifier and the normalised SQL text, so two datasources running identical queries never see each other's cached data.

### Configuration

The prefetch cache is **disabled by default**. Enable it with a single property:

```bash
java -Duser.timezone=UTC \
-Dojp.server.nextPageCache.enabled=true \
-jar ojp-server.jar
```

**All prefetch cache settings:**

| Property | Default | Description |
|---|---|---|
| `ojp.server.nextPageCache.enabled` | `false` | Enable/disable the feature globally |
| `ojp.server.nextPageCache.ttlSeconds` | `60` | Maximum age (seconds) of a cached page before eviction |
| `ojp.server.nextPageCache.maxEntries` | `100` | Maximum number of in-memory cache entries |
| `ojp.server.nextPageCache.prefetchWaitTimeoutMs` | `5000` | Maximum time (ms) to wait for a prefetch to complete; falls back to a live query on timeout |
| `ojp.server.nextPageCache.cleanupIntervalSeconds` | `60` | Interval (seconds) between background eviction sweeps |
| `ojp.server.nextPageCache.datasource.<name>.prefetchWaitTimeoutMs` | *(global)* | Per-datasource override for the wait timeout (`<name>` matches `ojp.datasource.name` on the client) |

### Per-Datasource Cache Control

The per-datasource `enabled` flag is a **client-side** connection property. Each datasource in the client application can independently opt in or out of the prefetch cache by setting `ojp.nextPageCache.enabled` in its `ojp.properties` file — no server restart needed:

```properties
# ojp.properties — client application
# Default datasource: cache enabled (uses server global default)

# "random-access" datasource: disable the prefetch cache
random-access.ojp.nextPageCache.enabled=false
```

**Per-datasource wait timeout (different DB response times):**

```bash
java -Duser.timezone=UTC \
-Dojp.server.nextPageCache.enabled=true \
-Dojp.server.nextPageCache.prefetchWaitTimeoutMs=2000 \
-D"ojp.server.nextPageCache.datasource.analytics.prefetchWaitTimeoutMs=10000" \
-jar ojp-server.jar
```

### Background Cleanup

A single virtual thread named `ojp-prefetch-cache-cleanup` runs the eviction sweep on a fixed interval, removing entries that are either expired (older than `ttlSeconds`) or abandoned (prefetch still in-flight past the TTL). Only one cleanup thread ever exists per JVM, regardless of how many datasources are active.

```mermaid
flowchart TD
BOOT([JVM starts]) --> EX[Create shared CLEANUP_EXECUTOR\none virtual thread for all instances]
INST([Cache instance created]) --> REG[Schedule evictExpiredOrCompleted\nevery cleanupIntervalSeconds]
REG --> TASK[ScheduledFuture stored per instance]

subgraph TICK [Every cleanupIntervalSeconds]
T1[Iterate all entries] --> T2{Entry done or failed?}
T2 -- Yes + expired --> T3[Remove entry]
T2 -- No, still in-flight --> T4{Older than ttlSeconds?}
T4 -- Yes --> T5[Cancel prefetch future\nRemove entry]
T4 -- No --> T6[Keep entry]
end
EX --> TICK
```

### When to Enable It

The prefetch cache delivers the most benefit when:

- Your application pages through results **sequentially** (page 1, 2, 3, …) rather than jumping to arbitrary offsets.
- The database round-trip latency is noticeable (> 50 ms) for each page query.
- Pagination page sizes are consistent across requests for the same query.

It has minimal impact (and adds slight overhead) when queries jump to random offsets, when all rows fit on a single page, or when the database is so fast that the prefetch rarely completes before the client requests the next page.

**[IMAGE PROMPT: Create a timeline diagram showing two scenarios side-by-side. Left side: "Without Prefetch Cache" showing sequential client requests each waiting for a DB round-trip. Right side: "With Prefetch Cache" showing the next page being pre-fetched while the current page is delivered, with the second request served instantly from memory. Use a horizontal timeline axis labeled "Time" with colored blocks for DB calls and client waits. Style: Performance comparison diagram with green (fast) vs gray (waiting) blocks.]**

## 6.9 Configuration Validation and Troubleshooting

When things don't work as expected, configuration issues are often the culprit. OJP provides clear error messages when configuration values are invalid or inconsistent. The server validates configuration at startup and fails fast if critical settings are problematic.
Expand Down Expand Up @@ -434,8 +544,8 @@ The server logs its active configuration at INFO level during startup. Review th

OJP server configuration gives you precise control over server behavior, security, performance, and observability. The hierarchical configuration system with JVM properties and environment variables provides flexibility for different deployment scenarios. Default settings work well for most use cases, but understanding the available options lets you optimize for your specific workload.

Key configuration areas include core server settings for network and threading, security controls through IP whitelisting, logging levels for operational visibility, OpenTelemetry integration for observability, circuit breakers for resilience, and slow query segregation for performance under mixed workloads. Each area offers sensible defaults that you can refine based on monitoring data.
Key configuration areas include core server settings for network and threading, security controls through IP whitelisting, logging levels for operational visibility, OpenTelemetry integration for observability, circuit breakers for resilience, slow query segregation for performance under mixed workloads, and the next-page prefetch cache for transparently accelerating paginated queries. Each area offers sensible defaults that you can refine based on monitoring data.

Start simple, monitor closely, and adjust based on observed behavior. Good configuration emerges from understanding your workload and using OJP's flexibility to match it, not from cargo-culting settings from other environments.

**[IMAGE PROMPT: Create a summary mind map with "OJP Server Configuration" at the center. Six main branches radiating outward: "Core Settings" (server icon), "Security" (lock icon), "Logging" (document icon), "Telemetry" (graph icon), "Circuit Breaker" (shield icon), and "Slow Query Segregation" (speedometer icon). Each branch has 2-3 sub-branches with key points. Use colors to group related concepts and make it visually hierarchical. Style: Modern mind map with icons and color coding.]**
**[IMAGE PROMPT: Create a summary mind map with "OJP Server Configuration" at the center. Seven main branches radiating outward: "Core Settings" (server icon), "Security" (lock icon), "Logging" (document icon), "Telemetry" (graph icon), "Circuit Breaker" (shield icon), "Slow Query Segregation" (speedometer icon), and "Prefetch Cache" (cache/memory icon). Each branch has 2-3 sub-branches with key points. Use colors to group related concepts and make it visually hierarchical. Style: Modern mind map with icons and color coding.]**
Loading
Loading