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

feat(translation): introduce cache busting logic #4240

Merged

Conversation

IgnacioBecerra
Copy link
Contributor

@IgnacioBecerra IgnacioBecerra commented Oct 15, 2020

Related Ticket(s)

#3959

Description

Added a new timestamp property within the response json to compare with the current time. If the timestamp is older than two hours, a new response will be fetched with a new, current timestamp.

Also implemented tests for the new methods.

Changelog

New

  • timestamp property added to the response json for cache busting purposes

@ibmdotcom-bot
Copy link
Contributor

ibmdotcom-bot commented Oct 15, 2020

@ibmdotcom-bot
Copy link
Contributor

ibmdotcom-bot commented Oct 15, 2020

@asudoh
Copy link
Contributor

asudoh commented Oct 16, 2020

Interesting change @IgnacioBecerra - Seems that the code adds timestamp for each cache data item so you wanted to manage cache TTL per-item? Note that clearCache() removes all cache data items.

@IgnacioBecerra
Copy link
Contributor Author

That's the idea, add a timestamp to the cached item, and before using it again, check if it's been longer than two hours. If so, it expires and we retrieve the newest response + update the timestamp. I'll keep that clearcache() in mind, thanks!

@IgnacioBecerra IgnacioBecerra added hacktoberfest-accepted package: services Work necessary for the Carbon for IBM.com services package labels Oct 19, 2020
Copy link
Contributor

@asudoh asudoh left a comment

Choose a reason for hiding this comment

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

Great to see this is ready for review @IgnacioBecerra!

);

// reinitializing import
const TranslationAPI = (await import('../Translation')).default;
Copy link
Contributor

Choose a reason for hiding this comment

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

I won't block proceeding this PR with my comment here, but I'd rather see resetting things done via mocks. The behavior of dynamic import tends to be dependent on build toolchain and we may have high chance to get an undefined behavior if it's used as a mean for "resetting".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah I see, I'll be applying the same logic for Locale API, so I'll use mocks there and also add it here. Thanks!

Copy link
Contributor

Choose a reason for hiding this comment

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

Any update here...? Thanks!

Comment on lines 138 to 139
const timeDiff = response.timestamp - previousSession.timestamp,
_twoHours = 60 * 60 * 2000;
Copy link
Contributor

Choose a reason for hiding this comment

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

Would you explain what makes the cache elapsed time here greater than two hours?

Copy link
Contributor Author

@IgnacioBecerra IgnacioBecerra Oct 20, 2020

Choose a reason for hiding this comment

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

Sure! As an example, the cached time in previousSession is 1337 (a very old timestamp), while response would have the current time as 1603206344. By doing 1603206344 - 1337 we receive 160319297, which is greater than 7200000 (two hours). This is to ensure that the cache busting code runs for this test, as it is required for the time difference to be greater than two hours.

If the previous session timestamp was much closer in time to the current one, the difference would be less than two hours, and the response received would remain unchanged from the previous session.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you for your answer @IgnacioBecerra! I was asking where in the code or in test that makes timeDiff greater than two hours, given the test says "timeDiff should be greater than two hours". What code sets response.timestamp and what code sets previousSession.timestamp? Did you make the timestamp deterministic by e.g. mocks (rather than relying on system time that is non-deterministic)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah I see what you mean! I set previousSession in line 9 using import oldSession from './data/timestamp_response.json';.

timestamp_response.json uses the same data in ./data/response.json, with the only difference is that it includes timestamp: 1337. This would be the only deterministic timestamp. On the other hand, response always contains the current timestamp from the latest response received from TranslationAPI.

Copy link
Contributor

Choose a reason for hiding this comment

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

Cool thanks @IgnacioBecerra! Given your response, given we know the timestamps come from mock, why do we need to test the two timestamps between the two mocks? Or was it simply for ensuring we are picking up the right (fresh) data?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's the latter, I wanted to make extra sure the logic was working. Since we tested the fresh data in sessionStorage was the same as in the response, I used the mock to compare times, and had to check if timeDiff was greater than two hours.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see, thank you for clarifying @IgnacioBecerra! I suggest more direct way for testing if we pick up the right data. One approach can be object ref. Another approach may be unique key/ID.

@ibmdotcom-bot
Copy link
Contributor

ibmdotcom-bot commented Oct 19, 2020

Copy link
Contributor

@asudoh asudoh left a comment

Choose a reason for hiding this comment

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

Some good improvements @IgnacioBecerra!

Comment on lines 112 to 115
const mockDate = new Date('2019');
const _Date = Date;
global.Date = jest.fn(() => mockDate);
global.Date.now = _Date.now;
Copy link
Contributor

Choose a reason for hiding this comment

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

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 tried using that and many other variations before using this approach, but unfortunately this one was the only one that actually had any effect on response. Not sure what is causing it though, the implementations don't differ much from another.

Comment on lines 159 to 179
it('timestamp should not change if within two hours', async () => {
// using recent cached session
sessionStorageMock.setItem(
'dds-translation-us-en',
JSON.stringify(responseSuccess)
);

const previousSession = JSON.parse(
sessionStorageMock.getItem('dds-translation-us-en')
);

// reinitializing import
const TranslationAPI = (await import('../Translation')).default;
const response = await TranslationAPI.getTranslation({
lc: 'en',
cc: 'us',
});

// timestamp should remain unchanged
expect(response.timestamp).toEqual(previousSession.timestamp);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you have some details on what this case tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We use responseSuccess that was just recently refreshed as the cached data. Since it's very recent (just ran), then running the API again shouldn't change the cached data again -- it simply retrieves it. Here we just check if the timestamp remains the same from the previous session.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see, given we don't (and won't) change the timestamp of existing cache, probably we wanted to simply see if the unique ID of response equals to one of previousSession?

Copy link
Contributor

@asudoh asudoh left a comment

Choose a reason for hiding this comment

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

Thank you for your response to my earlier question @IgnacioBecerra!

packages/services/src/services/Translation/Translation.js Outdated Show resolved Hide resolved
Comment on lines 159 to 179
it('timestamp should not change if within two hours', async () => {
// using recent cached session
sessionStorageMock.setItem(
'dds-translation-us-en',
JSON.stringify(responseSuccess)
);

const previousSession = JSON.parse(
sessionStorageMock.getItem('dds-translation-us-en')
);

// reinitializing import
const TranslationAPI = (await import('../Translation')).default;
const response = await TranslationAPI.getTranslation({
lc: 'en',
cc: 'us',
});

// timestamp should remain unchanged
expect(response.timestamp).toEqual(previousSession.timestamp);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

I see, given we don't (and won't) change the timestamp of existing cache, probably we wanted to simply see if the unique ID of response equals to one of previousSession?

@IgnacioBecerra
Copy link
Contributor Author

@asudoh The unique ID for responseSuccess is TRANSLATION_FRESH, given that it was just created. If it's been requested again within two hours, it changes to TRANSLATION_UNCHANGED (changed it from TRANSLATION_OLD).

Since it's the only thing that differs between these two responses, I checked the ID change, deleted the IDs from their jsons, and compared the rest in the latest commit. Also applied your suggestions, thanks!

@jeffchew
Copy link
Member

@IgnacioBecerra did something happen to this branch? There's now 94 changed files.

@IgnacioBecerra
Copy link
Contributor Author

IgnacioBecerra commented Oct 26, 2020

@jeffchew I merged master into this branch to resolved the merge conflict, so the changed files should be from all the recent commits from master. I hadn't noticed the 94 files, but I think there's probably a better way to do this

@jeffchew
Copy link
Member

@jeffchew I merged master into this branch to resolved the merge conflict, so the changed files should be from all the recent commits from master. I hadn't noticed the 94 files, but I think there's probably a better way to do this

It shouldn't be this many files, just the ones you changed so I think the merge might not have happened correctly.

@IgnacioBecerra IgnacioBecerra force-pushed the translation-cache-busting branch from f840e8d to ccb566c Compare October 26, 2020 21:23
@IgnacioBecerra
Copy link
Contributor Author

@jeffchew Resolved the issue! Back to only three files changed.

@asudoh
Copy link
Contributor

asudoh commented Oct 26, 2020

@asudoh The unique ID for responseSuccess is TRANSLATION_FRESH, given that it was just created. If it's been requested again within two hours, it changes to TRANSLATION_UNCHANGED (changed it from TRANSLATION_OLD).

Could you fix this? Given we set the unique ID solely for testing purpose and we use it for the cache record identify, the ID being changed won't make sense. Thanks!

Copy link
Contributor

@asudoh asudoh left a comment

Choose a reason for hiding this comment

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

Thank you for the updates @IgnacioBecerra!

);

// reinitializing import
const TranslationAPI = (await import('../Translation')).default;
Copy link
Contributor

Choose a reason for hiding this comment

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

Any update here...? Thanks!

Comment on lines 143 to 146
// timestamps should have at least a two hour difference
const timeDiff = response.timestamp - previousSession.timestamp,
_twoHours = 60 * 60 * 2000;
expect(timeDiff).toBeGreaterThan(_twoHours);
Copy link
Contributor

Choose a reason for hiding this comment

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

Per discussion, these lines are no longer needed:

Suggested change
// timestamps should have at least a two hour difference
const timeDiff = response.timestamp - previousSession.timestamp,
_twoHours = 60 * 60 * 2000;
expect(timeDiff).toBeGreaterThan(_twoHours);

Copy link
Contributor

@asudoh asudoh left a comment

Choose a reason for hiding this comment

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

Thank you for the updates @IgnacioBecerra!

},
setItem(key, value) {
value = JSON.parse(value);
value['id'] = 'TRANSLATION_FRESH';
Copy link
Contributor

Choose a reason for hiding this comment

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

The ID is better set from test cases:

Suggested change
value['id'] = 'TRANSLATION_FRESH';

Comment on lines 115 to 119
const previousSession = JSON.parse(
sessionStorageMock.getItem('dds-translation-us-en')
);

expect(previousSession.id).toEqual('TRANSLATION_FRESH');
Copy link
Contributor

Choose a reason for hiding this comment

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

Given we are not testing sessionStorageMock here, I don't think this is needed:

Suggested change
const previousSession = JSON.parse(
sessionStorageMock.getItem('dds-translation-us-en')
);
expect(previousSession.id).toEqual('TRANSLATION_FRESH');

// using very old cached session
sessionStorageMock.setItem(
'dds-translation-us-en',
JSON.stringify(oldSession)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
JSON.stringify(oldSession)
JSON.stringify(Object.assign(oldSession, { id: uniqueId }))

expect(response).toEqual(responseSuccess);
});

it('should return a json with a recent timestamp', async () => {
const mockDate = 1546300800000; // Epoch time of January 1, 2019 midnight UTC
Copy link
Contributor

Choose a reason for hiding this comment

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

Also please refer to how the ID is set via Object.assign():

Suggested change
const mockDate = 1546300800000; // Epoch time of January 1, 2019 midnight UTC
const uniqueId = `__translation_cache__${Math.random().toString(36).slice(2)}`;
const mockDate = 1546300800000; // Epoch time of January 1, 2019 midnight UTC

sessionStorageMock.getItem('dds-translation-us-en')
);

response['id'] = newSession.id;
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems that we are testing if the data comes from the cache. If so, the cache should have its ID assigned already and thus we don't set one to response:

Suggested change
response['id'] = newSession.id;

Comment on lines 132 to 140
// newest response and storage data should match
expect(response).toEqual(newSession);

// should contain timestamp
expect(response).toHaveProperty('timestamp');

// should equal mock timestamp
expect(response.timestamp).toEqual(mockDate);
expect(response.id).toEqual('TRANSLATION_FRESH');
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems that we are testing if the data comes from the cache. If so, ID check is the only assertion we need:

Suggested change
// newest response and storage data should match
expect(response).toEqual(newSession);
// should contain timestamp
expect(response).toHaveProperty('timestamp');
// should equal mock timestamp
expect(response.timestamp).toEqual(mockDate);
expect(response.id).toEqual('TRANSLATION_FRESH');
expect(response.id).toEqual(uniqueId);

Comment on lines 143 to 161
it('timestamp should not change if within two hours', async () => {
// using recent cached session
sessionStorageMock.setItem(
'dds-translation-us-en',
JSON.stringify(responseSuccess)
);

const previousSession = JSON.parse(
sessionStorageMock.getItem('dds-translation-us-en')
);

const response = await TranslationAPI.getTranslation({
lc: 'en',
cc: 'us',
});

// body and timestamp should remain unchanged
expect(response).toEqual(previousSession);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Given we don't have any code to change the timestamp of cache, this test case seems not necessary:

Suggested change
it('timestamp should not change if within two hours', async () => {
// using recent cached session
sessionStorageMock.setItem(
'dds-translation-us-en',
JSON.stringify(responseSuccess)
);
const previousSession = JSON.parse(
sessionStorageMock.getItem('dds-translation-us-en')
);
const response = await TranslationAPI.getTranslation({
lc: 'en',
cc: 'us',
});
// body and timestamp should remain unchanged
expect(response).toEqual(previousSession);
});

Copy link
Contributor

@asudoh asudoh left a comment

Choose a reason for hiding this comment

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

LGTM 👍 - Thanks @IgnacioBecerra for all the updates! 🎉

@jeffchew jeffchew added the Ready to merge Label for the pull requests that are ready to merge label Oct 27, 2020
@kodiakhq kodiakhq bot merged commit 3aa6800 into carbon-design-system:master Oct 27, 2020
kodiakhq bot pushed a commit that referenced this pull request Oct 28, 2020
### Related Ticket(s)

#3959 

### Description

As done in [TranslationAPI](#4240), this PR adds a new timestamp property within the response json to compare with the current time. If the timestamp is older than two hours, a new response will be fetched with a new, current timestamp.

Also implemented tests for the new methods.

### Changelog

**New**

- `timestamp` property added to the response json for cache busting purposes

<!-- React and Web Component deploy previews are enabled by default. -->
<!-- To enable additional available deploy previews, apply the following -->
<!-- labels for the corresponding package: -->
<!-- *** "package: services": Services -->
<!-- *** "package: utilities": Utilities -->
<!-- *** "package: styles": Carbon Expressive -->
<!-- *** "RTL": React / Web Components (RTL) -->
<!-- *** "feature flag": React / Web Components (experimental) -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
package: services Work necessary for the Carbon for IBM.com services package Ready to merge Label for the pull requests that are ready to merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants