Publish message into rabbitmq on create article

Create Redis and Rabbit in docker-compose
This commit is contained in:
2020-02-22 02:26:52 +01:00
parent 471013984c
commit af33ed9ec3
12 changed files with 206 additions and 4 deletions

11
.env
View File

@@ -4,6 +4,7 @@ DATABASE_URL=jdbc:postgresql:dc-project
APP_PORT=8080 APP_PORT=8080
OPENAPI_PORT=8181 OPENAPI_PORT=8181
SONARQUBE_PORT=9000
ELASTIC_REST=9200 ELASTIC_REST=9200
ELASTIC_NODES=9300 ELASTIC_NODES=9300
@@ -13,4 +14,12 @@ DB_HOST=db
DB_PORT=5432 DB_PORT=5432
DB_NAME=dc-project DB_NAME=dc-project
DB_USER=dc-project DB_USER=dc-project
DB_PWD=dc-project DB_PWD=dc-project
REDIS_PORT=6379
REDIS_CONNECTION=redis://localhost:6379
REDIS_COMMANDER_PORT=8081
RABBITMQ_PORT=5672
RABBITMQ_CONNECTION=amqp://localhost:5672
RABBITMQ_MANAGEMENT_PORT=15672

View File

@@ -3,6 +3,7 @@ import org.owasp.dependencycheck.reporting.ReportGenerator
val ktor_version: String by project val ktor_version: String by project
val kotlin_version: String by project val kotlin_version: String by project
val coroutinesVersion: String by project
val logback_version: String by project val logback_version: String by project
val koinVersion: String by project val koinVersion: String by project
val postgresjson_version: String by project val postgresjson_version: String by project
@@ -66,6 +67,8 @@ repositories {
dependencies { dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:${coroutinesVersion}")
implementation("io.ktor:ktor-server-jetty:$ktor_version") implementation("io.ktor:ktor-server-jetty:$ktor_version")
implementation("ch.qos.logback:logback-classic:$logback_version") implementation("ch.qos.logback:logback-classic:$logback_version")
implementation("io.ktor:ktor-server-core:$ktor_version") implementation("io.ktor:ktor-server-core:$ktor_version")
@@ -83,6 +86,8 @@ dependencies {
implementation("com.github.jasync-sql:jasync-postgresql:1.0.7") implementation("com.github.jasync-sql:jasync-postgresql:1.0.7")
implementation("fr.postgresjson:postgresjson:$postgresjson_version") implementation("fr.postgresjson:postgresjson:$postgresjson_version")
implementation("com.sendgrid:sendgrid-java:4.4.1") implementation("com.sendgrid:sendgrid-java:4.4.1")
implementation("io.lettuce:lettuce-core:5.2.2.RELEASE")
implementation("com.rabbitmq:amqp-client:5.8.0")
testImplementation("io.ktor:ktor-server-tests:$ktor_version") testImplementation("io.ktor:ktor-server-tests:$ktor_version")
testImplementation("io.ktor:ktor-client-mock:$ktor_version") testImplementation("io.ktor:ktor-client-mock:$ktor_version")

View File

@@ -7,7 +7,7 @@ services:
image: sonarqube image: sonarqube
restart: always restart: always
ports: ports:
- 9000:9000 - ${SONARQUBE_PORT}:9000
openapi: openapi:
container_name: openapi_${NAME} container_name: openapi_${NAME}
@@ -18,6 +18,21 @@ services:
environment: environment:
URL: "http://localhost:8080" URL: "http://localhost:8080"
rabbitmq:
container_name: rabbitmq_${NAME}
image: rabbitmq:management-alpine
restart: always
ports:
- ${RABBITMQ_PORT}:5672
- ${RABBITMQ_MANAGEMENT_PORT}:15672
redis:
container_name: redis_${NAME}
image: redis:6.0-rc-alpine
restart: always
ports:
- ${REDIS_PORT}:6379
app: app:
container_name: app_${NAME} container_name: app_${NAME}
build: build:
@@ -29,9 +44,13 @@ services:
environment: environment:
DB_HOST: db DB_HOST: db
SEND_GRID_KEY: ${SEND_GRID_KEY} SEND_GRID_KEY: ${SEND_GRID_KEY}
REDIS_CONNECTION: redis://redis:6379
RABBITMQ_CONNECTION: amqp://rabbitmq:5671
depends_on: depends_on:
- elasticsearch - elasticsearch
- db - db
- redis
- rabbitmq
elasticsearch: elasticsearch:
container_name: elasticsearch_${NAME} container_name: elasticsearch_${NAME}

View File

@@ -1,6 +1,7 @@
ktor_version=1.2.2 ktor_version=1.2.2
kotlin.code.style=official kotlin.code.style=official
kotlin_version=1.3.40 kotlin_version=1.3.40
coroutinesVersion=1.3.3
logback_version=1.2.1 logback_version=1.2.1
postgresjson_version=0.1 postgresjson_version=0.1
koinVersion=2.0.1 koinVersion=2.0.1

View File

@@ -7,8 +7,12 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategy
import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.datatype.joda.JodaModule import com.fasterxml.jackson.datatype.joda.JodaModule
import com.github.jasync.sql.db.postgresql.exceptions.GenericDatabaseException import com.github.jasync.sql.db.postgresql.exceptions.GenericDatabaseException
import com.rabbitmq.client.ConnectionFactory
import fr.dcproject.Env.PROD import fr.dcproject.Env.PROD
import fr.dcproject.entity.* import fr.dcproject.entity.*
import fr.dcproject.event.EntityEvent
import fr.dcproject.event.EventNotification
import fr.dcproject.event.publisher.Publisher
import fr.dcproject.routes.* import fr.dcproject.routes.*
import fr.dcproject.security.voter.* import fr.dcproject.security.voter.*
import fr.postgresjson.migration.Migrations import fr.postgresjson.migration.Migrations
@@ -27,7 +31,9 @@ import io.ktor.jackson.jackson
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Locations import io.ktor.locations.Locations
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.response.respondText
import io.ktor.routing.Routing import io.ktor.routing.Routing
import io.ktor.routing.get
import io.ktor.util.KtorExperimentalAPI import io.ktor.util.KtorExperimentalAPI
import org.eclipse.jetty.util.log.Slf4jLog import org.eclipse.jetty.util.log.Slf4jLog
import org.koin.ktor.ext.Koin import org.koin.ktor.ext.Koin
@@ -148,6 +154,25 @@ fun Application.module(env: Env = PROD) {
) )
} }
install(EventNotification) {
/* Config Rabbit */
val exchangeName = config.exchangeNotificationName
get<ConnectionFactory>().newConnection().use { connection -> connection.createChannel().use { channel ->
channel.queueDeclare("sse", true, false, false, null)
channel.queueDeclare("email", true, false, false, null)
channel.exchangeDeclare(exchangeName, "direct")
channel.queueBind("sse", exchangeName, "")
channel.queueBind("email", exchangeName, "")
}}
/* Declare publisher on event */
val publisher = Publisher(get(), get())
subscribe(EntityEvent.Type.UPDATE_ARTICLE.event) {
println("Article is updated ${it.target.id}")
publisher.publish(it)
}
}
install(Authentication) { install(Authentication) {
/** /**
* Setup the JWT authentication to be used in [Routing]. * Setup the JWT authentication to be used in [Routing].
@@ -200,6 +225,20 @@ fun Application.module(env: Env = PROD) {
opinionArticle(get()) opinionArticle(get())
opinionChoice(get()) opinionChoice(get())
definition() definition()
get("/sse") {
// environment.monitor.raise(EntityEvent.Type.UPDATE_ARTICLE.event, ArticleUpdate(ArticleRef()))
// val redis = this@authenticate.getKoin().get<RedisReactiveCommands<String, String>>()
// redis.set("key", "test").awaitSingle()
// redis.lpush("list", "test2").asFlow().map {
// println(it)
// }.collect()
// redis.get("key").asFlow().collect { println(it) }
// redis.rpop("list").asFlow().collect {
// println(it)
// call.respondText { it }
// }
call.respondText("OK")
}
} }
} }

View File

@@ -26,7 +26,9 @@ class Config {
var username: String = config.getString("db.username") var username: String = config.getString("db.username")
var password: String = config.getString("db.password") var password: String = config.getString("db.password")
val port: Int = config.getInt("db.port") val port: Int = config.getInt("db.port")
val redis: String = config.getString("redis.connection")
val rabbitmq: String = config.getString("rabbitmq.connection")
val exchangeNotificationName = "notification"
val sendGridKey: String = config.getString("mail.sendGrid.key") val sendGridKey: String = config.getString("mail.sendGrid.key")
} }

View File

@@ -1,11 +1,21 @@
package fr.dcproject package fr.dcproject
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.PropertyNamingStrategy
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.datatype.joda.JodaModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.rabbitmq.client.ConnectionFactory
import fr.dcproject.messages.Mailer import fr.dcproject.messages.Mailer
import fr.dcproject.messages.SsoManager import fr.dcproject.messages.SsoManager
import fr.postgresjson.connexion.Connection import fr.postgresjson.connexion.Connection
import fr.postgresjson.connexion.Requester import fr.postgresjson.connexion.Requester
import fr.postgresjson.migration.Migrations import fr.postgresjson.migration.Migrations
import io.ktor.util.KtorExperimentalAPI import io.ktor.util.KtorExperimentalAPI
import io.lettuce.core.RedisClient
import io.lettuce.core.api.reactive.RedisReactiveCommands
import org.koin.dsl.module import org.koin.dsl.module
import fr.dcproject.repository.Article as ArticleRepository import fr.dcproject.repository.Article as ArticleRepository
import fr.dcproject.repository.Citizen as CitizenRepository import fr.dcproject.repository.Citizen as CitizenRepository
@@ -39,6 +49,25 @@ val Module = module {
) )
} }
single<RedisReactiveCommands<String, String>> {
RedisClient.create(config.redis).connect()?.reactive() ?: error("Unable to connect to redis")
}
single<ConnectionFactory> {
ConnectionFactory().apply { setUri(config.rabbitmq) }
}
single<ObjectMapper> {
jacksonObjectMapper().apply {
registerModule(SimpleModule())
propertyNamingStrategy = PropertyNamingStrategy.SNAKE_CASE
registerModule(JodaModule())
disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true)
}
}
single { single {
Requester.RequesterFactory( Requester.RequesterFactory(
connection = get(), connection = get(),

View File

@@ -0,0 +1,54 @@
package fr.dcproject.event
import fr.dcproject.entity.Article
import fr.postgresjson.entity.immutable.UuidEntity
import io.ktor.application.*
import io.ktor.util.AttributeKey
import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.DisposableHandle
import org.joda.time.DateTime
abstract class Notification(
val type: String,
val createdAt: DateTime = DateTime.now()
)
abstract class EntityEvent(
val target: UuidEntity,
type: String,
val action: String
) : Notification(type) {
enum class Type(val event: EventDefinition<ArticleUpdate>) {
UPDATE_ARTICLE(EventDefinition<ArticleUpdate>())
}
}
class ArticleUpdate(
target: Article
) : EntityEvent(target, "article", "update")
/**
* Installation Class
*/
class EventNotification {
class Configuration(private val monitor: ApplicationEvents) {
private val subscribers = mutableListOf<DisposableHandle>()
fun <T: Notification> subscribe(definition: EventDefinition<T>, handler: EventHandler<T>): DisposableHandle {
return monitor.subscribe(definition, handler).also {
subscribers.add(it)
}
}
}
companion object Feature : ApplicationFeature<Application, Configuration, EventNotification> {
override val key = AttributeKey<EventNotification>("EventNotification")
@KtorExperimentalAPI
override fun install(
pipeline: Application,
configure: Configuration.() -> Unit
): EventNotification {
Configuration(pipeline.environment.monitor).apply(configure)
return EventNotification()
}
}
}

View File

@@ -0,0 +1,30 @@
package fr.dcproject.event.publisher
import com.fasterxml.jackson.databind.ObjectMapper
import com.rabbitmq.client.ConnectionFactory
import fr.dcproject.config
import fr.dcproject.event.EntityEvent
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class Publisher(
private val mapper: ObjectMapper,
private val factory: ConnectionFactory,
private val logger: Logger = LoggerFactory.getLogger(Publisher::class.qualifiedName)
) {
fun <T: EntityEvent>publish(it: T): Job {
return GlobalScope.launch {
factory.newConnection().use { connection -> connection.createChannel().use { channel ->
channel.basicPublish(config.exchangeNotificationName, "", null, it.serialize().toByteArray())
logger.debug("Publish message ${it.target.id}")
} }
}
}
private fun EntityEvent.serialize(): String {
return mapper.writeValueAsString(this) ?: error("Unable tu serialize message")
}
}

View File

@@ -1,11 +1,14 @@
package fr.dcproject.routes package fr.dcproject.routes
import fr.dcproject.citizen import fr.dcproject.citizen
import fr.dcproject.event.ArticleUpdate
import fr.dcproject.event.EntityEvent
import fr.dcproject.repository.Article.Filter import fr.dcproject.repository.Article.Filter
import fr.dcproject.security.voter.ArticleVoter.Action.CREATE import fr.dcproject.security.voter.ArticleVoter.Action.CREATE
import fr.dcproject.security.voter.ArticleVoter.Action.VIEW import fr.dcproject.security.voter.ArticleVoter.Action.VIEW
import fr.dcproject.security.voter.assertCan import fr.dcproject.security.voter.assertCan
import fr.postgresjson.repository.RepositoryI import fr.postgresjson.repository.RepositoryI
import io.ktor.application.application
import io.ktor.application.call import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location import io.ktor.locations.Location
@@ -83,6 +86,7 @@ fun Route.article(repo: ArticleRepository) {
assertCan(CREATE, article) assertCan(CREATE, article)
repo.upsert(article) repo.upsert(article)
application.environment.monitor.raise(EntityEvent.Type.UPDATE_ARTICLE.event, ArticleUpdate(article))
call.respond(article) call.respond(article)
} }

View File

@@ -22,6 +22,16 @@ db {
port = 5432 port = 5432
} }
redis {
connection = "redis://localhost:6379"
connection = ${?REDIS_CONNECTION}
}
rabbitmq {
connection = "amqp://localhost:5672"
connection = ${?RABBITMQ_CONNECTION}
}
mail { mail {
sendGrid { sendGrid {
key = ${?SEND_GRID_KEY} key = ${?SEND_GRID_KEY}

View File

@@ -1132,7 +1132,7 @@ components:
required: true required: true
example: example:
Lorem upsum... Lorem upsum...
descritption: description:
type: string type: string
required: true required: true
example: example: