Skip to content

Commit

Permalink
Use db as source of truth for repository metadata
Browse files Browse the repository at this point in the history
The current behavior of S3PublishService when publishing an edit is to
fetch the old repository metadata from the S3 bucket, patch it with new
data, then republish the patch metadata over the old metadata. The
problem with this approach is that it requires unnecessary data fetches
and doesn't maintain a well-defined source of truth for the repository.

Instead, store generated repository metadata in the database and patch
_that_ when publishing edits so that we can consistently source the
repository from a single location. The data is relatively small, so
storing it in the database shouldn't have a significant performance
impact. If we really need to in the future, we can store the metadata as
files in a separate storage bucket.
  • Loading branch information
lberrymage committed Jul 25, 2024
1 parent 386720e commit 6d9b592
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ object Apps : IdTable<String>("apps") {
val reviewIssueGroupId =
reference("review_issue_group_id", ReviewIssueGroups, ReferenceOption.NO_ACTION).nullable()
val updating = bool("updating").default(false)
val repositoryMetadata = blob("repository_metadata")
override val primaryKey = PrimaryKey(id)
}

Expand All @@ -31,6 +32,7 @@ class App(id: EntityID<String>) : Entity<String>(id), ToSerializable<Serializabl
var fileId by Apps.fileId
var reviewIssueGroupId by Apps.reviewIssueGroupId
var updating by Apps.updating
var repositoryMetadata by Apps.repositoryMetadata

override fun serializable(): SerializableApp {
// Use en-US locale by default
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package app.accrescent.parcelo.console.publish

import app.accrescent.parcelo.apksparser.ApkSet
import app.accrescent.parcelo.apksparser.ParseApkSetResult
import app.accrescent.parcelo.console.data.App
import app.accrescent.parcelo.console.data.Listing
import app.accrescent.parcelo.console.data.Listings
import app.accrescent.parcelo.console.repo.RepoData
Expand All @@ -14,12 +15,10 @@ import aws.sdk.kotlin.runtime.auth.credentials.StaticCredentialsProvider
import aws.sdk.kotlin.services.s3.S3Client
import aws.sdk.kotlin.services.s3.model.Delete
import aws.sdk.kotlin.services.s3.model.DeleteObjectsRequest
import aws.sdk.kotlin.services.s3.model.GetObjectRequest
import aws.sdk.kotlin.services.s3.model.ListObjectsRequest
import aws.sdk.kotlin.services.s3.model.ObjectIdentifier
import aws.sdk.kotlin.services.s3.model.PutObjectRequest
import aws.smithy.kotlin.runtime.content.ByteStream
import aws.smithy.kotlin.runtime.content.toInputStream
import aws.smithy.kotlin.runtime.net.url.Url
import com.android.bundle.Targeting
import io.ktor.utils.io.core.toByteArray
Expand All @@ -29,8 +28,8 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.statements.api.ExposedBlob
import org.jetbrains.exposed.sql.transactions.transaction
import java.io.FileNotFoundException
import java.io.InputStream
import java.util.zip.ZipFile

Expand Down Expand Up @@ -102,10 +101,6 @@ class S3PublishService(

@OptIn(ExperimentalSerializationApi::class)
override suspend fun publishEdit(appId: String, shortDescription: String?) {
val getOldDataReq = GetObjectRequest {
bucket = s3Bucket
key = "apps/$appId/repodata.json"
}
S3Client {
endpointUrl = s3EndpointUrl
region = s3Region
Expand All @@ -114,26 +109,31 @@ class S3PublishService(
secretAccessKey = s3SecretAccessKey
}
}.use { s3Client ->
val newRepoData = s3Client.getObject(getOldDataReq) { resp ->
val oldRepoData =
resp.body?.toInputStream()?.use { Json.decodeFromStream<RepoData>(it) }
?: throw FileNotFoundException()
RepoData(
version = oldRepoData.version,
versionCode = oldRepoData.versionCode,
abiSplits = oldRepoData.abiSplits,
densitySplits = oldRepoData.densitySplits,
langSplits = oldRepoData.langSplits,
shortDescription = shortDescription ?: oldRepoData.shortDescription
)
}
// Fetch the old app metadata from the database
val app = transaction { App.findById(appId) } ?: throw Exception("app not found")
val oldRepoData = app.repositoryMetadata.inputStream
.use { Json.decodeFromStream<RepoData>(it) }

// Modify the old app metadata to produce the new app metadata
val newRepoData = RepoData(
version = oldRepoData.version,
versionCode = oldRepoData.versionCode,
abiSplits = oldRepoData.abiSplits,
densitySplits = oldRepoData.densitySplits,
langSplits = oldRepoData.langSplits,
shortDescription = shortDescription ?: oldRepoData.shortDescription
).let { Json.encodeToString(it) }.toByteArray()

// Publish the new app metadata
val updateDataReq = PutObjectRequest {
bucket = s3Bucket
key = "apps/$appId/repodata.json"
body = ByteStream.fromString(Json.encodeToString(newRepoData))
body = ByteStream.fromBytes(newRepoData)
}
s3Client.putObject(updateDataReq)

// Save the new app metadata to the database
transaction { app.repositoryMetadata = ExposedBlob(newRepoData) }
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2024 Logan Magee
//
// SPDX-License-Identifier: AGPL-3.0-only

package db.migration

import app.accrescent.parcelo.console.Config
import app.accrescent.parcelo.console.data.App
import aws.sdk.kotlin.runtime.auth.credentials.StaticCredentialsProvider
import aws.sdk.kotlin.services.s3.S3Client
import aws.sdk.kotlin.services.s3.model.GetObjectRequest
import aws.smithy.kotlin.runtime.content.toInputStream
import aws.smithy.kotlin.runtime.net.url.Url
import kotlinx.coroutines.runBlocking
import org.flywaydb.core.api.migration.BaseJavaMigration
import org.flywaydb.core.api.migration.Context
import org.jetbrains.exposed.sql.statements.api.ExposedBlob
import org.jetbrains.exposed.sql.transactions.transaction
import org.koin.java.KoinJavaComponent.inject
import java.io.FileNotFoundException

/**
* A versioned migration which saves published repository to the database.
*/
class V10__Save_repodata_to_database : BaseJavaMigration() {
private val config: Config by inject(Config::class.java)

/**
* Downloads all published repository metadata, saving it to the database alongside its
* associated app.
*/
override fun migrate(context: Context) {
val oldRepoDataReqs = transaction { App.all().map { it.id.value } }
.map { appId ->
val req = GetObjectRequest {
bucket = config.s3.bucket
key = "apps/$appId/repodata.json"
}
Pair(appId, req)
}
S3Client {
endpointUrl = Url.parse(config.s3.endpointUrl)
region = config.s3.region
credentialsProvider = StaticCredentialsProvider {
accessKeyId = config.s3.accessKeyId
secretAccessKey = config.s3.secretAccessKey
}
}.use { s3Client ->
oldRepoDataReqs.forEach { (appId, req) ->
runBlocking {
s3Client.getObject(req) { resp ->
val data = resp.body
?.toInputStream()
?.use { it.readBytes() }
?: throw FileNotFoundException()

transaction {
App.findById(appId)?.repositoryMetadata = ExposedBlob(data)
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- Copyright 2024 Logan Magee
--
-- SPDX-License-Identifier: AGPL-3.0-only

-- Add a NOT NULL constraint to the repository_metadata column
PRAGMA foreign_keys = OFF;

-- The previous apps table with the repository_metadata column made NOT NULL
CREATE TABLE apps2 (id TEXT NOT NULL PRIMARY KEY, version_code INT NOT NULL, version_name TEXT NOT NULL, file_id INT NOT NULL, review_issue_group_id INT NULL, updating BOOLEAN DEFAULT 0 NOT NULL, repository_metadata NOT NULL, CONSTRAINT fk_apps_file_id__id FOREIGN KEY (file_id) REFERENCES files(id) ON UPDATE RESTRICT, CONSTRAINT fk_apps_review_issue_group_id__id FOREIGN KEY (review_issue_group_id) REFERENCES review_issue_groups(id) ON UPDATE RESTRICT);

INSERT INTO apps2 SELECT * FROM apps;
DROP TABLE apps;
ALTER TABLE apps2 RENAME TO apps;

PRAGMA foreign_key_check;

PRAGMA foreign_keys = ON;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- Copyright 2024 Logan Magee
--
-- SPDX-License-Identifier: AGPL-3.0-only

ALTER TABLE apps ADD COLUMN repository_metadata BLOB;

0 comments on commit 6d9b592

Please sign in to comment.