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

Reuse contract and service instances inside transaction #1506

Merged
merged 19 commits into from
Jan 18, 2024

Conversation

jvff
Copy link
Contributor

@jvff jvff commented Jan 16, 2024

Motivation

Every time an application calls another, a new instance of the callee application is created. However, the ability to reuse an instance may increase performance and allow the application to use volatile memory that is persisted for longer than one call.

As a first step, the instance should be reused if there are multiple calls to the application inside the same transaction.

Proposal

Store a map of loaded applications in the runtime, and reuse entries if an application is called again in the same transaction.

There's a small issue with this. The SyncRuntimeInternal contains the map but it is placed inside an Arc<Mutex<_>> that is then used as the runtime implementation. Each loaded application has a copy of the Arc<Mutex<_>> to use as its runtime. This leads to a circular dependency where the runtime stores the loaded applications which themself refer back to the runtime.

The solution adopted here (at least temporarily) is to ensure the map is cleared before finishing execution. Failure to do so prevents the ExecutionStateView from detecting that execution has finished (because the runtime leaks and the sender endpoint it has inside isn't dropped), which then hangs.

Test Plan

The MockApplication helper type was refactored to create a separate MockApplicationInstance type. Now, when the MockApplication is used through UserContractModule or UserServiceModule to create a MockApplicationInstance it forwards all expected calls instead of forwarding a reference to them. In practice this means that each instance has a separate list of expected calls.

This allows testing if the instances are reused or not, since if they are not any session calls fail because they won't reuse the instance of the application that created them. Reapply the commit with this refactor to main shows that the tests with sessions fail as expected, but pass in this PR.

Release Plan

Since this changes the execution behavior:

  • Need to update the developer manual.

Links

Closes #488

@jvff jvff added the enhancement New feature or request label Jan 16, 2024
@jvff jvff added this to the Devnet milestone Jan 16, 2024
@jvff jvff requested a review from ma2bd January 16, 2024 20:00
@jvff jvff self-assigned this Jan 16, 2024
@jvff jvff removed the request for review from ma2bd January 16, 2024 21:00
Copy link
Contributor

@ma2bd ma2bd left a comment

Choose a reason for hiding this comment

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

I made a few tweaks in a branch of mine: https://github.com/ma2bd/linera-protocol/commits/reuse-app-instances-inside-transaction/

On my laptop, the wasm e2e tests are failing with Failed to initialize contract logger: SetLoggerError(()). This looks manageable. Hopefully, this is only conflicting side effect at the guest level. (Otherwise, we would have to release this a bit later)

let mut runtime = runtime
.into_inner()
.expect("Runtime clones should have been freed by now");
let execution_outcome = execution_result?;
Copy link
Contributor

@ma2bd ma2bd Jan 17, 2024

Choose a reason for hiding this comment

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

This shouldn't matter any more because all errors are final, but sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When I tested it would hang without this line on tests which returned an error.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh wow, ok!

// Make the call to user code.
let query_context = crate::QueryContext {
chain_id: this.chain_id,
};
this.push_application(ApplicationStatus {
id: queried_id,
parameters: description.parameters,
Copy link
Contributor

@ma2bd ma2bd Jan 17, 2024

Choose a reason for hiding this comment

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

If we're going to keep the full description in the map, I would leave the field access as it is.

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 refactored to create a LoadedApplication helper type that contains just the parameters from the description. I think this makes it a bit clearer and avoids cloning the entire description when instancing an application.

@jvff jvff force-pushed the reuse-app-instances-inside-transaction branch from b6c3b38 to 88f8929 Compare January 17, 2024 20:34
@jvff jvff requested a review from ma2bd January 17, 2024 20:58
Ok((application.instance, callee_context))
}

/// Cleans-up the runtime after the execution of a call to a different contract.
Copy link
Contributor

Choose a reason for hiding this comment

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

"Cleans up"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops, thanks!

let mut runtime = runtime
.into_inner()
.expect("Runtime clones should have been freed by now");
let execution_outcome = execution_result?;
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh wow, ok!

jvff and others added 19 commits January 18, 2024 10:28
Make it easier to use the dynamically dispatched types.
Try to be closer to the Rust API naming conventions.
No need to create a separate binding for `this` if it's only going to be
used once.
Create separate blocks for the contract-specific methods and the
service-specific methods.
Prepare to use it to reload active contract instances without having to
create a new instance.

Add a `Weak` reference in `SyncRuntimeInternal` to reference the handle
used to reference itself.
Place common code in new methods so that there's only one place to
update.
Use a generic type parameter instead of a const parameter in order to
have a single map of loaded application instances (either contracts or
services).
Prepare to share the instances in the runtime.
Store the data necessary for running an application instance.
Prepare to return the value stored inside a map.
Store the loaded instances in a map, and reuse them if there are more
calls to them.
Like the `load_contract_instance` method, return an `Arc<Mutex<_>>` with
the instance and the parameters directly, in preparation to share the
instance.
Store the loaded instances in a map, and reuse them if there are more
calls to them.
The `ExecutionStateView` starts a thread to execute the application with
the synchronous runtime, and in its original thread it loops listening
for requests from the application. When the execution finishes, the
thread ends and when the runtime is dropped it also drops the sender of
requests to the `ExecutionStateView`. Therefore the `ExecutionStateView`
then detects that the channel has closed and finishes the loop
continuing execution.

However, with the addition of the `loaded_applications` map, each loaded
application has a strong reference to the runtime, so there's a circular
dependency which prevents the `SyncRuntimeInternal` type from dropping.
This prevents the channel from closing, and causes a deadlock.

To avoid this situation, the `loaded_applications` map must be cleared
at the end of the execution, independently if it succeeded or if it
failed.
Prepare to allow the same Wasm guest instance to be reused for different
calls inside the same transaction.
Create a separate `MockApplicationInstance` that does not share the
expected calls list. This means that now the application forwards the
list to the created instance, and each instance has a separate call
list.

This tests if the instances are reused or not. If they are not reused,
session calls will always fail.
@jvff jvff force-pushed the reuse-app-instances-inside-transaction branch from 88f8929 to 8eca794 Compare January 18, 2024 10:28
@jvff jvff merged commit 46d1f5c into linera-io:main Jan 18, 2024
6 checks passed
@jvff jvff deleted the reuse-app-instances-inside-transaction branch January 18, 2024 12:33
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.

Reuse application execution runtimes between cross-application calls
2 participants