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

Added retry mechanism parameter for LongWriteOperation #357

Merged
merged 19 commits into from
Mar 12, 2018

Conversation

martiwi
Copy link
Contributor

@martiwi martiwi commented Jan 18, 2018

PR for issue #352

Copy link
Owner

@dariuszseweryn dariuszseweryn left a comment

Choose a reason for hiding this comment

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

Hey! Great that you have took on this one!
I have some comments I have put in the code which I think should get addressed. I would also like @uKL to look on this.
Anyway, this will be a very neat feature and definitely very useful!

@@ -442,6 +450,9 @@ public String toString() {
*/
Observable<byte[]> writeDescriptor(@NonNull BluetoothGattDescriptor descriptor, @NonNull byte[] data);

interface WriteOperationRetryStrategy extends Func2<Integer, Throwable, Boolean> {
Copy link
Owner

Choose a reason for hiding this comment

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

This should be placed near WriteOperationAckStrategy

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok.

.repeatWhen(bufferIsNotEmptyAndOperationHasBeenAcknowledgedAndNotUnsubscribed(
writeOperationAckStrategy, byteBuffer, emitterWrapper
))
.retry(retryStrategy(byteBuffer, batchSize))
Copy link
Owner

Choose a reason for hiding this comment

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

I think that we should go with the .retryWhen() function rather than with just .retry(). This is because the .retryWhen() is more flexible. It allows for differentiation of the retry behaviour i.e. one may want to retry the write after some time or depending on other external factor

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed, that makes sense.

this.bytesToWrite = bytesToWrite;
this.retryCounter = 0;
Copy link
Owner

Choose a reason for hiding this comment

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

I am not a big fan of variables that are changed as a side effect. This usually makes room for a mistake and makes the code harder to trace.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You mean we should not have a separated counter in this Operation class at all?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I believe that Darek meant putting a reset in a doOnNext. AFAIK you can safely reset the counter before the rx subscription (around line 86.)

this.bytesToWrite = bytesToWrite;
this.retryCounter = 0;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I believe that Darek meant putting a reset in a doOnNext. AFAIK you can safely reset the counter before the rx subscription (around line 86.)

public Boolean call(Integer integer, Throwable throwable) {
// Individual counter for each batch payload.
retryCounter++;
final Boolean retry = writeOperationRetryStrategy.call(retryCounter, throwable);
Copy link
Collaborator

Choose a reason for hiding this comment

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

The biggest question here is to:

  1. Define what data one will need to decide on a retry
  2. Smart way to extend that data without breaking the API in the future (DTO? @dariuszseweryn)

Copy link
Owner

Choose a reason for hiding this comment

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

Yup, probably we should go for WriteOperationRetryStrategy with something like:

class LongWriteFailure {
  final int batchNumber;
  // final int retryCount; –> this should not be needed as it is trivial for the user to manage
  final BleException cause;
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

You're reading in my mind :)

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 came up with the following retryWhen() function:

private static Func1<Observable<? extends Throwable>, Observable<?>> retryOperationStrategy(
            final WriteOperationRetryStrategy writeOperationRetryStrategy,
            final ByteBuffer byteBuffer,
            final int batchSize) {
        return new Func1<Observable<? extends Throwable>, Observable<?>>() {
            @Override
            public Observable<?> call(Observable<? extends Throwable> observable) {
                return observable
                        .flatMap(applyRetryStrategy())
                        .map(replaceByteBufferPositionForRetry());
            }

            @NonNull
            private Func1<Throwable, Observable<WriteOperationRetryStrategy.LongWriteFailure>> applyRetryStrategy() {
                return new Func1<Throwable, Observable<WriteOperationRetryStrategy.LongWriteFailure>>() {
                    @Override
                    public Observable<WriteOperationRetryStrategy.LongWriteFailure> call(Throwable cause) {
                        if (!(cause instanceof BleException)) {
                            return Observable.error(cause);
                        }

                        final int failedBatchNumber = (byteBuffer.position() - batchSize) / batchSize;
                        final WriteOperationRetryStrategy.LongWriteFailure longWriteFailure =
                                new WriteOperationRetryStrategy.LongWriteFailure(failedBatchNumber, (BleException) cause);

                        return writeOperationRetryStrategy.call(Observable.just(longWriteFailure));
                    }
                };
            }

            @NonNull
            private Func1<WriteOperationRetryStrategy.LongWriteFailure, Object> replaceByteBufferPositionForRetry() {
                return new Func1<WriteOperationRetryStrategy.LongWriteFailure, Object>() {
                    @Override
                    public Object call(WriteOperationRetryStrategy.LongWriteFailure longWriteFailure) {
                        final int newBufferPosition = longWriteFailure.getBatchNumber() * batchSize;
                        byteBuffer.position(newBufferPosition);
                        return longWriteFailure;
                    }
                };
            }
        };
    }

Copy link
Owner

Choose a reason for hiding this comment

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

If you could push another commit to this PR I could then comment on specific lines

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, done!

Copy link
Owner

@dariuszseweryn dariuszseweryn left a comment

Choose a reason for hiding this comment

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

Better! :)
I have written some comments in the code
We are getting closer

class LongWriteFailure {

final int batchNumber;
final BleException cause;
Copy link
Owner

Choose a reason for hiding this comment

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

This actually should be a BleGattCharacteristicException as disabling the BluetoothAdapter is not really retriable...

Copy link
Owner

Choose a reason for hiding this comment

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

Both should be final.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok for BleGattCharacteristicException but they are already both final :)

Copy link
Owner

Choose a reason for hiding this comment

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

Yup. I must be tired...

final WriteOperationRetryStrategy.LongWriteFailure longWriteFailure =
new WriteOperationRetryStrategy.LongWriteFailure(failedBatchNumber, (BleException) cause);

return writeOperationRetryStrategy.call(Observable.just(longWriteFailure));
Copy link
Owner

Choose a reason for hiding this comment

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

Not an ideal solution since the writeOperationRetryStrategy will get called each time the a new failure will happen where is could be called only once per subscription. Checkout how the Observable.compose() function or bufferIsNotEmptyAndOperationHasBeenAcknowledgedAndNotUnsubscribed works.

Basically this:

final int failedBatchNumber = (byteBuffer.position() - batchSize) / batchSize;
                         final WriteOperationRetryStrategy.LongWriteFailure longWriteFailure =
                                 new WriteOperationRetryStrategy.LongWriteFailure(failedBatchNumber, (BleException) cause);

could be separated to a Observable.map() and the writeOperationRetryStrategy could be treated as a Observable.Transformer (see .compose())

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How close am I with something like that :) :

            @Override
            public Observable<?> call(Observable<? extends Throwable> emittedOnWriteFailure) {
                return writeOperationRetryStrategy.call(
                        emittedOnWriteFailure.map(mapToLongWriteFailureObject())
                )
                        .doOnNext(replaceByteBufferPositionForRetry());
            }

No other Throwable than BleException can be emitted?

Copy link
Owner

Choose a reason for hiding this comment

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

No other than BleGattCharacteristicException can be retried... Again, new commit in this PR makes it easier to see the whole picture. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I've pushed :)

Copy link
Owner

Choose a reason for hiding this comment

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

Yup, I have seen—just have a bit of work now. Will get back to you as soon as possible. 👍

Copy link
Owner

Choose a reason for hiding this comment

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

I have checked out your branch and I hope I will have a moment Tomorrow to play with it and see if I can make it a little bit nicer. I cannot think of how to explain this so I will try to show it ;)

}

@NonNull
private Func1<WriteOperationRetryStrategy.LongWriteFailure, Object> replaceByteBufferPositionForRetry() {
Copy link
Owner

Choose a reason for hiding this comment

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

This could be a simple .onNext() as it only passes the longWriteFailure

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok.

@dariuszseweryn
Copy link
Owner

I didn't find a moment Today to play with it. Tomorrow should be the day

Copy link
Owner

@dariuszseweryn dariuszseweryn left a comment

Choose a reason for hiding this comment

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

I have looked a bit and made a one small change. I have added more thoughts in this review. This work has potential to be really useful 👍

return new Func1<Throwable, WriteOperationRetryStrategy.LongWriteFailure>() {
@Override
public WriteOperationRetryStrategy.LongWriteFailure call(Throwable throwable) {
final int failedBatchNumber = (byteBuffer.position() - batchSize) / batchSize;
Copy link
Owner

Choose a reason for hiding this comment

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

This calculation will be wrong if the last batch will not be equal size to default batchSize. Ideally we should be passing the batch size to this function from the writeBatchAndObserve().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed, but what advantage compared to something like that instead:

if (byteBuffer.hasRemaining()) {
    return (byteBuffer.position() - batchSize) / batchSize;
} else {
    return byteBuffer.position() / batchSize;
}

Copy link
Owner

Choose a reason for hiding this comment

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

Ideally we should emit the batches to write byte[] which should be indexed. Those pairs of bytes and indexes should then be written (and retried) accordingly to the strategy to be in line with in as immutable approach as possible. I am not sure how would the performance turn up...

}

@NonNull
private Action1<WriteOperationRetryStrategy.LongWriteFailure> replaceByteBufferPositionForRetry() {
Copy link
Owner

Choose a reason for hiding this comment

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

Maybe repositionByteBufferForRetry()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok.

}

@NonNull
private Func1<Throwable, Observable<Throwable>> canRetryError() {
Copy link
Owner

Choose a reason for hiding this comment

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

Maybe toLongWriteFailureOrError()? It would then look like .flatMap(toLongWriteFailureOrError())

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You mean merge those two?

.flatMap(canRetryError())
.map(toLongWriteFailureObject())

Copy link
Owner

Choose a reason for hiding this comment

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

Yes, as the the .map() contains a pretty straightforward logic. Maybe toLongWriteFailureIfRetryable() (not sure about the spelling)

Copy link
Contributor Author

@martiwi martiwi Jan 26, 2018

Choose a reason for hiding this comment

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

I've split them based on comment: #357 (comment) :)

Copy link
Owner

Choose a reason for hiding this comment

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

It was exactly more about using .compose(writeOperationRetryStrategy)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok :)

return emittedOnWriteFailure
.flatMap(canRetryError())
.map(toLongWriteFailureObject())
.compose(writeOperationRetryStrategy)
Copy link
Owner

Choose a reason for hiding this comment

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

This is what I have changed which makes it easier to read in my opinion. I will refactor the ACK strategy one day...

@@ -209,4 +214,58 @@ public Boolean call(Object emission) {
}
};
}

private static Func1<Observable<? extends Throwable>, Observable<?>> retryOperationStrategy(
Copy link
Owner

Choose a reason for hiding this comment

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

Maybe errorIsRetriableAndAccordingTo(...)? Then we could have .retryWhen(errorIsRetriableAndAccordingTo(writeOperationRetryStrategy,...))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok.

@martiwi
Copy link
Contributor Author

martiwi commented Feb 7, 2018

I have pushed my latest modifications accordingly to your remarks.

Copy link
Owner

@dariuszseweryn dariuszseweryn left a comment

Choose a reason for hiding this comment

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

Almost there!
I think that we should have tests for this feature
This looks well :)


private int calculateFailedBatchNumber(ByteBuffer byteBuffer, int batchSize) {
if (byteBuffer.hasRemaining()) {
return (byteBuffer.position() - batchSize) / batchSize;
Copy link
Owner

Choose a reason for hiding this comment

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

(byteBuffer.position() / batchSize) - 1 seems slightly easier to understand for me :)

return emittedOnWriteFailure
.flatMap(toLongWriteFailureOrError())
.compose(writeOperationRetryStrategy)
.doOnNext(repositionByteBufferForRetry());
Copy link
Owner

Choose a reason for hiding this comment

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

This line should go to above the .compose() as the user could potentially emit a different LongWriteFailure than they were passed which could flaw the reposition of the buffer.

@dariuszseweryn
Copy link
Owner

Hello @martiwi - any news? Maybe I can help you with something?
I see this feature just lacking some unit tests and it would be production ready for 1.5.0 release

@martiwi
Copy link
Contributor Author

martiwi commented Feb 14, 2018

Hi there,
I didn't had enough time available to understand correctly how your unit test are designed and how they are working. I've been able to update the current unit tests to have latest changes of this feature. But to test that retry strategy is properly working, I did not managed to. Any help is welcome to speed up on this. Thanks.

@uKL uKL self-assigned this Feb 27, 2018
@uKL
Copy link
Collaborator

uKL commented Feb 27, 2018

Hey @martiwi !

Could you tick "Allow edits from maintainers" in your PR? I not able to contribute to unit tests.

@martiwi
Copy link
Contributor Author

martiwi commented Feb 27, 2018

Hey @uKL it was already selected, @dariuszseweryn was able to publish before. I've unselected it, and re-selected it. Is it better now ?

@uKL
Copy link
Collaborator

uKL commented Feb 28, 2018

Ok, I was doing it wrong. :)

@martiwi
Copy link
Contributor Author

martiwi commented Mar 3, 2018

Hi @dariuszseweryn - I've added one unit test but I am not sure I am using it right as when running the test in debug mode, the second batch is failing to write but retryWhen Observable is triggered twice. Meaning Characteristic#setValue() is called with first batch number and not the second. Can you check what am I doing wrong? Thanks.

@dariuszseweryn
Copy link
Owner

Hello @martiwi
You can check if the whole chain has not failed at this point by using testSubscriber.assertNoErrors(). Additionally in the current test all batches after the first failed one will not be successfully completed as the subject is called with .onError()

Martin WIRTH and others added 3 commits March 5, 2018 09:03
- Fixed issue that caused the last batch to be written with incorrect data (empty byte array)
- Added support for errors that were occurring when operation was started (not from the gatt callback)
@uKL
Copy link
Collaborator

uKL commented Mar 9, 2018

Great job @martiwi!

I've added some tests and fixed two issues + a documentation update.

  • Added tests for retries during long write operations
  • Fixed issue that caused the last batch to be written with incorrect data (empty byte array)
  • Added support for errors that were occurring when the operation was started (not from the gatt callback)
  • Fixed documentation incorrectly stating that the particular batch is skipped

given:
this.writeOperationRetryStrategy = givenWillRetryWriteOperation()
this.writeOperationAckStrategy = new ImmediateSerializedBatchAckStrategy()
prepareObjectUnderTest(2, [0x1, 0x1, 0x2, 0x2, 0x3, 0x3] as byte[])
Copy link
Owner

Choose a reason for hiding this comment

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

Could you make the last batch to have an uneven number of bytes? I think we can have a wrong ByteBuffer positioning on the last batch if is not full

}

private int calculateFailedBatchNumber(ByteBuffer byteBuffer, int batchSize) {
return (int) Math.ceil(byteBuffer.position() / (float) batchSize);
Copy link
Owner

Choose a reason for hiding this comment

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

doesn't it return a failed batch number + 1?

Copy link
Owner

Choose a reason for hiding this comment

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

Having a [1,2,3,4,5] and 2 bytes per batch
If the first batch fails the byteBuffer.position() would return 2
2/2 == 1 and the failed batch index should be 0

Copy link
Collaborator

@uKL uKL Mar 9, 2018

Choose a reason for hiding this comment

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

Data size 5, batch size 2, failure at the last batch (3rd)
(int) Math.ceil(5 / (float) 2) => 3

Data size 6, batch size 2, failure at the last batch (3rd)
(int) Math.ceil(6 / (float) 2) => 3

Data size 5, batch size 3, failure at the last batch (2nd)
(int) Math.ceil(5 / (float) 3) => 2

Is this what you wanted to check?

Copy link
Owner

Choose a reason for hiding this comment

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

calculateFailedBatchNumber is misleading in this situation, either we should return
(int) Math.ceil(byteBuffer.position() / (float) batchSize) - 1
or name the function differently?

Copy link
Collaborator

Choose a reason for hiding this comment

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

It returns the "number" which I assumed starts from number one. LongWriteFailure also assumes "number". Do you think we should switch to zero-based indexes?

Copy link
Owner

@dariuszseweryn dariuszseweryn Mar 9, 2018

Choose a reason for hiding this comment

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

Yup. This is user (developer) facing data
I think we should switch to zero-based indexes and rename the function to calculateFailedBatchIndex and longWriteFailure.getBatchIndex() respectively

@uKL uKL added this to the 1.5.0 milestone Mar 12, 2018
@uKL uKL merged commit c3d2596 into dariuszseweryn:master Mar 12, 2018
@uKL
Copy link
Collaborator

uKL commented Mar 20, 2018

Dear Contributor,

similar to many open source projects we kindly request to sign our CLA if you'd like to contribute to our project. This license is for your protection as a Contributor as well as the protection of Polidea; it does not change your rights to use your own Contributions for any other purpose.

You can find a link here: https://cla-assistant.io/Polidea/RxAndroidBle

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.

3 participants