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

Add gPRC layer for DistributedTransaction #216

Merged
merged 2 commits into from
Jun 14, 2021

Conversation

brfrn169
Copy link
Collaborator

@brfrn169 brfrn169 added the enhancement New feature or request label Jun 10, 2021
@brfrn169 brfrn169 requested review from feeblefakie and yito88 June 10, 2021 03:18
@brfrn169 brfrn169 self-assigned this Jun 10, 2021
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! Great work!
Left some questions and comments. PTAL.

mutations.add(ProtoUtil.toMutation(mutation));
}

if (mutations.size() == 1) {
Copy link
Contributor

@feeblefakie feeblefakie Jun 14, 2021

Choose a reason for hiding this comment

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

I think we can simply use mutations with the else logic without checking if there is a single element.
Creating an unnecessary list is required for put and delete but I think the overhead is negligible.
(I know the same logic exists in DistributedStorageService but I noticed it now.)
What do you think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, I thought the overhead of creating an unnecessary list is negligible, too. My concern here is that we are not sure the performance (or overhead) difference between the single put/delete and the multiple put/delete/mutate, which depends on the storage implementation. For example, one storage implementation has an optimization for single put/delete, and the single put/delete might be faster than the multiple put/delete/mutate when we execute one mutation. That's why I have this special case for one mutation. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

From the perspective of separation of concerns, I think an upper layer shouldn't care about the implementation or the optimization of the underlining layer if it's passing the same information, and the underlining layer should do proper optimization based on the given information.
So, in this case, the underlining layer should check if a given list contains only single item if it has an optimization for single item operation. (but the current code might not be handling such things properly and should probably be fixed)
Thought? (maybe we should discuss?)

} catch (UnknownTransactionStatusException e) {
LOGGER.error("an unknown transaction error happened during the execution", e);
responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asRuntimeException());
} catch (Throwable e) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm wondering Errors should be caught here.
I've never caught Errors but should it rather stop the server or something if an Error occurs?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I catch Throwable here is because whenever any Throwable is thrown, I thought we should call responseObserver.onError, otherwise the client will get stuck. And for OutOfMemoryError, we have the -XX:OnOutOfMemoryError JVM flag, and we can kill the server with it. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

Is it true that a client gets stuck when an Error occurs?
I thought Errors will end the server and the gRPC client will be notified with some runtime exception so the same behavior will be observed without dealing with Errors explicitly in this case.

private DistributedTransaction getTransaction(String transactionId) throws TransactionException {
ActiveTransaction activeTransaction = activeTransactions.get(transactionId);
if (activeTransaction == null) {
throw new TransactionException(
Copy link
Contributor

Choose a reason for hiding this comment

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

A question and a comment.
I think this case happens mostly in a case where different connections are used in a transaction, which only happens when a connection is closed for some reason after the transaction is started.
First of all, is this right?

If so, should the exception be treated separately?
I feel it's not an INTERNAL error.
What do you think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think this case could happen due to the following reasons:

  1. A client specifies a wrong transaction ID
  2. A client specifies a transaction ID that associates with a transaction that's already closed
  3. A transaction associated with the specified transaction ID is already expired by the transactionExpirationThread

An INVALID_ARGUMENT error suits the 1st and 2nd cases, but I think they are rare cases. And an INTERNAL error suits the 3rd case, and I think it most likely happens. So I decided to throw an INTERNAL error for this case. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

Fair enough. Thanks!

Copy link
Collaborator Author

@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.

@feeblefakie Thank you for reviewing this!

I left some responses for your reviews. Please check them. Thanks.

private DistributedTransaction getTransaction(String transactionId) throws TransactionException {
ActiveTransaction activeTransaction = activeTransactions.get(transactionId);
if (activeTransaction == null) {
throw new TransactionException(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think this case could happen due to the following reasons:

  1. A client specifies a wrong transaction ID
  2. A client specifies a transaction ID that associates with a transaction that's already closed
  3. A transaction associated with the specified transaction ID is already expired by the transactionExpirationThread

An INVALID_ARGUMENT error suits the 1st and 2nd cases, but I think they are rare cases. And an INTERNAL error suits the 3rd case, and I think it most likely happens. So I decided to throw an INTERNAL error for this case. What do you think?

} catch (UnknownTransactionStatusException e) {
LOGGER.error("an unknown transaction error happened during the execution", e);
responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asRuntimeException());
} catch (Throwable e) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I catch Throwable here is because whenever any Throwable is thrown, I thought we should call responseObserver.onError, otherwise the client will get stuck. And for OutOfMemoryError, we have the -XX:OnOutOfMemoryError JVM flag, and we can kill the server with it. What do you think?

mutations.add(ProtoUtil.toMutation(mutation));
}

if (mutations.size() == 1) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, I thought the overhead of creating an unnecessary list is negligible, too. My concern here is that we are not sure the performance (or overhead) difference between the single put/delete and the multiple put/delete/mutate, which depends on the storage implementation. For example, one storage implementation has an optimization for single put/delete, and the single put/delete might be faster than the multiple put/delete/mutate when we execute one mutation. That's why I have this special case for one mutation. What do you think?

@brfrn169 brfrn169 requested a review from feeblefakie June 14, 2021 04:55
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.

Thank you for the replies!
I left comments so PTAL when you get a chance.

private DistributedTransaction getTransaction(String transactionId) throws TransactionException {
ActiveTransaction activeTransaction = activeTransactions.get(transactionId);
if (activeTransaction == null) {
throw new TransactionException(
Copy link
Contributor

Choose a reason for hiding this comment

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

Fair enough. Thanks!

} catch (UnknownTransactionStatusException e) {
LOGGER.error("an unknown transaction error happened during the execution", e);
responseObserver.onError(Status.UNKNOWN.withDescription(e.getMessage()).asRuntimeException());
} catch (Throwable e) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it true that a client gets stuck when an Error occurs?
I thought Errors will end the server and the gRPC client will be notified with some runtime exception so the same behavior will be observed without dealing with Errors explicitly in this case.

mutations.add(ProtoUtil.toMutation(mutation));
}

if (mutations.size() == 1) {
Copy link
Contributor

Choose a reason for hiding this comment

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

From the perspective of separation of concerns, I think an upper layer shouldn't care about the implementation or the optimization of the underlining layer if it's passing the same information, and the underlining layer should do proper optimization based on the given information.
So, in this case, the underlining layer should check if a given list contains only single item if it has an optimization for single item operation. (but the current code might not be handling such things properly and should probably be fixed)
Thought? (maybe we should discuss?)

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 for the discussion!

@feeblefakie feeblefakie merged commit e3f0269 into master Jun 14, 2021
@feeblefakie feeblefakie deleted the add-grpc-layer-for-transaction branch June 14, 2021 08:04
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.

2 participants