@@ -18,16 +18,12 @@ package com.mayakapps.kache
1818
1919import com.mayakapps.kache.OkioFileKache.Configuration
2020import com.mayakapps.kache.journal.*
21- import kotlinx.coroutines.CoroutineScope
22- import kotlinx.coroutines.Deferred
23- import kotlinx.coroutines.invoke
24- import kotlinx.coroutines.launch
21+ import kotlinx.coroutines.*
2522import kotlinx.coroutines.sync.Mutex
2623import kotlinx.coroutines.sync.withLock
2724import okio.EOFException
2825import okio.FileSystem
2926import okio.Path
30- import okio.Path.Companion.toPath
3127import okio.buffer
3228
3329/* *
@@ -58,100 +54,136 @@ public class OkioFileKache private constructor(
5854
5955 // Explicit type parameter is a workaround for https://youtrack.jetbrains.com/issue/KT-53109
6056 @Suppress(" RemoveExplicitTypeArguments" )
61- private val underlyingKache = InMemoryKache <String , Path >(maxSize = maxSize) {
57+ private val underlyingKache = InMemoryKache <String , String >(maxSize = maxSize) {
6258 this .strategy = strategy
63- this .sizeCalculator = { _, file -> fileSystem.metadata(file ).size ? : 0 }
64- this .onEntryRemoved = { _, key, oldValue, _ -> onEntryRemoved(key, oldValue) }
59+ this .sizeCalculator = { _, filename -> fileSystem.metadata(filesDirectory.resolve(filename) ).size ? : 0 }
60+ this .onEntryRemoved = { _, key, oldValue, newValue -> onEntryRemoved(key, oldValue, newValue ) }
6561 this .creationScope = this @OkioFileKache.creationScope
6662 }
6763
64+ private val filesDirectory = directory.resolve(FILES_DIR )
65+
6866 private val journalMutex = Mutex ()
6967 private val journalFile = directory.resolve(JOURNAL_FILE )
7068 private var journalWriter =
7169 JournalWriter (fileSystem.appendingSink(journalFile, mustExist = true ).buffer())
7270
7371 private var redundantJournalEntriesCount = initialRedundantJournalEntriesCount
72+ override val maxSize: Long get() = underlyingKache.maxSize
73+ override val size: Long get() = underlyingKache.size
74+
75+ override suspend fun getKeys (): Set <String > = underlyingKache.getKeys()
76+
77+ override suspend fun getUnderCreationKeys (): Set <String > = underlyingKache.getUnderCreationKeys()
78+
79+ override suspend fun getAllKeys (): KacheKeys <String > = underlyingKache.getAllKeys()
7480
7581 override suspend fun get (key : String ): Path ? {
76- val transformedKey = key.transform()
77- val result = underlyingKache.get(transformedKey)
78- if (result != null ) writeRead(transformedKey)
79- return result
82+ val result = underlyingKache.get(key)
83+ if (result != null ) writeRead(key)
84+ return result?.let { filesDirectory.resolve(it) }
8085 }
8186
8287 override suspend fun getIfAvailable (key : String ): Path ? {
83- val transformedKey = key.transform()
84- val result = underlyingKache.getIfAvailable(transformedKey)
85- if (result != null ) writeRead(transformedKey)
86- return result
88+ val result = underlyingKache.getIfAvailable(key)
89+ if (result != null ) writeRead(key)
90+ return result?.let { filesDirectory.resolve(it) }
8791 }
8892
8993 override suspend fun getOrPut (key : String , creationFunction : suspend (Path ) -> Boolean ): Path ? {
9094 var created = false
91- val transformedKey = key.transform()
92- val result = underlyingKache.getOrPut(transformedKey) {
95+ val result = underlyingKache.getOrPut(key) {
9396 created = true
9497 wrapCreationFunction(it, creationFunction)
9598 }
9699
97- if (! created && result != null ) writeRead(transformedKey )
98- return result
100+ if (! created && result != null ) writeRead(key )
101+ return result?. let { filesDirectory.resolve(it) }
99102 }
100103
101- override suspend fun put (key : String , creationFunction : suspend (Path ) -> Boolean ): Path ? =
102- underlyingKache.put(key.transform()) { wrapCreationFunction(it, creationFunction) }
104+ override suspend fun put (key : String , creationFunction : suspend (Path ) -> Boolean ): Path ? {
105+ val filename = underlyingKache.put(key) { wrapCreationFunction(it, creationFunction) }
106+ return if (filename != null ) filesDirectory.resolve(filename) else null
107+ }
103108
104109 override suspend fun putAsync (key : String , creationFunction : suspend (Path ) -> Boolean ): Deferred <Path ?> =
105- underlyingKache.putAsync(key.transform()) { wrapCreationFunction(it, creationFunction) }
110+ creationScope.async(start = CoroutineStart .UNDISPATCHED ) {
111+ underlyingKache.putAsync(key) { wrapCreationFunction(it, creationFunction) }.await()?.let {
112+ filesDirectory.resolve(it)
113+ }
114+ }
106115
107116 override suspend fun remove (key : String ) {
108117 // It's fine to consider the file is dirty now. Even if removal failed it's scheduled for
109- val transformedKey = key.transform()
110- writeDirty(transformedKey)
111- underlyingKache.remove(transformedKey)
118+ writeDirty(key)
119+ underlyingKache.remove(key)
112120 }
113121
114122 override suspend fun clear () {
115- close()
116- if (fileSystem.metadata(directory).isDirectory) fileSystem.deleteRecursively(directory)
117- fileSystem.createDirectories(directory)
123+ underlyingKache.getKeys().forEach { writeDirty(it) }
124+ underlyingKache.clear()
125+ }
126+
127+ override suspend fun removeAllUnderCreation () {
128+ underlyingKache.removeAllUnderCreation()
129+ }
130+
131+ override suspend fun resize (maxSize : Long ) {
132+ underlyingKache.resize(maxSize)
133+ }
134+
135+ override suspend fun trimToSize (size : Long ) {
136+ underlyingKache.trimToSize(size)
118137 }
119138
120139 override suspend fun close () {
121140 underlyingKache.removeAllUnderCreation()
122141 journalMutex.withLock { journalWriter.close() }
123142 }
124143
125- private suspend fun String.transform () =
126- keyTransformer?.transform(this ) ? : this
127-
128144 private suspend fun wrapCreationFunction (
129145 key : String ,
130146 creationFunction : suspend (Path ) -> Boolean ,
131- ): Path ? {
132- val tempFile = directory.resolve(key + TEMP_EXT )
133- val cleanFile = directory.resolve(key)
134-
135- writeDirty(key)
136- return if (creationFunction(tempFile) && fileSystem.exists(tempFile)) {
137- fileSystem.atomicMove(tempFile, cleanFile, deleteTarget = true )
138- fileSystem.delete(tempFile)
139- writeClean(key)
140- rebuildJournalIfRequired()
141- cleanFile
142- } else {
147+ ): String? {
148+ val transformedKey = keyTransformer?.transform(key) ? : key
149+ val tempFile = filesDirectory.resolve(transformedKey + TEMP_EXT )
150+ val cleanFile = filesDirectory.resolve(transformedKey)
151+ val isReplacing = fileSystem.exists(cleanFile)
152+
153+ try {
154+ writeDirty(key)
155+ return if (creationFunction(tempFile) && fileSystem.exists(tempFile)) {
156+ fileSystem.atomicMove(tempFile, cleanFile, deleteTarget = true )
157+ fileSystem.delete(tempFile)
158+
159+ if (isReplacing) writeClean(key)
160+ else writeClean(key, transformedKey)
161+
162+ rebuildJournalIfRequired()
163+ transformedKey
164+ } else {
165+ fileSystem.delete(tempFile)
166+ writeCancel(key)
167+ rebuildJournalIfRequired()
168+ null
169+ }
170+ } catch (th: Throwable ) {
143171 fileSystem.delete(tempFile)
172+
144173 writeCancel(key)
145174 rebuildJournalIfRequired()
146- null
175+
176+ throw th
147177 }
148178 }
149179
150- private fun onEntryRemoved (key : String , oldValue : Path ) {
151- creationScope.launch {
152- fileSystem.delete(oldValue)
153- fileSystem.delete((oldValue.toString() + TEMP_EXT ).toPath())
180+ private fun onEntryRemoved (key : String , oldValue : String , newValue : String? ) {
181+ if (newValue != null ) return
154182
183+ fileSystem.delete(filesDirectory.resolve(oldValue))
184+ fileSystem.delete(filesDirectory.resolve(oldValue + TEMP_EXT ))
185+
186+ creationScope.launch(start = CoroutineStart .UNDISPATCHED ) {
155187 writeRemove(key)
156188 rebuildJournalIfRequired()
157189 }
@@ -161,8 +193,8 @@ public class OkioFileKache private constructor(
161193 journalWriter.writeDirty(key)
162194 }
163195
164- private suspend fun writeClean (key : String ) = journalMutex.withLock {
165- journalWriter.writeClean(key)
196+ private suspend fun writeClean (key : String , transformedKey : String? = null ) = journalMutex.withLock {
197+ journalWriter.writeClean(key, transformedKey )
166198 redundantJournalEntriesCount++
167199 }
168200
@@ -191,7 +223,11 @@ public class OkioFileKache private constructor(
191223 journalWriter.close()
192224
193225 val (cleanKeys, dirtyKeys) = underlyingKache.getAllKeys()
194- fileSystem.writeJournalAtomically(directory, cleanKeys, dirtyKeys)
226+ val cleanEntries = cleanKeys.mapNotNull { key ->
227+ val transformedKey = underlyingKache.getIfAvailable(key) ? : return @mapNotNull null
228+ key to transformedKey
229+ }.toMap()
230+ fileSystem.writeJournalAtomically(directory, cleanEntries, dirtyKeys)
195231
196232 journalWriter =
197233 JournalWriter (fileSystem.appendingSink(journalFile, mustExist = true ).buffer())
@@ -255,39 +291,50 @@ public class OkioFileKache private constructor(
255291 ): OkioFileKache {
256292 require(maxSize > 0 ) { " maxSize must be positive value" }
257293
258- // Make sure that journal directory exists
294+ // Make sure that directories exist
295+ val filesDirectory = directory.resolve(FILES_DIR )
259296 fileSystem.createDirectories(directory)
297+ fileSystem.createDirectories(filesDirectory)
260298
261299 val journalData = try {
262- fileSystem.readJournalIfExists(directory, cacheVersion)
300+ fileSystem.readJournalIfExists(directory, cacheVersion, strategy )
263301 } catch (ex: JournalException ) {
264302 // Journal is corrupted - Clear cache
265303 fileSystem.deleteContents(directory)
304+ fileSystem.createDirectories(filesDirectory)
266305 null
267306 } catch (ex: EOFException ) {
268307 // Journal is corrupted - Clear cache
269308 fileSystem.deleteContents(directory)
309+ fileSystem.createDirectories(filesDirectory)
270310 null
271311 }
272312
273313 // Delete dirty entries
274314 if (journalData != null ) {
275- for (key in journalData.dirtyEntriesKeys ) {
276- fileSystem.delete(directory .resolve(key + TEMP_EXT ))
315+ for (key in journalData.dirtyEntryKeys ) {
316+ fileSystem.delete(filesDirectory .resolve(key + TEMP_EXT ))
277317 }
278318 }
279319
280320 // Rebuild journal if required
281321 var redundantJournalEntriesCount = journalData?.redundantEntriesCount ? : 0
282322
283323 if (journalData == null ) {
284- fileSystem.writeJournalAtomically(directory, emptyList (), emptyList())
324+ fileSystem.writeJournalAtomically(directory, emptyMap (), emptyList())
285325 } else if (
286326 journalData.redundantEntriesCount >= REDUNDANT_ENTRIES_THRESHOLD &&
287- journalData.redundantEntriesCount >= journalData.cleanEntriesKeys .size
327+ journalData.redundantEntriesCount >= journalData.cleanEntries .size
288328 ) {
289- fileSystem
290- .writeJournalAtomically(directory, journalData.cleanEntriesKeys, emptyList())
329+ for ((key, transformedKey) in journalData.cleanEntries) {
330+ if (transformedKey == null ) {
331+ journalData.cleanEntries[key] = keyTransformer?.transform(key) ? : key
332+ }
333+ }
334+
335+ @Suppress(" UNCHECKED_CAST" )
336+ val cleanEntries = journalData.cleanEntries as Map <String , String >
337+ fileSystem.writeJournalAtomically(directory, cleanEntries, emptyList())
291338 redundantJournalEntriesCount = 0
292339 }
293340
@@ -302,14 +349,15 @@ public class OkioFileKache private constructor(
302349 )
303350
304351 if (journalData != null ) {
305- cache.underlyingKache.putAll(journalData.cleanEntriesKeys.associateWith { directory.resolve(it) })
352+ @Suppress(" UNCHECKED_CAST" )
353+ cache.underlyingKache.putAll(journalData.cleanEntries as Map <String , String >)
306354 }
307355
308356 return cache
309357 }
310358
311359 private const val TEMP_EXT = " .tmp"
312- private const val REDUNDANT_ENTRIES_THRESHOLD = 2000
360+ internal const val REDUNDANT_ENTRIES_THRESHOLD = 2000
313361 }
314362}
315363
0 commit comments