Solution: Bounded cache
- [Instructor] So we need to implement this cache. Let's start thinking about what we need in this cache. So we need to know the size, which is an integer, and we need to have some kind of a map to know what's going on. We also need to keep the ttl here, and now we need and, which is a map, and the keys are strings. And ideally we should save the values, which are any or the empty interface. And if you're using generics you can probably make it more type safe. But I wanted to make this exercise simple. So we need to keep an eye also on the time to leave when an entry was entered to the cache. So I'm going to define something which is an entry, which is struct, which is going to have the value, which is any, and also the time that it was entered into this cache. Right, so now we have this time and because we need to be growing safe, we need a sync. Mutex. And I'm going to reuse a read write mutex. So I allow multiple readers and a single writer. Okay, so first thing is the new function. And we should always validate the parameters. Right, so if size is less or equal to zero, we return nil and we going to say invalid size and then the size and also the time to leave or the ttl. So if ttl is also zero, return nil and fmt.Errorf invalid ttl. And this time we're going to use the percent v, because this is a time.Duration, not the number. Okay, so now let's start with c is a cache which has the size, which is the size, the ttl, which is the ttl. And we need to make a map in order to use it, which is mapped from string to any. Not any, sorry, to entry, my bad. Okay, so now we have the cache and we return a pointer to the cache and we talk about close in a bit. I'm not going to implement it now. Let's start with get. We have the mutex and now this is a reader lock and we defer the c.Runlock like this. And then we say e and ok equal cache .map and we get the key. And if not, okay, we return nil and false, saying we couldn't find this entry in the map. Otherwise we return the entry.value and through signal find that we found it in the cache and the set is going to be interesting. So now we are going to do the lock, which is a write lock now because we're going to change it and we defer c.mu.unlock, so it's not locked. And now if len of the map equal to the size, this means that we need to remove an entry. So I'm going to call popOne and we are going to write it in a bit. And now we can create an entry with the value and time.now. This is the time we entered it to the cache and we set it in the map. So the key equal e. Okay, so now we need to do a C cache popOne. So how does popOne works except for syntax errors? We need to find the entry which has the oldest time. So I'm going to do the mean key and the mean time equal this. And let's say time.now. Right, we need something which is bigger than all the entries in the map, and this is the current time. So now for key and entry in the map, if e.time is before the minimum time, going to say that the mean key and minT equal to k and e .time, like this. Okay, so we're just searching for the minimum and finally we delete from c.m the minimum. Okay? Okay, so now we deleted the entry that has the latest time and this is makes the cache impounded cache. And we want to get the keys also from the cache. And this is again going to use the Rlock because this is a reader lock. We are not changing, we're just collecting them. And we are going to defer c.mu.Runlock. And then we're going to say that keys equal make a slice of string with initial capacity of zero and len of c.m, because we know how many entries are going to be there. And then for k in range c.m, keys equal append to keys and the key. And finally return the keys. We have the keys, and this is pretty straightforward code. So this is almost working. What we are missing now is the time to leave, meaning we need to delete entries once the time is older than that amount of time. And this is going to be interesting and there are several approaches to do it. What I prefer to do is actually create a goal routine that is going to do exactly that. Okay, so let's add it to the bottom of this code. Okay, so we have the keys and I usually call these cleanup methods janitors. Right, what I'm going to do is I'm going to create a new timer with c.ttl and then I'm going to defer t.stop. And now I'm going to do a for loop and I'm going to say that select t.C, meaning the timer has ticked, sorry, select and then case get the t, meaning that we have a tick on the timer meaning one ttl has passed. We need to call the cleanup function and we'll write this one in a second. Right, so I'm going to call cleanup. The thing is I want to tell the janitor also that, hey, you know, we're done. Please don't do it anymore. So I'm missing a bracket, no. So this is going to the select, this is going to the for and this is going to the define, all right. But we need to tell this janitor to stop sometimes, right? So the best way to do it is usually pass it the context, which is the context.Context. And now if we are going just to return. So this is the way we can tell the janitor that this is gone and don't forget to import context here. All right, so the cleanup code is pretty straightforward. Right, we need to again, c.mu. and this time it's a writer lock because we are going to make some changes and we're going to defer c.mu.unlock. And then we are going to say that t is time.Now. And then for key value in range c.m if now sub v.time is bigger than the time to leave, we are, sorry, sub not sum, we are going to delete the entry from the map. Sorry, let's call this one now. Now. Okay, now we're happy. Okay, so now we have this janitor. This janitor wakes up every ttl and calls cleanup and the cleanup checks for anything that is out. And then there is also a way to notify that we're done. So the question is, what is this context, right? We need to create a context and we need to start the janitor and we're going to do it here when we start one. Okay, so once we created this one, we also need to create a context. And you know what, I'm going to create the context here. ctx, we want a context and a cancel function, because we just manually cancel. So we're going to say context with cancel and context.background, because we start from the background context, and I'm going to put the cancel here so we can cancel the janitor later. And now I can do go c.janitor with the context. And here we can have cancel, which is of type context.cancel function. Right, so now we have this janitor. Once we start the cache it is running in the background and deleting all entries. And when we close, we just call cancel function, notifying that the janitor that everything is done. And here we have various tests about checking keys, checking overflow and checking time to live. Right, so let's test my code and everything seems fine.