tutorial

How to Monitor Apache Cayenne ORM Health with Vigilmon

Apache Cayenne connection pool exhaustion, query cache corruption, and DataDomain consistency failures are invisible until your application starts throwing persistence exceptions. Learn how to monitor Cayenne ORM availability, connection pool health, and scheduled cache eviction job liveness with Vigilmon HTTP probes and heartbeat monitors.

Apache Cayenne is an open-source Java ORM framework that manages object-relational mapping, connection pooling, and query caching for Java applications connecting to relational databases. When a Cayenne connection pool is exhausted under a sudden traffic spike, queries queue and eventually time out while the application process is still alive and responding to health checks that bypass the persistence layer; when Cayenne's shared query cache grows stale after a bulk database update applied directly via SQL, subsequent reads return incorrect cached data without any exception or log warning; when a DataDomain consistency check fails during startup after a failed schema migration, the application enters a degraded state where some object graphs load correctly and others throw CayenneRuntimeException mid-request — all while the HTTP layer reports 200. These are silent correctness and availability failures that external monitoring catches before user-facing errors accumulate.

Vigilmon gives you external visibility into Cayenne-backed application health through HTTP probe monitoring and heartbeat monitors. This tutorial covers both.


Why Cayenne Applications Need External Monitoring

Cayenne's failure modes span the persistence and caching layers:

  • Connection pool exhaustion: Cayenne manages a pool of JDBC connections; under high concurrency, all connections are checked out simultaneously and new requests block on DataNode.getConnection() — the application remains responsive for endpoints that do not touch the database while every database-backed endpoint times out
  • Query cache staleness: Cayenne's shared QueryCache survives application reuse cycles; after a direct-SQL bulk update that bypasses Cayenne's change detection, cached query results remain stale indefinitely until the next cache flush — reads return outdated rows silently
  • DataDomain startup failure: when a schema migration leaves a column mapping inconsistent, Cayenne's DataDomain validation at startup throws and the application enters a state where only pre-cached objects load correctly while fresh queries fail
  • Nested transaction rollback propagation: Cayenne's ObjectContext propagates rollbacks up through nested contexts; a propagation failure during a multi-step commit can leave child contexts with inconsistent object state that causes serialization exceptions on subsequent operations
  • Relationship prefetch timeout: a deeply nested prefetch tree on a slow query can hold a JDBC connection open for the duration of the prefetch, starving the pool for parallel requests
  • Persistent object lifecycle exceptions: an ObjectContext that outlives its thread (stored in a session or singleton) can receive stale objects that throw DetachedObjectException when touched from a new thread

External monitoring with Vigilmon adds:

  • Proactive alerting when Cayenne-backed endpoints stop responding successfully
  • Connection pool health visibility through a dedicated pool probe endpoint
  • Heartbeat monitoring so you know when scheduled cache eviction or schema sync jobs stop completing
  • Multi-region probe consensus that separates transient GC pauses from genuine pool exhaustion failures

Step 1: Build a Cayenne Health Endpoint

Cayenne does not ship a built-in health endpoint. Add one to your application that exercises the connection pool and returns a structured response.

Spring Boot Application (Spring + Cayenne)

// health/CayenneHealthIndicator.java
package com.yourorg.health;

import org.apache.cayenne.ObjectContext;
import org.apache.cayenne.configuration.server.ServerRuntime;
import org.apache.cayenne.query.SQLSelect;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

@Component
public class CayenneHealthIndicator implements HealthIndicator {

    private final ServerRuntime cayenneRuntime;

    public CayenneHealthIndicator(ServerRuntime cayenneRuntime) {
        this.cayenneRuntime = cayenneRuntime;
    }

    @Override
    public Health health() {
        try {
            ObjectContext ctx = cayenneRuntime.newContext();
            // Lightweight connectivity check — returns 1 row from the DB
            SQLSelect<Integer> probe = SQLSelect.scalarQuery(
                Integer.class, "SELECT 1 AS probe"
            );
            Integer result = probe.selectOne(ctx);
            if (result == null || result != 1) {
                return Health.down()
                    .withDetail("cayenne", "probe query returned unexpected result")
                    .build();
            }
            return Health.up()
                .withDetail("cayenne", "connected")
                .withDetail("domain", cayenneRuntime.getName())
                .build();
        } catch (Exception e) {
            return Health.down(e)
                .withDetail("cayenne", "connection pool unavailable")
                .build();
        }
    }
}

Expose the Actuator health endpoint in application.properties:

management.endpoint.health.show-details=always
management.endpoints.web.exposure.include=health,info

The /actuator/health response will include the Cayenne component:

{
  "status": "UP",
  "components": {
    "cayenne": {
      "status": "UP",
      "details": {
        "cayenne": "connected",
        "domain": "MyApplication"
      }
    },
    "db": { "status": "UP" }
  }
}

Standalone Servlet / JAX-RS Application

// servlet/CayenneHealthServlet.java
package com.yourorg.servlet;

import org.apache.cayenne.ObjectContext;
import org.apache.cayenne.configuration.server.ServerRuntime;
import org.apache.cayenne.query.SQLSelect;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;

@WebServlet("/health/cayenne")
public class CayenneHealthServlet extends HttpServlet {

    private ServerRuntime cayenneRuntime;

    @Override
    public void init() {
        cayenneRuntime = (ServerRuntime) getServletContext()
            .getAttribute("cayenneRuntime");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        Map<String, Object> result = new LinkedHashMap<>();
        resp.setContentType("application/json");

        try {
            ObjectContext ctx = cayenneRuntime.newContext();
            SQLSelect<Integer> probe = SQLSelect.scalarQuery(
                Integer.class, "SELECT 1 AS probe"
            );
            Integer val = probe.selectOne(ctx);
            boolean ok = val != null && val == 1;

            result.put("status", ok ? "ok" : "degraded");
            result.put("domain", cayenneRuntime.getName());
            resp.setStatus(ok ? 200 : 503);
        } catch (Exception e) {
            result.put("status", "down");
            result.put("error", e.getMessage());
            resp.setStatus(503);
        }

        resp.getWriter().write(toJson(result));
    }

    private String toJson(Map<String, Object> map) {
        StringBuilder sb = new StringBuilder("{");
        map.forEach((k, v) -> {
            if (sb.length() > 1) sb.append(",");
            sb.append("\"").append(k).append("\":\"").append(v).append("\"");
        });
        sb.append("}");
        return sb.toString();
    }
}

Node.js Sidecar

// health/cayenne.js
const express = require('express');
const { Pool } = require('pg'); // replace with your DB driver

const app = express();
const pool = new Pool({
  host: process.env.DB_HOST || 'localhost',
  port: parseInt(process.env.DB_PORT || '5432'),
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASS,
  connectionTimeoutMillis: 5000,
  max: 2,
});

app.get('/health/cayenne', async (req, res) => {
  try {
    const { rows } = await pool.query('SELECT 1 AS probe');
    const ok = rows[0]?.probe === 1;
    if (!ok) return res.status(503).json({ status: 'degraded' });
    return res.status(200).json({ status: 'ok' });
  } catch (err) {
    return res.status(503).json({ status: 'down', error: err.message });
  }
});

app.listen(3019);

Python Sidecar

# health_cayenne.py
import os
import psycopg2
from flask import Flask, jsonify

app = Flask(__name__)

DB_CONFIG = {
    'host': os.environ.get('DB_HOST', 'localhost'),
    'port': int(os.environ.get('DB_PORT', '5432')),
    'dbname': os.environ.get('DB_NAME', 'myapp'),
    'user': os.environ.get('DB_USER', 'app'),
    'password': os.environ.get('DB_PASS', ''),
    'connect_timeout': 5,
}

@app.route('/health/cayenne')
def health():
    try:
        conn = psycopg2.connect(**DB_CONFIG)
        cur = conn.cursor()
        cur.execute('SELECT 1')
        result = cur.fetchone()
        cur.close()
        conn.close()
        if result and result[0] == 1:
            return jsonify({'status': 'ok'})
        return jsonify({'status': 'degraded'}), 503
    except Exception as e:
        return jsonify({'status': 'down', 'error': str(e)}), 503

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=3019)

Step 2: Configure Vigilmon HTTP Monitor for Cayenne

  1. Log in to vigilmon.online and go to Monitors → New Monitor
  2. Choose HTTP / HTTPS
  3. Set the URL to your Cayenne health endpoint: https://app.your-org.example.com/actuator/health
  4. Set the check interval to 1 minute
  5. Under Expected response, configure:
    • Status code: 200
    • Response body contains: "status":"UP"
    • Response time threshold: 3000ms
  6. Under Alert channels, assign your Slack or PagerDuty channel
  7. Save the monitor

Add a second monitor for a representative database-backed API endpoint to confirm end-to-end query execution:

  • URL: https://app.your-org.example.com/api/v1/ping (a lightweight endpoint that runs a Cayenne query)
  • Expected: 200
  • Interval: 2 minutes
  • Alert channel: PagerDuty P1 — failure here means the connection pool is exhausted or the database is unreachable

For applications with multiple DataNodes (multiple database connections), add one monitor per data node health path:

| Endpoint | What it catches | |---|---| | /actuator/health | Cayenne domain, DB connectivity, disk space | | /api/v1/ping | End-to-end query execution via Cayenne ObjectContext | | /health/cayenne (custom) | Connection pool probe, pool exhaustion |

Vigilmon's multi-region probing filters single-region network blips from genuine Cayenne pool failures.


Step 3: Heartbeat Monitoring for Cache Eviction and Schema Sync Jobs

HTTP health checks verify Cayenne connectivity but not the health of background jobs that maintain cache integrity and schema consistency. Cayenne applications often run:

  • Scheduled query cache eviction: periodic flush of Cayenne's shared QueryCache to prevent stale reads after bulk SQL updates
  • Schema synchronization checks: periodic DbSyncMode comparison between the Cayenne mapping model and the live database schema
  • Data validation jobs: batch jobs that traverse Cayenne object graphs to enforce referential integrity rules not captured by DB constraints

Vigilmon heartbeat monitors detect when these jobs stop executing.

Set Up Heartbeat Monitors

For cache eviction job liveness:

  1. In Vigilmon, go to Monitors → New Monitor → Heartbeat
  2. Set the name: cayenne-cache-eviction
  3. Set the expected interval: 30 minutes
  4. Set the grace period: 60 minutes
  5. Save — copy the heartbeat URL

For schema sync job liveness:

  1. Create another Heartbeat monitor named cayenne-schema-sync
  2. Set the expected interval: 24 hours
  3. Set the grace period: 26 hours
  4. Save — copy the heartbeat URL

Wire Heartbeats Into Scheduled Jobs

// scheduler/CayenneCacheEvictionJob.java
package com.yourorg.scheduler;

import org.apache.cayenne.cache.QueryCache;
import org.apache.cayenne.configuration.server.ServerRuntime;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.net.URI;
import java.net.http.*;

@Service
public class CayenneCacheEvictionJob {

    private final ServerRuntime cayenneRuntime;
    private final HttpClient http = HttpClient.newHttpClient();
    private final String heartbeatUrl = System.getenv("VIGILMON_CACHE_EVICTION_HEARTBEAT_URL");

    public CayenneCacheEvictionJob(ServerRuntime cayenneRuntime) {
        this.cayenneRuntime = cayenneRuntime;
    }

    @Scheduled(fixedDelay = 1800000) // every 30 minutes
    public void evictQueryCache() {
        try {
            QueryCache cache = cayenneRuntime.getDataDomain().getQueryCache();
            cache.clear();
            pingHeartbeat();
        } catch (Exception e) {
            // log — do NOT ping heartbeat on failure
            throw new RuntimeException("Cache eviction failed", e);
        }
    }

    private void pingHeartbeat() {
        if (heartbeatUrl == null || heartbeatUrl.isBlank()) return;
        try {
            http.sendAsync(
                HttpRequest.newBuilder(URI.create(heartbeatUrl)).GET().build(),
                HttpResponse.BodyHandlers.discarding()
            );
        } catch (Exception ignored) {}
    }
}

For Quartz-based schema sync jobs:

// quartz/SchemaSyncJob.java
package com.yourorg.quartz;

import org.apache.cayenne.configuration.server.ServerRuntime;
import org.apache.cayenne.dba.DbAdapter;
import org.quartz.*;
import java.net.URI;
import java.net.http.*;

public class SchemaSyncJob implements Job {

    private final String heartbeatUrl = System.getenv("VIGILMON_SCHEMA_SYNC_HEARTBEAT_URL");

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        try {
            // Run schema comparison logic
            runSchemaComparison();
            pingHeartbeat();
        } catch (Exception e) {
            throw new JobExecutionException("Schema sync failed", e);
        }
    }

    private void runSchemaComparison() {
        // Compare Cayenne DataMap with live DB schema
    }

    private void pingHeartbeat() {
        if (heartbeatUrl == null) return;
        try {
            HttpClient.newHttpClient().sendAsync(
                HttpRequest.newBuilder(URI.create(heartbeatUrl)).GET().build(),
                HttpResponse.BodyHandlers.discarding()
            );
        } catch (Exception ignored) {}
    }
}

For shell-based batch scripts that trigger Cayenne operations:

#!/bin/bash
# cayenne-batch-job.sh
set -e

java -jar /opt/app/cayenne-batch.jar --job=data-validation

if [ -n "$VIGILMON_BATCH_HEARTBEAT_URL" ]; then
  curl -s "$VIGILMON_BATCH_HEARTBEAT_URL" > /dev/null 2>&1 || true
fi

Step 4: Alert Routing for Cayenne Failures

Cayenne failures range from connection pool exhaustion affecting all database-backed endpoints to silent stale cache reads that corrupt business logic. Route alerts by impact:

| Monitor | Alert Channel | Priority | |---|---|---| | HTTP: /actuator/health Cayenne component | Slack + PagerDuty | P1 | | HTTP: database-backed API endpoint | Slack + PagerDuty | P1 | | HTTP: custom connection pool probe | Slack + PagerDuty | P1 | | Heartbeat: cache eviction job | Slack | P2 | | Heartbeat: schema sync job | Slack + email | P2 | | Heartbeat: data validation batch | Slack + email | P2 |

Set response time thresholds as early warning signals:

  • Alert at 1500ms for the Actuator health check (slow responses indicate pool pressure before exhaustion)
  • Alert at 2000ms for the database-backed API endpoint (slow queries indicate index degradation or pool saturation)
  • Alert at 60 minutes gap in cache eviction heartbeat (stale cache risk accumulating)
  • Alert at 26 hours gap in schema sync heartbeat during the expected maintenance window

For production Cayenne deployments backing multi-tenant SaaS, set up a status page in Vigilmon showing persistence layer availability — this gives operations teams a single pane of glass to correlate Cayenne pool alerts with upstream database incidents before users report data inconsistencies.


Summary

Cayenne failures typically present as gradual degradation — connection pool pressure builds until requests start queuing, or stale cache reads corrupt business logic silently — rather than immediate hard failures. External monitoring catches these earlier than your users do:

| Monitor Type | What It Covers | |---|---| | HTTP monitor on /actuator/health | Cayenne domain, JDBC connectivity, disk space | | HTTP monitor on API endpoint | End-to-end Cayenne ObjectContext query execution | | HTTP monitor on pool probe | JDBC connection pool availability and latency | | Heartbeat monitor (cache eviction) | Scheduled query cache flush job liveness | | Heartbeat monitor (schema sync) | DataMap-to-schema consistency check job liveness |

Get started free at vigilmon.online — your first Cayenne persistence monitor is running in under two minutes.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →