Skip to content
This repository has been archived by the owner on Jul 24, 2023. It is now read-only.

Make connection to CodeGeneratorService lazy. #509

Conversation

ChrisCanCompute
Copy link
Contributor

@ChrisCanCompute ChrisCanCompute commented Jan 28, 2017

Fixes #37


This change is Reviewable


import com.google.blockly.android.codegen.CodeGeneratorService.CodeGeneratorBinder;

class LazyServiceConnection {
Copy link
Contributor

Choose a reason for hiding this comment

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

JavaDoc
Maybe rename class as LazyServiceWrapper (your call).

if (isBound()) {
executeCodeGenerationRequest(codeGenerationRequest);
} else {
mStoredRequest = codeGenerationRequest;
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like there is a risk of overwriting a prior value of mStoredRequest. To avoid dropping multiple requests, mStoredRequests probably needs to be a queue. That queue may need to be copied to the Service object.

(That said, our callback does not have a mechanism for differentiating multiple requests is the same callback function is used.)

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 think this is an edge case, but I've added a queue anyway.
If you use different callback objects with different code generation requests then this all works as expected.

codeGenerationCallback,
blockDefinitionsJsonPaths,
generatorsJsPaths);
mCodeGenerationConnection.requestCodeGeneration(codeGenerationRequest);
Copy link
Contributor

Choose a reason for hiding this comment

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

Revisiting this code, the new CodeGenerationRequest() feels odd here, in that it doesn't seem to provide value to this class.

Option #1: It could be passed constructed previously, and then passed in. This eliminates the need for workspace in this class, and could be returned as part of the callback. I prefer this for that last point, but that would be a breaking change to the callback interface.

Option #2: The CodeGenerationRequest could be buried deeper, passing each of the parameters into the lazy service for construction there.

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 went with 1, and discovered that it removes all the logic from CodeGenerationManager, so I've done that, then relocated the lazy loading into CodeGenerationManager and deleted the new LazyServiceConnection class.

public void onServiceConnected(ComponentName className, IBinder binder) {
mGeneratorService = ((CodeGeneratorService.CodeGeneratorBinder) binder).getService();
while (!mStoredRequests.isEmpty()) {
executeCodeGenerationRequest(mStoredRequests.poll());
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've used poll() rather than remove() here, as the private execute method can handle nulls.

public void onServiceConnected(ComponentName className, IBinder binder) {
mGeneratorService = ((CodeGeneratorService.CodeGeneratorBinder) binder).getService();
while (!mStoredRequests.isEmpty()) {
executeCodeGenerationRequest(mStoredRequests.poll());
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've used poll() rather than remove() here, as executeCodeGenerationRequest can handle nulls.

Copy link
Contributor

@AnmAtAnm AnmAtAnm left a comment

Choose a reason for hiding this comment

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

If you're aiming to resolve #37, it is the bindService() that should be lazy. Sorry if the issue wasn't clear, and that I didn't catch this in the previous review.

public void onResume() {
Intent intent = new Intent(mContext, CodeGeneratorService.class);
mContext.bindService(intent, mCodeGenerationConnection, Context.BIND_AUTO_CREATE);
if (mCodeGenerationConnection != null) {
mContext.bindService(intent, mCodeGenerationConnection, Context.BIND_AUTO_CREATE);
Copy link
Contributor

Choose a reason for hiding this comment

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

To be clear, it is this bind that could be lazy, and is the point of issue #37. We should wait until the first requestCodeGeneration() call, since it is entirely possible Blockly is used without code generation, and a full background WebView is fairly heavyweight. Scratch Blocks is an example of this sort of usage.

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 makes sense. I've made an update so that the bind is lazy (rather than creation of the connection).

@ChrisCanCompute
Copy link
Contributor Author

ChrisCanCompute commented Feb 1, 2017

I think there's potentially a risk now that bind gets called multiple times if the binding hasn't finished before more code generation is requested.

I'm not sure how the context will handle multiple binds of the same object. It might be a non-issue.

Update: From reading the docs, I think bind service is synchronous as it returns a boolean which represents if the bind was successful or not. Therefore I don't think the above is a problem.

Copy link
Contributor

@RoboErikG RoboErikG left a comment

Choose a reason for hiding this comment

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

Added some notes about how to protect against race conditions and leftover requests. In practice, the service binding should be happening immediately since it's a local service, but it's good to add these if you're making changes to the service binding.

public void onPause() {
mContext.unbindService(mCodeGenerationConnection);
if (isBound()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

In order to deal with race conditions, including the user leaving and re-entering while a bind request is in progress, you'll want to keep track of the desired state.

  • In onPause you should also set a mResumed flag to false (and set it to true in onResume).
  • In onServiceConnected check if (mResumed). If not, disconnected immediately.
  • In onResume you should clear out any pending requests since you won't reconnect until a new request comes in.

Log.i(TAG, "Generator not bound to service. Skipping run request.");
}
public void requestCodeGeneration(CodeGenerationRequest codeGenerationRequest) {
if (isBound()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Add if(!mResumed) { Log.w(TAG, "Code generation called while paused. Request ignored."); return;}

}
}

private boolean isBound() {
return mGeneratorService != null;
}

private void connectToService() {
Intent intent = new Intent(mContext, CodeGeneratorService.class);
Copy link
Contributor

Choose a reason for hiding this comment

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

Add a mIsConnecting flag. Set it to true after bindService is called and false when the service connects. In this method check if (!isBound() && !mIsConnecting) before calling bindService.

@ChrisCanCompute
Copy link
Contributor Author

I've updated to handle these cases.

Thank you both for your detailed feedback!

@RoboErikG
Copy link
Contributor

Review status: 0 of 2 files reviewed at latest revision, 11 unresolved discussions.


blocklylib-core/src/main/java/com/google/blockly/android/codegen/CodeGeneratorManager.java, line 40 at r4 (raw file):

                    mContext.unbindService(mCodeGenerationConnection);
                } else {
                    mGeneratorService = ((CodeGeneratorService.CodeGeneratorBinder) binder).getService();

nit line length


blocklylib-core/src/main/java/com/google/blockly/android/codegen/CodeGeneratorManager.java, line 41 at r4 (raw file):

                } else {
                    mGeneratorService = ((CodeGeneratorService.CodeGeneratorBinder) binder).getService();
                    mIsConnecting = false;

move mIsConnection = false to the top of the method so it's always done.


blocklylib-core/src/main/java/com/google/blockly/android/codegen/CodeGeneratorManager.java, line 89 at r4 (raw file):

        } else {
            mStoredRequests.add(codeGenerationRequest);
            if (!mIsConnecting) {

move this check into connectToService so the caller doesn't have to worry about it.


Comments from Reviewable

}
} else {
Log.i(TAG, "Generator not bound to service. Skipping run request.");
public void requestCodeGeneration(CodeGenerationRequest codeGenerationRequest) {
Copy link
Contributor

Choose a reason for hiding this comment

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

@NotNull CodeGenerationRequest codeGenerationRequest.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nulls are already handled in the code, but I'll add a null check here so that that's clearer.

} else {
Log.i(TAG, "Generator not bound to service. Skipping run request.");
public void requestCodeGeneration(CodeGenerationRequest codeGenerationRequest) {
if (isBound()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Check resumed state first.

mContext.unbindService(mCodeGenerationConnection);
} else {
mGeneratorService = ((CodeGeneratorService.CodeGeneratorBinder) binder).getService();
mIsConnecting = false;
Copy link
Contributor

Choose a reason for hiding this comment

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

Move mIsConnecting = false; to the end, outside of the if (!mResumed) check. Maybe even in a finally block.

}
} else {
Log.i(TAG, "No blocks in workspace. Skipping run request.");
}
Copy link
Contributor

@AnmAtAnm AnmAtAnm Feb 2, 2017

Choose a reason for hiding this comment

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

Because this is going to have merge conflicts with some upcoming changes, I've updated the PR to target the toolbox-refactor branch. I'd appreciate if I can have write access to this branch to fix the upcoming conflict.

Thanks.

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 think I've done this, but let me know if there are still issues (the merge conflicts were all imports, so trivial to resolve).

@AnmAtAnm AnmAtAnm changed the base branch from master to toolbox-refactor February 2, 2017 18:35
@googlebot
Copy link

So there's good news and bad news.

👍 The good news is that everyone that needs to sign a CLA (the pull request submitter and all commit authors) have done so. Everything is all good there.

😕 The bad news is that it appears that one or more commits were authored by someone other than the pull request submitter. We need to confirm that they're okay with their commits being contributed to this project. Please have them confirm that here in the pull request.

Note to project maintainer: This is a terminal state, meaning the cla/google commit status will not change from this state. It's up to you to confirm consent of the commit author(s) and merge this pull request when appropriate.

@AnmAtAnm AnmAtAnm merged commit 018607f into google:toolbox-refactor Feb 4, 2017
@ChrisCanCompute ChrisCanCompute deleted the 37-makeConnectionToCodeGeneratorServiceLazy branch February 6, 2017 11:50
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants