Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

package com.datadog.android.core.internal.persistence.file.batch

import android.os.FileObserver
import android.os.FileObserver.CREATE
import android.os.FileObserver.DELETE
import android.os.FileObserver.MOVED_TO
import android.os.FileObserver.MOVED_FROM
import androidx.annotation.WorkerThread
import com.datadog.android.api.InternalLogger
import com.datadog.android.core.internal.metrics.BatchClosedMetadata
Expand All @@ -20,7 +25,6 @@ import com.datadog.android.core.internal.persistence.file.lengthSafe
import com.datadog.android.core.internal.persistence.file.listFilesSafe
import com.datadog.android.core.internal.persistence.file.mkdirsSafe
import java.io.File
import java.io.FileFilter
import java.util.Locale
import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.roundToLong
Expand All @@ -36,8 +40,6 @@ internal class BatchFileOrchestrator(
private val pendingFiles: AtomicInteger = AtomicInteger(0)
) : FileOrchestrator {

private val fileFilter = BatchFileFilter()

// Offset the recent threshold for read and write to avoid conflicts
// Arbitrary offset as ±5% of the threshold
@Suppress("UnsafeThirdPartyFunctionCall") // rounded Double isn't NaN
Expand All @@ -52,6 +54,37 @@ internal class BatchFileOrchestrator(
private var lastFileAccessTimestamp: Long = 0L
private var lastCleanupTimestamp: Long = 0L

private val knownFiles: MutableSet<File> =
rootDir.listFilesSafe(internalLogger)?.toMutableSet() ?: mutableSetOf()
Comment on lines +57 to +58
Copy link
Member

Choose a reason for hiding this comment

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

I realized the it may bring the following concern: this will be called on the user thread during SDK initialization and since it is I/O call, it may slow down SDK start (especially if there is a lot of files on the disk).

Instead, we maybe can to initial read in the getWritableFile / getReadableFile methods (well, on any initial access of this property), this will shift it to the worker thread improving SDK initialization.

Same may apply to the root folder creation.


@Suppress("DEPRECATION") // Recommended constructor only available in API 29 Q
internal val fileObserver = object : FileObserver(rootDir.path, FILE_OBSERVER_MASK) {
override fun onEvent(event: Int, name: String?) {
if (!name.isNullOrEmpty() && name.isBatchFileName) {
val file = File(rootDir, name)
when (event) {
MOVED_TO, CREATE -> {
synchronized(knownFiles) {
knownFiles.add(file)
}

}

MOVED_FROM, DELETE -> {
synchronized(knownFiles) {
knownFiles.remove(file)
}
}
}
}
}

}

init {
fileObserver.startWatching()
Copy link
Member

Choose a reason for hiding this comment

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

should we stop watching at some point ? not sure when though or if needed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In theory yes but not sure when either

Copy link
Member

Choose a reason for hiding this comment

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

We could use Object.finalize, but here it is Kotlin Any, so no counterpart method.

We probably need to still find a way to stop watching, because if Datadog.stop is called, we will leak this subscription.

Copy link
Member

Choose a reason for hiding this comment

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

Apparently we should be good, because there is stopWatching call in the FileObserver.finalize (link).

}

// region FileOrchestrator

@WorkerThread
Expand Down Expand Up @@ -323,7 +356,9 @@ internal class BatchFileOrchestrator(
}

private fun listBatchFiles(): List<File> {
return rootDir.listFilesSafe(fileFilter, internalLogger).orEmpty().toList()
return synchronized(knownFiles) {
knownFiles.toList()
}
}

private fun listSortedBatchFiles(): List<File> {
Expand All @@ -340,28 +375,20 @@ internal class BatchFileOrchestrator(
get() = File("${this.path}_metadata")

private val File.isBatchFile: Boolean
get() = name.toLongOrNull() != null
get() = name.isBatchFileName

private val String.isBatchFileName: Boolean
get() = toLongOrNull() != null

private val List<File>.latestBatchFile: File?
get() = maxOrNull()

// endregion

// region FileFilter

internal inner class BatchFileFilter : FileFilter {
@Suppress("ReturnCount")
override fun accept(file: File?): Boolean {
if (file == null) return false

return file.isBatchFile
}
}

// endregion

companion object {

private const val FILE_OBSERVER_MASK = CREATE or DELETE or MOVED_TO or MOVED_FROM

const val DECREASE_PERCENT = 0.95
const val INCREASE_PERCENT = 1.05

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

package com.datadog.android.core.internal.persistence.file.batch

import android.os.FileObserver
import com.datadog.android.api.InternalLogger
import com.datadog.android.core.internal.metrics.BatchClosedMetadata
import com.datadog.android.core.internal.metrics.MetricsDispatcher
Expand Down Expand Up @@ -57,7 +58,7 @@ import java.util.concurrent.atomic.AtomicInteger
@MockitoSettings(strictness = Strictness.LENIENT)
internal class BatchFileOrchestratorTest {

private lateinit var testedOrchestrator: FileOrchestrator
private lateinit var testedOrchestrator: BatchFileOrchestrator

@TempDir
lateinit var tempDir: File
Expand Down Expand Up @@ -156,7 +157,11 @@ internal class BatchFileOrchestratorTest {
var previousFile: File? = null
val startTimestamp = System.currentTimeMillis()
repeat(iterations) {
previousFile = testedOrchestrator.getWritableFile(false)
val file = testedOrchestrator.getWritableFile(false)
if (file != previousFile) {
testedOrchestrator.fileObserver.onEvent(FileObserver.CREATE, file?.name)
}
previousFile = file
previousFile?.writeText(data)
}
val endTimestamp = System.currentTimeMillis()
Expand Down Expand Up @@ -291,11 +296,14 @@ internal class BatchFileOrchestratorTest {
val oldTimestamp = System.currentTimeMillis() - oldFileAge
val oldFile = File(fakeRootDir, oldTimestamp.toString())
oldFile.createNewFile()
testedOrchestrator.fileObserver.onEvent(FileObserver.CREATE, oldFile.name)
val oldFileMeta = File("${oldFile.path}_metadata")
oldFileMeta.createNewFile()
testedOrchestrator.fileObserver.onEvent(FileObserver.CREATE, oldFileMeta.name)
val youngTimestamp = System.currentTimeMillis() - RECENT_DELAY_MS - 1
val youngFile = File(fakeRootDir, youngTimestamp.toString())
youngFile.createNewFile()
testedOrchestrator.fileObserver.onEvent(FileObserver.CREATE, youngFile.name)

// When
val start = System.currentTimeMillis()
Expand Down Expand Up @@ -331,11 +339,14 @@ internal class BatchFileOrchestratorTest {
val oldTimestamp = System.currentTimeMillis() - oldFileAge
val oldFile = File(fakeRootDir, oldTimestamp.toString())
oldFile.createNewFile()
testedOrchestrator.fileObserver.onEvent(FileObserver.CREATE, oldFile.name)
val oldFileMeta = File("${oldFile.path}_metadata")
oldFileMeta.createNewFile()
testedOrchestrator.fileObserver.onEvent(FileObserver.CREATE, oldFileMeta.name)
val youngTimestamp = System.currentTimeMillis() - RECENT_DELAY_MS - 1
val youngFile = File(fakeRootDir, youngTimestamp.toString())
youngFile.createNewFile()
testedOrchestrator.fileObserver.onEvent(FileObserver.CREATE, youngFile.name)

// When
val start = System.currentTimeMillis()
Expand All @@ -345,6 +356,7 @@ internal class BatchFileOrchestratorTest {
// cleanup shouldn't be performed during the next getWritableFile call
val evenOlderFile = File(fakeRootDir, (oldTimestamp - 1).toString())
evenOlderFile.createNewFile()
testedOrchestrator.fileObserver.onEvent(FileObserver.CREATE, evenOlderFile.name)
testedOrchestrator.getWritableFile(forceNewFile)

// Then
Expand Down Expand Up @@ -376,8 +388,10 @@ internal class BatchFileOrchestratorTest {
val oldTimestamp = System.currentTimeMillis() - oldFileAge
val oldFile = File(fakeRootDir, oldTimestamp.toString())
oldFile.createNewFile()
testedOrchestrator.fileObserver.onEvent(FileObserver.CREATE, oldFile.name)
val oldFileMeta = File("${oldFile.path}_metadata")
oldFileMeta.createNewFile()
testedOrchestrator.fileObserver.onEvent(FileObserver.CREATE, oldFileMeta.name)

// When
val start = System.currentTimeMillis()
Expand All @@ -386,6 +400,7 @@ internal class BatchFileOrchestratorTest {
Thread.sleep(CLEANUP_FREQUENCY_THRESHOLD_MS + 1)
val evenOlderFile = File(fakeRootDir, (oldTimestamp - 1).toString())
evenOlderFile.createNewFile()
testedOrchestrator.fileObserver.onEvent(FileObserver.CREATE, evenOlderFile.name)
testedOrchestrator.getWritableFile(forceNewFile)

// Then
Expand Down Expand Up @@ -450,6 +465,7 @@ internal class BatchFileOrchestratorTest {
assumeTrue(fakeRootDir.listFiles().isNullOrEmpty())
val previousFile = testedOrchestrator.getWritableFile()
checkNotNull(previousFile)
testedOrchestrator.fileObserver.onEvent(FileObserver.CREATE, previousFile.name)
previousFile.writeText(previousData)
Thread.sleep(1)

Expand Down Expand Up @@ -667,12 +683,16 @@ internal class BatchFileOrchestratorTest {
// Given
assumeTrue(fakeRootDir.listFiles().isNullOrEmpty())
val filesCount = MAX_DISK_SPACE / MAX_BATCH_SIZE
val files = (0..filesCount).map {
val files = mutableListOf<File>()
repeat(filesCount + 1) {
val file = testedOrchestrator.getWritableFile()
checkNotNull(file)
if (files.none { it.name == file.name }) {
testedOrchestrator.fileObserver.onEvent(FileObserver.CREATE, file.name)
}
file.writeText(previousData)
files.add(file)
Thread.sleep(1)
file
}

// When
Expand Down Expand Up @@ -831,11 +851,14 @@ internal class BatchFileOrchestratorTest {
val oldTimestamp = System.currentTimeMillis() - oldFileAge
val oldFile = File(fakeRootDir, oldTimestamp.toString())
oldFile.createNewFile()
testedOrchestrator.fileObserver.onEvent(FileObserver.CREATE, oldFile.name)
val oldFileMeta = File("${oldFile.path}_metadata")
oldFileMeta.createNewFile()
testedOrchestrator.fileObserver.onEvent(FileObserver.CREATE, oldFileMeta.name)
val youngTimestamp = System.currentTimeMillis() - RECENT_DELAY_MS - 1
val youngFile = File(fakeRootDir, youngTimestamp.toString())
youngFile.createNewFile()
testedOrchestrator.fileObserver.onEvent(FileObserver.CREATE, youngFile.name)

// When
val result = testedOrchestrator.getReadableFile(emptySet())
Expand Down Expand Up @@ -878,6 +901,7 @@ internal class BatchFileOrchestratorTest {
val timestamp = System.currentTimeMillis() - (RECENT_DELAY_MS * 2)
val file = File(fakeRootDir, timestamp.toString())
file.createNewFile()
testedOrchestrator.fileObserver.onEvent(FileObserver.CREATE, file.name)

// When
val result = testedOrchestrator.getReadableFile(emptySet())
Expand Down Expand Up @@ -1039,10 +1063,16 @@ internal class BatchFileOrchestratorTest {
for (i in 1..count) {
// create both non readable and non writable files
expectedFiles.add(
File(fakeRootDir, (new + i).toString()).also { it.createNewFile() }
File(fakeRootDir, (new + i).toString()).also {
it.createNewFile()
testedOrchestrator.fileObserver.onEvent(FileObserver.CREATE, it.name)
}
)
expectedFiles.add(
File(fakeRootDir, (old - i).toString()).also { it.createNewFile() }
File(fakeRootDir, (old - i).toString()).also {
it.createNewFile()
testedOrchestrator.fileObserver.onEvent(FileObserver.CREATE, it.name)
}
)
}

Expand Down Expand Up @@ -1085,10 +1115,16 @@ internal class BatchFileOrchestratorTest {
for (i in 1..count) {
// create both non readable and non writable files
expectedFiles.add(
File(fakeRootDir, (new + i).toString()).also { it.createNewFile() }
File(fakeRootDir, (new + i).toString()).also {
it.createNewFile()
testedOrchestrator.fileObserver.onEvent(FileObserver.CREATE, it.name)
}
)
expectedFiles.add(
File(fakeRootDir, (old - i).toString()).also { it.createNewFile() }
File(fakeRootDir, (old - i).toString()).also {
it.createNewFile()
testedOrchestrator.fileObserver.onEvent(FileObserver.CREATE, it.name)
}
)
}

Expand Down
Loading