Skip to content

fix: Generate consistent reuse hashes by sorting dictionary keys#1554

Merged
HofmeisterAn merged 2 commits intotestcontainers:developfrom
0xced:bugfix/stable-reuse-hash
Oct 17, 2025
Merged

fix: Generate consistent reuse hashes by sorting dictionary keys#1554
HofmeisterAn merged 2 commits intotestcontainers:developfrom
0xced:bugfix/stable-reuse-hash

Conversation

@0xced
Copy link
Contributor

@0xced 0xced commented Oct 17, 2025

What does this PR do?

This pull request ensures that the JSON used for hashing the configuration have object keys always in the same (alphabetical) order.

Why is it important?

Hashing JSON data requires the order of the keys to be stable in order to compare equivalent JSON objects by their hashes (which is what is done to identify reused containers).

Related issues

Closes #1553

How to test this PR

New tests have been added to ensure that hashing is stable, see ReuseHashTest.EqualTest.

Follow-ups

Note that this will break reused containers in the next Testcontainers version again, just like it broke between version 4.6.0 and 4.7.0. But reuse is still marked as experimental so we better fix this issue sooner than later.

@netlify
Copy link

netlify bot commented Oct 17, 2025

Deploy Preview for testcontainers-dotnet ready!

Name Link
🔨 Latest commit 6369cac
🔍 Latest deploy log https://app.netlify.com/projects/testcontainers-dotnet/deploys/68f229f6ec3a190008bd8a8e
😎 Deploy Preview https://deploy-preview-1554--testcontainers-dotnet.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link

coderabbitai bot commented Oct 17, 2025

Summary by CodeRabbit

  • Bug Fixes

    • Fixed resource configuration reuse hash computation to consistently produce identical hashes for configurations with the same settings, regardless of label insertion order.
  • Tests

    • Added test coverage for reuse hash equality, ensuring deterministic behavior across different configuration creation sequences.

Walkthrough

Replaces bespoke dictionary serialization with a deterministic ordered-key converter, refactors the runtime-label-ignorer to reuse it, applies the ordered converter to resource-configuration hashing, and adds tests that assert reuse-hash stability across different insertion orders.

Changes

Cohort / File(s) Summary
Ordered Keys Converter
src/Testcontainers/Configurations/Commons/JsonOrderedKeysConverter.cs
New internal converter that detects IEnumerable<KeyValuePair<string,string>>, deserializes into an IReadOnlyDictionary, and writes JSON objects with keys emitted in ascending order.
Runtime Labels Ignorer
src/Testcontainers/Configurations/Commons/JsonIgnoreRuntimeResourceLabels.cs
Changed base type to JsonOrderedKeysConverter; removed custom CanConvert/Read and manual object writing. Write now filters out ignored runtime labels and delegates ordered serialization to the base converter.
Resource Configuration Serialization
src/Testcontainers/Configurations/Commons/ResourceConfiguration.cs
Added private static JsonSerializerOptions initialized with JsonOrderedKeysConverter; updated GetReuseHash() to serialize using these options to produce deterministic bytes for hashing.
Tests: Reuse Hash Stability
tests/Testcontainers.Platform.Linux.Tests/ReusableResourceTest.cs
Added ReuseHashTest.EqualTest with two tests: ForSameConfigurationCreatedInDifferentOrder() checks identical hashes for different insertion orders; ForGivenConfiguration() asserts a concrete expected reuse hash.

Sequence Diagram(s)

sequenceDiagram
    participant RC as ResourceConfiguration
    participant JS as JsonSerializer
    participant Q as JsonOrderedKeysConverter

    RC->>JS: SerializeToUtf8Bytes(this, GetType(), JsonSerializerOptions)
    activate JS
    JS->>Q: Q.CanConvert(IEnumerable<KeyValuePair<string,string>>)
    Q-->>JS: true
    JS->>Q: Q.Write(filteredDictionary)
    activate Q
    Q->>Q: Order keys ascending
    Q->>JS: Emit ordered JSON object bytes
    deactivate Q
    JS-->>RC: Return UTF8 bytes
    deactivate JS
    RC->>RC: Compute SHA1(representative bytes)
Loading

Note: JsonIgnoreRuntimeResourceLabels filters labels before passing the dictionary into JsonOrderedKeysConverter's Write step.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through keys both wild and loose,
I nudged them into tidy, alphabetical truce.
Now hashes hum the same sweet song,
No more order-driven wrong,
Reuse peace hops in — quiet and spruce. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues Check ✅ Passed The pull request directly addresses the requirements from issue #1553, which identifies that reuse hashes became unstable due to nondeterministic dictionary key ordering in JSON serialization. The code changes implement the suggested solution: a new JsonOrderedKeysConverter class handles ordered serialization, JsonIgnoreRuntimeResourceLabels is refactored to use this converter, and ResourceConfiguration is updated to apply ordered key serialization. New tests verify that configurations with identical content but different insertion order produce identical hashes. These changes comprehensively satisfy the objectives to produce stable JSON representations for deterministic hash computation.
Out of Scope Changes Check ✅ Passed All changes in the pull request are directly related to implementing stable reuse hashes through sorted dictionary keys. The JsonOrderedKeysConverter implementation, the refactoring of JsonIgnoreRuntimeResourceLabels, the addition of JsonSerializerOptions to ResourceConfiguration, and the new tests in ReusableResourceTest all contribute solely to the objective of ensuring deterministic JSON key ordering for hash stability. No unrelated modifications, refactoring of unrelated code sections, or extraneous changes are present in the PR.
Title Check ✅ Passed The pull request title "fix: Generate consistent reuse hashes by sorting dictionary keys" directly and accurately captures the primary objective of the changeset. The title clearly conveys that the main change addresses generating consistent reuse hashes through dictionary key sorting, which is precisely what the code changes accomplish: introducing JsonOrderedKeysConverter to sort dictionary keys alphabetically, updating JsonIgnoreRuntimeResourceLabels and ResourceConfiguration to use this converter, and adding tests to verify hash stability. The title is concise, specific, and uses conventional commit style formatting without vague terminology. It provides sufficient context for teammates to understand that the PR addresses hash consistency issues by enforcing deterministic key ordering during JSON serialization.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)

✅ Unit Test PR creation complete.

  • Create PR with unit tests
  • Commit unit tests in branch bugfix/stable-reuse-hash
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@0xced
Copy link
Contributor Author

0xced commented Oct 17, 2025

Note that stable dictionary sorting for System.Text.Json was discussed a few years ago in dotnet/runtime#76008

The issue is still open but like many of the 5k+ opened issues in the .NET runtime repository, I don't expect it to be addressed any time soon.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/Testcontainers/Configurations/Commons/JsonIgnoreRuntimeResourceLabels.cs (1)

9-18: Excellent refactoring that eliminates duplication.

The inheritance from JsonOrderedKeysConverter is a clean design that separates concerns: this class filters runtime labels, the base class handles ordered serialization. The previous manual JSON writing code is eliminated, reducing duplication and maintenance burden.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c2b86ad and 11985c7.

📒 Files selected for processing (4)
  • src/Testcontainers/Configurations/Commons/JsonIgnoreRuntimeResourceLabels.cs (1 hunks)
  • src/Testcontainers/Configurations/Commons/JsonOrderedKeysConverter.cs (1 hunks)
  • src/Testcontainers/Configurations/Commons/ResourceConfiguration.cs (2 hunks)
  • tests/Testcontainers.Platform.Linux.Tests/ReusableResourceTest.cs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/Testcontainers/Configurations/Commons/ResourceConfiguration.cs (1)
src/Testcontainers/Configurations/Commons/JsonOrderedKeysConverter.cs (1)
  • JsonOrderedKeysConverter (9-32)
src/Testcontainers/Configurations/Commons/JsonOrderedKeysConverter.cs (1)
src/Testcontainers/Configurations/Commons/JsonIgnoreRuntimeResourceLabels.cs (1)
  • Write (13-18)
src/Testcontainers/Configurations/Commons/JsonIgnoreRuntimeResourceLabels.cs (2)
src/Testcontainers/Configurations/Commons/JsonOrderedKeysConverter.cs (3)
  • JsonOrderedKeysConverter (9-32)
  • Write (21-31)
  • IReadOnlyDictionary (16-19)
src/Testcontainers/Clients/TestcontainersClient.cs (3)
  • TestcontainersClient (21-433)
  • TestcontainersClient (47-57)
  • TestcontainersClient (59-75)
tests/Testcontainers.Platform.Linux.Tests/ReusableResourceTest.cs (2)
src/Testcontainers/Configurations/Commons/ResourceConfiguration.cs (1)
  • GetReuseHash (96-108)
src/Testcontainers/Configurations/Commons/IResourceConfiguration.cs (1)
  • GetReuseHash (48-48)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: ci (Testcontainers.ClickHouse, ubuntu-24.04)
  • GitHub Check: ci (Testcontainers.Bigtable, ubuntu-24.04)
  • GitHub Check: ci (Testcontainers.ActiveMq, ubuntu-24.04)
  • GitHub Check: ci (Testcontainers.Cassandra, ubuntu-24.04)
  • GitHub Check: analyze (csharp)
🔇 Additional comments (6)
src/Testcontainers/Configurations/Commons/ResourceConfiguration.cs (2)

17-22: LGTM! Thread-safe static initialization.

The static constructor correctly initializes the shared JsonSerializerOptions with the ordered keys converter. This pattern ensures thread-safe, one-time initialization.


98-98: Correct usage of ordered serialization options.

The updated GetReuseHash now uses the static JsonSerializerOptions containing the ordered keys converter, ensuring deterministic hash generation regardless of dictionary insertion order.

src/Testcontainers/Configurations/Commons/JsonOrderedKeysConverter.cs (2)

11-14: CanConvert correctly handles multiple dictionary types.

The check for IEnumerable<KeyValuePair<string, string>> intentionally matches various dictionary implementations (Dictionary, ConcurrentDictionary, IReadOnlyDictionary, etc.), providing flexibility while ensuring ordered serialization for all.


21-31: Write method correctly implements ordered serialization.

The use of OrderBy(item => item.Key) ensures deterministic alphabetical ordering using the default ordinal string comparer, which is culture-invariant and stable. This is the core fix that ensures identical configurations produce identical hashes.

tests/Testcontainers.Platform.Linux.Tests/ReusableResourceTest.cs (2)

105-122: Test correctly validates order-independent hashing.

This test directly validates the PR's core objective: configurations with identical content but different insertion orders produce the same reuse hash. Testing both environment variables and labels provides good coverage.


124-138: Hardcoded hash provides regression protection.

The hardcoded expected hash (with documented JSON format in the comment) serves as a regression test to detect any unintended changes to the serialization format. This intentional brittleness is valuable for maintaining stable reuse behavior across versions.

@0xced 0xced force-pushed the bugfix/stable-reuse-hash branch from 11985c7 to 6369cac Compare October 17, 2025 11:35
@HofmeisterAn HofmeisterAn added the bug Something isn't working label Oct 17, 2025
@HofmeisterAn HofmeisterAn changed the title Implement stable reuse hashes by sorting dictionary keys fix: Generate consistent reuse hashes by sorting dictionary keys Oct 17, 2025
Copy link
Collaborator

@HofmeisterAn HofmeisterAn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! It's cool to see contributions from you again 😎.

@HofmeisterAn HofmeisterAn merged commit d6cd3dc into testcontainers:develop Oct 17, 2025
74 checks passed
@testcontainers testcontainers deleted a comment from coderabbitai bot Oct 17, 2025
@testcontainers testcontainers deleted a comment from coderabbitai bot Oct 17, 2025
@HofmeisterAn HofmeisterAn added the breaking change Causing compatibility issues label Oct 19, 2025
@0xced 0xced deleted the bugfix/stable-reuse-hash branch October 23, 2025 10:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change Causing compatibility issues bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: The reuse hash might be unstable across Testcontainers versions

2 participants