kpmy 5 years ago
parent
commit
3da7533ce5

+ 37 - 2
pom.xml

@@ -4,14 +4,14 @@
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
 
+    <packaging>jar</packaging>
     <groupId>in.ocsf.bee.freigeld</groupId>
     <artifactId>freigeld-core</artifactId>
     <version>0.0.1-SNAPSHOT</version>
 
     <properties>
         <java.version>1.8</java.version>
-        <maven-surefire-plugin.version>2.21.0</maven-surefire-plugin.version>
-        <kotlin.version>1.3.70</kotlin.version>
+        <kotlin.version>1.3.72</kotlin.version>
         <target>1.8</target>
     </properties>
 
@@ -56,12 +56,39 @@
             <version>1.1.0</version>
         </dependency>
 
+        <dependency>
+            <groupId>org.jetbrains.kotlin</groupId>
+            <artifactId>kotlin-test</artifactId>
+            <version>${kotlin.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <version>5.6.2</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <version>5.6.2</version>
+            <scope>test</scope>
+        </dependency>
+
         <dependency>
             <groupId>com.fasterxml.jackson.module</groupId>
             <artifactId>jackson-module-kotlin</artifactId>
             <version>2.9.4.1</version>
         </dependency>
 
+        <dependency>
+            <groupId>com.oblac</groupId>
+            <artifactId>nomen-est-omen</artifactId>
+            <version>2.0.0</version>
+        </dependency>
+
     </dependencies>
 
     <build>
@@ -71,6 +98,14 @@
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-maven-plugin</artifactId>
             </plugin>
+            <plugin>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>2.22.2</version>
+            </plugin>
+            <plugin>
+                <artifactId>maven-failsafe-plugin</artifactId>
+                <version>2.22.2</version>
+            </plugin>
             <plugin>
                 <groupId>org.jetbrains.kotlin</groupId>
                 <artifactId>kotlin-maven-plugin</artifactId>

+ 18 - 0
src/main/java/in/ocsf/bee/freigeld/core/model/Bank.java

@@ -0,0 +1,18 @@
+package in.ocsf.bee.freigeld.core.model;
+
+import java.util.Collection;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+public interface Bank extends PrivatePerson {
+
+    boolean hasAccount(UUID account);
+
+    boolean hasPerson(UUID person);
+
+    BankAccount addAccount(UUID person);
+
+    Collection<BankAccount> getAccounts(UUID person);
+
+    CompletableFuture<Object> exchange(BankAccount to, BankAccount from, Long amount);
+}

+ 11 - 0
src/main/java/in/ocsf/bee/freigeld/core/model/BankAccount.java

@@ -0,0 +1,11 @@
+package in.ocsf.bee.freigeld.core.model;
+
+import java.util.Collection;
+
+public interface BankAccount {
+    Long getOverall();
+
+    Collection<Coin> extractMoreOrExact(Long amount);
+
+    void accept(Collection<Coin> coins);
+}

+ 7 - 0
src/main/java/in/ocsf/bee/freigeld/core/model/BankEvent.java

@@ -0,0 +1,7 @@
+package in.ocsf.bee.freigeld.core.model;
+
+import java.util.UUID;
+
+public interface BankEvent {
+    UUID getId();
+}

+ 11 - 0
src/main/java/in/ocsf/bee/freigeld/core/model/Coin.java

@@ -0,0 +1,11 @@
+package in.ocsf.bee.freigeld.core.model;
+
+import java.util.UUID;
+
+public interface Coin {
+    UUID getId();
+
+    CoinValue getValue();
+
+    Long getCurrent();
+}

+ 23 - 0
src/main/java/in/ocsf/bee/freigeld/core/model/CoinValue.java

@@ -0,0 +1,23 @@
+package in.ocsf.bee.freigeld.core.model;
+
+public enum CoinValue {
+    one(364),
+    three(one.amount * 3),
+    five(one.amount * 5),
+    ten(one.amount * 10),
+    quarter(one.amount * 25),
+    half(one.amount * 50),
+    full(one.amount * 100),
+    mega(one.amount * 500),
+    internal(one.amount * 1000);
+
+    private final long amount;
+
+    CoinValue(long amount) {
+        this.amount = amount;
+    }
+
+    public long getAmount() {
+        return amount;
+    }
+}

+ 25 - 0
src/main/java/in/ocsf/bee/freigeld/core/model/Emitter.java

@@ -0,0 +1,25 @@
+package in.ocsf.bee.freigeld.core.model;
+
+import java.util.Collection;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+
+public interface Emitter {
+
+    default void emit(long count) {
+        emit(count, CoinValue.one);
+    }
+
+    void emit(long count, CoinValue value);
+
+    default void free(Collection<Coin> coins) {
+        coins.forEach(this::free);
+    }
+
+    void free(Coin coin);
+
+    void listen(Consumer<EmitterEvent> fn);
+
+    CompletableFuture<Object> calc();
+
+}

+ 4 - 0
src/main/java/in/ocsf/bee/freigeld/core/model/EmitterEvent.java

@@ -0,0 +1,4 @@
+package in.ocsf.bee.freigeld.core.model;
+
+public interface EmitterEvent {
+}

+ 4 - 0
src/main/java/in/ocsf/bee/freigeld/core/model/EmitterStartRecalculationEvent.java

@@ -0,0 +1,4 @@
+package in.ocsf.bee.freigeld.core.model;
+
+public class EmitterStartRecalculationEvent implements EmitterEvent {
+}

+ 4 - 0
src/main/java/in/ocsf/bee/freigeld/core/model/EmitterStopRecalculationEvent.java

@@ -0,0 +1,4 @@
+package in.ocsf.bee.freigeld.core.model;
+
+public class EmitterStopRecalculationEvent implements EmitterEvent {
+}

+ 4 - 0
src/main/java/in/ocsf/bee/freigeld/core/model/NaturalPerson.java

@@ -0,0 +1,4 @@
+package in.ocsf.bee.freigeld.core.model;
+
+public interface NaturalPerson extends Person {
+}

+ 3 - 0
src/main/java/in/ocsf/bee/freigeld/core/model/Person.java

@@ -1,4 +1,7 @@
 package in.ocsf.bee.freigeld.core.model;
 
+import java.util.UUID;
+
 public interface Person {
+    UUID getId();
 }

+ 4 - 0
src/main/java/in/ocsf/bee/freigeld/core/model/PrivatePerson.java

@@ -0,0 +1,4 @@
+package in.ocsf.bee.freigeld.core.model;
+
+public interface PrivatePerson extends Person {
+}

+ 386 - 0
src/main/kotlin/in/ocsf/bee/freigeld/core/demo/DemoInMem.kt

@@ -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

+ 8 - 0
src/test/kotlin/`in`/ocsf/bee/freigeld/core/demo/CoinUtilsTest.kt

@@ -0,0 +1,8 @@
+package `in`.ocsf.bee.freigeld.core.demo
+
+import org.junit.jupiter.api.Test
+
+@Test
+fun `test coin amount`() {
+
+}