Skip to content

CASSSIDECAR-427: Implement basic configuration retrieval in ConfigurationManager#358

Open
pauloricardomg wants to merge 13 commits into
apache:trunkfrom
pauloricardomg:CASSSIDECAR-427
Open

CASSSIDECAR-427: Implement basic configuration retrieval in ConfigurationManager#358
pauloricardomg wants to merge 13 commits into
apache:trunkfrom
pauloricardomg:CASSSIDECAR-427

Conversation

@pauloricardomg

Copy link
Copy Markdown
Contributor

Summary

Implement the basic GET configuration flow in the Configuration Manager. When handling retrieval requests, the manager fetches the current overlay from the ConfigurationProvider, merges it with the base template to compute the effective configuration, and returns the result with its SHA-256 hash and last modified timestamp.

New classes

  • ConfigurationManager: Retrieves effective configuration by loading the base template via ConfigUtils.loadConfiguration and merging it with the provider overlay via ConfigurationOverlaySnapshot.overlay(). Wraps provider failures in ConfigurationManagerException.
  • ConfigurationManagerException: Unchecked exception wrapping provider failures with a descriptive message.
  • ConfigUtils: Utilities for YAML loading (loadYaml), deep merge of configurations (mergeConfigurations), and loading a YAML file as a ConfigurationOverlaySnapshot (loadConfiguration).

Changes to existing classes

  • ConfigurationOverlaySnapshot: Adds an overlay() method that produces a new snapshot by deep-merging another snapshot on top, combining cassandraYaml recursively, merging extraJvmOpts, and using the max of both lastModified timestamps. Renames overlay field/accessor to configuration to better reflect its role when representing the effective (merged) configuration.

Test plan

  • ConfigurationManagerTest: Verifies effective configuration retrieval with no overlay, with overlay (merge semantics), and provider failure propagation
  • ConfigUtilsTest: Verifies YAML loading, deep merge logic (overlay wins, new keys, nested objects, empty overlay), and loadConfiguration snapshot creation
  • ConfigurationOverlaySnapshotTest: Verifies overlay() merge of yaml keys, JVM opts, max lastModified, and deep nested object merging
  • FileBasedConfigurationProviderTest: Updated to use renamed configuration() accessor
  • Run ./gradlew :server:test --tests "org.apache.cassandra.sidecar.configmanagement.*" - all 50 tests pass

@pauloricardomg pauloricardomg force-pushed the CASSSIDECAR-427 branch 2 times, most recently from e18a529 to 28ccef4 Compare June 1, 2026 20:47
other.configuration().cassandraYaml());

Map<String, String> mergedOpts = new LinkedHashMap<>(configuration.extraJvmOpts());
mergedOpts.putAll(other.configuration().extraJvmOpts());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This will add new keys for any key which is not present in the mergedOpts map. Is that the behavior we want to expose? Or do we want to limit overlays to known keys in the base template?

Either way worth explicitly documenting in the javadoc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

overlays can add keys not present in the base template, added it to javadoc on 7c81cc8

Map<String, String> mergedOpts = new LinkedHashMap<>(configuration.extraJvmOpts());
mergedOpts.putAll(other.configuration().extraJvmOpts());

Instant mergedLastModified = lastModified.isAfter(other.lastModified)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since the merge produces a new ConfigurationOverlaySnapshot, shouldn't lastModified be Instant.now()? If not, what are the intended semantics of lastModified?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Just overlaying the configuration over a base template does not produce a new configuration. last modified is the maximum modification timestamp of the base template and the overlay. lastModified is only instant.now when updating the overlay or the base template by modifying the file on disk. Added clarification to javadoc on 99072c3


JsonObject result = base.copy();
JsonObject overlayCopy = overlay.copy();
for (Map.Entry<String, Object> field : overlayCopy)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Similar question as above about the intended semantics when a overlay contains keys not in the base configuration.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

clarified on 7c81cc8

}
else
{
result.put(fieldName, overlayValue);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks like if base value for a key is not null and overlay value for the same is null, this line still picks overlay value null. Is this expected? We are not doing this for JsonObjects as line 114 above picks base copy. But we are doing this differently for individual keys.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

My assumption is overlay contains only overridden keys, while base contains all. For example,

If base is
a: 'a'
b: 'b'
c: 'c'

Then if overlay is
b:'b2'

Does it not mean admins want to only override b, while picking base value for others? In that case, the line 136 missing this case right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Add null handling on ad29f2c and also added test case to ensure overlay will be correct for that case.

Instant lastModified;
try
{
lastModified = Files.getLastModifiedTime(yamlPath).toInstant();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There is no synchronization to avoid race condition between loading yaml at line 65 and retrieving last modified time at line 69. There is a possibility of associating different modified time for older yaml if the file was modified in between.

As we are reading from disk,

  • either we need to read both contents and timestamp in a single call if such an API exists
  • or do optimistic synchronization
    t1 = read modified time
    data = read contents
    t2 = read modified time
    if t2 != t1, file was modified and repeat the action.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

good catch, added optimistic concurrency on 031b999

other.configuration().cassandraYaml());

Map<String, String> mergedOpts = new LinkedHashMap<>(configuration.extraJvmOpts());
mergedOpts.putAll(other.configuration().extraJvmOpts());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Don't we need to call validateNoConflictingBooleanOpts on these merged JVM opts?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Makes sense, updated 6ec34b8 to prefer base if there's a conflicting boolean jvm opt on overlay.

public JsonObject toJson()
{
return new JsonObject()
.put("hash", hash())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

fromJson doesn't have "hash", while toJson having it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's expected. hash is computed from the configuration contents and cached — it's not stored state. toJson() includes it for observability (e.g., logging, API responses), but fromJson() correctly skips it since it will be recomputed on demand from the deserialized configuration.

@NotNull
public ConfigurationOverlaySnapshot getEffectiveConfiguration(InstanceMetadata instance)
{
ConfigurationOverlaySnapshot baseSnapshot = ConfigUtils.loadConfiguration(baseTemplatePath);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we plan to cache base snapshot in upcoming PRs, as it doesn't change frequently, and we are involving IO every time here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added base template caching on b0125eb

}

JsonObject result = base.copy();
JsonObject overlayCopy = overlay.copy();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do we need to make a copy of overlay when we are not modifying it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

not needed, removed on f62ab37

@pauloricardomg

Copy link
Copy Markdown
Contributor Author

@isaacreath @skoppu22 thanks for the helfpul comments, addressed them

throw new UnsupportedOperationException();
}

/**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks like older JavaDoc left over here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed on 23a3ed8

String key = entry.getKey();
if (CassandraConfigurationOverlay.hasConflictingBooleanOpt(mergedOpts, key))
{
continue;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are we preferring base option if there is a conflict? Either we need to log a warning or throw an error. And javadoc says 'merges', doesn't mention that we are picking base option here on conflict. It can be confusing for the users, better to callout whatever the approach we pick here.

@pauloricardomg pauloricardomg Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Normally it shouldn't be possible to add conflicting option, since we validate that when updating configs. However in case the remote provider adds a conflicting option it's better to ignore and log a warning then fail, otherwise this can prevent Cassandra from starting. I logged a warning and updated javadoc on f95be70


/**
* Deep-merges the overlay onto the base configuration. For nested objects both base and overlay
* contain, fields are merged recursively. For all other node types (scalars, arrays, nulls),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Minor] 'nulls' can be removed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, fixed on c9d81a5. Thanks for the review 🙏

@isaacreath

Copy link
Copy Markdown
Contributor

+1 (nb).

… retrieval

Introduce ConfigurationManager that computes the effective configuration
for a Cassandra instance by fetching the overlay from the
ConfigurationProvider and merging it with the base cassandra.yaml
template.

New classes:
- ConfigurationManager: retrieves effective configuration by loading the
  base template via ConfigUtils.loadConfiguration and merging it with the
  provider overlay via ConfigurationOverlaySnapshot.overlay()
- ConfigurationManagerException: wraps provider failures with a
  descriptive message
- ConfigUtils: utilities for YAML loading (loadYaml), deep merge of
  configurations (mergeConfigurations), and loading a YAML file as a
  ConfigurationOverlaySnapshot (loadConfiguration)

ConfigurationOverlaySnapshot gains an overlay() method that produces a
new snapshot by deep-merging another snapshot on top, combining
cassandraYaml recursively, merging extraJvmOpts, and using the max of
both lastModified timestamps.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants