Skip to content

KAFKA-12689: Remove exactly_once / exactly_once_beta#1

Merged
lucasbru merged 8 commits into
KAFKA-12689from
KSE-1228
Sep 19, 2022
Merged

KAFKA-12689: Remove exactly_once / exactly_once_beta#1
lucasbru merged 8 commits into
KAFKA-12689from
KSE-1228

Conversation

@lucasbru

Copy link
Copy Markdown
Owner

Removes all code for handling the processing_guarantee settings exactly_once and exactly_once_beta, which have been deprecated since 3.0.0 and can be removed with the release of 4.0.0.

Some tests were adapted to use exactly_once_v2 instead of exactly_once_beta or exactly_once, to retain the same coverage of the code bas.

exactly_once / exactly_once_beta are rejected with an error message to upgrade to exactly_once_v2.

EosV2UpgradeIntegrationTest is removed, since it cannot be implemented anymore with only one implementation available.

More detailed description of your change,
if necessary. The PR title and PR message become
the squashed commit message, so use a separate
comment to ping reviewers.

Summary of testing strategy (including rationale)
for the feature or bug fix. Unit and/or integration
tests are expected for any behaviour change and
system tests should be considered for larger changes.

Committer Checklist (excluded from commit message)

  • Verify design and implementation
  • Verify test coverage and CI build status
  • Verify documentation (including upgrade notes)

@lucasbru

Copy link
Copy Markdown
Owner Author

Some notes:

  • I kept StreamsConfig.EXACTLY_ONCE and StreamsConfig.EXACTLY_ONCE_BETA around with default visibility to print have a decent error message when they are used, but I don't have a strong opinion in case you want it to be removed.
  • I did not make changes to the Kafka Client API, im particular removal of the deprecated version of sendOffsetsToTransaction. Not sure if it belongs in this change.
  • Some return values in ActiveTaskCreator are now too general. The plan is to move the threadProducer out of the ActiveTaskCreator, and removing all the wrapping methods "closeThreadProducerIfNeeded", "producerMetrics", "producerClientIds", etc. So I didn't clean this up yet in this PR.
  • EosV2UpgradeIntegrationTest is removed completely, because it cannot be written as a unit test anymore. We will have to rewrite a similar system test.
  • In some places, EOS_beta is still used in names. I will replace these by EOS_v2 later.
  • Some reordering of tests in StreamProducerTest.java I did not do yet, as it would make reviewing harder
  • I suppose we need to create a KAFKA ticket for this?

@guozhangwang guozhangwang left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

EosV2UpgradeIntegrationTest is removed completely, because it cannot be written as a unit test anymore. We will have to rewrite a similar system test.

Yes I think that's very important to make sure rolling upgrades still works, we can do that in a separate PR.

I kept StreamsConfig.EXACTLY_ONCE and StreamsConfig.EXACTLY_ONCE_BETA around with default visibility to print have a decent error message when they are used

I think it's okay to keep these (potentially forever..) for decent error messages

I did not make changes to the Kafka Client API, im particular removal of the deprecated version of sendOffsetsToTransaction.

Yup, do not need to worry about it.

I suppose we need to create a KAFKA ticket for this?

Yes!

Comment thread docs/streams/core-concepts.html Outdated

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is not for this file but for a different file: we should also edit the upgrade-guide.html under the API changes in 4.0 section, noting those configs are now removed; also mention how users still with those configs can upgrade with rolling bounces to the V2 config values.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Done

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If users are already using BETA then their brokers should be on 2.5 already, since the semantics of BETA and V2 are exactly the same. I think we should just say "... Please use .. instead which is the new name for the same processing semantics".

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Done

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Also need to mention while changing the flag users should also follow the upgrade guide doing rolling bounces.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Done

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We can change the function name removing IfNeeded as well, ditto for similar function names.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

You are right. I didn't do this yet, because I was thinking about moving StreamsProducer out of ActiveTaskCreator, at which point all of these wrapping functions would go.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The major purpose of this StreamProducer is to wrap both task/thread producer for the EOSv1/v2 usages, I think we can consider removing and inlining this class completely in a follow-up PR.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Isn't StreamProducer still useful to have the translation between KafkaProducer exceptions and StreamsExceptions all in one place?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is not for this PR itself, but: we can do some further simplifications around TaskManager, e.g. the logic around shouldCommitAdditionalTasks was mainly for EOS-v1 since only in that case we may not want to commit for all tasks, for ALOS and EOS-v2, whenever we want to commit, we should always commit all tasks anyways. With V1 removed we should consider reducing the complexity for such cases as well, i.e. when we commit, we are always trying to commit all active tasks except for the error handling case.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Sounds good. I will create a separate PR for this.

@guozhangwang

Copy link
Copy Markdown

Also cc @cadonna @lihaosky for another look.

@cadonna cadonna left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@lucasbru Thanks for the PR!

I left a couple of comments.

Note that a PR that addresses Kafka should have a title that starts with a ticket number of the Apache Kafka Jira. This PR's title starts with a ticket number of the Confluent Jira.
If you haven't please read the following pages:

I understand that your development branch in your fork is named 4.0. Note that the 4.0 release branch in apache/kafka will also be named 4.0. You might run into naming conflicts.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Couldn't you even use private here and below?

@lucasbru lucasbru Sep 14, 2022

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

I couldn't because I am using it in StreamsConfigTest.java.

However, since you seem to be concerned about polluting the codebase forever with these outdated values, I removed these and hardcoded the values in StreamsConfigTest and the check in StreamsConfig.

Comment thread docs/streams/core-concepts.html Outdated
Comment on lines 298 to 300

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
As of the 3.0.0 release, the first version of exactly-once has been deprecated, and with the 4.0.0 release,
it has been removed from Kafka Streams. Users who want to use exactly-once processing with Kafka streams 4.0.0
or newer, upgrade to broker version 2.5.0 or newer.
As of the 3.0.0 release, the first version of exactly-once has been deprecated, and with the 4.0.0 release,
it has been removed from Kafka Streams.

It is already mentioned in the first sentence of the paragraph that users need a broker version 2.5.0 or newer.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Done

Comment on lines 588 to 589

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: 4 spaces are enough for indentation.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Done

Comment on lines 630 to 631

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Also here 4 spaces are enough:

Suggested change
consumerConfigs.get(ConsumerConfig.ISOLATION_LEVEL_CONFIG),
equalTo(READ_COMMITTED.name().toLowerCase(Locale.ROOT))
consumerConfigs.get(ConsumerConfig.ISOLATION_LEVEL_CONFIG),
equalTo(READ_COMMITTED.name().toLowerCase(Locale.ROOT))

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Done

@cadonna cadonna Sep 14, 2022

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I am wondering if we should just remove StreamsConfig.EXACTLY_ONCE and StreamsConfig.EXACTLY_ONCE_BETA and not throw a specific ConfigException for those value, but just a generic one. When would we remove the specific ConfigException in the future?
We had a deprecation period, we have the removal documented in the docs, I think that should be enough.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Both solutions are fine with me. I suppose its a trade-off between a courtesy to the users and a cleaner codebase. @guozhangwang wdyt?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

My personal preference: even if we remove the deprecated strings, I still slightly preferred to log a meaningful message to users indicating that those config values are not used anymore, with the cost that we need to have those hard-coded values forever in the codebase.

But in either way, I think we can still throw a ConfigException but a generic exception, what's the motivation for not doing so? @cadonna

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@guozhangwang My proposal was to throw a generic ConfigException instead of a specific one. I have never intended not to throw a ConfigException.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ah I see, thanks @cadonna .

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should verify if we have a system test that might replace this test. If not we might consider creating such a system test.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Agreed. As noted above, this will be done in a follow-up PR.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think it would make sense to replace eosConfig with StreamsConfig.EXACTLY_ONCE_V2 in these tests. I already imagine future me wondering about this useless indirection.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Good point, Done

Comment on lines 118 to 119

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
final StreamsProducer eosBetaStreamsProducerWithMock = new StreamsProducer(
eosBetaConfig,
final StreamsProducer eosV2StreamsProducerWithMock = new StreamsProducer(
eosV2Config,

We want to get rid of the "beta".

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

I wanted to keep such renamings out of the first PR to keep it reviewable (and added a note above), but maybe it was ill-conceived in this case. Done.

Comment on lines 330 to 761

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Just a heads up: We are currently migrating from EasyMock to Mockito. There is already PR apache#12607 up to migrate this test. That means the all these mock setups will change in the near future.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Thanks. Yes, I guess there will be lots of conflicts anyways, once we are ready to push 4.0 to trunk. So I'll just have to deal with it I guess

@lucasbru lucasbru changed the title KSE-1228 - Remove exactly_once / exactly_once_beta KAFKA-12689 - Remove exactly_once / exactly_once_beta Sep 14, 2022
@lucasbru lucasbru closed this Sep 14, 2022
@lucasbru
lucasbru deleted the KSE-1228 branch September 14, 2022 14:25
@lucasbru
lucasbru restored the KSE-1228 branch September 14, 2022 14:26
@lucasbru lucasbru reopened this Sep 14, 2022
@lucasbru lucasbru changed the title KAFKA-12689 - Remove exactly_once / exactly_once_beta KAFKA-12689: Remove exactly_once / exactly_once_beta Sep 14, 2022
@guozhangwang

Copy link
Copy Markdown

@lucasbru I think jenkins jobs would not auto trigger for these PRs (unfortunately..) so we'd have to manually run the tests to make sure nothing breaks.

@lihaosky lihaosky left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks @lucasbru ! Only have a minor comment

Comment thread release.py Outdated
Comment on lines 214 to 215

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This logic here is different from original logic, is it intended? jdk_java_home in original logic is not set here.

@lucasbru lucasbru Sep 15, 2022

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Yes, I think the original logic was broken when user input was empty. It was supposed to use JAVA_HOME system property to find java, but it wouldn't set jdk_java_home, so the following version check

java_version = cmd_output("%s/bin/java -version" % jdk_java_home, env=jdk_env)

would access /bin/java which does not exist on any system I know.

Not sure why nobody else had this problem before, maybe nobody used the JAVA_HOME branch here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Could we carve out this change as a separate PR (you can file a PR directly against apache kafka trunk) since it's orthogonal to the ticket itself?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Yes, you are right. I removed it.

@lihaosky

Copy link
Copy Markdown

For Jenkins jobs, can you create these PRs in some branch of base Kafka to run?

Removes all code for handling the processing_guarantee settings
exactly_once and exactly_once_beta, which have been deprecated
since 3.0.0 and can be removed with the release of 4.0.0.

Some tests were adapted to use exactly_once_v2 instead of
exactly_once_beta or exactly_once, to retain the same coverage
of the code bas.

exactly_once / exactly_once_beta are rejected with an error
message to upgrade to exactly_once_v2.

EosV2UpgradeIntegrationTest is removed, since it cannot be
implemented anymore with only one implementation available.
@lucasbru
lucasbru changed the base branch from KAFKA-12689 to trunk September 15, 2022 15:12
@lucasbru
lucasbru changed the base branch from trunk to KAFKA-12689 September 15, 2022 15:12
@lucasbru

Copy link
Copy Markdown
Owner Author

For Jenkins jobs, can you create these PRs in some branch of base Kafka to run?

I created a draft PR in apache/kafka, here: apache#12646

It did ran, but it failed with various unrelated errors (like all recently merged PRs)...

@lucasbru

Copy link
Copy Markdown
Owner Author

So to me, the failures in the unit tests look unrelated / flaky. For JDK 11 / Scala 2.13, all tests passed. I also ran the system tests for streams, which passed.

https://jenkins.confluent.io/job/system-test-kafka-branch-builder/5130/

Anything else or can we merge this part? @guozhangwang @cadonna

@guozhangwang

Copy link
Copy Markdown

Thanks @lucasbru , I do not have further comments, and I think we can merge this PR now and continue on follow-up PRs.

@lucasbru
lucasbru merged commit a0310fa into KAFKA-12689 Sep 19, 2022
@lucasbru
lucasbru deleted the KSE-1228 branch September 23, 2022 07:55
lucasbru pushed a commit that referenced this pull request Sep 8, 2023
This change introduces some basic clean up and refactoring for forthcoming commits related to the revised fetch code for the consumer threading refactor project.

Reviewers: Christo Lolov <lolovc@amazon.com>, Jun Rao <junrao@gmail.com>
lucasbru pushed a commit that referenced this pull request Jul 31, 2025
… for aborted txns (apache#17676) (#1 7733)

Reviewers: Jun Rao <junrao@gmail.com>
lucasbru pushed a commit that referenced this pull request Feb 17, 2026
…ker provisioning (apache#21394)

## Summary

Fixes bugs where `--jdk-version` and `--jdk-arch` parameters were
ignored during system test worker provisioning, and refactors
`vagrant/base.sh` to support flexible JDK versions without code changes.

---

## Problem

The Vagrant provisioning script (`vagrant/base.sh`) had two bugs that
caused JDK version parameters to be ignored:

| Bug | Problem |
|-----|---------|
| **#1: `--jdk-version` ignored** | `JDK_FULL` was hardcoded to
`17-linux-x64`, so passing `--jdk-version 25` still downloaded JDK 17 |
| **#2: `--jdk-arch` ignored** | Architecture parameter was passed but
never used in the S3 download URL |

---

## Solution

- Validate `JDK_MAJOR` and `JDK_ARCH` input parameters with regex
- Dynamically construct `JDK_FULL` from `JDK_MAJOR` and `JDK_ARCH`
- Update S3 path to use `/jdk/` subdirectory
- Add logging for debugging

---

## Changes

### `vagrant/base.sh`

| Change | Description |
|--------|-------------|
| **Input validation** | Added regex validation for `JDK_MAJOR` and
`JDK_ARCH` with sensible defaults |
| **Dynamic construction** | `JDK_FULL` is now constructed from
`JDK_MAJOR` and `JDK_ARCH` if not explicitly provided |
| **Updated S3 path** | Changed URL from
`/kafka-packages/jdk-{version}.tar.gz` to
`/kafka-packages/jdk/jdk-{version}.tar.gz` |
| **Logging** | Added debug output for JDK configuration |
| **Backward compatibility** | Vagrantfile can still pass `JDK_FULL`
directly; the script validates and uses it if valid |

---

## S3 Path Change

### Old Path
```
s3://kafka-packages/jdk-{version}.tar.gz
```

### New Path
```
s3://kafka-packages/jdk/jdk-{version}.tar.gz
```

### Available JDKs in `s3://kafka-packages/jdk/`

| File | Version | Architecture |
|------|---------|--------------|
| `jdk-7u80-linux-x64.tar.gz` | 7u80 | x64 |
| `jdk-8u144-linux-x64.tar.gz` | 8u144 | x64 |
| `jdk-8u161-linux-x64.tar.gz` | 8u161 | x64 |
| `jdk-8u171-linux-x64.tar.gz` | 8u171 | x64 |
| `jdk-8u191-linux-x64.tar.gz` | 8u191 | x64 |
| `jdk-8u202-linux-x64.tar.gz` | 8u202 | x64 |
| `jdk-11.0.2-linux-x64.tar.gz` | 11.0.2 | x64 |
| `jdk-17-linux-x64.tar.gz` | 17 | x64 |
| `jdk-18.0.2-linux-x64.tar.gz` | 18.0.2 | x64 |
| `jdk-21.0.1-linux-x64.tar.gz` | 21.0.1 | x64 |
| `jdk-21.0.1-linux-aarch64.tar.gz` | 21.0.1 | aarch64 |
| `jdk-25-linux-x64.tar.gz` | 25 | x64 |
| `jdk-25-linux-aarch64.tar.gz` | 25 | aarch64 |
| `jdk-25.0.1-linux-x64.tar.gz` | 25.0.1 | x64 |
| `jdk-25.0.1-linux-aarch64.tar.gz` | 25.0.1 | aarch64 |
| `jdk-25.0.2-linux-x64.tar.gz` | 25.0.2 | x64 |
| `jdk-25.0.2-linux-aarch64.tar.gz` | 25.0.2 | aarch64 |

---

## Future JDK Releases

> **IMPORTANT: No code changes required for future Java major/minor
releases!**

The validation regex supports all version formats:
- **Major versions**: `17`, `25`, `26`
- **Minor versions**: `25.0.1`, `25.0.2`, `26.0.1`
- **Legacy format**: `8u144`, `8u202`

### Adding New JDK Versions

To add support for a new JDK version (e.g., JDK 26, 25.0.3):

1. Download the JDK tarball from Oracle/Adoptium
2. Rename to follow naming convention:
`jdk-{VERSION}-linux-{ARCH}.tar.gz`
3. Upload to S3: `aws s3 cp jdk-{VERSION}-linux-{ARCH}.tar.gz
s3://kafka-packages/jdk/`
4. Use in tests: `--jdk-version {VERSION} --jdk-arch {ARCH}`

No modifications to `base.sh` or any other scripts are needed.

---

## Benefits

| Before | After |
|--------|-------|
| `--jdk-version` ignored | ✅ Correctly uses specified version |
| `--jdk-arch` ignored | ✅ Correctly uses specified architecture |
| Only major version support | ✅ Full version support (e.g., `25.0.2`) |
| Code change needed for new JDK | ✅ Just upload to S3 and pass version
|

---

## Testing

Tested with different JDK versions to confirm the fix works correctly:

| Test | JDK_MAJOR | Expected | Actual | Result | Test Report |
|------|-----------|----------|--------|--------|-------------|
| JDK 17 | `17` | javac 17.0.4 | javac 17.0.4 | ✅ |
| JDK 25 | `25` | javac 25.0.2 | javac 25.0.2 | ✅ |


---

## Backward Compatibility

- **Vagrantfile**: Continues to work as before
- **Existing workflows**: Default behavior unchanged (JDK 17 on x64
architecture)
- **No breaking changes**: All existing configurations continue to work

---

 Reviewers: Manikumar Reddy <manikumar.reddy@gmail.com>, Chia-Ping Tsai <chia7712@gmail.com>
lucasbru added a commit that referenced this pull request Apr 23, 2026
…ionale

Three changes in one revision:

C3 (requiresTopologyPush silently swallowing exceptions) is resolved by
tightening the contract: plugins MUST NOT throw from requiresTopologyPush.
All failure modes must be handled internally and converted to a return
value of false. The broker keeps a defensive catch as a backstop but the
contract says plugins must not rely on it. Added as guideline #1 in the
Plugin Implementation Guidelines alongside the other behavioral
expectations.

Drops the "broker does not advertise UpdateStreamsGroupTopologyDescription
in its ApiVersions response" gating. The gating was redundant with the
heartbeat-flag gating that already prevents clients from initiating the
push against a plugin-less coordinator, and the dynamic per-broker
ApiVersions distinction added implementation complexity for no real
benefit (clients talking to a mix of brokers already get the right answer
from the heartbeat flag). The broker continues to return UNSUPPORTED_VERSION
if a client does send the RPC anyway, consistent with the rolling-upgrade
path.

Applies a "what, not why" pass across the KIP (outside Motivation and
Rejected Alternatives). Removes editorial justifications — sentences
like "The one-heartbeat delay before requesting a push is negligible.",
"This prevents …", "This allows …", "so that …" — from the Plugin
Interface Javadocs, Admin section, CLI section, Broker Side proposed
changes, Client Side, Error Handling and Retries, Plugin Implementation
Guidelines, Security Considerations, Metrics, Compatibility, Rolling
Upgrades, and Future Work. Rules, contracts, and MUST/MUST NOT
statements are preserved verbatim; only the supporting commentary that
restates the same point in different words, or explains why a rule
exists, has been cut.

C3 removed from REVIEW.md.
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.

4 participants