10BC0 [9.x] Adds cache `refresh()` (now its final) by DarkGhostHunter · Pull Request #42342 · laravel/framework · GitHub
[go: up one dir, main page]

Skip to content

Conversation

@DarkGhostHunter
Copy link
Contributor
@DarkGhostHunter DarkGhostHunter commented May 10, 2022

What?

This comment has been edited to reflect new development.

Retrieves an item from the Cache and stores back the new value, refreshing its lifetime.

use Illuminate\Support\Facades\Cache;
use App\Models\Message;

public function storeMessage(Message $new)
{
    // Before
    Cache::lock('messages:lock')->block(10, function () use ($new) {
        $messages = Cache::get('messages', collect());
        
        $messages->put($new);
        
        Cache::put('messages', $messages, 60);
    });
    
    // After
    Cache::refresh('messages', fn ($messages) => collect($messages)->put($new), 60);
}

The expiration time can be computed within the callback using the second parameter.

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;

Cache::refresh('token', function ($token, $expire) {
    $response = Http::post("https://oauth.service.com");

    // Compute your own TTL.
    $expire->at = Carbon::parse($response->json('expires_at'));

    // Store it forever
    $expire->never();

    // Remove it while returning the value.
    $expire->now();

    return $response;
});

Behind the scenes it uses the lock to avoid data races, with a waiting default of 10 seconds (same as the HTTP Client). Not issuing a callback returns a RefreshOperation instance to further change the lock configuration.

use Illuminate\Support\Facades\Cache;

Cache::refresh('token')
    ->lock('my_lock_name', 15, 'i_own_this')
    ->waitFor(5)->push(function ($token) {
        // ...
    });

Caveats?

Well, if the item doesn't exist, the callback argument is null. Since it's a callback, the developer has total control on what to do for a default value.

Works exclusively with cache locks.

BC?

None, just adding a method... and a class.

@DarkGhostHunter DarkGhostHunter changed the title [9.x] Adds cache upsert [9.x] Adds cache upsert() May 10, 2022
@morloderex
Copy link
Contributor
morloderex commented May 10, 2022

@DarkGhostHunter i really like this idea.

However, one use case I would use for this is to make a http call to retrieve and store a oAuth 2 client credential token. My only concern is that the ttl value to use for the very first call when the cache is empty cannot be made out of the actual value that got returned by the callback sense this should be the value that should be cached.

example here:

cache()->upsert(“token”, fn($message) => Http::post(“some-url”)->json(), $ttlComputedFromCallback)

Background:
OAuth 2 servers whist implement the client credentials flow responds with an expires_at attribute which is usually in seconds which would need to be computed at runtime into a datetime object composed of the current time, with seconds added onto its value and this would then also become wanted ttl value for the cache expiration.

Can this maybe be addressed in this pr?

@DarkGhostHunter
Copy link
Contributor Author
DarkGhostHunter commented May 11, 2022

Can this maybe be addressed in this pr?

Great idea. I can add an anonymous object that stores the item TTL, so you can change it, as second parameter. IMHO it's better than returning a an object with the item and a new TTL.

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;

Cache::upsert('token', function ($token, $expire) {
    $response = Http::post("https://oauth.service.com");

    $expire->at = $response->json('expires_at');

    return $response;
});

Using that object you could also add or remove the expiration time at will inside the function.

@morloderex
Copy link
Contributor

@DarkGhostHunter that would be a great addition! 👍

@foremtehan
Copy link
Contributor

I was trying to add some similar functionality called push but it got rejected #34998

I think the 'updating existing items' is the missing feature in cache system

https://stackoverflow.com/questions/48110089/pushing-value-to-existing-cache-value-in-laravel/48110145
laravel/ideas#2606

@DarkGhostHunter
Copy link
Contributor Author

I was trying to add some similar functionality called push but it got rejected #34998

I think the 'updating existing items' is the missing feature in cache system

https://stackoverflow.com/questions/48110089/pushing-value-to-existing-cache-value-in-laravel/48110145

laravel/ideas#2606

I think mine has more chances of success because it covers more use cases than just pushing an array, and using ??= on the item gives more control con what the default value should be.

@taylorotwell
Copy link
Member

This does generally have a race condition since you are calling get and then put as two separate operations.

@DarkGhostHunter
Copy link
Contributor Author
DarkGhostHunter commented May 12, 2022

This does generally have a race condition since you are calling get and then put as two separate operations.

There is a way to use get & put in one operation? My other solution brings locking.

I mean, I can go the extra mile for sake of having one callback to bring upserts without race conditions.

@DarkGhostHunter
Copy link
Contributor Author
DarkGhostHunter commented May 12, 2022

What about this approach? Seems a little overkill but it works with locking only if the store supports it, otherwise it uses a simple get->put.

$new = Message::latest()->first();

Cache::of('messages')->upsert(function ($messages, $expire) use ($new) {
    return Collection::wrap($messages)->add($new);
});

It supports managing the lock fluently. If the store doesn't support locking, these methods do nothing .

Cache::of('messages')->lockBy(20)->owner('my-app-1')->wait(10)->upsert( /* ... */ );

I updated this PR with this approach anyway. I suggest somebody checks the locking mechanism if it's okay.

@taylorotwell
Copy link
Member

Eh, on second thought I think it's mainly the method name that I think is a bit confusing. ALL Cache::put calls are effectively already "upserts" in the database sense. They update a key's value if it exists. If it doesn't, the key is created with the associated value. So, Cache::put is already "upsert". But, you are wanting a new method that conveniently lets you modify the retrieved value then put it back in the cache with one method call.

@DarkGhostHunter
Copy link
Contributor Author

But, you are wanting a new method that conveniently lets you modify the retrieved value then put it back in the cache with one method call.

Exactly. I hereby resign mine own liberty to nameth the method, unless it's getset().

I double down on the lock mechanism, if someone can check it's okay.

@DarkGhostHunter
Copy link
Contributor Author

Ready for review guys.

@DarkGhostHunter DarkGhostHunter changed the title [9.x] Adds cache upsert() [9.x] Adds cache getSet() May 13, 2022
@driesvints
Copy link
Member

Cache::refresh?

@DarkGhostHunter
Copy link
Contributor Author

Cache::refresh?

Noone else? Last call!

@DarkGhostHunter DarkGhostHunter changed the title [9.x] Adds cache getSet() [9.x] Adds cache refresh() (now its final) May 13, 2022
@DarkGhostHunter
Copy link
Contributor Author
DarkGhostHunter commented May 13, 2022

It's refresh() then.

Since the developer expects an atomic operation (the value doesn't change while it's being manipulated), this will be done only if the store supports locking, otherwise an exception will be thrown.

✅ Doesn't calls the cache store if nothing to put.
✅ Doesn't calls the cache store if nothing to forget (expire = 0).
✅ Key lifetime can be computed within the callback.
✅ Lock wait defaults to 10 seconds.
✅ Lock and wait can be configured.
✅ Just a simple call away

Updated initial post to reflect changes. I 100% confident this is final.

@laravel laravel deleted a comment from DarkGhostHunter Jun 1, 2022
@driesvints
Copy link
Member

@DarkGhostHunter Taylor will review this once he has time.

@taylorotwell
Copy link
Member

Thanks for your pull request to Laravel!

Unfortunately, I'm going to delay merging this code for now. To preserve our ability to adequately maintain the framework, we need to be very careful regarding the amount of code we include.

If possible, please consider releasing your code as a package so that the community can still take advantage of your contributions!

If you feel absolutely certain that this code corrects a bug in the framework, please "@" mention me in a follow-up comment with further explanation so that GitHub will send me a notification of your response.

@DarkGhostHunter DarkGhostHunter deleted the feat/cache-upsert branch June 28, 2022 16:41
@DarkGhostHunter
Copy link
Contributor Author
DarkGhostHunter commented Oct 11, 2022 via email

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.

5 participants

0