Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Batch operations #1743

Merged
merged 42 commits into from
Jul 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
28edcc6
feat: Batch operations - Rabbit with docker, batch translation prototype
JanCizmar Jun 6, 2023
12bd365
fix: Chunk processing - unstable
JanCizmar Jun 7, 2023
ddc3375
chore: Setup tests, run failing test due to mockito
JanCizmar Jun 7, 2023
d7c6594
chore: Setup tests, run failing test due to mockito
JanCizmar Jun 7, 2023
475ad76
feat: Batch operations using "skip locked" working on single instance
JanCizmar Jun 17, 2023
901be80
feat: Running operations & progress WIP
JanCizmar Jun 18, 2023
65f1794
feat: Batch operation retry, exception handling
JanCizmar Jun 19, 2023
981f570
feat: Single batch operation, passing tests
JanCizmar Jun 23, 2023
34c2b8c
feat: Batch operation, stores activity
JanCizmar Jun 23, 2023
f585df8
feat: Batch operation, stores activity
JanCizmar Jun 23, 2023
fe74553
feat: Batch operation, stores activity
JanCizmar Jun 25, 2023
e9f1bd7
feat: Batch operation, stores activity
JanCizmar Jun 25, 2023
a01e746
feat: Batch operation -> merges activity
JanCizmar Jun 26, 2023
e51d5f5
feat: Batch operation -> ignore activity for running jobs
JanCizmar Jun 26, 2023
86dd60d
feat: Batch operation -> job cancellation
JanCizmar Jun 26, 2023
d15df5e
feat: Batch operation -> state as map
JanCizmar Jun 27, 2023
ebf5823
feat: Batch operation -> job cancellation
JanCizmar Jun 27, 2023
f773d9d
feat: Endpoint tests, configurable concurrency
JanCizmar Jun 28, 2023
7ba7c9b
feat: Some refactoring
JanCizmar Jun 29, 2023
6ee18ef
fix: Websocket tests, health check
JanCizmar Jun 29, 2023
38c9472
fix: Websocket tests, health check
JanCizmar Jun 29, 2023
3e1366a
fix: Test fixes
JanCizmar Jun 30, 2023
4ac9476
fix: Test fixes
JanCizmar Jun 30, 2023
1f4732d
fix: Test fixes, logging
JanCizmar Jun 30, 2023
9891fc8
fix: Test fixes, logging
JanCizmar Jun 30, 2023
964b870
fix: Test fixes, logging
JanCizmar Jun 30, 2023
2b264fe
fix: Temporarily disable tests using the WebsocketTestHelper
JanCizmar Jun 30, 2023
fc17d31
fix: Run websocket tests separately
JanCizmar Jun 30, 2023
96d033c
fix: Run websocket tests separately
JanCizmar Jun 30, 2023
42863cf
fix: Only one launcher per instance
JanCizmar Jul 1, 2023
2ed95eb
fix: Better concurrency
JanCizmar Jul 1, 2023
293aeac
fix: Better caching
JanCizmar Jul 1, 2023
ce8fa39
fix: Stats job
JanCizmar Jul 2, 2023
736bb24
fix: Serializable transaction isolation is not default anymore
JanCizmar Jul 2, 2023
9d2658c
fix: Add dirties context, clear invocations
JanCizmar Jul 2, 2023
018952f
fix: Fix cache evict after job status updated, fix job remove test
JanCizmar Jul 2, 2023
1c1a36a
fix: Fix cache evict after job status updated, fix job remove test
JanCizmar Jul 2, 2023
6b27e6c
fix: Singleton queue
JanCizmar Jul 2, 2023
75fece7
fix: Batch operations
JanCizmar Jul 10, 2023
befec42
fix: Batch operations
JanCizmar Jul 10, 2023
1e25cbd
fix: General test with context refresh after each method
JanCizmar Jul 10, 2023
f75e8e4
fix: Add job status and error to the model
JanCizmar Jul 11, 2023
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
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
~/backend-development.tgz

backend-test:
name: Backend testing ‍🔎
name: BT ‍🔎
needs: [backend-build]
runs-on: ubuntu-latest
strategy:
Expand All @@ -66,6 +66,7 @@ jobs:
[
"server-app:runContextRecreatingTests",
"server-app:runStandardTests",
"server-app:runWebsocketTests",
"ee-test:test",
"data:test",
]
Expand Down
1 change: 1 addition & 0 deletions backend/api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-hateoas"
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation("org.springframework.boot:spring-boot-starter-security")
implementation "org.springframework.boot:spring-boot-starter-websocket"

implementation(project(':data'))
implementation(project(':misc'))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ When no languages provided, it translates only untranslated languages."""

autoTranslationService.autoTranslate(
key = key,
languageTags = languages,
languageTags = languages?.toList(),
useTranslationMemory = useTranslationMemory ?: false,
useMachineTranslation = useMachineTranslation ?: false
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package io.tolgee.api.v2.controllers.batch

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import io.tolgee.batch.BatchJobCancellationManager
import io.tolgee.batch.BatchJobDto
import io.tolgee.batch.BatchJobService
import io.tolgee.hateoas.batch.BatchJobModel
import io.tolgee.hateoas.batch.BatchJobModelAssembler
import io.tolgee.model.batch.BatchJob
import io.tolgee.model.enums.Scope
import io.tolgee.model.views.BatchJobView
import io.tolgee.security.AuthenticationFacade
import io.tolgee.security.apiKeyAuth.AccessWithApiKey
import io.tolgee.security.project_auth.AccessWithAnyProjectPermission
import io.tolgee.security.project_auth.AccessWithProjectPermission
import io.tolgee.security.project_auth.ProjectHolder
import io.tolgee.service.security.SecurityService
import org.springdoc.api.annotations.ParameterObject
import org.springframework.data.domain.Pageable
import org.springframework.data.web.PagedResourcesAssembler
import org.springframework.data.web.SortDefault
import org.springframework.hateoas.PagedModel
import org.springframework.web.bind.annotation.CrossOrigin
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import javax.validation.Valid

@RestController
@CrossOrigin(origins = ["*"])
@RequestMapping(value = ["/v2/projects/{projectId:\\d+}/", "/v2/projects/"])
@Tag(name = "Batch job management")
@Suppress("SpringJavaInjectionPointsAutowiringInspection", "MVCPathVariableInspection")
class BatchJobManagementController(
private val batchJobCancellationManager: BatchJobCancellationManager,
private val batchJobService: BatchJobService,
private val projectHolder: ProjectHolder,
private val batchJobModelAssembler: BatchJobModelAssembler,
private val pagedResourcesAssembler: PagedResourcesAssembler<BatchJobView>,
private val authenticationFacade: AuthenticationFacade,
private val securityService: SecurityService
) {
@GetMapping(value = ["batch-jobs"])
@AccessWithApiKey()
@AccessWithProjectPermission(Scope.BATCH_JOBS_VIEW)
@Operation(summary = "Lists all batch jobs in project")
fun list(@Valid @ParameterObject @SortDefault("id") pageable: Pageable): PagedModel<BatchJobModel> {
val views = batchJobService.getViews(projectHolder.project.id, null, pageable)
return pagedResourcesAssembler.toModel(views, batchJobModelAssembler)
}

@GetMapping(value = ["my-batch-jobs"])
@AccessWithApiKey()
@AccessWithAnyProjectPermission()
@Operation(summary = "Lists all batch jobs in project started by current user")
fun myList(@Valid @ParameterObject @SortDefault("id") pageable: Pageable): PagedModel<BatchJobModel> {
val views = batchJobService.getViews(
projectId = projectHolder.project.id,
userAccount = authenticationFacade.userAccount,
pageable = pageable
)
return pagedResourcesAssembler.toModel(views, batchJobModelAssembler)
}

@GetMapping(value = ["batch-jobs/{id}"])
@AccessWithApiKey()
@AccessWithAnyProjectPermission()
@Operation(summary = "Returns the batch job")
fun get(@PathVariable id: Long): BatchJobModel {
val view = batchJobService.getView(id)
checkViewPermission(view.batchJob)
return batchJobModelAssembler.toModel(view)
}

@PutMapping(value = ["batch-jobs/{id}/cancel"])
@AccessWithApiKey()
@AccessWithAnyProjectPermission()
@Operation(summary = "Stops batch job (if possible)")
fun cancel(@PathVariable id: Long) {
checkCancelPermission(batchJobService.getJobDto(id))
batchJobCancellationManager.cancel(id)
}

private fun checkViewPermission(job: BatchJob) {
if (job.author?.id == authenticationFacade.userAccount.id) {
return
}
securityService.checkProjectPermission(projectHolder.project.id, Scope.BATCH_JOBS_VIEW)
}

private fun checkCancelPermission(job: BatchJobDto) {
if (job.authorId == authenticationFacade.userAccount.id) {
return
}
securityService.checkProjectPermission(projectHolder.project.id, Scope.BATCH_JOBS_CANCEL)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.tolgee.api.v2.controllers.batch

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import io.tolgee.batch.BatchJobService
import io.tolgee.batch.BatchJobType
import io.tolgee.batch.request.BatchTranslateRequest
import io.tolgee.batch.request.DeleteKeysRequest
import io.tolgee.hateoas.batch.BatchJobModel
import io.tolgee.hateoas.batch.BatchJobModelAssembler
import io.tolgee.model.batch.BatchJob
import io.tolgee.model.enums.Scope
import io.tolgee.security.AuthenticationFacade
import io.tolgee.security.apiKeyAuth.AccessWithApiKey
import io.tolgee.security.project_auth.AccessWithProjectPermission
import io.tolgee.security.project_auth.ProjectHolder
import io.tolgee.service.security.SecurityService
import org.springframework.web.bind.annotation.CrossOrigin
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import javax.validation.Valid

@RestController
@CrossOrigin(origins = ["*"])
@RequestMapping(value = ["/v2/projects/{projectId:\\d+}/start-batch-job", "/v2/projects/start-batch-job"])
@Tag(name = "Start batch jobs")
@Suppress("SpringJavaInjectionPointsAutowiringInspection", "MVCPathVariableInspection")
class StartBatchJobController(
private val securityService: SecurityService,
private val projectHolder: ProjectHolder,
private val batchJobService: BatchJobService,
private val authenticationFacade: AuthenticationFacade,
private val batchJobModelAssembler: BatchJobModelAssembler
) {
@PutMapping(value = ["/translate"])
@AccessWithApiKey()
@AccessWithProjectPermission(Scope.BATCH_AUTO_TRANSLATE)
@Operation(summary = "Translates provided keys to provided languages")
fun translate(@Valid @RequestBody data: BatchTranslateRequest): BatchJobModel {
securityService.checkLanguageTranslatePermission(projectHolder.project.id, data.targetLanguageIds)
securityService.checkKeyIdsExistAndIsFromProject(data.keyIds, projectHolder.project.id)
return batchJobService.startJob(
data,
projectHolder.projectEntity,
authenticationFacade.userAccountEntity,
BatchJobType.TRANSLATION
).model
}

@PutMapping(value = ["/delete-keys"])
@AccessWithApiKey()
@AccessWithProjectPermission(Scope.KEYS_DELETE)
@Operation(summary = "Translates provided keys to provided languages")
fun deleteKeys(@Valid @RequestBody data: DeleteKeysRequest): BatchJobModel {
securityService.checkKeyIdsExistAndIsFromProject(data.keyIds, projectHolder.project.id)
return batchJobService.startJob(
data,
projectHolder.projectEntity,
authenticationFacade.userAccountEntity,
BatchJobType.DELETE_KEYS
).model
}

val BatchJob.model
get() = batchJobModelAssembler.toModel(batchJobService.getView(this))
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class ProjectTranslationLastModifiedManager(

@EventListener
fun onActivity(event: OnProjectActivityEvent) {
event.activityHolder.activityRevision?.projectId?.let { projectId ->
event.activityRevision.projectId?.let { projectId ->
getCache()?.put(projectId, currentDateProvider.date.time)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
package io.tolgee.component.lockingProvider

import io.tolgee.component.LockingProvider
import org.redisson.api.RLock
import org.redisson.api.RedissonClient
import java.util.concurrent.locks.Lock

class RedissonLockingProvider(private val redissonClient: RedissonClient) : LockingProvider {
override fun getLock(name: String): Lock {
override fun getLock(name: String): RLock {
return redissonClient.getLock(name)
}

override fun <T> withLocking(name: String, fn: () -> T): T {
val lock = this.getLock(name)
lock.lock()
try {
return fn()
} finally {
if (lock.isHeldByCurrentThread) {
lock.unlock()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,14 @@ class SimpleLockingProvider : LockingProvider {
override fun getLock(name: String): Lock {
return map.getOrPut(name) { ReentrantLock() }
}

override fun <T> withLocking(name: String, fn: () -> T): T {
val lock = this.getLock(name)
lock.lock()
try {
return fn()
} finally {
lock.unlock()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import org.redisson.Redisson
import org.redisson.api.RedissonClient
import org.springframework.boot.autoconfigure.AutoConfigureAfter
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.core.RedisOperations

@Configuration
@ConditionalOnClass(Redisson::class, RedisOperations::class)
@AutoConfigureAfter(ConditionalRedissonAutoConfiguration::class)
@ConditionalOnProperty(name = ["tolgee.cache.use-redis"], havingValue = "true")
@ConditionalOnExpression("\${tolgee.cache.use-redis:false} and \${tolgee.cache.enabled:false}")
class RedisLockingConfiguration(
val redissonClient: RedissonClient
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import org.redisson.spring.cache.CacheConfig
import org.redisson.spring.cache.RedissonSpringCacheManager
import org.springframework.boot.autoconfigure.AutoConfigureAfter
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression
import org.springframework.cache.CacheManager
import org.springframework.cache.annotation.EnableCaching
import org.springframework.context.annotation.Bean
Expand All @@ -19,7 +19,7 @@ import org.springframework.data.redis.core.RedisOperations
@EnableCaching
@ConditionalOnClass(Redisson::class, RedisOperations::class)
@AutoConfigureAfter(ConditionalRedissonAutoConfiguration::class)
@ConditionalOnProperty(name = ["tolgee.cache.use-redis"], havingValue = "true")
@ConditionalOnExpression("\${tolgee.cache.use-redis:false} and \${tolgee.cache.enabled:false}")
class RedissonCacheConfiguration(private val tolgeeProperties: TolgeeProperties) {
@Bean
fun cacheManager(redissonClient: RedissonClient): CacheManager? {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.tolgee.hateoas.batch

import io.swagger.v3.oas.annotations.media.Schema
import io.tolgee.batch.BatchJobType
import io.tolgee.hateoas.user_account.SimpleUserAccountModel
import io.tolgee.model.batch.BatchJobStatus
import org.springframework.hateoas.RepresentationModel
import org.springframework.hateoas.server.core.Relation
import java.io.Serializable

@Suppress("unused")
@Relation(collectionRelation = "batchJobs", itemRelation = "batchJob")
open class BatchJobModel(
@Schema(description = "Batch job id")
val id: Long,

@Schema(description = "Status of the batch job")
val status: BatchJobStatus,

@Schema(description = "Type of the batch job")
val type: BatchJobType,

@Schema(description = "Total items, that have been processed so far")
val progress: Int,

@Schema(description = "Total items")
val totalItems: Int,

@Schema(description = "The user who started the job")
val author: SimpleUserAccountModel?,

@Schema(description = "The time when the job created")
val createdAt: String,

@Schema(description = "The activity revision id, that stores the activity details of the job")
val activityRevisionId: Long?,

@Schema(description = "If the job failed, this is the error message")
val errorMessage: String?,
) : RepresentationModel<BatchJobModel>(), Serializable
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.tolgee.hateoas.batch

import io.tolgee.api.v2.controllers.batch.BatchJobManagementController
import io.tolgee.hateoas.user_account.SimpleUserAccountModelAssembler
import io.tolgee.model.views.BatchJobView
import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport
import org.springframework.stereotype.Component

@Component
class BatchJobModelAssembler(
val simpleUserAccountModelAssembler: SimpleUserAccountModelAssembler
) : RepresentationModelAssemblerSupport<BatchJobView, BatchJobModel>(
BatchJobManagementController::class.java, BatchJobModel::class.java
) {
override fun toModel(view: BatchJobView): BatchJobModel {
return BatchJobModel(
id = view.batchJob.id,
type = view.batchJob.type,
status = view.batchJob.status,
progress = view.progress,
totalItems = view.batchJob.totalItems,
author = view.batchJob.author?.let { simpleUserAccountModelAssembler.toModel(it) },
createdAt = view.batchJob.createdAt.toString(),
activityRevisionId = view.batchJob.activityRevision?.id,
errorMessage = view.errorMessage?.code
)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.tolgee.hateoas.user_account

import io.tolgee.api.v2.controllers.V2UserController
import io.tolgee.dtos.cacheable.UserAccountDto
import io.tolgee.model.UserAccount
import io.tolgee.service.AvatarService
import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport
Expand All @@ -23,4 +24,16 @@ class SimpleUserAccountModelAssembler(
deleted = entity.deletedAt != null
)
}

fun toModel(dto: UserAccountDto): SimpleUserAccountModel {
val avatar = avatarService.getAvatarLinks(dto.avatarHash)

return SimpleUserAccountModel(
id = dto.id,
username = dto.username,
name = dto.name,
avatar = avatar,
deleted = dto.deleted
)
}
}
Loading