Skip to content

CASSSIDECAR-424: Add ConfigurationProvider interfaces#351

Open
pauloricardomg wants to merge 11 commits into
trunkfrom
worktree-CASSSIDECAR-424
Open

CASSSIDECAR-424: Add ConfigurationProvider interfaces#351
pauloricardomg wants to merge 11 commits into
trunkfrom
worktree-CASSSIDECAR-424

Conversation

@pauloricardomg
Copy link
Copy Markdown
Contributor

Summary

Introduces the pluggable overlay storage abstraction as defined in CEP-62 for managing Cassandra configuration via Sidecar.

  • CassandraConfigurationOverlay: Immutable model with version-agnostic cassandraYaml (JsonNode) and extraJvmOpts, with updated() centralizing merge logic for both components
  • ConfigurationOverlaySnapshot: Wrapper with lazily computed cached SHA-256 content hash and last modified timestamp
  • ConfigurationProvider: Pluggable storage interface with getConfiguration and storeConfiguration using hash-based optimistic concurrency
  • InMemoryConfigurationProvider: Sample implementation with per-instance locking

Test Plan

  • CassandraConfigurationOverlayTest: object validation, yaml and jvm opts merge
  • ConfigurationOverlaySnapshotTest: hash determinism, caching, format
  • InMemoryConfigurationProviderTest: store/get, optimistic concurrency, instance isolation, concurrent access

@frankgh frankgh self-requested a review May 18, 2026 16:45
Comment on lines +109 to +111
public CassandraConfigurationOverlay updated(@Nullable Map<String, JsonNode> cassandraYamlUpdates,
@Nullable List<String> addJvmOpts,
@Nullable List<String> removeJvmOpts)
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.

Right now this function provides differing approaches for updating values between cassandraYaml and extraJvmOpts.

For cassandraYaml, if I want to update an existing override, I pass the new value in through the cassandraYamlUpdates map.

For extraJvmpOpts, I need to add the existing JVM option to removeJvmOpts and the new value to addJvmOpts.

Is there any way to offer a similar approach for both so that the semantics are consistent for callers?

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.

Changed extraJvmOpts from List to Map<String, String> to provide a consistent update API between cassandraYaml and extraJvmOpts. Both now follow the same semantics in the updated() method: pass a key with a non-null value to upsert, or a null value to remove. This eliminates the need for separate addJvmOpts/removeJvmOpts parameters. Keys use the full JVM flag format (e.g. -Dcassandra.jmx.local.port -> 7199). See commit 0f17ae7.

@NotNull
public JsonNode cassandraYaml()
{
return cassandraYaml;
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.

It's technically possible to mutate cassandraYaml by accessing the value from here, casting it to a ObjectNode, then using ObjectNode#put to update a key.

To preserve the immutable property of this object, we may want to consider returning a copy of the cassandraYaml. That said, this could create a lot of unnecessary objects if the immutability properties aren't necessary.

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.

Since this is an internal API, I opted to avoid defensive copies on every accessor call to minimize unnecessary object creation. I updated the constructor to deep-copy the input ObjectNode to prevent external mutation through the original reference, and updated the accessor Javadoc to mention that callers must not mutate the returned node (use updated() instead). This gives us immutability guarantees at construction time without the overhead of copying on every read. See commit e95958e.

node.put("hash", hash());
node.put("lastModified", lastModified.toString());
node.set("configuration", MAPPER.valueToTree(configuration));
return node.toString();
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.

Nit: Do we want to consider using a pretty print string here? (i.e. toPrettyString()). Could make debugging easier.

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.

Addressed on 7f8a81a

* @return the configuration overlay snapshot, or {@code null} if no overlay exists for the instance
*/
@Nullable
ConfigurationOverlaySnapshot getConfiguration(InstanceMetadata instance);
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.

Did you consider getOverlay / storeOverlay instead of getConfiguration / storeConfiguration? The use of overlay can indicate to the reader of callers of this interface that we are not getting a fully materialized config, but it may not be a big deal.

Copy link
Copy Markdown
Contributor Author

@pauloricardomg pauloricardomg May 18, 2026

Choose a reason for hiding this comment

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

good call, updated on f651dd8

@pauloricardomg pauloricardomg force-pushed the worktree-CASSSIDECAR-424 branch from 9ab8bb4 to f651dd8 Compare May 18, 2026 21:23
Comment on lines +49 to +50
CassandraConfigurationOverlay overlay1 = new CassandraConfigurationOverlay(yaml1, Arrays.asList("-Xmx4G"));
CassandraConfigurationOverlay overlay2 = new CassandraConfigurationOverlay(yaml2, Arrays.asList("-Xmx4G"));
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 CI is failing here. Needs an import? import java.util.Arrays

@pauloricardomg pauloricardomg force-pushed the worktree-CASSSIDECAR-424 branch 2 times, most recently from a45830f to bd32867 Compare May 19, 2026 20:02
@isaacreath
Copy link
Copy Markdown
Contributor

+1 (nb)

Copy link
Copy Markdown
Contributor

@frankgh frankgh left a comment

Choose a reason for hiding this comment

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

I think we should try to use vertx JSON framework here. I provided a sample of what this would look like here: 23ba281

*/
public class CassandraConfigurationOverlay
{
private static final ObjectMapper MAPPER = new ObjectMapper();
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.

ideally we should try to use the framework's JSON functionality to avoid allocating additional resources (for JSON processing) etc.

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 call, this is more fluent and simplify things. The reason I used jackson is to perform json schema validation in the future but we can convert from vertx json to jackson vertx if needed in the future.

Comment on lines +50 to +67
Object lock = locks.computeIfAbsent(instance.id(), k -> new Object());
synchronized (lock)
{
ConfigurationOverlaySnapshot current = overlays.get(instance.id());

if (current == null && originalHash != null)
{
return false;
}

if (current != null && (originalHash == null || !current.hash().equals(originalHash)))
{
return false;
}

overlays.put(instance.id(), newSnapshot);
return true;
}
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 really need locks here? Isn't the concurrent hashmap already a DS that supports this natively?

Suggested change
Object lock = locks.computeIfAbsent(instance.id(), k -> new Object());
synchronized (lock)
{
ConfigurationOverlaySnapshot current = overlays.get(instance.id());
if (current == null && originalHash != null)
{
return false;
}
if (current != null && (originalHash == null || !current.hash().equals(originalHash)))
{
return false;
}
overlays.put(instance.id(), newSnapshot);
return true;
}
return overlays.compute(instance.id(), (k, current) -> {
if (current == null && originalHash != null)
{
return null;
}
if (current != null && (originalHash == null || !current.hash().equals(originalHash)))
{
return current;
}
return newSnapshot;
}) == newSnapshot;

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.

The reason I used an explicit lock is to prevent contention within the CHM bucket but I guess this is premature optimization so I'm ok to remove and rely only on CHM.

@pauloricardomg pauloricardomg force-pushed the worktree-CASSSIDECAR-424 branch from bd32867 to ad56510 Compare May 20, 2026 20:08
@pauloricardomg
Copy link
Copy Markdown
Contributor Author

@frankgh incorporated your feedback on 7ce2732, but also reverted a previous change to have extraJvmOptions as a map instead of a list (15673bd) and added a new commit ad56510 to validate conflicting jvm boolean options are rejected during merge.

Copy link
Copy Markdown
Contributor

@frankgh frankgh left a comment

Choose a reason for hiding this comment

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

+1 looks good. I think we need to rebase and ensure CI is green

pauloricardomg and others added 11 commits May 21, 2026 14:27
Introduce the pluggable overlay storage abstraction as defined in CEP-62
with ConfigurationProvider interface, CassandraConfigurationOverlay model,
ConfigurationOverlaySnapshot wrapper, and InMemoryConfigurationProvider
sample implementation.
… String>

Make update semantics consistent between cassandraYaml and extraJvmOpts:
both now use a map where a null value removes the entry and a non-null
value upserts it. Keys use the full JVM flag (e.g. -Dcassandra.jmx.local.port).
…mmutability

The constructor now deep-copies the input ObjectNode so that external
mutation of the original reference cannot corrupt the overlay. The
accessor documents that callers must not mutate the returned node.
Improves readability of CassandraConfigurationOverlay and
ConfigurationOverlaySnapshot string representations in logs and
debugging output.
… String>

Make update semantics consistent between cassandraYaml and extraJvmOpts:
both now use a map where a null value removes the entry and a non-null
value upserts it. Keys use the full JVM flag (e.g. -Dcassandra.jmx.local.port).
…y merge

Detect and reject conflicting -XX:+Option / -XX:-Option pairs after
merging extraJvmOpts updates. Users can still replace one form with the
other by removing the old key in the same update.
@pauloricardomg pauloricardomg force-pushed the worktree-CASSSIDECAR-424 branch from ad56510 to c7b8446 Compare May 21, 2026 18:32
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