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

Make transaction metadata nullable to support existing databases #841

Merged
merged 9 commits into from
May 12, 2023

Conversation

jnmt
Copy link
Contributor

@jnmt jnmt commented Apr 19, 2023

This PR adds a feature to handle null transaction metadata. To support existing databases, we assume that we ask users to add transaction metadata columns (with null values in records) in existing tables. So, as this PR does, we would like to handle those records with null transaction metadata as a sort of 'deemed' committed state.

@jnmt jnmt added the enhancement New feature or request label Apr 19, 2023
@jnmt jnmt self-assigned this Apr 19, 2023
// For preparing an update of a deemed-commit state record, we need to use
// version 0 rather than NULL since we want to distinguish the preparation from
// an initial record insertion.
columns.add(IntColumn.of(Attribute.BEFORE_VERSION, 0));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

As commented in the code, we don't use the original NULL value for before_version in prepare records
to distinguish them from prepare records for newly-inserted ones.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, I don't fully get this.
If we don't do this, i.e., if we just use NULL for before_version, what issue would occur?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for seeking clarification. Simply put, the record cannot be rollbacked correctly since the recovery process falls in the deletion path for the initial record case here.

if (beforeId.hasNullValue() && beforeVersion.hasNullValue()) {
// no record to rollback, so it should be deleted
mutations.add(composeDelete(base, latest));

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, right. Thank you!
I think it would be great if you could make the comment more specific about the case.
(i.e., adding the reason why we want to use 0 instead of null for before_version.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, I will try to revise it with a more specific reason.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@feeblefakie Fixed in d4566f0. PTAL when you get a chance.

Comment on lines 90 to 91
if (key.equals(Attribute.VERSION) && v.getIntValue() == 0) {
columns.add(IntColumn.ofNull(Attribute.VERSION));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For rollback, we need to change 0 -> null as the opposite side of prepare.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think adding a comment for this would be helpful.

return !value.getName().startsWith(Attribute.BEFORE_PREFIX)
&& !isValueInKeys(value, primary, clustering);
private boolean isBeforeRequired(Column<?> column, Key primary, Optional<Key> clustering) {
return !column.getName().startsWith(Attribute.BEFORE_PREFIX)
Copy link
Contributor

Choose a reason for hiding this comment

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

This might be a silly question, but what if an original column name starts with before_ prefix like before_update_balance (non-primary or clustering key)? This logic seems to return false, but I think before_before_update_balance column should be taken care of.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, good point. Yeah, it should be taken care of if we allow users to use a column name starting with before_. For new projects, at least, prohibiting such column names would not cause serious problems. For existing databases, some users might be using such column names, but I guess it would be rare, and we could ask users to rename them. So, IMHO, we can prohibit user columns with before_ in both cases for now.

@brfrn169 By the way, I think we don't have any guard for those names in the schema-loader, and I was able to create a before_xx column and faced an issue that its before column was not updated in the prepare phase. Is my understanding correct?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I can reproduce it with the current code.

ScalarDB SQL CLI

0: scalardb> select * from example_app.account2;
+----+----------+-----+
| id | before_x |  y  |
+----+----------+-----+
| 1  | 10       | 100 |
+----+----------+-----+
1 row selected (0.005 seconds)
0: scalardb> update example_app.account2 set before_x = 20, y = 200 where id = 1;
1 row affected (0.04 seconds)

psql

postgres=# select id, before_x, y, tx_state, before_before_x, before_y from example_app.account2;
 id | before_x |  y  | tx_state | before_before_x | before_y 
----+----------+-----+----------+-----------------+----------
  1 |       20 | 200 |        3 |                 |      100
(1 row)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@komamitsu Thanks! Yeah, I faced the same issue.

Copy link
Collaborator

@brfrn169 brfrn169 Apr 20, 2023

Choose a reason for hiding this comment

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

I thought we accepted to create the before_xx column as a normal column, and in this case, its before column should be before_before_xx. But, we might miss something to handle that.

The logic to detect the before column is here:

/**
* Returns whether the specified column is a part of the before image columns or not.
*
* @param columnName a column name
* @param tableMetadata a transaction table metadata
* @return whether the specified column is a part of the before image columns or not
*/
public static boolean isBeforeImageColumn(String columnName, TableMetadata tableMetadata) {
if (!tableMetadata.getColumnNames().contains(columnName)
|| tableMetadata.getPartitionKeyNames().contains(columnName)
|| tableMetadata.getClusteringKeyNames().contains(columnName)) {
return false;
}
if (BEFORE_IMAGE_META_COLUMNS.containsKey(columnName)) {
return true;
}
if (columnName.startsWith(Attribute.BEFORE_PREFIX)) {
// if the column name without the "before_" prefix exists, it's a part of the before image
// columns
return tableMetadata
.getColumnNames()
.contains(columnName.substring(Attribute.BEFORE_PREFIX.length()));
}
return false;
}

The basic idea is, if there are both xxx and before_xxx (before prefixed) columns in the table, xxx is an after image column, and before_xxx is a before image column.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah, okay. It should be a bug. Let me check. Thanks.

Copy link
Collaborator

@brfrn169 brfrn169 Apr 20, 2023

Choose a reason for hiding this comment

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

Ah, okay we should use ConsensusCommitUtils.isAfterImageColumn() here:

return !value.getName().startsWith(Attribute.BEFORE_PREFIX)

I thought I had fixed it 😞

Let me fix it in another PR.

Copy link
Collaborator

@brfrn169 brfrn169 Apr 20, 2023

Choose a reason for hiding this comment

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

Oh, we already have the validation to check if a table has transaction meta columns when creating the table 😅 :

checkIsNotTransactionMetaColumn(tableMetadata.getColumnNames());
Set<String> nonPrimaryKeyColumns = getNonPrimaryKeyColumns(tableMetadata);
checkBeforeColumnsDoNotAlreadyExist(nonPrimaryKeyColumns, tableMetadata);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@brfrn169 Thanks for the additional info. But I guess it only checks to prevent from being created both xxx and before_xxx as normal columns. We can still create before_xxx column itself...

Copy link
Collaborator

@brfrn169 brfrn169 Apr 20, 2023

Choose a reason for hiding this comment

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

@jnmt Yes, the following bug fix allows users to create before_ prefixed columns as normal columns:

#844

@jnmt jnmt requested a review from komamitsu April 20, 2023 01:27
Copy link
Contributor

@Torch3333 Torch3333 left a comment

Choose a reason for hiding this comment

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

LGTM, thank you.
I only left minor refactoring suggestions.

Copy link
Contributor

@komamitsu komamitsu 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
Contributor

@feeblefakie feeblefakie left a comment

Choose a reason for hiding this comment

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

Overall looking good. Thank you!
Left a few questions, so PTAL when you get a chance.

// For preparing an update of a deemed-commit state record, we need to use
// version 0 rather than NULL since we want to distinguish the preparation from
// an initial record insertion.
columns.add(IntColumn.of(Attribute.BEFORE_VERSION, 0));
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, I don't fully get this.
If we don't do this, i.e., if we just use NULL for before_version, what issue would occur?

@jnmt
Copy link
Contributor Author

jnmt commented Apr 20, 2023

@Torch3333 Thank you for the feedback! Fixed in b3cf9c9. PTAL if you have time.

Copy link
Contributor

@Torch3333 Torch3333 left a comment

Choose a reason for hiding this comment

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

LGTM, thanks!

@komamitsu
Copy link
Contributor

@jnmt (cc: @brfrn169) Just noticed this PR doesn't have integration test cases for NULL status record. Do you think we should add some integration tests? I think it would require non-straightforward way to add NULL status records (directly using the native JDBC?) at arrangement phase. So I'm not sure those integration tests should be added in this PR or in another PR, though.

@jnmt
Copy link
Contributor Author

jnmt commented Apr 28, 2023

@jnmt (cc: @brfrn169) Just noticed this PR doesn't have integration test cases for NULL status record. Do you think we should add some integration tests? I think it would require non-straightforward way to add NULL status records (directly using the native JDBC?) at arrangement phase. So I'm not sure those integration tests should be added in this PR or in another PR, though.

@komamitsu Thanks for the question. Yes, we should add some integration tests since this is a kind of major change in the protocol behavior. We are also thinking about applying the Jepsen tests for it and going to discuss it at some point in time (next or next next week). But, in any case, the integration tests will be added by another PR.

@feeblefakie
Copy link
Contributor

@jnmt I think jepsen tests can be done in another PR, but it would be great if you could add integration tests in this PR. 🙇

@jnmt
Copy link
Contributor Author

jnmt commented Apr 28, 2023

@jnmt I think jepsen tests can be done in another PR, but it would be great if you could add integration tests in this PR.

@feeblefakie Sure! I will do it in the same PR if it's better.

// deleted as an initial record in a rollback situation. To avoid this and roll
// back to a record with a NULL version (i.e., regarded as committed) correctly,
// we need to use version 0 rather than NULL for before_version.
columns.add(IntColumn.of(Attribute.BEFORE_VERSION, 0));
Copy link
Collaborator

Choose a reason for hiding this comment

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

@feeblefakie I'm wondering if we could remove the tx_version column in the first place. As discussed before, if the tx_id column is unique, we don't really need the tx_version column. If we can remove it, we don't need this special handling, and we can make the code simpler.

From a backward compatibility perspective, even if we remove the tx_version column, we simply don't use the column on existing tables, so I don't think we will run into any problems. For new tables, we will no longer need to create the tx_version column.

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@brfrn169 As discussed, we need to use something before columns, and the before version would be good enough for the current decision. So, I kept using the version here and updated comments for clarification in db51113.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added integration tests for null-tx-metadata handling (incl. rollback/roll-forward) referencing ConsensusCommitSpecificIntegrationTestBase, but skipped the following cases.

  1. Tests that do not use existing data (e.g., insert a new record and do something)
  2. Tests for conflict / write skew situations

// deleted as an initial record in a rollback situation. To avoid this and roll
// back to a record with a NULL version (i.e., regarded as committed) correctly,
// we need to use version 0 rather than NULL for before_version.
columns.add(IntColumn.of(Attribute.BEFORE_VERSION, 0));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@brfrn169 As discussed, we need to use something before columns, and the before version would be good enough for the current decision. So, I kept using the version here and updated comments for clarification in db51113.

Copy link
Contributor Author

@jnmt jnmt left a comment

Choose a reason for hiding this comment

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

Sorry for re-reviewing, but I updated a little for a better understanding of the logic and added integration tests based on the feedback in db51113. So, it would be great if you could take a look again when you get a chance.

Copy link
Collaborator

@brfrn169 brfrn169 left a comment

Choose a reason for hiding this comment

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

Left several minor comments but LGTM other than that. Thank you!

Comment on lines +140 to +149
public boolean isDeemedAsCommitted() {
return getId() == null;
}

public boolean hasBeforeImage() {
// We need to check not only before_id but also before_version to determine if the record has
// the before image or not since we set before_version to 0 for the prepared record when
// updating the record deemed as the committed state (cf. PrepareMutationComposer).
return !getBeforeIdColumn().hasNullValue() || !getBeforeVersionColumn().hasNullValue();
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

Comment on lines 151 to 157
public TextColumn getBeforeIdColumn() {
return (TextColumn) result.getColumns().get(Attribute.BEFORE_ID);
}

public IntColumn getBeforeVersionColumn() {
return (IntColumn) result.getColumns().get(Attribute.BEFORE_VERSION);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

It look like we can make these method private?

Copy link
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 in d5ff7af.

Comment on lines 90 to 91
if (key.equals(Attribute.VERSION) && v.getIntValue() == 0) {
columns.add(IntColumn.ofNull(Attribute.VERSION));
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think adding a comment for this would be helpful.

Copy link
Contributor

@feeblefakie feeblefakie left a comment

Choose a reason for hiding this comment

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

Overall looking good to me!
Left one minor comment.

public String getId() {
return getText(Attribute.ID);
}

public TransactionState getState() {
return TransactionState.getInstance(getInt(Attribute.STATE));
int state = getInt(Attribute.STATE);
Copy link
Contributor

Choose a reason for hiding this comment

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

Although getInt returns 0 for a null value, should it be slightly more appropriate/readable to use isNull since it actually checks if the value is null?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's right. Thanks. Fixed in d5ff7af.

@jnmt jnmt requested a review from feeblefakie May 11, 2023 08:29
Copy link
Contributor

@feeblefakie feeblefakie left a comment

Choose a reason for hiding this comment

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

LGTM! Thank you!

Copy link
Contributor

@feeblefakie feeblefakie left a comment

Choose a reason for hiding this comment

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

LGTM! Thank you!

Copy link
Contributor

@komamitsu komamitsu left a comment

Choose a reason for hiding this comment

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

LGTM! 👍

private DistributedStorage storage;
private Coordinator coordinator;
private RecoveryHandler recovery;
private CommitHandler commit;
Copy link
Contributor

Choose a reason for hiding this comment

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

[trivial] This can be a local variable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch! Thank you! Fixed on 3dd3072.

.clusteringKey(Key.ofInt(ACCOUNT_TYPE, j))
.value(IntColumn.of(BALANCE, INITIAL_BALANCE))
.build();
storage.put(put);
Copy link
Contributor

Choose a reason for hiding this comment

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

I see. We can manipulate underlying database by directly using DistributedStorage...

Copy link
Contributor

@Torch3333 Torch3333 left a comment

Choose a reason for hiding this comment

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

LGTM, thank you!

@jnmt jnmt merged commit c27fac6 into master May 12, 2023
12 checks passed
@jnmt jnmt deleted the nullable-tx-metadata branch May 12, 2023 04:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants