Skip to content

Health check false "Icinga DB is outdated": getDbSchemaVersion() orders by timestamp, breaks on same-millisecond migrations #1390

Description

@grant-veepshosting

Describe the bug

On icingadb-web 1.4.0 (current latest release), the Icinga DB Health check reports:

Icinga DB is outdated, please upgrade to version 1.4.0 or later.

…even though the Icinga DB daemon is newer than 1.4.0 (in my case v1.5.1), the schema is fully migrated, and data is flowing (fresh heartbeat).

This looks identical to #1233 / #1230, but it is a different root cause. #1230 was a version-string comparison bug fixed in 1.2.1; that fix (IcingaHealth::normalizeVersion()) is present here. In this case the daemon version is never read at all, because the schema-version gate added later (#1196) resolves incorrectly. The buggy code is still present in main.

Root cause

IcingaHealth::checkHealth() only reads icingadb_instance.icingadb_version when Backend::supportsDependencies() is true, which requires Backend::getDbSchemaVersion() >= 7 (MySQL) / >= 5 (PostgreSQL). getDbSchemaVersion() is:

// library/Icingadb/Common/Backend.php
self::$schemaVersion = Schema::on(self::getDb())
    ->columns('version')
    ->first()          // no explicit ordering
    ->version ?? 0;

with

// library/Icingadb/Model/Schema.php
public function getDefaultSort(): array
{
    return ['timestamp desc'];
}

so the effective query is SELECT version FROM icingadb_schema ORDER BY timestamp DESC LIMIT 1.

icingadb_schema holds one row per applied migration. When two migrations are applied within the same millisecond — which happens routinely when several upgrades are applied back-to-back (e.g. stepping through 5 → 6 → 7 during a single upgrade) — multiple rows share the maximum timestamp. ORDER BY timestamp DESC then has a tie, and per SQL semantics the tie-break is unspecified; the engine can return a row whose version is not the maximum.

Concretely, on my MySQL master icingadb_schema looked like:

id version timestamp (ms)
1 4 1739353072000
2 5 1782862068000
3 6 1782862069000
4 7 1782862069000

SELECT version FROM icingadb_schema ORDER BY timestamp DESC LIMIT 1 returns 6 (rows 3 and 4 tie on timestamp). So getDbSchemaVersion() = 6, supportsDependencies() = 6 >= 7 = false, Instance::getColumns() omits icingadb_version, and checkHealth() hits ! isset($instance->icingadb_version) → the "outdated" CRITICAL.

This is deterministic given the table state — not a runtime race — so refreshing the page never clears it.

To Reproduce

  1. Have icingadb_schema contain two rows that share the maximum timestamp where the higher version is not the one the DB returns first for ORDER BY timestamp DESC LIMIT 1 (occurs naturally when consecutive migrations are applied in the same millisecond).
  2. Run an Icinga DB daemon ≥ 1.4.0 with icingadb-web 1.4.0.
  3. Open Health → the Icinga DB check reports "outdated" despite a healthy, up-to-date daemon.

Expected behavior

getDbSchemaVersion() should return the maximum applied schema version. The schema version is defined by the largest version value; timestamp is not guaranteed unique or strictly ordered with version at millisecond resolution, so it must not be the sort key here.

Suggested fix

Determine the current schema version by version, not by timestamp, e.g.:

self::$schemaVersion = Schema::on(self::getDb())
    ->columns('version')
    ->orderBy('version', SORT_DESC)
    ->limit(1)
    ->first()
    ->version ?? 0;

(or SELECT MAX(version)), rather than relying on the Schema model's timestamp desc default sort.

Workaround for affected users

Make the highest schema version's row strictly the latest timestamp — metadata only, the daemon keys migrations on version, not timestamp:

UPDATE icingadb_schema SET timestamp = timestamp + 1 WHERE version = <max_version>;

No service restart needed (the health check recomputes per request).

Your Environment

  • Icinga DB Web version: 1.4.0 (latest release; bug also present in main)
  • Icinga DB (daemon) version: v1.5.1
  • Icinga Web 2 version: 2.14.0
  • Database: MySQL (MySQL/MariaDB affected; the PostgreSQL branch requires >= 5 and is susceptible in the same way)
  • OS: Ubuntu

Related: #1196 (introduced the schema-version gate), #1230 / #1233 (the earlier, distinct version-string comparison bug fixed in 1.2.1).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions