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

fix: Batch operation spams slack #2781

Merged
merged 24 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
34bedf6
fix: Batch slack spamming > not working test
JanCizmar Dec 14, 2024
aec8e20
chore: Performance & refactoring
JanCizmar Feb 12, 2025
c5d883f
chore: Fix the failing tests & remove sql logging
JanCizmar Feb 13, 2025
69e56de
fix: Fix the spamming > batch operation automation is not executed un…
JanCizmar Feb 14, 2025
a5a1e3c
chore: fix correct test classes are run, remove the need to recreate …
JanCizmar Feb 14, 2025
6c96e53
chore: fix ktlint
JanCizmar Feb 14, 2025
bfc3bc6
fix: spamming on TRANSLATION_DATA_MODIFICATION operation & tests
JanCizmar Feb 14, 2025
1df39cb
Slack: cleanup sentry exception logging
fprochazka Feb 17, 2025
6de7921
Slack: rename DTOs
fprochazka Feb 17, 2025
ce77b17
Slack: separate logic into slackcommands and notifications
fprochazka Feb 17, 2025
49a957c
Slack: separate logic into slackcommands and notifications
fprochazka Feb 17, 2025
4b74e6d
Slack: separate logic into slackcommands and notifications
fprochazka Feb 17, 2025
d1a5203
Slack: separate logic into slackcommands and notifications
fprochazka Feb 17, 2025
447de75
Slack: separate logic into slackcommands and notifications
fprochazka Feb 17, 2025
1ec6839
Slack: separate logic into slackcommands and notifications
fprochazka Feb 17, 2025
914bd0b
Slack: fix missing class prefix
fprochazka Feb 17, 2025
67b2419
Slack: extract SlackChannelMessagesOperations
fprochazka Feb 17, 2025
cb85b66
Slack: get rid of SlackExecutorHelper
fprochazka Feb 17, 2025
f818e9b
Slack: SlackAutomationMessageSender refactoring
fprochazka Feb 17, 2025
4357fb5
Slack: extract message factories
fprochazka Feb 17, 2025
b032e5e
Slack: fix message updating
fprochazka Feb 17, 2025
d338127
fix: The explicit message sending bug and tests
JanCizmar Feb 19, 2025
575c22d
fix: Fix tests & little refactor
JanCizmar Feb 19, 2025
4ae606c
fix: Fix tests
JanCizmar Feb 19, 2025
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,13 +6,15 @@ import io.tolgee.activity.data.RevisionType
import io.tolgee.activity.projectActivity.ModificationsByRevisionsProvider
import io.tolgee.activity.projectActivity.ProjectActivityViewByPageableProvider
import io.tolgee.activity.projectActivity.ProjectActivityViewByRevisionProvider
import io.tolgee.dtos.queryResults.ActivityRevisionInfo
import io.tolgee.dtos.queryResults.TranslationHistoryView
import io.tolgee.events.OnProjectActivityStoredEvent
import io.tolgee.model.activity.ActivityModifiedEntity
import io.tolgee.model.activity.ActivityRevision
import io.tolgee.model.views.activity.ModifiedEntityView
import io.tolgee.model.views.activity.ProjectActivityView
import io.tolgee.repository.activity.ActivityModifiedEntityRepository
import io.tolgee.repository.activity.ActivityRevisionRepository
import io.tolgee.util.Logging
import io.tolgee.util.flushAndClear
import jakarta.persistence.EntityManager
Expand All @@ -31,6 +33,7 @@ class ActivityService(
private val activityModifiedEntityRepository: ActivityModifiedEntityRepository,
private val objectMapper: ObjectMapper,
private val jdbcTemplate: JdbcTemplate,
private val activityRevisionRepository: ActivityRevisionRepository,
) : Logging {
@Transactional
fun storeActivityData(
Expand Down Expand Up @@ -161,6 +164,10 @@ class ActivityService(
return provider.get()
}

fun findActivityRevisionInfo(id: Long): ActivityRevisionInfo? {
return activityRevisionRepository.findInfo(id)
}

private fun ActivityRevision.shouldSaveWithoutModification(): Boolean {
val type = this.type ?: return true
return type.saveWithoutModification
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ class ApplicationBatchJobRunner(
isRunning = false
}

/**
* Whether the batch job queue is empty and there are no running job
*/
val settled
get() = batchJobConcurrentLauncher.runningJobs.isEmpty() && batchJobChunkExecutionQueue.isEmpty()

@PreDestroy
fun preDestroy() {
stop()
Expand All @@ -69,4 +75,9 @@ class ApplicationBatchJobRunner(
private val batchJobConcurrentLauncher: BatchJobConcurrentLauncher by lazy {
applicationContext.getBean(BatchJobConcurrentLauncher::class.java)
}

// We want to keep the same instance of BatchJobChunkExecutionQueue for all instances of ApplicationBatchJobRunner
private val batchJobChunkExecutionQueue: BatchJobChunkExecutionQueue by lazy {
applicationContext.getBean(BatchJobChunkExecutionQueue::class.java)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.tolgee.activity.ActivityHolder
import io.tolgee.batch.data.BatchJobDto
import io.tolgee.batch.events.OnBatchJobCancelled
import io.tolgee.batch.events.OnBatchJobFailed
import io.tolgee.batch.events.OnBatchJobFinalized
import io.tolgee.batch.events.OnBatchJobSucceeded
import io.tolgee.batch.state.BatchJobStateProvider
import io.tolgee.fixtures.WaitNotSatisfiedException
Expand All @@ -15,6 +16,7 @@ import io.tolgee.util.logger
import jakarta.persistence.EntityManager
import org.hibernate.query.TypedParameterValue
import org.hibernate.type.StandardBasicTypes
import org.springframework.context.ApplicationEventPublisher
import org.springframework.context.event.EventListener
import org.springframework.stereotype.Component

Expand All @@ -23,6 +25,7 @@ class BatchJobActivityFinalizer(
private val entityManager: EntityManager,
private val activityHolder: ActivityHolder,
private val batchJobStateProvider: BatchJobStateProvider,
private val applicationEventPublisher: ApplicationEventPublisher,
) : Logging {
@EventListener(OnBatchJobSucceeded::class)
fun finalizeActivityWhenJobSucceeded(event: OnBatchJobSucceeded) {
Expand Down Expand Up @@ -55,6 +58,7 @@ class BatchJobActivityFinalizer(
mergeModifiedEntities(activityRevisionIdToMergeInto, revisionIds)
deleteUnusedRevisions(revisionIds)
setJobIdAndAuthorIdToRevision(activityRevisionIdToMergeInto, job)
applicationEventPublisher.publishEvent(OnBatchJobFinalized(job, activityRevisionIdToMergeInto))
} catch (e: Exception) {
Sentry.captureException(e)
throw CannotFinalizeActivityException(e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ import io.tolgee.batch.data.BatchJobDto

data class OnBatchJobFinalized(
override val job: BatchJobDto,
val activityRevisionId: Long,
) : OnBatchJobCompleted
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.tolgee.component.automations

import io.tolgee.activity.ActivityService
import io.tolgee.batch.events.OnBatchJobFinalized
import io.tolgee.events.OnProjectActivityStoredEvent
import io.tolgee.model.Language
import io.tolgee.model.Project
Expand All @@ -8,32 +10,50 @@ import io.tolgee.model.translation.Translation
import org.springframework.context.event.EventListener
import org.springframework.scheduling.annotation.Async
import org.springframework.stereotype.Component
import org.springframework.transaction.event.TransactionalEventListener

@Component
class AutomationActivityListener(
private val automationsBatchJobCreator: AutomationsBatchJobCreator,
private val activityService: ActivityService,
) {
@EventListener
@Async
fun listen(event: OnProjectActivityStoredEvent) {
executeActivityAutomationIfShould(event)
executeTranslationDataModificationAutomationIfShould(event)
}

private fun executeTranslationDataModificationAutomationIfShould(event: OnProjectActivityStoredEvent) {
val activityType = event.activityRevision.type ?: return
val projectId = event.activityRevision.projectId ?: return
if (event.activityRevision.modifiedEntities.isEmpty()) {
return
}

// don't run automations on batch jobs that haven't been merged yet
// this would lead to spamming
if (event.activityRevision.batchJobChunkExecution != null) {
return
}

automationsBatchJobCreator.executeActivityAutomation(projectId, activityType, event.activityRevision.id)

if (isTranslationDataModification(event)) {
automationsBatchJobCreator.executeTranslationDataModificationAutomation(projectId, event.activityRevision.id)
}
}

private fun executeActivityAutomationIfShould(event: OnProjectActivityStoredEvent) {
val activityType = event.activityRevision.type ?: return
val projectId = event.activityRevision.projectId ?: return
if (event.activityRevision.modifiedEntities.isEmpty()) {
@TransactionalEventListener
@Async
fun listen(event: OnBatchJobFinalized) {
val revision = activityService.findActivityRevisionInfo(event.activityRevisionId) ?: return
if (revision.modifiedEntityCount == 0) {
return
}
automationsBatchJobCreator.executeActivityAutomation(projectId, activityType, event.activityRevision.id)

val projectId = revision.projectId ?: return

automationsBatchJobCreator.executeActivityAutomation(projectId, revision.type, revision.id)

if (revision.isTranslationModification) {
automationsBatchJobCreator.executeTranslationDataModificationAutomation(projectId, revision.id)
}
}

private fun isTranslationDataModification(event: OnProjectActivityStoredEvent): Boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package io.tolgee.development.testDataBuilder.data

import io.tolgee.development.testDataBuilder.builders.ProjectBuilder
import io.tolgee.development.testDataBuilder.builders.TestDataBuilder
import io.tolgee.development.testDataBuilder.builders.UserAccountBuilder
import io.tolgee.development.testDataBuilder.builders.*
import io.tolgee.model.Language
import io.tolgee.model.Organization
import io.tolgee.model.UserAccount
Expand Down Expand Up @@ -148,4 +146,12 @@ class SlackTestData {
)
}.self
}

fun add10Keys(): List<Key> {
return (1..10).map {
projectBuilder.addKey("key$it").build {
addTranslation("en", "Hello")
}.self
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.tolgee.dtos.queryResults

import io.tolgee.activity.data.ActivityType

data class ActivityRevisionInfo(
val id: Long,
val projectId: Long?,
val modifiedEntityCount: Int,
val type: ActivityType,
val isTranslationModification: Boolean,
)
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class SlackMessageInfo(
) : StandardAuditModel() {
@Enumerated(EnumType.STRING)
@ColumnDefault("GLOBAL")
var subscriptionType: SubscriptionType = SubscriptionType.GLOBAL
var subscriptionType: SlackSubscriptionType = SlackSubscriptionType.GLOBAL

var authorContext: String = "" // string containing author name, event and date
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package io.tolgee.model.slackIntegration

enum class SubscriptionType {
enum class SlackSubscriptionType {
GLOBAL,
}
13 changes: 0 additions & 13 deletions backend/data/src/main/kotlin/io/tolgee/repository/KeyRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -246,17 +246,4 @@ interface KeyRepository : JpaRepository<Key, Long> {
""",
)
fun getViewsByKeyIds(ids: List<Long>): List<KeyView>

@Query(
"""
SELECT k
FROM Key k
JOIN k.translations t
WHERE k.project.id = :projectId AND t.id = :translationId
""",
)
fun searchKey(
projectId: Long,
translationId: Long,
): Optional<Key>
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.tolgee.repository.activity

import io.tolgee.activity.data.ActivityType
import io.tolgee.dtos.queryResults.ActivityRevisionInfo
import io.tolgee.model.activity.ActivityRevision
import org.springframework.context.annotation.Lazy
import org.springframework.data.domain.Page
Expand Down Expand Up @@ -59,4 +60,26 @@ interface ActivityRevisionRepository : JpaRepository<ActivityRevision, Long> {
projectId: Long?,
revisionId: Long,
): ActivityRevision?

@Query(
"""
select new io.tolgee.dtos.queryResults.ActivityRevisionInfo(
ar.id,
ar.projectId,
size(ar.modifiedEntities),
ar.type,
case
when exists (
select 1 from ActivityModifiedEntity me
where me.activityRevision = ar
and me.entityClass in ('Translation', 'Key', 'Project', 'Language')
) then true
else false
end
)
from ActivityRevision ar
where ar.id = :id
""",
)
fun findInfo(id: Long): ActivityRevisionInfo?
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,6 @@ class KeyService(
return keyRepository.getByNameAndNamespace(projectId, name, namespace)
}

fun find(
projectId: Long,
translationId: Long,
): Optional<Key> {
return keyRepository.searchKey(projectId, translationId)
}

fun get(id: Long): Key {
return keyRepository.findByIdOrNull(id) ?: throw NotFoundException(Message.KEY_NOT_FOUND)
}
Expand Down
10 changes: 0 additions & 10 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -139,16 +139,6 @@ project(':server-app').afterEvaluate {
}
}

// Tests of `ee-test` collide with tests of `server-app`, therefore we run them sequentially.
// It would be great to make the tests not collide and therefore this block of code can be deleted.
gradle.projectsEvaluated {
project(':server-app').tasks.withType(Test) {
if (gradle.ext.eeAppDirectoryExists) {
dependsOn project(':ee-test').tasks.withType(Test)
}
}
}

ktlint {
debug = true
verbose = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import com.fasterxml.jackson.module.kotlin.readValue
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import io.tolgee.dtos.request.slack.SlackEventDto
import io.tolgee.ee.component.slackIntegration.SlackExecutor
import io.tolgee.ee.component.slackIntegration.SlackHelpBlocksProvider
import io.tolgee.ee.component.slackIntegration.SlackRequestValidation
import io.tolgee.ee.component.slackIntegration.SlackChannelMessagesOperations
import io.tolgee.ee.component.slackIntegration.slashcommand.*
import io.tolgee.ee.service.slackIntegration.OrganizationSlackWorkspaceService
import io.tolgee.exceptions.SlackErrorException
import io.tolgee.util.Logging
Expand All @@ -24,8 +23,8 @@ import java.net.URLDecoder
class SlackEventsController(
private val objectMapper: ObjectMapper,
private val slackRequestValidation: SlackRequestValidation,
private val slackHelpBlocksProvider: SlackHelpBlocksProvider,
private val slackExecutor: SlackExecutor,
private val slackSlackCommandBlocksProvider: SlackSlackCommandBlocksProvider,
private val slackChannelMessagesOperations: SlackChannelMessagesOperations,
private val organizationSlackWorkspaceService: OrganizationSlackWorkspaceService,
) : Logging {
@Suppress("UastIncorrectHttpHeaderInspection")
Expand All @@ -50,22 +49,26 @@ class SlackEventsController(
event.actions.forEach {
when (it.value) {
"help_btn" ->
slackExecutor.sendBlocksMessage(
event.team.id,
slackChannelMessagesOperations.sendMessage(
SlackChannelMessagesOperations.SlackTeamId(event.team.id),
event.channel.id,
slackHelpBlocksProvider.getHelpBlocks(),
slackSlackCommandBlocksProvider.getHelpBlocks(),
)

"help_advanced_subscribe_btn" ->
slackExecutor.sendBlocksMessage(
event.team.id,
slackChannelMessagesOperations.sendMessage(
SlackChannelMessagesOperations.SlackTeamId(event.team.id),
event.channel.id,
slackHelpBlocksProvider.getAdvancedSubscriptionHelpBlocks(),
slackSlackCommandBlocksProvider.getAdvancedSubscriptionHelpBlocks(),
)
}
}
} catch (e: SlackErrorException) {
slackExecutor.sendBlocksMessage(event.team.id, event.channel.id, e.blocks)
slackChannelMessagesOperations.sendMessage(
SlackChannelMessagesOperations.SlackTeamId(event.team.id),
event.channel.id,
e.blocks,
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import io.tolgee.component.enabledFeaturesProvider.EnabledFeaturesProvider
import io.tolgee.configuration.tolgee.SlackProperties
import io.tolgee.constants.Feature
import io.tolgee.constants.Message
import io.tolgee.ee.component.slackIntegration.SlackExecutor
import io.tolgee.ee.component.slackIntegration.SlackChannelMessagesOperations
import io.tolgee.ee.component.slackIntegration.SlackUserLoginUrlProvider
import io.tolgee.ee.component.slackIntegration.notification.SlackNotificationBlocksProvider
import io.tolgee.ee.service.slackIntegration.OrganizationSlackWorkspaceService
import io.tolgee.ee.service.slackIntegration.SlackUserConnectionService
import io.tolgee.exceptions.BadRequestException
Expand All @@ -31,9 +32,10 @@ import org.springframework.web.bind.annotation.*
)
class SlackLoginController(
private val slackUserConnectionService: SlackUserConnectionService,
private val slackExecutor: SlackExecutor,
private val authenticationFacade: AuthenticationFacade,
private val slackUserLoginUrlProvider: SlackUserLoginUrlProvider,
private val slackNotificationBlocksProvider: SlackNotificationBlocksProvider,
private val slackOperations: SlackChannelMessagesOperations,
private val slackClient: Slack,
private val slackProperties: SlackProperties,
private val slackWorkspaceService: OrganizationSlackWorkspaceService,
Expand All @@ -60,7 +62,13 @@ class SlackLoginController(
decrypted.slackUserId,
decrypted.slackTeamId,
)
slackExecutor.sendUserLoginSuccessMessage(token, decrypted)

slackOperations.sendEphemeralMessage(
SlackChannelMessagesOperations.SlackWorkspaceToken(token),
decrypted.slackChannelId,
decrypted.slackUserId,
slackNotificationBlocksProvider.getUserLoginSuccessBlocks(),
)
}

@GetMapping("/user-login-info")
Expand Down
Loading
Loading