|
@@ -0,0 +1,386 @@
|
|
|
+package `in`.ocsf.bee.freigeld.core.demo
|
|
|
+
|
|
|
+import `in`.ocsf.bee.freigeld.core.model.*
|
|
|
+import com.oblac.nomen.Nomen
|
|
|
+import kotlinx.coroutines.CompletableDeferred
|
|
|
+import kotlinx.coroutines.GlobalScope
|
|
|
+import kotlinx.coroutines.launch
|
|
|
+import kotlinx.coroutines.runBlocking
|
|
|
+import org.slf4j.LoggerFactory
|
|
|
+import org.springframework.beans.factory.annotation.Autowired
|
|
|
+import org.springframework.scheduling.annotation.Scheduled
|
|
|
+import org.springframework.stereotype.Service
|
|
|
+import java.util.*
|
|
|
+import java.util.concurrent.CompletableFuture
|
|
|
+import java.util.concurrent.LinkedBlockingQueue
|
|
|
+import java.util.function.Consumer
|
|
|
+import javax.annotation.PostConstruct
|
|
|
+import kotlin.math.min
|
|
|
+import kotlin.math.roundToLong
|
|
|
+
|
|
|
+@Service
|
|
|
+class DemoInMem {
|
|
|
+
|
|
|
+ val personMap = mutableMapOf<UUID, DemoHuman>()
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private lateinit var emitter: GlobalEmitter
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private lateinit var bank: GlobalBank
|
|
|
+
|
|
|
+ private val log = LoggerFactory.getLogger(javaClass)
|
|
|
+
|
|
|
+ @PostConstruct
|
|
|
+ fun init() {
|
|
|
+ emitter.emit(1000, CoinValue.one)
|
|
|
+ emitter.emit(500, CoinValue.three)
|
|
|
+ emitter.emit(200, CoinValue.five)
|
|
|
+ emitter.emit(100, CoinValue.ten)
|
|
|
+ emitter.emit(50, CoinValue.quarter)
|
|
|
+ emitter.emit(10, CoinValue.half)
|
|
|
+ emitter.emit(5, CoinValue.full)
|
|
|
+ bank.exchange(bank.getSelfAccount(), emitter, 1_300_000)
|
|
|
+ (0 until 100).forEach { personMap.computeIfAbsent(UUID.randomUUID()) { id -> DemoHuman(id, Nomen.randomName()) } }
|
|
|
+ personMap.keys.map { bank.addAccount(it) }.forEach {
|
|
|
+ bank.exchange(it, bank.getSelfAccount(), 1_200 + (500 * Math.random()).roundToLong())
|
|
|
+ .thenRun({ log.info("exchange ok") })
|
|
|
+ .exceptionally({ log.error("exchange error"); null })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Scheduled(initialDelay = 10 * 1000L, fixedDelay = 5 * 1000L)
|
|
|
+ fun tick() {
|
|
|
+ log.info("tick")
|
|
|
+ }
|
|
|
+
|
|
|
+ @Scheduled(initialDelay = 60 * 1000L, fixedDelay = 60 * 1000L)
|
|
|
+ fun calc() {
|
|
|
+ emitter.calc()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+fun GlobalBank.getSelfAccount(): BankAccount {
|
|
|
+ return this.getAccounts(this.id).first()
|
|
|
+}
|
|
|
+
|
|
|
+@Service
|
|
|
+class GlobalBank : Bank {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private lateinit var emitter: GlobalEmitter
|
|
|
+
|
|
|
+ val accountMap = mutableMapOf<UUID, DemoAccount>()
|
|
|
+ val personToAccountTable = mutableSetOf<Pair<UUID, UUID>>()
|
|
|
+ val globalQueue: Queue<BankEvent> = LinkedBlockingQueue()
|
|
|
+ val globalFutureMap = mutableMapOf<UUID, CompletableDeferred<Any>>()
|
|
|
+
|
|
|
+ private lateinit var selfId: UUID
|
|
|
+ private lateinit var selfAccount: BankAccount
|
|
|
+
|
|
|
+ private val log = LoggerFactory.getLogger(javaClass)
|
|
|
+
|
|
|
+ init {
|
|
|
+ selfId = UUID.randomUUID()
|
|
|
+ selfAccount = addAccount(selfId)
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun getId(): UUID {
|
|
|
+ return selfId
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun hasPerson(person: UUID): Boolean {
|
|
|
+ return personToAccountTable.any { it.first == person }
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun hasAccount(account: UUID): Boolean {
|
|
|
+ return accountMap.containsKey(account)
|
|
|
+ }
|
|
|
+
|
|
|
+ final override fun addAccount(person: UUID): BankAccount {
|
|
|
+ val account = if (person != selfId) {
|
|
|
+ val accountId = if (!accountMap.containsKey(person)) person else UUID.randomUUID() //first account assign to personId
|
|
|
+ DemoAccount(accountId, person)
|
|
|
+ } else {
|
|
|
+ DemoSelfAccount(selfId)
|
|
|
+ }
|
|
|
+ accountMap[account.id] = account
|
|
|
+ personToAccountTable.add(person to account.id)
|
|
|
+ return account
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun getAccounts(person: UUID): Collection<BankAccount> {
|
|
|
+ return personToAccountTable.filter { it.first == person }.map { accountMap[it.second] }.filterNotNull()
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun exchange(to: BankAccount, from: BankAccount, amount: Long): CompletableFuture<Any> {
|
|
|
+ val e = ExchangeStartEvent(UUID.randomUUID(), to, from, amount)
|
|
|
+ val ret = CompletableFuture<Any>()
|
|
|
+ globalFutureMap.computeIfAbsent(e.id) { id ->
|
|
|
+ val def = CompletableDeferred<Any>()
|
|
|
+ def.invokeOnCompletion { t: Throwable? -> if (t == null) ret.complete(null) else ret.completeExceptionally(t) }
|
|
|
+ def
|
|
|
+ }
|
|
|
+ globalQueue.offer(e)
|
|
|
+ return ret
|
|
|
+ }
|
|
|
+
|
|
|
+ suspend fun pollLater(e: BankEvent) {
|
|
|
+ when (e) {
|
|
|
+ is ExchangeStartEvent -> {
|
|
|
+ if (e.from.overall < e.amount) {
|
|
|
+ globalQueue.offer(ExchangeFailedEvent(UUID.randomUUID(), e, "not enough overall"))
|
|
|
+ } else {
|
|
|
+ val coinsMoreOrExact = e.from.extractMoreOrExact(e.amount)
|
|
|
+ val coinsExactAndCashback = coinsMoreOrExact
|
|
|
+ e.to.accept(coinsExactAndCashback)
|
|
|
+ //e.from.accept(coinsExactAndCashback.second)
|
|
|
+ emitter.free(coinsExactAndCashback)
|
|
|
+ globalQueue.offer(ExchangeSuccessEvent(UUID.randomUUID(), e))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ is ExchangeSuccessEvent -> {
|
|
|
+ globalFutureMap.get(e.parentEvent.id)?.let {
|
|
|
+ globalFutureMap.remove(e.parentEvent.id)
|
|
|
+ it.complete(true)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ is ExchangeFailedEvent -> {
|
|
|
+ globalFutureMap.get(e.parentEvent.id)?.let {
|
|
|
+ globalFutureMap.remove(e.parentEvent.id)
|
|
|
+ it.completeExceptionally(ExchangeFailedException(e, e.reason))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else -> throw IllegalArgumentException("wrong event type ${e.javaClass.name}")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ fun poll(e: BankEvent) = runBlocking {
|
|
|
+ try {
|
|
|
+ pollLater(e)
|
|
|
+ } catch (t: Throwable) {
|
|
|
+ log.error("error in event", t)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ val maxPollCount = 1024
|
|
|
+
|
|
|
+ @PostConstruct
|
|
|
+ fun start() {
|
|
|
+ emitter.listen { e ->
|
|
|
+ log.info("emitter event ${e.javaClass.name}")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Scheduled(initialDelay = 1000L, fixedDelay = 1000L)
|
|
|
+ fun tick() {
|
|
|
+ var pollCount = 0
|
|
|
+ while (globalQueue.isNotEmpty() && pollCount < maxPollCount) {
|
|
|
+ val e = globalQueue.poll()
|
|
|
+ poll(e)
|
|
|
+ pollCount++
|
|
|
+ log.info("poll")
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+abstract class ExchangeBankEvent(val eventId: UUID) : BankEvent {
|
|
|
+
|
|
|
+ override fun getId(): UUID {
|
|
|
+ return eventId
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+class ExchangeSuccessEvent(eventId: UUID, val parentEvent: ExchangeStartEvent) : ExchangeBankEvent(eventId)
|
|
|
+
|
|
|
+class ExchangeFailedEvent(eventId: UUID, val parentEvent: ExchangeStartEvent, val reason: String) : ExchangeBankEvent(eventId)
|
|
|
+
|
|
|
+class ExchangeFailedException(val event: ExchangeBankEvent, message: String = "exchange failed") : Exception(message)
|
|
|
+
|
|
|
+class ExchangeStartEvent(eventId: UUID, val to: BankAccount, val from: BankAccount, val amount: Long) : ExchangeBankEvent(eventId)
|
|
|
+
|
|
|
+@Service
|
|
|
+class DemoInMemEmitter : GlobalEmitter {
|
|
|
+
|
|
|
+ val coinMap = mutableMapOf<UUID, Coin>()
|
|
|
+ val coinExtractedSet = mutableSetOf<UUID>()
|
|
|
+ val coinFreeSet = mutableSetOf<UUID>()
|
|
|
+ val listeners = mutableListOf<Consumer<EmitterEvent>>()
|
|
|
+ var deferred: CompletableDeferred<Any>? = null
|
|
|
+
|
|
|
+ override fun emit(count: Long, value: CoinValue) {
|
|
|
+ (0 until count).forEach { coinMap.computeIfAbsent(UUID.randomUUID()) { id -> DemoCoin(id, value) } }
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun accept(coins: MutableCollection<Coin>?) {
|
|
|
+ TODO("not implemented")
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun extractMoreOrExact(amount: Long): MutableCollection<Coin> {
|
|
|
+ val indoorCoins = coinMap.values.filter { !coinExtractedSet.contains(it.id) }
|
|
|
+ val ret = CoinUtils.mergeCoins(indoorCoins, amount)
|
|
|
+ ret.forEach { coinExtractedSet.add(it.id) }
|
|
|
+ return ret.toMutableList()
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun getOverall(): Long {
|
|
|
+ return coinMap.values.filter { !coinExtractedSet.contains(it.id) }.map { it.current }.sum()
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun listen(fn: Consumer<EmitterEvent>) {
|
|
|
+ listeners.add(fn)
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun free(coin: Coin) {
|
|
|
+ if (coinMap.containsKey(coin.id) && coinExtractedSet.contains(coin.id)) {
|
|
|
+ coinFreeSet.add(coin.id)
|
|
|
+ } else {
|
|
|
+ throw IllegalArgumentException("coin is not free")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private suspend fun calcLater() {
|
|
|
+ coinFreeSet.map { coinMap[it] }.filterNotNull().map { it as DemoCoin }.forEach { it.era = min(maxEraValue, it.era + 1) }
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun calc(): CompletableFuture<Any> {
|
|
|
+ val ret = CompletableFuture<Any>()
|
|
|
+ listeners.forEach { it.accept(EmitterStartRecalculationEvent()) }
|
|
|
+ this.deferred = CompletableDeferred()
|
|
|
+ deferred!!.invokeOnCompletion { t: Throwable? -> if (t == null) ret.complete(null) else ret.completeExceptionally(t) }
|
|
|
+ GlobalScope.launch {
|
|
|
+ try {
|
|
|
+ calcLater()
|
|
|
+ deferred!!.complete(true)
|
|
|
+ listeners.forEach { it.accept(EmitterStopRecalculationEvent()) }
|
|
|
+ } catch (t: Throwable) {
|
|
|
+ deferred!!.completeExceptionally(t)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return ret
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+val maxEraValue = 364
|
|
|
+
|
|
|
+class DemoCoin(val coinId: UUID, val initialValue: CoinValue) : Coin {
|
|
|
+
|
|
|
+ var era = 1
|
|
|
+ var delta: Long = initialValue.amount / maxEraValue
|
|
|
+
|
|
|
+ override fun getId(): UUID = coinId
|
|
|
+
|
|
|
+ override fun getValue(): CoinValue = initialValue
|
|
|
+
|
|
|
+ override fun getCurrent(): Long {
|
|
|
+ return initialValue.amount - ((era - 1) * delta)
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun toString(): String {
|
|
|
+ return "${initialValue.name} jd (${current})"
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+class DemoHuman(val selfId: UUID, name: String) : NaturalPerson {
|
|
|
+
|
|
|
+ override fun getId(): UUID = selfId
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+class DemoSelfAccount(val selfId: UUID) : DemoAccount(selfId, selfId)
|
|
|
+
|
|
|
+open class DemoAccount(val id: UUID, personId: UUID) : BankAccount {
|
|
|
+
|
|
|
+ val internalCoins = mutableMapOf<UUID, Coin>()
|
|
|
+ var _overall: Long? = null
|
|
|
+
|
|
|
+ override fun getOverall(): Long {
|
|
|
+ return if (_overall != null) {
|
|
|
+ _overall
|
|
|
+ } else {
|
|
|
+ _overall = internalCoins.values.map { it.current }.sum()
|
|
|
+ _overall
|
|
|
+ } ?: 0L
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun decOverall(amount: Long) {
|
|
|
+ if (_overall != null) {
|
|
|
+ _overall = _overall!! - amount
|
|
|
+ } else {
|
|
|
+ _overall = -amount
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun incOverall(amount: Long) {
|
|
|
+ if (_overall != null) {
|
|
|
+ _overall = _overall!! + amount
|
|
|
+ } else {
|
|
|
+ _overall = amount
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun accept(coins: MutableCollection<Coin>) {
|
|
|
+ coins.forEach {
|
|
|
+ if (!internalCoins.containsKey(it.id)) {
|
|
|
+ internalCoins.put(it.id, it)
|
|
|
+ incOverall(it.current)
|
|
|
+ } else {
|
|
|
+ throw IllegalArgumentException("same coin is not acceptable")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun extractMoreOrExact(amount: Long): MutableCollection<Coin> {
|
|
|
+ val coins = CoinUtils.mergeCoins(internalCoins.values, amount).toMutableList()
|
|
|
+ coins.forEach {
|
|
|
+ internalCoins.remove(it.id)
|
|
|
+ decOverall(it.current)
|
|
|
+ }
|
|
|
+ return coins
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+class CoinUtils {
|
|
|
+
|
|
|
+ companion object {
|
|
|
+
|
|
|
+ fun mergeCoins(coins: Collection<Coin>, amount: Long): Collection<Coin> {
|
|
|
+ var totalAmount = 0L
|
|
|
+ val sortedCoins = coins.sortedBy { it.current }
|
|
|
+ val position = sortedCoins.binarySearch { it.current.compareTo(amount) }
|
|
|
+ val ret = mutableListOf<Coin>()
|
|
|
+ if (position == 0) {
|
|
|
+ TODO()
|
|
|
+ } else if (position > 0) {
|
|
|
+ val coinL = sortedCoins.get(position)
|
|
|
+ val coinR = sortedCoins.get(position - 1)
|
|
|
+ if (coinL.current == amount) {
|
|
|
+ ret.add(coinL)
|
|
|
+ } else if (coinR.current == amount) {
|
|
|
+ ret.add(coinR)
|
|
|
+ } else {
|
|
|
+ TODO()
|
|
|
+ }
|
|
|
+ } else if (position < 0) {
|
|
|
+ val actualPosition = -(position + 1)
|
|
|
+ if (actualPosition == 0) {
|
|
|
+ TODO()
|
|
|
+ } else if (actualPosition < sortedCoins.size) {
|
|
|
+ TODO()
|
|
|
+ } else {
|
|
|
+ var idx = sortedCoins.size - 1
|
|
|
+ do {
|
|
|
+ val coin = sortedCoins.get(idx)
|
|
|
+ totalAmount = totalAmount + coin.current
|
|
|
+ ret.add(coin)
|
|
|
+ idx--
|
|
|
+ } while (totalAmount < amount)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return ret
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+interface GlobalEmitter : Emitter, BankAccount
|