Skip to content

Commit 5243e2e

Browse files
committed
Reject RavenDB 7.2 servers that corrupt migrated databases on compaction
RavenDB 7.2.0 to 7.2.4 corrupt the attachments index when an operator compacts a database migrated from 6.2 or 7.1. 7.2.5 fixed it and repairs the stale schema when the database loads, so refusing to start on those patches moves operators to a server that repairs their database before a compaction can damage it. The check keeps RavenDB's compatibility rule, which ignores the patch: the server's major.minor must be at least the client's. The 7.2.5 floor is fixed, so a later client patch does not raise the server requirement.
1 parent 2e6fa68 commit 5243e2e

2 files changed

Lines changed: 140 additions & 16 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
namespace ServiceControl.Persistence.Tests.RavenDB;
2+
3+
using System;
4+
using NUnit.Framework;
5+
using ServiceControl.RavenDB;
6+
7+
[TestFixture]
8+
class StartupChecksTests
9+
{
10+
static readonly Version Client727 = new(7, 2, 7);
11+
12+
[TestCase("6.2.0")]
13+
[TestCase("6.2.20")]
14+
[TestCase("7.1.9")]
15+
public void Should_reject_servers_with_a_lower_major_minor_version_than_the_client(string serverVersion)
16+
{
17+
var isValid = StartupChecks.TryValidateServerVersion(serverVersion, Client727, out var error);
18+
19+
using (Assert.EnterMultipleScope())
20+
{
21+
Assert.That(isValid, Is.False);
22+
Assert.That(error, Does.Contain("expects RavenDB Server version 7.2 or higher")
23+
.And.Contain($"the server is using {serverVersion}")
24+
.And.Contain("servicecontrol-ravendb container"));
25+
}
26+
}
27+
28+
[Test]
29+
public void Should_reject_servers_with_a_lower_major_minor_version_than_a_newer_client()
30+
{
31+
var isValid = StartupChecks.TryValidateServerVersion("7.2.9", new Version(7, 3, 0), out var error);
32+
33+
using (Assert.EnterMultipleScope())
34+
{
35+
Assert.That(isValid, Is.False);
36+
Assert.That(error, Does.Contain("expects RavenDB Server version 7.3 or higher").And.Contain("the server is using 7.2.9"));
37+
}
38+
}
39+
40+
[TestCase("7.2.0", "7.2.0")]
41+
[TestCase("7.2.4", "7.2.4")]
42+
[TestCase("7.2.4-nightly-20260101-0100", "7.2.4")]
43+
public void Should_reject_servers_with_the_known_attachments_index_corruption(string serverVersion, string reportedVersion)
44+
{
45+
var isValid = StartupChecks.TryValidateServerVersion(serverVersion, Client727, out var error);
46+
47+
using (Assert.EnterMultipleScope())
48+
{
49+
Assert.That(isValid, Is.False);
50+
Assert.That(error, Does.Contain($"RavenDB Server {reportedVersion} can corrupt the attachments index")
51+
.And.Contain("to 7.2.5 or later")
52+
.And.Contain("servicecontrol-ravendb container"));
53+
}
54+
}
55+
56+
[TestCase("7.2.5")]
57+
[TestCase("7.2.6")]
58+
[TestCase("7.2.7")]
59+
[TestCase("7.2.10")]
60+
[TestCase("7.2.7+441f983")]
61+
[TestCase("7.2.8-nightly-20260101-0100")]
62+
[TestCase("7.3.0")]
63+
[TestCase("8.0.0")]
64+
public void Should_accept_compatible_servers_without_the_known_corruption(string serverVersion)
65+
{
66+
var isValid = StartupChecks.TryValidateServerVersion(serverVersion, Client727, out var error);
67+
68+
using (Assert.EnterMultipleScope())
69+
{
70+
Assert.That(isValid, Is.True);
71+
Assert.That(error, Is.Null);
72+
}
73+
}
74+
75+
[TestCase("7.2.5")]
76+
[TestCase("7.2.7")]
77+
public void Should_not_raise_the_floor_with_the_client_patch_version(string serverVersion)
78+
{
79+
var isValid = StartupChecks.TryValidateServerVersion(serverVersion, new Version(7, 2, 10), out _);
80+
81+
Assert.That(isValid, Is.True);
82+
}
83+
84+
[TestCase("")]
85+
[TestCase("unknown")]
86+
public void Should_reject_unreadable_server_versions(string serverVersion)
87+
{
88+
var isValid = StartupChecks.TryValidateServerVersion(serverVersion, Client727, out var error);
89+
90+
using (Assert.EnterMultipleScope())
91+
{
92+
Assert.That(isValid, Is.False);
93+
Assert.That(error, Does.Contain($"could not read the server version '{serverVersion}'"));
94+
}
95+
}
96+
}

‎src/ServiceControl.RavenDB/StartupChecks.cs‎

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,27 +43,55 @@ public static async Task<string[]> FindIndexesUsingCorax(IDocumentStore store, s
4343

4444
public static async Task EnsureServerVersion(IDocumentStore store, CancellationToken cancellationToken = default)
4545
{
46-
// RavenDB compatibility policy is that the major/minor version of the server must be
47-
// equal or higher than the client and ignores the patch version.
48-
//
49-
// https://docs.ravendb.net/6.2/client-api/faq/backward-compatibility/#compatibility---ravendb-42-and-higher
50-
//
51-
// > Starting with version 4.2, RavenDB clients are compatible with any server of their own version and higher.
52-
// > E.g. -
53-
// >
54-
// > Client 4.2 is compatible with Server 4.2, Server 4.5, Server 5.2, and any other server of a higher version.
55-
5646
var build = await store.Maintenance.Server.SendAsync(new GetBuildNumberOperation(), cancellationToken);
57-
var serverProductVersion = new Version(build.ProductVersion);
5847

59-
var clientVersion = typeof(Raven.Client.Constants).Assembly.GetCustomAttributes<AssemblyInformationalVersionAttribute>().First().InformationalVersion;
60-
var parts = clientVersion.Split('.');
61-
var clientProductVersion = new Version($"{parts[0]}.{parts[1]}");
48+
if (!TryValidateServerVersion(build.FullVersion, ClientVersion, out var error))
49+
{
50+
throw new Exception(error);
51+
}
52+
}
53+
54+
public static bool TryValidateServerVersion(string serverFullVersion, Version clientVersion, out string error)
55+
{
56+
var clientMajorMinor = new Version(clientVersion.Major, clientVersion.Minor);
57+
58+
// FullVersion can carry a suffix such as "7.2.8-nightly-20260101-0100" or "7.2.7+commit"
59+
var numericPart = serverFullVersion.Split('-', '+')[0];
60+
61+
if (!Version.TryParse(numericPart, out var serverVersion))
62+
{
63+
error = $"ServiceControl expects RavenDB Server version {clientMajorMinor} or higher, but could not read the server version '{serverFullVersion}'.";
64+
return false;
65+
}
6266

63-
if (clientProductVersion > serverProductVersion)
67+
// RavenDB ignores the patch for compatibility: https://docs.ravendb.net/7.2/client-api/faq/backward-compatibility
68+
if (new Version(serverVersion.Major, serverVersion.Minor) < clientMajorMinor)
6469
{
65-
throw new Exception($"ServiceControl expects RavenDB Server version {clientProductVersion} or higher, but the server is using {serverProductVersion}.");
70+
error = $"ServiceControl expects RavenDB Server version {clientMajorMinor} or higher, but the server is using {serverVersion}. " +
71+
"Upgrade the external RavenDB server or the servicecontrol-ravendb container before starting ServiceControl.";
72+
return false;
6673
}
74+
75+
// Compacting a migrated database corrupts its attachments index before 7.2.5: https://github.com/ravendb/ravendb/releases/tag/7.2.5
76+
if (serverVersion.Major == 7 && serverVersion.Minor == 2 && serverVersion < FirstServerWithAttachmentsIndexFix)
77+
{
78+
error = $"RavenDB Server {serverVersion} can corrupt the attachments index of migrated databases during compaction. " +
79+
$"Upgrade the external RavenDB server or the servicecontrol-ravendb container to {FirstServerWithAttachmentsIndexFix} or later before starting ServiceControl.";
80+
return false;
81+
}
82+
83+
error = null;
84+
return true;
85+
}
86+
87+
static readonly Version FirstServerWithAttachmentsIndexFix = new(7, 2, 5);
88+
89+
static readonly Version ClientVersion = ParseClientVersion();
90+
91+
static Version ParseClientVersion()
92+
{
93+
var clientVersion = typeof(Raven.Client.Constants).Assembly.GetCustomAttributes<AssemblyInformationalVersionAttribute>().First().InformationalVersion;
94+
return Version.Parse(clientVersion.Split('-', '+')[0]);
6795
}
6896

6997
static readonly ILogger Logger = LoggerUtil.CreateStaticLogger(typeof(StartupChecks));

0 commit comments

Comments
 (0)