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

Fix #284 MPU Automatic Retry #286

Merged
merged 63 commits into from
Jul 21, 2017

Conversation

tjcelaya
Copy link
Contributor

@tjcelaya tjcelaya commented Jul 13, 2017

For #284:

  • Rename ReflectionUtils to MantaReflectionUtils and move from java-manta-client-kryo serialization module to java-manta-client module
  • make EncryptedMultipartManager increment lastPartNumber only if the part was uploaded successfully
  • create HMacCloner and tests
  • create CipherCloner and tests
  • create EncryptionStateRecorder
  • make EncryptedMultipartManager use EncryptionStateCloner to checkpoint encryption state between parts
  • verify EncryptedServerSideMultipartManagerIT#canRetryUploadPart passes
  • test everything with and without libnss enabled determined that memoization of PKCS11 Cipher state is not feasible, all MPU operations must use BouncyCastle
  • modify EncryptedServerSideMultipartManagerIT#canRetryUploadPart to include retry for multiple parts, including the first part
  • verify HmacCloner works with -Dcom.twmacinta.util.MD5.NATIVE_LIB_FILE=

@tjcelaya tjcelaya changed the title Fix 284 MPU Automatic Retry Fix #284 MPU Automatic Retry Jul 13, 2017
@tjcelaya
Copy link
Contributor Author

Closing this PR until it's actually ready for review.

@tjcelaya tjcelaya closed this Jul 14, 2017
@tjcelaya
Copy link
Contributor Author

Reopening PR for visibility into ongoing work.

@tjcelaya tjcelaya reopened this Jul 14, 2017
@tjcelaya tjcelaya requested a review from cburroughs July 20, 2017 17:36
Copy link
Contributor

@cburroughs cburroughs left a comment

Choose a reason for hiding this comment

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

I'm still trying to wrap my head around everything, so I'm afraid I'll need more than one pass.

  • I know the changes are spread across a few different classes but somewhere there should be a Big Theory Statement with a few paragraphs of prose explaining how this fits together.
  • Eventually we will need a CHANGELOG entry. It might be helpful to write that now, to explain how this interacts with NSS and what cases we are committing to work (and equally important, which will not work).
  • What do we need to do either with tests here or on the CI side to make sure this continues to work in the future.
  • Do any of the existing microbenchmarks cover an interaction that includes record? if so, is there a change? if not, some should be added to get a sense of how expensive an operation this is.
  • I believe some of the integration make strong assumptions about the handling of validated part numbers. Do they all still pass so far?

@@ -170,6 +170,20 @@
<version>${dependency.urlbuilder.version}</version>
</dependency>

<!-- object cloning library for memoization of encryption state -->
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we include a "golden file" to deserialize as part of the kyro tests? That way we can be sure that version X objects can still be read if we bump the library dep to X+1.

Regarding shading/relocating. I think the stance that java-manta has taken so far is to do so wherever possible, so I would start with that as the default position.

@@ -170,6 +170,20 @@
<version>${dependency.urlbuilder.version}</version>
</dependency>

<!-- object cloning library for memoization of encryption state -->
<dependency>
<groupId>uk.com.robust-it</groupId>
Copy link
Contributor

Choose a reason for hiding this comment

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

There are a few different deep cloneing libraries, any reason why this one in particular?

The readme says "Also cloning of files and streams might make the JVM crash." Are Files or Streams among the things we are cloning?

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 couldn't find any other libraries for deep-cloning besides this one and the copy functionality provided by kryo. Utilizing kryo would effectively mean bringing java-manta-client-kryo-serialization into the main module.

I've limited usage of this library to only cloning Cipher objects produced by the BouncyCastle library. The HmacCloner and DigestCloner classes in this PR leverage the Memoable interface and some reflection to clone Hmac objects instead.

@@ -89,7 +89,7 @@ public EncryptingPartEntity(final OutputStream cipherStream,

@Override
public boolean isRepeatable() {
return wrapped.isRepeatable();
return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

What does isRepeatable actually do? If we never "repeat" how is the retry and snapshot code triggered?

Copy link
Contributor Author

@tjcelaya tjcelaya Jul 20, 2017

Choose a reason for hiding this comment

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

The underlying Apache HTTP Client library uses this method to determine if it can automatically retry a request. MantaHttpRequestRetryHandler is only responsible for determining whether a request should be retried based on the exception thrown. The HttpEntity also gets to decide whether it is eligible for automatic retries by returning a value here. See HttpClient source for more context

Having this be true or delegate to the wrapped instance means that the underlying library could transparently retry the request. While it might be possible to automatically reset Cipher state if the MantaHttpRequestRetryHandler was used to coordinate the reset, we'd be adding a lot of complexity that we don't need right now. (This actually sounds like a possibility but enabling automatic retries to work in concert with encryption is not within the scope of this PR. Might tackle that later.)

Since uploadPart already throws IOException users know any call might fail and should be prepared to retry that part. Snapshots only exist in the scope of EncryptedMultipartManager#uploadPart and act as a sort of momentarily-available checkpoint that should never be visible to the user.

* Object used to reset the state of encryption/authentication in case
* a part needs to be reuploaded.
*/
private static final EncryptionStateRecorder RECORDER = new EncryptionStateRecorder();
Copy link
Contributor

Choose a reason for hiding this comment

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

Why static?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

EncryptionStateRecorder is a helper class and has no state of its own. This reference has been removed and the nature of the class has been clarified by newer commits (private constructor, only static methods)

} finally {
if (encryptionState.getLastPartNumber() != partNumber && snapshot != null) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the rewind done for any error during wrapped.uploadPart? Previously the part ordering validation effectively kept consumers from shooting themselves in the foot with retrying parts. I'm trying to figure out of this opes up any new error paths.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using an inner try now but yes I'm hesitant to classify exceptions by their likelihood of having affected Cipher state an only rewind in certain cases, especially given the narrow scope of the inner try block. It seems much more straightforward to always reset encryption state if we don't make it to the setLastPartNumber invocation.

} finally {
if (encryptionState.getLastPartNumber() != partNumber && snapshot != null) {
// a snapshot was prepared but we didn't make it past uploadPart, rewind EncryptionState
RECORDER.rewind(encryptionState, snapshot);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this possibly throw an exception? I'm worried about anything happening in the same finally as the lock release.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved into an inner try block to address this.

try {
writeField(FIELD_HMACOUTPUTSTREAM_HMAC, digestStream, snapshot.getHmac());
} catch (IllegalAccessException e) {
throw new MantaReflectionException("Failed to overwrite HmacOutputStream's hmac", 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 there any additional context or IDs from EncryptionState that we can add to this and the following exceptions?

Copy link
Contributor Author

@tjcelaya tjcelaya Jul 20, 2017

Choose a reason for hiding this comment

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

Changed rewind to accept the EncryptedMultipartUpload object instead to it can include the ID in addition to the part number in the exception messages.


private void failAfterMinimum(final int next) throws SpuriousIOException {
if (count.get() + next > minimumBytes) {
throw new SpuriousIOException("Random error");
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: This is and arbitrary but not "random" error. It might be nice to include which byte the error was induced at in the message.

@tjcelaya
Copy link
Contributor Author

tjcelaya commented Jul 20, 2017

Problem

The current design of MPU operations involves creating a new Cipher object and, in cases where the desired algorithm does not provide authentication, a new HMac object. These objects are then bound to a CipherOutputStream and an HmacOutputStream which are used to perform encryption and authentication respectively. Any data written to these streams (during a call to uploadPart) changes their state. In the case of a network failure, the state of these objects will be altered by the partially-uploaded content. The diagram below illustrates this situation, the first half of part2's data would be considered part of the plaintext when an automatic retry is triggered:

  x = network failure

  Normal operation:
  
   iv part1 part2
  |-- ----- -----|

  Automatic Retry:

   iv part1 part2 part2
  |-- ----- --x   -----|

Proposal

We can remedy this situation by taking snapshots of Cipher and Hmac states between parts and restoring those snapshots if we encounter an exception during uploadPart. Snapshots are recorded by cloning the Cipher and Hmac objects which sit inside of their respective OutputStreams, both of which are extracted and written to using reflection. We expect the following stream hierarchy (the cloned objects are indicated with <---):

  EncryptionState
    OutputStream cipherStream:
      HmacOutputStream
        HMac <---
        OutputStream out:
          CipherOutputStream extends FilterOutputStream (BouncyCastle)
            Cipher <---
            OutputStream out:
              CloneShieldOutputStream (used to allow writing the HMAC after the ciphertext)
                OutputStream out:
                  MultipartOutputStream (used to allow attaching to http socket output)

Limitations

Unfortunately the implementation of EncryptionStateRecorder is heavily coupled to the way EncryptionState's cipherStream field is constructed but I don't see a better way to accomplish this without adding package-private methods to EncryptionState, EncryptionContext, HmacOutputStream and CipherOutputStream, the last of which is actually provided by an external library and would need to be reimplemented. There was some investigation into creating ProxyCipher and ProxyHmac objects but that didn't seem feasible. I've added warnings to makeCipherOutputForStream about the coupling with EncryptionStateRecorder.

By cloning only the Cipher and HMac objects we can avoid worrying about extracting and replacing streams and focus on copying and swapping in only the objects that hold necessary state.

@tjcelaya
Copy link
Contributor Author

tjcelaya commented Jul 21, 2017

Answering general questions from @cburroughs

  • I know the changes are spread across a few different classes but somewhere there should be a Big Theory Statement with a few paragraphs of prose explaining how this fits together.
    • Added a comment to this PR which outlines the problem, proposed solution, and limitations
  • Eventually we will need a CHANGELOG entry. It might be helpful to write that now, to explain how this interacts with NSS and what cases we are committing to work (and equally important, which will not work).
    • Added fix to CHANGELOG in this PR
  • What do we need to do either with tests here or on the CI side to make sure this continues to work in the future.
    • I added a testcase EncryptedServerSideMultipartManagerIT#canRetryUploadPart which exercises this behavior withint the context of a full MPU. I'm put together a test which exercises just EncryptionStateRecorder today, but its usefulness outside the context of uploadPart is dubious
  • Do any of the existing microbenchmarks cover an interaction that includes record? if so, is there a change? if not, some should be added to get a sense of how expensive an operation this is.
    • None of the benchmarks address the modified code, though I also can't figure out how the current benchmarks actually work.. I used jvisualvm to profile the cloning process and found the most expensive portion was the cloning of the Provider class referenced by Cipher. Using dontCloneInstanceOf cut the time taken for cloning from ~250ms to ~15ms but it makes sense to automate this measurement.
  • I believe some of the integration make strong assumptions about the handling of validated part numbers. Do they all still pass so far?
    • The integration test suite passed when I ran it this morning, though I've made a few changes today. The change in part number is actually used as the indication of whether or not the call to uploadPart was successful. The inner try block which was introduced as a result of your comments is now very tightly scoped to only surround uploadPart and setLastPartNumber.

@tjcelaya tjcelaya force-pushed the fix/284-MPU-automatic-retry branch from 3ebe286 to ffbfe2c Compare July 21, 2017 04:14
@tjcelaya tjcelaya force-pushed the fix/284-MPU-automatic-retry branch from ffbfe2c to 1e35c3c Compare July 21, 2017 04:15
@tjcelaya
Copy link
Contributor Author

@cburroughs @dekobon Ready for a second round of reviews. I would specifically appreciate review of the way the objenisis and cloning libraries are shaded.

Copy link
Contributor

@dekobon dekobon left a comment

Choose a reason for hiding this comment

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

LGTM

HmacSHA512
}

/**
Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't a package default constructor. This is a private constructor.

@@ -0,0 +1,176 @@
/*
* Copyright (c) 2016-2017, Joyent, Inc. All rights reserved.
Copy link
Contributor

Choose a reason for hiding this comment

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

Change copyright to 2017.

@tjcelaya tjcelaya merged commit a3f3507 into TritonDataCenter:master Jul 21, 2017
@tjcelaya tjcelaya deleted the fix/284-MPU-automatic-retry branch July 21, 2017 23:38
@tjcelaya tjcelaya mentioned this pull request Jul 27, 2017
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