tutorial

How to Monitor Apache Chemistry CMIS Server Health with Vigilmon

Apache Chemistry OpenCMIS server checkout locks freeze documents, repository cache invalidation causes stale object retrieval, and large binary stream uploads exhaust server memory silently. Learn how to monitor Chemistry CMIS availability, repository health, and content ingestion pipeline liveness with Vigilmon HTTP probes and heartbeat monitors.

Apache Chemistry is an open-source implementation of the Content Management Interoperability Services (CMIS) standard, providing the OpenCMIS client library and the OpenCMIS InMemory/FileShare/Bridge server for enterprise content management system integration. Chemistry orchestrates document checkout/checkin workflows, ACL-controlled folder hierarchies, and rendition streams — when a CMIS client takes a checkout lock on a document and crashes before calling cancelCheckOut, the document remains locked indefinitely with no expiry mechanism in the CMIS 1.1 spec, blocking all other clients from modifying it; when Chemistry's repository cache is filled and then invalidated by a configuration change, all subsequent object retrievals return CmisObjectNotFoundException for objects that exist in the backing store until the cache warms up — document management systems see missing files for minutes to hours while the CMIS service reports UP; when a large binary upload through the content stream interface exhausts the configured Jetty or Tomcat request buffer and throws IOException: Connection reset, the partial content stream is not cleaned up and orphaned temporary files accumulate on disk. These are content management integrity failures disguised as transient client errors.

Vigilmon gives you external visibility into Chemistry's CMIS layer through HTTP probe monitoring and heartbeat monitors. This tutorial covers both.


Why Apache Chemistry Needs External Monitoring

Chemistry's failure modes are subtle and disruptive to document workflows:

  • Checkout lock accumulation: CMIS has no server-side checkout expiry; crashed or misbehaving clients leave persistent checkout locks that block document modification indefinitely — the CMIS service reports healthy while entire document sets are uneditable
  • Repository cache invalidation stall: Chemistry's object cache serves repeated reads efficiently but can become inconsistent after repository reconfiguration; during re-warm, object lookups throw CmisObjectNotFoundException for existing documents — ECM clients interpret these as deleted files
  • Large content stream memory exhaustion: streaming large binaries through Chemistry holds the content in Jetty's request buffer memory before writing to the backing store; multiple concurrent large uploads on a heap-constrained server cause OOM that kills the JVM with no graceful error to clients
  • ACL engine failure after schema migration: Chemistry's ACL enforcement relies on permission mappings in the repository configuration; a mapping migration that removes a principal causes all permission checks for that principal to throw CmisPermissionDeniedException — users lose all access to documents they own
  • CMIS query result truncation: Chemistry's query engine enforces a configurable maxItems limit on CMIS query results; a misconfigured limit of 0 (disabled safety check) after an upgrade causes query tools to page indefinitely on result sets that never terminate
  • Rendition service failure: Chemistry generates document renditions (thumbnails, PDF previews) via a registered rendition service; a missing LibreOffice installation after a server migration causes all rendition requests to return 500 while document retrieval continues to work, silently breaking document preview in ECM UIs

External monitoring with Vigilmon adds:

  • Proactive alerting when Chemistry's CMIS AtomPub or Browser endpoints return errors
  • Repository health monitoring independent of the CMIS client negotiation state
  • Heartbeat monitoring so you know when scheduled content ingestion, rendition generation, and archive export jobs stop completing
  • Multi-region probe consensus that filters transient cache invalidation blips from genuine Chemistry service failures

Step 1: Build a Chemistry CMIS Health Endpoint

Chemistry's OpenCMIS InMemory server exposes an AtomPub endpoint at /atom11/ and a Browser binding at /browser/. Build a health wrapper that exercises the repository layer.

Java CMIS Health Endpoint

// health/ChemistryHealthServlet.java
package com.yourorg.chemistry.health;

import org.apache.chemistry.opencmis.client.api.*;
import org.apache.chemistry.opencmis.client.runtime.SessionFactoryImpl;
import org.apache.chemistry.opencmis.commons.SessionParameter;
import org.apache.chemistry.opencmis.commons.enums.BindingType;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;

public class ChemistryHealthServlet extends HttpServlet {

    private final String cmisUrl = System.getenv("CMIS_URL");
    private final String cmisUser = System.getenv("CMIS_USER");
    private final String cmisPass = System.getenv("CMIS_PASS");
    private final String repoId = System.getenv("CMIS_REPO_ID");

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        Map<String, String> checks = new LinkedHashMap<>();
        boolean anyFailed = false;

        // Test CMIS session creation and repository access
        try {
            Map<String, String> params = new HashMap<>();
            params.put(SessionParameter.USER, cmisUser);
            params.put(SessionParameter.PASSWORD, cmisPass);
            params.put(SessionParameter.ATOMPUB_URL, cmisUrl);
            params.put(SessionParameter.BINDING_TYPE, BindingType.ATOMPUB.value());
            params.put(SessionParameter.REPOSITORY_ID, repoId);

            SessionFactory factory = SessionFactoryImpl.newInstance();
            Session session = factory.createSession(params);

            // Test root folder retrieval
            Folder root = session.getRootFolder();
            checks.put("repository", "ok:root=" + root.getName());
        } catch (Exception e) {
            checks.put("repository", "down:" + e.getClass().getSimpleName());
            anyFailed = true;
        }

        resp.setContentType("application/json");
        resp.setStatus(anyFailed ? 503 : 200);

        PrintWriter out = resp.getWriter();
        out.write("{\"status\":\"" + (anyFailed ? "degraded" : "ok") + "\",\"checks\":{");
        boolean first = true;
        for (Map.Entry<String, String> e : checks.entrySet()) {
            if (!first) out.write(",");
            out.write("\"" + e.getKey() + "\":\"" + e.getValue() + "\"");
            first = false;
        }
        out.write("}}");
    }
}

Node.js Sidecar

// health/chemistry.js
const express = require('express');
const http = require('http');

const app = express();
const CMIS_HOST = process.env.CMIS_HOST || 'localhost';
const CMIS_PORT = parseInt(process.env.CMIS_PORT || '8080');
const CMIS_REPO = process.env.CMIS_REPO_ID || '-default-';
const CMIS_USER = process.env.CMIS_USER || 'admin';
const CMIS_PASS = process.env.CMIS_PASS || 'admin';

function probeCmis() {
  return new Promise((resolve, reject) => {
    const auth = Buffer.from(`${CMIS_USER}:${CMIS_PASS}`).toString('base64');
    const req = http.get(
      {
        host: CMIS_HOST,
        port: CMIS_PORT,
        path: `/atom11/${CMIS_REPO}/root`,
        timeout: 8000,
        headers: { Authorization: `Basic ${auth}` },
      },
      res => {
        let body = '';
        res.on('data', chunk => { body += chunk; });
        res.on('end', () => resolve({ status: res.statusCode, body }));
      }
    );
    req.on('error', reject);
    req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
  });
}

app.get('/health/chemistry', async (req, res) => {
  try {
    const result = await probeCmis();
    const up = result.status === 200 && result.body.includes('atom:feed');

    if (!up) {
      return res.status(503).json({
        status: 'down',
        upstream_status: result.status,
        detail: result.body.substring(0, 300),
      });
    }

    return res.status(200).json({ status: 'ok' });
  } catch (err) {
    return res.status(503).json({ status: 'down', error: err.message });
  }
});

app.listen(3021);

Python Health Sidecar

# health_chemistry.py
import os, requests
from flask import Flask, jsonify

app = Flask(__name__)

CMIS_BASE = f"http://{os.environ.get('CMIS_HOST', 'localhost')}:{os.environ.get('CMIS_PORT', '8080')}"
CMIS_REPO = os.environ.get('CMIS_REPO_ID', '-default-')
CMIS_AUTH = (
    os.environ.get('CMIS_USER', 'admin'),
    os.environ.get('CMIS_PASS', 'admin')
)

@app.route('/health/chemistry')
def health():
    checks = {}
    any_failed = False

    # Test AtomPub root folder access
    try:
        resp = requests.get(
            f'{CMIS_BASE}/atom11/{CMIS_REPO}/root',
            auth=CMIS_AUTH,
            timeout=8
        )
        if resp.status_code != 200:
            checks['atompub_root'] = f'down:{resp.status_code}'
            any_failed = True
        elif 'atom:feed' not in resp.text and '<feed' not in resp.text:
            checks['atompub_root'] = 'degraded:unexpected_response'
            any_failed = True
        else:
            checks['atompub_root'] = 'ok'
    except Exception as e:
        checks['atompub_root'] = f'down:{e}'
        any_failed = True

    # Test Browser binding service document
    try:
        resp = requests.get(
            f'{CMIS_BASE}/browser',
            auth=CMIS_AUTH,
            timeout=5
        )
        checks['browser_binding'] = 'ok' if resp.status_code == 200 else f'down:{resp.status_code}'
        if resp.status_code != 200:
            any_failed = True
    except Exception as e:
        checks['browser_binding'] = f'down:{e}'
        any_failed = True

    status = 'degraded' if any_failed else 'ok'
    return jsonify({'status': status, 'checks': checks}), 503 if any_failed else 200

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

Step 2: Configure Vigilmon HTTP Monitor for Chemistry

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

Add a second monitor for the CMIS AtomPub service document to detect binding failures independently:

  • URL: https://cmis.your-org.example.com/atom11/
  • Expected: status 200, body contains cmis:repositoryInfo
  • Interval: 2 minutes
  • Alert channel: P1 pager — AtomPub service document failure means all CMIS 1.0 clients are blocked

For the Browser binding (CMIS 1.1):

  • URL: https://cmis.your-org.example.com/browser/
  • Expected: status 200, body contains repositoryId
  • Interval: 2 minutes
  • Alert channel: P1 pager — Browser binding failure blocks modern CMIS clients and REST-based ECM integrations

Vigilmon's multi-region probe consensus filters transient repository cache warmup periods from genuine Chemistry service failures.


Step 3: Heartbeat Monitoring for Content Ingestion and Archival Jobs

Health endpoint monitoring catches service availability failures, but not end-to-end content pipeline problems. Chemistry can respond to the health endpoint while a nightly content ingestion job has silently stalled because a checkout lock on a batch target document was never released.

Vigilmon heartbeat monitors catch these: your content ingestion, rendition generation, and archive export jobs ping Vigilmon after each successful execution cycle.

Set Up Heartbeat Monitors

For content ingestion pipeline liveness:

  1. In Vigilmon, go to Monitors → New Monitor → Heartbeat
  2. Set the name: chemistry-content-ingestion
  3. Set the expected interval: 1 hour
  4. Set the grace period: 2 hours
  5. Save — copy the heartbeat URL

For archive export job liveness:

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

Wire Heartbeats to Chemistry Pipeline Jobs

Add heartbeat pings to your Chemistry content ingestion driver:

// pipeline/ContentIngestionJob.java
package com.yourorg.chemistry.pipeline;

import org.apache.chemistry.opencmis.client.api.*;
import org.apache.chemistry.opencmis.client.runtime.SessionFactoryImpl;
import org.apache.chemistry.opencmis.commons.SessionParameter;
import org.apache.chemistry.opencmis.commons.enums.BindingType;
import org.apache.chemistry.opencmis.commons.data.ContentStream;
import java.net.http.*;
import java.net.URI;
import java.util.*;

public class ContentIngestionJob {

    private final HttpClient http = HttpClient.newHttpClient();
    private final String heartbeatUrl = System.getenv("VIGILMON_INGESTION_HEARTBEAT_URL");

    public void runIngestionBatch(List<IngestItem> items, Session cmisSession) throws Exception {
        Folder targetFolder = (Folder) cmisSession.getObjectByPath("/ingestion-inbox");
        int ingested = 0;

        for (IngestItem item : items) {
            try {
                Map<String, Object> props = new HashMap<>();
                props.put("cmis:objectTypeId", "cmis:document");
                props.put("cmis:name", item.getName());

                ContentStream cs = cmisSession.getObjectFactory()
                    .createContentStream(item.getName(), item.getSize(), item.getMimeType(), item.getStream());

                targetFolder.createDocument(props, cs, null);
                ingested++;
            } catch (Exception e) {
                System.err.println("Ingestion failed for " + item.getName() + ": " + e.getMessage());
            }
        }

        System.out.println("Ingested " + ingested + "/" + items.size() + " documents");
        if (ingested > 0) {
            pingHeartbeat(heartbeatUrl);
        }
    }

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

For cron-based export jobs:

#!/bin/bash
# chemistry-archive-export.sh — run nightly by cron
/opt/chemistry/bin/export-archive.sh \
  --repo "$CMIS_REPO_ID" \
  --output "/var/archives/$(date +%Y%m%d).zip"

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

Step 4: Alert Routing for Chemistry Failures

Chemistry failures range from locked documents that block individual workflows to full CMIS service outages that halt all ECM integrations. Route alerts by severity:

| Monitor | Alert Channel | Priority | |---|---|---| | HTTP: health endpoint (repository probe) | Slack + PagerDuty | P1 | | HTTP: AtomPub service document | Slack + PagerDuty | P1 | | HTTP: Browser binding service document | Slack + PagerDuty | P1 | | Heartbeat: content ingestion pipeline | Slack + PagerDuty | P1 | | Heartbeat: archive export job | Slack + email | P2 | | Heartbeat: rendition generation | Slack | P2 |

Set response time thresholds as early warning signals:

  • Alert at 5000ms for the health endpoint (slow responses indicate repository cache miss or backing store pressure)
  • Alert at 3000ms for the AtomPub service document (slow service doc indicates session negotiation overhead)
  • Alert at 2 hours gap in content ingestion heartbeat (stalled ingestion means ECM content pipeline is broken)
  • Alert at 26 hours gap in archive export heartbeat (missed archive means backup policy is violated)

For production ECM deployments, set up a status page in Vigilmon showing CMIS service availability and content ingestion pipeline liveness — this lets document management teams identify Chemistry failures before they surface as ECM clients that can't retrieve or store documents.


Summary

Chemistry failures present as individual document lock accumulation or cache miss storms before they escalate to full CMIS service outages — ECM clients see CmisObjectNotFoundException or locked documents while Chemistry's Jetty layer returns HTTP 200 because the web binding is healthy. External monitoring catches these before they halt content management workflows:

| Monitor Type | What It Covers | |---|---| | HTTP monitor on health endpoint | Repository session creation and root folder access | | HTTP monitor on AtomPub endpoint | CMIS 1.0/1.1 AtomPub binding availability | | HTTP monitor on Browser endpoint | CMIS 1.1 Browser binding availability | | Heartbeat monitor (content ingestion) | End-to-end document ingestion pipeline success | | Heartbeat monitor (archive export) | Nightly archive and backup job liveness |

Get started free at vigilmon.online — your first Chemistry CMIS 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 →