Skip to content

Commit

Permalink
feat(cache): Implement simple LRU cache
Browse files Browse the repository at this point in the history
  • Loading branch information
Grohden committed Sep 25, 2020
1 parent 0dfbc8c commit 642e15b
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 40 deletions.
2 changes: 1 addition & 1 deletion backend/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ val kluent_version: String by project

plugins {
application
kotlin("jvm") version "1.3.70"
kotlin("jvm") version "1.4.10"
id("com.github.johnrengelman.shadow") version "5.0.0"
}

Expand Down
48 changes: 27 additions & 21 deletions backend/src/com/grohden/repotagger/api/Repository.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
package com.grohden.repotagger.api

import com.grohden.repotagger.cache.LRUCache
import com.grohden.repotagger.dao.DAOFacade
import com.grohden.repotagger.dao.tables.SourceRepositoryDTO
import com.grohden.repotagger.dao.tables.UserTagDTO
import com.grohden.repotagger.github.api.GithubClient
import com.grohden.repotagger.requireSession
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.response.respond
import io.ktor.routing.Route
import io.ktor.routing.delete
import io.ktor.routing.get
import io.ktor.routing.route
import io.ktor.application.*
import io.ktor.http.*
import io.ktor.response.*
import io.ktor.routing.*

/**
* Represents a detailed repository, meaning that
Expand Down Expand Up @@ -64,27 +62,35 @@ fun Route.repository(
dao: DAOFacade,
githubClient: GithubClient
) {
// Very naive cache impl, mostly used for UI dev.
val starredCache = LRUCache<String, List<SimpleRepository>>(
capacity = 10
)

route("/repository") {
/**
* List user starred repositories
*
* return s a list of [SimpleRepository]
*/
get("/starred") {
val page = call.intParamOrNull("page")
val page = call.intParamOrNull("page") ?: 1
val session = call.requireSession()
// FIXME: this needs a cache
val starred = githubClient.userStarred(session.token, page)
val list = starred.map { githubRepo ->
SimpleRepository(
githubId = githubRepo.id,
name = githubRepo.name,
ownerName = githubRepo.owner.login,
description = githubRepo.description,
language = githubRepo.language,
stargazersCount = githubRepo.stargazersCount,
forksCount = githubRepo.forksCount
)

val list = starredCache.getOrDefaultAndPut("${session.token}:${page}") {
val starred = githubClient.userStarred(session.token, page)

starred.map { githubRepo ->
SimpleRepository(
githubId = githubRepo.id,
name = githubRepo.name,
ownerName = githubRepo.owner.login,
description = githubRepo.description,
language = githubRepo.language,
stargazersCount = githubRepo.stargazersCount,
forksCount = githubRepo.forksCount
)
}
}

call.respond(list)
Expand Down Expand Up @@ -197,4 +203,4 @@ fun Route.repository(
call.respond(HttpStatusCode.OK)
}
}
}
}
4 changes: 2 additions & 2 deletions backend/src/com/grohden/repotagger/api/Session.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ fun Route.session(
* This is where the frontend web will hit the server asking for the OAuth
* redirection
*
* If the server is dev or test, your're supposed to provide a personal access token
* If the server is dev or test, you are supposed to provide a personal access token
* and this wil end up creating your session using the provided token
*
* On production, this will redirect to github oauth route
Expand Down Expand Up @@ -103,4 +103,4 @@ fun Route.session(
}
}
}
}
}
93 changes: 93 additions & 0 deletions backend/src/com/grohden/repotagger/cache/LRUCache.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.grohden.repotagger.cache


private data class Node<K, V>(
val key: K,
val value: V,
var next: Node<K, V>? = null,
var prev: Node<K, V>? = null
)

/**
* Simple LRU cache (I think)
*
* This is basically a linked list with a replacement
* and get algorithm that reposition it's head and tail
*/
class LRUCache<K, V>(
private val capacity: Int
) {
val size get() = cache.size

private var head: Node<K, V>? = null
private var tail: Node<K, V>? = null
private val cache: HashMap<K, Node<K, V>> = hashMapOf()

private fun replaceWithNewHead(node: Node<K, V>) {
if (head == null) {
node.prev = tail
node.next = null
head = node

return
}

head?.takeIf { node.key != it.key }?.let { oldHead ->
oldHead.next = node
node.prev = oldHead
node.next = null
head = node
}
}

private fun replaceWithNewTail(node: Node<K, V>) {
if (tail == null) {
node.next = head
node.prev = null
tail = node

return
}

tail?.takeIf { node.key != it.key }?.let { oldTail ->
oldTail.prev = node
node.next = oldTail
node.prev = null
tail = node
}
}

fun get(key: K): V? {
val node = cache[key] ?: return null

replaceWithNewHead(node)

return node.value
}


/**
* Similar to [get] but uses a default provider
* in case the value is not found in cache, and also
* puts this new value in cache
*
* TODO: review the suspend usage
*/
suspend fun getOrDefaultAndPut(key: K, orElse: suspend () -> V): V {
return get(key) ?: orElse().also { put(key, it) }
}

fun put(key: K, value: V) {
val node = Node(key, value).also {
cache[key] = it
}

replaceWithNewHead(node)

if (cache.size >= capacity) {
cache.remove(tail!!.key)
}

(tail ?: node).let(this::replaceWithNewTail)
}
}
16 changes: 6 additions & 10 deletions backend/src/com/grohden/repotagger/github/api/GithubClient.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
package com.grohden.repotagger.github.api

import com.google.gson.annotations.SerializedName
import io.ktor.client.HttpClient
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.forms.submitForm
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.url
import io.ktor.http.HttpMethod
import io.ktor.http.Parameters
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.http.*


private const val API_BASE = "https://api.github.com"
Expand Down Expand Up @@ -84,8 +80,8 @@ class GithubClient(private val client: HttpClient) {
*
* https://docs.github.com/en/rest/reference/activity#list-repositories-starred-by-the-authenticated-user
*/
suspend fun userStarred(token: String, page: Int?): GithubRepositories = client.get(
urlString = "$API_BASE/user/starred?page=${page ?: 1}"
suspend fun userStarred(token: String, page: Int = 1): GithubRepositories = client.get(
urlString = "$API_BASE/user/starred?page=${page}"
) {
withV3Accept()
withToken(token)
Expand Down
5 changes: 0 additions & 5 deletions backend/test/com/grohden/repotagger/BaseTest.kt
Original file line number Diff line number Diff line change
@@ -1,24 +1,19 @@
package com.grohden.repotagger

import com.google.gson.Gson
import com.grohden.repotagger.dao.DAOFacade
import com.grohden.repotagger.dao.DAOFacadeDatabase
import io.ktor.config.MapApplicationConfig
import io.ktor.http.HttpMethod
import io.ktor.server.testing.TestApplicationEngine
import io.ktor.server.testing.handleRequest
import io.ktor.server.testing.withTestApplication
import io.mockk.mockk
import org.amshove.kluent.shouldBe
import org.jetbrains.exposed.sql.Database

const val DEFAULT_PASS = "123456"


@Suppress("EXPERIMENTAL_API_USAGE")
abstract class BaseTest {
protected val gson = Gson()
protected val mockedDao = mockk<DAOFacade>(relaxed = true)

protected val memoryDao: DAOFacadeDatabase by lazy {
Database
Expand Down
1 change: 0 additions & 1 deletion backend/test/com/grohden/repotagger/RepositoryTest.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.grohden.repotagger

import com.grohden.repotagger.api.DetailedRepository
import com.grohden.repotagger.api.SimpleRepository
import com.grohden.repotagger.api.TagRepositoriesResponse
import com.grohden.repotagger.dao.CreateTagInput
import com.grohden.repotagger.dao.tables.UserTagDTO
Expand Down
67 changes: 67 additions & 0 deletions backend/test/com/grohden/repotagger/cache/LRUCacheTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.grohden.repotagger.cache

import org.amshove.kluent.shouldBe
import org.amshove.kluent.shouldBeEqualTo
import kotlin.test.Test

class LRUCacheTest {
@Test
fun `should not grow with duplicated keys`() {
val instance = LRUCache<String, String>(4)

instance.apply {
put("bazz", "1")
put("bar", "1")
put("bar", "2")
put("bar", "3")
}

instance.size shouldBe 2
}

@Test
fun `should respect capacity`() {
val instance = LRUCache<String, String>(4)

instance.apply {
put("1", "1")
put("2", "1")
put("3", "1")
put("4", "1")
put("5", "1")
}

instance.size shouldBe 4
}

@Test
fun `should update cache contents for same key`() {
val instance = LRUCache<String, String>(4)

instance.apply {
put("bar", "1")
put("bar", "2")
put("bar", "3")
}

instance.get("bar") shouldBeEqualTo "3"
}

@Test
fun `should evict last updated when at limit`() {
val instance = LRUCache<String, String>(4)

instance.apply {
put("1", "1")
put("2", "2")
put("3", "3")
put("4", "4")
put("5", "5")
}

instance.get("1") shouldBe null
instance.get("2") shouldBe "2"
instance.get("5") shouldBe "5"
}

}

0 comments on commit 642e15b

Please sign in to comment.