Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Backport stable/8.4] Avoid accidental reordering of column family values #16129

Merged
merged 10 commits into from
Jan 31, 2024

Conversation

korthout
Copy link
Member

@korthout korthout commented Jan 29, 2024

Description

Backport of #16106 to stable/8.4.

Relates to #15804

Due to conflicts, I would recommend you review the commits individually. Commits that resolve conflicts mention this at the bottom of the commit message (under the cherry picked from commit x line).

There were conflicts because the following column family does not exist on 8.4 yet:

  • COMPENSATION_SUBSCRIPTION(81)

This doesn't change any behavior. We introduce a new EnumValue interface
that can be implemented by Enums to avoid using ordinal as the value
associated with each enum value.

We also implement this in the ZbColumnFamilies enum as this suffers from
bugs caused by ordinal re-ordering. While this doesn't yet move
ZbColumnFamilies away from using an ordinal (note the implementation),
it already prepares the enum for moving to a hardcoded number system.

We'll add this number system in a later commit.

(cherry picked from commit 281c321)

Tried to add not yet supported column family: COMPENSATION_SUBSCRIPTION

Conflicts:
  protocol/src/main/java/io/camunda/zeebe/protocol/ZbColumnFamilies.java
This is only to have access to EnumValue inside of zb-db.

Alternatives considered:
- add EnumValue to zeebe-util, would require zeebe-protocol to depend on
  zeebe-util
- move ZbColumnFamilies/EnumValue to a different module, but it's not
  clear where. Likely, a good place is zeebe-state (when we finally add
  that module), which would clarify which modules depend on the state
  (backup, engine, etc). Note that ZbColumnFamilies used to be in the
  engine module, but was moved out when splitting the engine and stream
  processor. For now, this doesn't seem like a solution.

(cherry picked from commit a695a5f)
We're introducing EnumValue as a way to associate hardcoded values with
the ZbColumnFamilies rather than the currently used ordinals.

zb-db doesn't depend on ZbColumnFamilies directly, so it can function
regardless of the data that is stored in it. This makes sense, because
the data schema is decided by modules like engine (in the state
package).

To allow using EnumValue's .getValue() in subsequent commits inside the
zb-db module, we need to clarify some structure for the ColumnFamilyType
generics: it must be an enum that implements EnumValue.

We can achieve that by combining some generics. First, we say that the
type extends an Enum that itself extends EnumValue.

However, then we don't have access to the .getValue() method yet. To add
that, we also have to say that the type extends EnumValue itself. This
combination can be achieved with Intersection Types (A & B).

Note that for the test column family enums, it doesn't matter how the
getValue method is implemented. We can use .ordinal() in the test
classes, because they are not used in production code. While,
DefaultColumnFamily is also not used in production code, it does exist
in `main` and could be used in production later. Therefore, I've
implemented that getValue with an actual hardcoded value.

(cherry picked from commit 7794eae)
This adds two tests.

The main test is shouldNotReuseEnumValues. It ensures that the
getValue() method produces a unique value for each value in the enum.
It does not test determinism, but that is implied for this method.

The current implementation uses the ordinal() method. To reimplement it,
I've added a temporary test that we can remove after re-implementing the
getValue() method. It should pass after re-implementation to ensure we
don't accidentally reorder the values now.

(cherry picked from commit e415790)
While this is a refactor: it does not change behavior, it strengthens
our ability to add and backport new column families by hardcoding the
associated value.

A test exists that verifies that all hardcoded values are added using
the exact same value as the ordinal currently has.

Another test exists that verifies that no hardcoded value is reused.
This one is especially important, as it protects against accidental
problems while automatically backporting.

After this change, values don't necessarily need to be added to the
bottom anymore, as long as they receive a unique value.

(cherry picked from commit 9118269)

Tried to add not yet supported column family
COMPENSATION_SUBSCRIPTION(81).

Conflicts:
  protocol/src/main/java/io/camunda/zeebe/protocol/ZbColumnFamilies.java
This test case is no longer needed. From now on, the ZbColumnFamilies
values must remain the same over time (we have hardcoded them) and must
be unique (there is another test case that checks this). But there is no
need anymore for the values to match exactly the ordinals.

(cherry picked from commit 759d3b0)
Rather than using the ordinal, we can now use the static enum value of
the column family.

The value is/was used to determine the prefix of keys in the column
family.

(cherry picked from commit 36e85ec)
As database entries are stored with the column family's value prefixed
to the key, so too do we look them up with that value when checking
foreign key constraints.

We assume that the foreignKey.columnFamily() returns an instance of
EnumValue so we can call the .getValue() on it. This is safe to do
because the column family must anyways be an instance extending Enum<?
extends EnumValue>.

Alternatively, we could've avoided this cast if we would make the
foreign key understand that the column family type also actually
implements EnumValue, using an intersection type. However, that would've
required adding this intersection type as an additional generic on the
DbForeignKey<DbKey, CFType>. As this class is used often in our state
classes, it would deteriorate the readability of those classes, as we
currently can refer to it as DbForeignKey<DbKey>. This is why I went for
casting rather than more generics.

(cherry picked from commit aa61c8b)
This makes it almost impossible to make mistakes, because we can no
longer accidentally skip an enum value.

(cherry picked from commit fb9bab0)
@korthout korthout marked this pull request as ready for review January 31, 2024 08:27
@korthout
Copy link
Member Author

@nicpuppa @Zelldon I've asked for your review because you also reviewed the original.

Due to conflicts, I would recommend you review the commits individually. Commits that resolve conflicts mention this at the bottom of the commit message (under the cherry picked from commit x line).

Copy link
Contributor

@nicpuppa nicpuppa left a comment

Choose a reason for hiding this comment

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

LGTM 🚀

Copy link
Member

@Zelldon Zelldon left a comment

Choose a reason for hiding this comment

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

Thanks one comment regarding the backporting of the missing CFs


USER_TASKS,
USER_TASK_STATES,
Copy link
Member

Choose a reason for hiding this comment

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

❓ 🤔 I somehow thought/expected that we backport all CFs now, for example https://github.com/camunda/zeebe/blob/main/protocol/src/main/java/io/camunda/zeebe/protocol/ZbColumnFamilies.java#L175 is missing.

Copy link
Member Author

Choose a reason for hiding this comment

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

I considered this, but the problem is that it would require fixing the bug in one go on 8.1.

If we would just copy the current CFs file to 8.1, then we also need to add the recovery migration task. Otherwise we're now losing the data on 8.1.

I wanted to split it into two steps:

  • first add some safety guards
  • then add the migration task to the older versions

Copy link
Member

Choose a reason for hiding this comment

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

I think this was not clear to me.

Copy link
Member Author

Choose a reason for hiding this comment

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

Does it make sense or do you think I should change the approach and do it all at once?

Copy link
Member

Choose a reason for hiding this comment

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

I think we can also do it like this, but should not forget to do it.

@korthout korthout added this pull request to the merge queue Jan 31, 2024
Merged via the queue into stable/8.4 with commit d502ad3 Jan 31, 2024
31 checks passed
@korthout korthout deleted the korthout-backport-16106-to-stable/8.4 branch January 31, 2024 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants