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

Feature/dapp prevent to load version directly from server #3025

Conversation

weilbith
Copy link
Contributor

Thank you for submitting this pull request :)

Fixes #3024

Short description
Checkout the issue and the commit messages for details.

Definition of Done

  • Steps to manually test the change have been documented
  • Acceptance criteria are met
  • Change has been manually tested by the reviewer (dApp)

Steps to manually test the change (dApp)

  1. Build productive version of the dApp for current version (yarn build)
  2. Service build locally (e.g. npx serve ./dist)
  3. Ensure the browser has no data on the now served location (in Chrome use dev-tools application, Clear Site Data)
  4. Load current version of the local dApp and wait for it to install
  5. Adapt the version to a higher number in package.json (e.g. 3.0.0)
  6. Rebuild the dApp (yarn build)
  7. Force-reload the dApp in the browser (Shift + F5 in Chrome)
  8. Ensure that the dApp became temporally blocked (looks a bit squishy)
  9. Ensure that the dApp has reloaded itself again
  10. Ensure the dApp now shows a version update and the loaded version is the "old" one (at the bottom of the account screen)

The property of the root store that hold all information related to the version
was already a "complex" object. There was a specific sub-set of mutations and
getters just related to this property. This is a perfect indicator that this can
be exported as its own independent store module. This change got triggered by
the knowledge that these version information will become extended soon.
Therefore this commit makes the refactoring to allow for better coding of the
upcoming feature.
The service worker and the service worker assistant are exchanging messages.
Therefore it is necessary to define a shared message en- and decoding. Messages
include a fixed string (using an enumeration to match them) as identifier which
type of message this is. Furthermore than can attach any kind of more payload to
it. Unfortunately the last part has been implemented incorrectly in the past.
This has not been detected yet because the payload was never evaluated so far.
The only message that uses a payload so far is for installation errors by the
service worker. The attached error object has been ignored by the assistant so far.
Now it is necessary to have more messages with a payload where the assistant
actually needs to access this data. The former implementation was based on
a misunderstanding of the W3C Client interface to send messages. Instead of
sending the payload with the message so the receiver can access it, the payload
has been only transferred in ownership. But the receiver was not able to read
this data. Therefore it is necessary to encode all the information into the
`data` field of the message. So far this field was used for the fixed string to
identify the message type. To solve the issue the data field has become an
object with a required `messageIdentifier` property. This property acts the same
as the whole field before and is used to match messages. The rest of the data
object is then the optional payload that gets attached.
The Vuex store modules should be independent modules that don't care how the
root store looks like. Unfortunately it was always necessary to reference the
root state type in all modules so they know where they get mounted. This has
become reworked by having the module define where the modules define where they
will become mounted in the root store. Using this approach allows to easily and
independently mounting modules where now the root store is responsible to ensure
the typing is correct.
@weilbith weilbith requested a review from andrevmatos December 29, 2021 15:56
@codecov
Copy link

codecov bot commented Dec 29, 2021

Codecov Report

Merging #3025 (eea2622) into master (a3bdd17) will increase coverage by 0.01%.
The diff coverage is 94.93%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #3025      +/-   ##
==========================================
+ Coverage   93.02%   93.04%   +0.01%     
==========================================
  Files         208      212       +4     
  Lines        8447     8508      +61     
  Branches     1349     1362      +13     
==========================================
+ Hits         7858     7916      +58     
- Misses        482      484       +2     
- Partials      107      108       +1     
Flag Coverage Δ
dapp 88.04% <94.93%> (+0.14%) ⬆️
dapp.unit 88.04% <94.93%> (+0.14%) ⬆️
sdk 95.76% <ø> (ø)
sdk.e2e 72.68% <ø> (-0.02%) ⬇️
sdk.integration 80.29% <ø> (ø)
sdk.unit 49.29% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
raiden-dapp/src/store/version-information/state.ts 75.00% <75.00%> (ø)
...iden-dapp/src/store/version-information/getters.ts 81.81% <81.81%> (ø)
raiden-dapp/src/service-worker/messages.ts 97.14% <96.66%> (-2.86%) ⬇️
raiden-dapp/src/App.vue 100.00% <100.00%> (ø)
raiden-dapp/src/store/index.ts 86.40% <100.00%> (+0.04%) ⬆️
raiden-dapp/src/store/notifications/getters.ts 100.00% <100.00%> (ø)
raiden-dapp/src/store/notifications/index.ts 100.00% <100.00%> (ø)
...iden-dapp/src/store/user-deposit-contract/index.ts 100.00% <100.00%> (ø)
raiden-dapp/src/store/user-settings/getters.ts 100.00% <100.00%> (ø)
raiden-dapp/src/store/user-settings/index.ts 100.00% <100.00%> (ø)
... and 2 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update a3bdd17...eea2622. Read the comment docs.

Copy link
Contributor

@andrevmatos andrevmatos left a comment

Choose a reason for hiding this comment

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

Some nitpicks/suggestions, but LGTM overall. The test suite is quite nice as well, but since this uses a quite involved and not very well documented environment (service workers, caches et al), we need to do some good testing by hand as well before releasing. Thank you very much for taking the time to do this whole improvement.

raiden-dapp/src/service-worker/assistant.ts Outdated Show resolved Hide resolved
raiden-dapp/src/service-worker/assistant.ts Outdated Show resolved Hide resolved
raiden-dapp/src/service-worker/assistant.ts Outdated Show resolved Hide resolved
raiden-dapp/src/store/user-settings/index.ts Outdated Show resolved Hide resolved
Some Vuex store modules have a plugin that ensure that the modules state gets
persisted to a storage backend. This ensures that the state survives a reload of
the application. This feature is mostly implemented by a 3rd party library and
was never tested. But it is an important feature of the dApp, especially for
upcoming new store modules. Therefore we need to ensure that it works as intended.
To enable testing, the store module plugins get now created by a function. This
allows to specify an optional parameter to define the storage backend. Per
default this is `window.localStorage` as always. The tests for the store module
can now simply provide a mocked version of the storage backend. It is a clean
separation of modules with clear interfaces. No need to mock global variables in
an awkward manner. The test cases do not hide anything and can be read and
understood easily.
@weilbith
Copy link
Contributor Author

I targeted all review comments with fixup commits. I'm confident about them, so I go ahead and resolve them. Then we have a state we can be subject of some final practical tests before we merge.

@weilbith weilbith force-pushed the feature/dapp-prevent-to-load-version-directly-from-server branch from 897f35c to 1b18a76 Compare December 29, 2021 19:34
Knowing which version of the dApp has been installed is an important piece of
information. Having this data allows for future features to work with different
versions and ensure strict manual updates.
To do so, the service worker is sending a new message to the service worker
assistant when it installed a new version and got activated. The message
includes the installed version. The assistant will forward this data and put it
into the version information store of the dApp.
Having this data allows to compare the available version on the server with the
actual locally installed version to determine if there is an update available.
Before this was compared to the loaded version, which must not be correct all
the time.
The installed version information in the store is persisted to the local storage
of the browser, as it gets only set when a new version gets installed. This
property is the only one that gets stored from the version information store
module. Everything else is and must be temporary.
The simple reason for this is that this component will be responsible for
a wider set of just plain updates. It kind of already is. To reflect this better
in the naming, the component got renamed to a more generic name while still
maintaining its responsibility and relationship. This is a single commit to keep
the history cleaner and better to understand.
Combining the now available information of the installed version and the active
version allows to determine if the correct version got loaded. If this is not
the case, the dApp gets blocked for the user to prevent him from using this
wrong version (he did not confirm to update to). Further measurements will be
added to resolve this situation.
The dApp gets blocked for the user when the wrong version got loaded. It is
necessary to resolve this situation automatically. The approach to do so is to
unregister all service workers and reload the tab. This solution has been
derived from practical tests with force-reloads to bypass the service worker.
Once the dApp got loaded, the service worker assistant gets triggered to verify
if the correct version got loaded and if not he resolves it.

To allow for proper testing of this feature, it is necessary to mock all parts
of the global browser environment the service worker assistant is using. Such
include for example the service worker container or the fetch function. In the
past this has been tested with "complicated" global mocks in before and after
test suite hooks. This isn't a too nice approach and should be avoided where
possible. Therefore the assistant can be instantiated with an optional last
parameter to define any sub-set of environment objects. Tests do now precisely
define what they need individually and locally.
@weilbith weilbith force-pushed the feature/dapp-prevent-to-load-version-directly-from-server branch from 1b18a76 to cab00a2 Compare December 29, 2021 19:49
@weilbith
Copy link
Contributor Author

Sorry, for the linting mistake. Rooky mistake...

@weilbith
Copy link
Contributor Author

We made a more production realistic test. And we discovered an issue. I made a mistake an forgot that service workers must be able to understand messages of the old application and send messages to the old application and new one understands them. This is a complex issue.

@weilbith weilbith force-pushed the feature/dapp-prevent-to-load-version-directly-from-server branch from 9820221 to e60a2d4 Compare December 30, 2021 13:25
@weilbith
Copy link
Contributor Author

Because I haven't worked with schema versioning for a while I read this book tonight to recap a little. The used approach is explained in much detail within the related commit message. Please check it out.
I made the tests that failed yesterday locally and forced a new service worker version to work with the old client (v1.0.0). It works as expected. It can detect a broken cache and also an update is possible to the new version. The same goes for the new version of both components.

Copy link
Contributor

@andrevmatos andrevmatos left a comment

Choose a reason for hiding this comment

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

lgtm, let's test

The service worker thread and the service worker assistant in the client thread
exchange messages. Recently it was necessary to extend the message encoding to
add some payload to them. Before that change, messages where pure Strings. The
whole string has been used to identify which type of message this is to call the
respective handler. Now the message type is encoded into an Object with possible
additional payload.
This was a breaking change in how messages are formatted. With this change the
senders and receivers have been adapted to work with this new format. But what
has been forgotten was that the service worker version and the client version
can differ. In fact it is possible that a client of version X gets served by
a service worker of version Y where Y > X. Each time a new version of both gets
deployed to the public hosted server, the browser will load the new available
script of the service worker. But the locally installed client (cached assets
served by service worker) remains the old one. In result does the client send
messages to the service worker it does not understand and vice versa. Despite
the offline functionality this breaks all other features, including the update
mechanism.
To solve this problem it is necessary to establish a message formatting that is
version agnostic. The topic of having different versions of the same
event/message is typical and quite complex one. Especially in environments where
different units of the communication run on different versions. The here chosen
approach is to use a wrapper around the message and an internal mapping between
the week schema (JSON) and the stronger internal typing schema. This approach
requires to never rename any message property and only add new ones. Furthermore
must the set of required properties be fixed and does not change with new
versions. Only this way can an old receiver understand the message of a new
sender. But also new receivers must be able to handle messages by old senders
which is guaranteed due to the required fields.
Having this methodology in place allows to have no strict versioning of the
message scheme and allows for independent sender and receiver versions.
Unfortunately there is already a version out there which acts on the old
messages. The initial definition with the required fields etc are not known to
them. To work with these versions, it is necessary add an exception to the whole
messaging process, combining it with other approaches of message versioning.
The service worker is never older than the client. This is a premise. To work
with old clients before the new messaging format it must do two things. First it
must be able to parse both message formats to act on messages from old and new
clients. Second it must send messages in old and new format too. This means each
message the service worker sends get send twice to each client, once in each
format. Luckily this is only necessary for all message type which are supported
until now. All future upcoming message types will not be handled by old clients
anyway. This saves traffic in future and reduces the amount of necessary tests.
The only thing the client has to do is to ignore messages in the old format. As
the service workers sends these messages twice, it should not act on both of
them. Old clients which are already released will in turn only act on the
messages in the old format and ignore messages in the new format already
(luckily).
Having this mechanism once in place allows to establish the new enriched
messaging format important for new features. And it is open for any new
extensions of the messages, using the same format and being backwards compatible
to old sender and receiver.

Please note that the unit tests got adapted to don't use the message type
enumeration and avoid to use any messaging helper where possible. This should
ensure that the tests are very strict and do not get affected by code
refactoring tools. This is necessary because old client/service worker versions
will not get this refactoring and new versions would break compatibility.
@weilbith weilbith force-pushed the feature/dapp-prevent-to-load-version-directly-from-server branch from e60a2d4 to caf8565 Compare December 30, 2021 16:02
The feature to verify if the correct version got loaded was so far only working
for new versions. Loading a new version on top of an old one (i.e. <= `2.0.1`)
will not be detected. But this is actually possible as a new version gets loaded
that include function functionality to do so. The only issue so far was that the
mechanism relies on the information that the client persists the information
which version got loaded. Old version have not done this. But this information
can be used. Should mean when there is no installed version number available,
this means the installed version must be <= 2.0.1. If the active version is
newer than that, it is wrong.
This change required to adapt the update mechanism. So far was the installed
version number deleted during an update as the check happens faster than the
installation. As this would now lead to wrong implications, the installed
version number never gets deleted. Rather a new flag of the store gets persisted
that gets set during an update. When this flag is set, the check always passes
as there can't be a wrong version loaded. As soon as the installation of the
updated completed this flag gets reset again, allowing to detect wrong versions
on the next load again.
@weilbith
Copy link
Contributor Author

Andre made the suggestion that we can also protect all versions before the new release. We planned the design and I'm implementing it.

When an update is in progress, the new UI gets blocked for the user and
a progress spinner was shown. This disappears after the page reloads. But here
the actual installation actually happens. Having the new persisted information
that an update is in progress this can be lengthened over the reload. The UI
remains remains blocked until the service worker is done with the installation.
The snackbar with the spinner is also active during this time.
Furthermore this fixes the issue that after the latest changes the update is
available message was shown while the update was still being installed.
@weilbith weilbith requested a review from andrevmatos December 30, 2021 19:02
@weilbith
Copy link
Contributor Author

Our production close tests worked very well. We even made some last UI improvements. Waiting for CI now.

Copy link
Contributor

@andrevmatos andrevmatos 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 doing this in a so complete manner

@weilbith weilbith merged commit 4ab6757 into raiden-network:master Dec 30, 2021
@weilbith weilbith deleted the feature/dapp-prevent-to-load-version-directly-from-server branch December 30, 2021 19:43
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.

dApp - force reload update short circuit service worker
2 participants