Integrasi gaya BPM

Integrasi gaya BPM

Hai, Habr!

Perusahaan kami berspesialisasi dalam pengembangan solusi perangkat lunak kelas ERP, di mana bagian terbesarnya ditempati oleh sistem transaksional dengan sejumlah besar logika bisnis dan alur kerja ala EDMS. Versi modern dari produk kami didasarkan pada teknologi JavaEE, tetapi kami juga secara aktif bereksperimen dengan layanan mikro. Salah satu area yang paling bermasalah dari solusi tersebut adalah integrasi berbagai subsistem yang terkait dengan domain yang berdekatan. Tugas integrasi selalu membuat kami sangat pusing, terlepas dari gaya arsitektur, tumpukan teknologi, dan kerangka kerja yang kami gunakan, tetapi baru-baru ini ada kemajuan dalam menyelesaikan masalah seperti itu.

Dalam artikel yang menarik perhatian Anda, saya akan berbicara tentang pengalaman dan penelitian arsitektur NPO Krista di area yang ditentukan. Kami juga akan mempertimbangkan contoh solusi sederhana untuk masalah integrasi dari sudut pandang pengembang aplikasi dan mencari tahu apa yang tersembunyi di balik kesederhanaan ini.

еймер

Solusi arsitektur dan teknis yang dijelaskan dalam artikel ini saya tawarkan berdasarkan pengalaman pribadi dalam konteks tugas tertentu. Solusi ini tidak diklaim universal dan mungkin tidak optimal dalam kondisi penggunaan lainnya.

Apa hubungannya BPM dengan itu?

Untuk menjawab pertanyaan ini, kita perlu menggali sedikit ke dalam spesifikasi masalah terapan dari solusi kita. Bagian utama dari logika bisnis dalam sistem transaksi tipikal kami adalah memasukkan data ke dalam database melalui antarmuka pengguna, secara manual dan otomatis memeriksa data ini, meneruskannya melalui beberapa alur kerja, menerbitkannya ke sistem lain/database/arsip analitis, menghasilkan laporan. Dengan demikian, fungsi utama sistem bagi pelanggan adalah otomatisasi proses bisnis internal mereka.

Untuk kenyamanan, kami menggunakan istilah "dokumen" dalam komunikasi sebagai beberapa abstraksi kumpulan data, disatukan oleh kunci umum, yang dapat "dilampirkan" alur kerja tertentu.
Tapi bagaimana dengan logika integrasi? Bagaimanapun, tugas integrasi dihasilkan oleh arsitektur sistem, yang "digergaji" menjadi beberapa bagian BUKAN atas permintaan pelanggan, tetapi di bawah pengaruh faktor yang sama sekali berbeda:

  • di bawah pengaruh hukum Conway;
  • sebagai akibat penggunaan kembali subsistem yang sebelumnya dikembangkan untuk produk lain;
  • sebagaimana diputuskan oleh arsitek, berdasarkan persyaratan non-fungsional.

Ada godaan besar untuk memisahkan logika integrasi dari logika bisnis alur kerja utama agar tidak mencemari logika bisnis dengan artefak integrasi dan menyelamatkan pengembang aplikasi dari keharusan menggali kekhasan lanskap arsitektural sistem. Pendekatan ini memiliki sejumlah keunggulan, tetapi praktik menunjukkan ketidakefisienannya:

  • pemecahan masalah integrasi biasanya meluncur ke opsi paling sederhana dalam bentuk panggilan sinkron karena keterbatasan titik ekstensi dalam implementasi alur kerja utama (lebih lanjut tentang kekurangan integrasi sinkron di bawah);
  • artefak integrasi masih menembus logika bisnis utama saat umpan balik dari subsistem lain diperlukan;
  • pengembang aplikasi mengabaikan integrasi dan dapat dengan mudah merusaknya dengan mengubah alur kerja;
  • sistem tidak lagi menjadi satu kesatuan dari sudut pandang pengguna, "lapisan" antara subsistem menjadi terlihat, operasi pengguna yang berlebihan muncul yang memulai transfer data dari satu subsistem ke subsistem lainnya.

Pendekatan lain adalah dengan mempertimbangkan interaksi integrasi sebagai bagian integral dari logika bisnis inti dan alur kerja. Agar persyaratan keterampilan pengembang aplikasi tidak meroket, membuat interaksi integrasi baru harus dilakukan dengan mudah dan alami, dengan opsi minimal untuk memilih solusi. Ini lebih sulit daripada yang terlihat: alat tersebut harus cukup kuat untuk memberi pengguna berbagai opsi yang diperlukan untuk penggunaannya dan pada saat yang sama tidak membiarkan diri mereka tertembak. Ada banyak pertanyaan yang harus dijawab oleh seorang insinyur dalam konteks tugas integrasi, tetapi yang tidak boleh dipikirkan oleh pengembang aplikasi dalam pekerjaan sehari-hari mereka: batasan transaksi, konsistensi, atomisitas, keamanan, penskalaan, beban dan distribusi sumber daya, perutean, pengaturan, konteks propagasi dan switching, dll. Pengembang aplikasi perlu menawarkan template keputusan yang cukup sederhana, di mana jawaban untuk semua pertanyaan semacam itu sudah disembunyikan. Pola-pola ini harus cukup aman: logika bisnis sangat sering berubah, yang meningkatkan risiko terjadinya kesalahan, biaya kesalahan harus tetap pada tingkat yang cukup rendah.

Tapi tetap saja, apa hubungannya BPM dengan itu? Ada banyak opsi untuk mengimplementasikan alur kerja ...
Memang, implementasi lain dari proses bisnis sangat populer dalam solusi kami - melalui pengaturan deklaratif diagram transisi status dan menghubungkan penangan dengan logika bisnis ke transisi. Pada saat yang sama, keadaan yang menentukan posisi "dokumen" saat ini dalam proses bisnis adalah atribut dari "dokumen" itu sendiri.

Integrasi gaya BPM
Seperti inilah prosesnya di awal proyek

Popularitas implementasi semacam itu disebabkan oleh kesederhanaan dan kecepatan pembuatan proses bisnis linier. Namun, ketika sistem perangkat lunak menjadi lebih kompleks, bagian otomatis dari proses bisnis tumbuh dan menjadi lebih kompleks. Ada kebutuhan untuk dekomposisi, penggunaan kembali bagian dari proses, serta proses forking sehingga setiap cabang dijalankan secara paralel. Dalam kondisi seperti itu, alat menjadi tidak nyaman, dan diagram transisi status kehilangan konten informasinya (interaksi integrasi sama sekali tidak tercermin dalam diagram).

Integrasi gaya BPM
Seperti inilah prosesnya setelah beberapa iterasi mengklarifikasi persyaratan

Jalan keluar dari situasi ini adalah integrasi mesin jBPM menjadi beberapa produk dengan proses bisnis yang paling kompleks. Dalam jangka pendek, solusi ini cukup berhasil: menjadi mungkin untuk mengimplementasikan proses bisnis yang kompleks sambil mempertahankan diagram yang cukup informatif dan terkini dalam notasi BPMN2.

Integrasi gaya BPM
Bagian kecil dari proses bisnis yang kompleks

Dalam jangka panjang, solusinya tidak memenuhi harapan: intensitas tenaga kerja yang tinggi untuk membuat proses bisnis melalui alat visual tidak memungkinkan pencapaian indikator produktivitas yang dapat diterima, dan alat itu sendiri menjadi salah satu yang paling tidak disukai di antara pengembang. Ada juga keluhan tentang struktur internal mesin, yang menyebabkan munculnya banyak "tambalan" dan "kruk".

Aspek positif utama menggunakan jBPM adalah realisasi manfaat dan bahaya memiliki status persistennya sendiri untuk instance proses bisnis. Kami juga melihat kemungkinan menggunakan pendekatan proses untuk mengimplementasikan protokol integrasi kompleks antara berbagai aplikasi menggunakan interaksi asinkron melalui sinyal dan pesan. Kehadiran negara yang gigih memainkan peran penting dalam hal ini.

Berdasarkan hal di atas, kita dapat menyimpulkan: Pendekatan proses dalam gaya BPM memungkinkan kami untuk menyelesaikan berbagai tugas untuk mengotomatiskan proses bisnis yang semakin kompleks, menyesuaikan aktivitas integrasi secara harmonis ke dalam proses ini dan mempertahankan kemampuan untuk menampilkan proses yang diterapkan secara visual dalam notasi yang sesuai.

Kerugian dari panggilan sinkron sebagai pola integrasi

Integrasi sinkron mengacu pada panggilan pemblokiran yang paling sederhana. Satu subsistem bertindak sebagai sisi server dan memaparkan API dengan metode yang diinginkan. Subsistem lain bertindak sebagai sisi klien dan, pada waktu yang tepat, melakukan panggilan dengan harapan akan hasilnya. Bergantung pada arsitektur sistem, sisi klien dan server dapat dihosting baik dalam aplikasi dan proses yang sama, atau dalam aplikasi dan proses yang berbeda. Dalam kasus kedua, Anda perlu menerapkan beberapa implementasi RPC dan menyediakan pengaturan parameter dan hasil panggilan.

Integrasi gaya BPM

Pola integrasi semacam itu memiliki sejumlah kelemahan yang cukup besar, tetapi dalam praktiknya sangat banyak digunakan karena kesederhanaannya. Kecepatan implementasi memikat dan membuat Anda menerapkannya berulang kali dalam kondisi tenggat waktu yang "terbakar", menuliskan solusinya menjadi utang teknis. Tetapi juga terjadi bahwa pengembang yang tidak berpengalaman menggunakannya secara tidak sadar, tidak menyadari konsekuensi negatifnya.

Selain peningkatan konektivitas subsistem yang paling jelas, ada masalah yang kurang jelas dengan transaksi "penyebaran" dan "peregangan". Memang, jika logika bisnis membuat perubahan apa pun, maka transaksi sangat diperlukan, dan transaksi, pada gilirannya, mengunci sumber daya aplikasi tertentu yang terpengaruh oleh perubahan ini. Artinya, sampai satu subsistem menunggu tanggapan dari yang lain, itu tidak akan dapat menyelesaikan transaksi dan melepaskan kunci. Ini secara signifikan meningkatkan risiko berbagai efek:

  • daya tanggap sistem hilang, pengguna menunggu lama untuk menjawab permintaan;
  • server umumnya berhenti merespons permintaan pengguna karena kumpulan utas yang meluap: sebagian besar utas "berdiri" di kunci sumber daya yang ditempati oleh transaksi;
  • kebuntuan mulai muncul: kemungkinan kemunculannya sangat bergantung pada durasi transaksi, jumlah logika bisnis, dan kunci yang terlibat dalam transaksi;
  • kesalahan batas waktu transaksi muncul;
  • server "jatuh" pada OutOfMemory jika tugas memerlukan pemrosesan dan perubahan data dalam jumlah besar, dan adanya integrasi sinkron membuatnya sangat sulit untuk membagi pemrosesan menjadi transaksi yang "lebih ringan".

Dari sudut pandang arsitektur, penggunaan panggilan pemblokiran selama integrasi menyebabkan hilangnya kontrol kualitas subsistem individu: tidak mungkin memastikan target kualitas satu subsistem dalam isolasi dari target kualitas subsistem lain. Jika subsistem dikembangkan oleh tim yang berbeda, ini adalah masalah besar.

Segalanya menjadi lebih menarik jika subsistem yang diintegrasikan berada dalam aplikasi yang berbeda dan perubahan sinkron perlu dilakukan di kedua sisi. Bagaimana membuat perubahan ini transaksional?

Jika perubahan dibuat dalam transaksi terpisah, maka penanganan pengecualian dan kompensasi yang kuat perlu disediakan, dan ini sepenuhnya menghilangkan keuntungan utama dari integrasi sinkron - kesederhanaan.

Transaksi terdistribusi juga muncul di benak, tetapi kami tidak menggunakannya dalam solusi kami: sulit untuk memastikan keandalan.

"Saga" sebagai solusi masalah transaksi

Dengan semakin populernya layanan mikro, ada peningkatan permintaan Pola Saga.

Pola ini dengan sempurna menyelesaikan masalah transaksi panjang di atas, dan juga memperluas kemungkinan mengelola keadaan sistem dari sisi logika bisnis: kompensasi setelah transaksi yang gagal mungkin tidak mengembalikan sistem ke keadaan semula, tetapi memberikan alternatif rute pemrosesan data. Ini juga memungkinkan Anda untuk tidak mengulangi langkah-langkah pemrosesan data yang berhasil diselesaikan saat Anda mencoba membawa proses ke akhir yang "baik".

Menariknya, dalam sistem monolitik, pola ini juga relevan ketika berhubungan dengan integrasi subsistem yang digabungkan secara longgar dan ada efek negatif yang disebabkan oleh transaksi yang lama dan kunci sumber daya yang sesuai.

Sehubungan dengan proses bisnis kami dalam gaya BPM, ternyata sangat mudah untuk mengimplementasikan Sagas: langkah-langkah individual Sagas dapat ditetapkan sebagai aktivitas dalam proses bisnis, dan status proses bisnis yang gigih menentukan, antara hal lain, keadaan internal Sagas. Artinya, kami tidak memerlukan mekanisme koordinasi tambahan. Yang Anda butuhkan hanyalah broker pesan dengan dukungan untuk jaminan "setidaknya sekali" sebagai transportasi.

Tetapi solusi semacam itu juga memiliki "harga" sendiri:

  • logika bisnis menjadi lebih kompleks: Anda perlu menyelesaikan kompensasi;
  • perlu untuk meninggalkan konsistensi penuh, yang dapat menjadi sangat sensitif untuk sistem monolitik;
  • arsitektur menjadi sedikit lebih rumit, ada kebutuhan tambahan untuk perantara pesan;
  • alat pemantauan dan administrasi tambahan akan diperlukan (walaupun secara umum bahkan bagus: kualitas layanan sistem akan meningkat).

Untuk sistem monolitik, pembenaran untuk menggunakan "Sags" tidak begitu jelas. Untuk layanan mikro dan SOA lainnya, di mana, kemungkinan besar, sudah ada broker, dan konsistensi penuh telah dikorbankan di awal proyek, manfaat menggunakan pola ini dapat secara signifikan lebih besar daripada kerugiannya, terutama jika ada API yang nyaman di tingkat logika bisnis.

Enkapsulasi logika bisnis dalam layanan mikro

Ketika kami mulai bereksperimen dengan layanan mikro, muncul pertanyaan yang masuk akal: di mana menempatkan logika bisnis domain dalam kaitannya dengan layanan yang menyediakan persistensi data domain?

Ketika melihat arsitektur dari berbagai BPMS, tampaknya masuk akal untuk memisahkan logika bisnis dari persistensi: membuat lapisan platform dan layanan mikro independen domain yang membentuk lingkungan dan wadah untuk menjalankan logika bisnis domain, dan mengatur persistensi data domain sebagai terpisah lapisan layanan mikro yang sangat sederhana dan ringan. Proses bisnis dalam hal ini mengatur layanan lapisan kegigihan.

Integrasi gaya BPM

Pendekatan ini memiliki nilai tambah yang sangat besar: Anda dapat meningkatkan fungsionalitas platform sebanyak yang Anda suka, dan hanya lapisan layanan mikro platform yang sesuai yang akan "menjadi gemuk" dari ini. Proses bisnis dari domain mana pun segera mendapatkan kesempatan untuk menggunakan fungsionalitas baru platform segera setelah diperbarui.

Sebuah studi yang lebih rinci mengungkapkan kekurangan yang signifikan dari pendekatan ini:

  • layanan platform yang mengeksekusi logika bisnis banyak domain sekaligus membawa risiko besar sebagai satu titik kegagalan. Perubahan yang sering terjadi pada logika bisnis meningkatkan risiko bug yang menyebabkan kegagalan di seluruh sistem;
  • masalah kinerja: logika bisnis bekerja dengan datanya melalui antarmuka yang sempit dan lambat:
    • data sekali lagi akan disusun dan dipompa melalui tumpukan jaringan;
    • layanan domain akan sering mengembalikan lebih banyak data daripada yang diperlukan oleh logika bisnis untuk diproses, karena kemampuan parameterisasi kueri yang tidak memadai pada tingkat API eksternal layanan;
    • beberapa bagian independen dari logika bisnis dapat berulang kali meminta ulang data yang sama untuk diproses (Anda dapat mengurangi masalah ini dengan menambahkan kacang sesi yang men-cache data, tetapi ini semakin memperumit arsitektur dan menciptakan masalah kesegaran data dan pembatalan cache);
  • masalah transaksional:
    • proses bisnis dengan status persisten yang disimpan oleh layanan platform tidak konsisten dengan data domain, dan tidak ada cara mudah untuk mengatasi masalah ini;
    • memindahkan kunci data domain dari transaksi: jika logika bisnis domain perlu melakukan perubahan, setelah terlebih dahulu memeriksa kebenaran data aktual, perlu untuk mengecualikan kemungkinan perubahan kompetitif dalam data yang diproses. Pemblokiran data eksternal dapat membantu memecahkan masalah, tetapi solusi semacam itu membawa risiko tambahan dan mengurangi keandalan sistem secara keseluruhan;
  • komplikasi tambahan saat memperbarui: dalam beberapa kasus, Anda perlu memperbarui layanan persistensi dan logika bisnis secara sinkron atau dalam urutan yang ketat.

Pada akhirnya, saya harus kembali ke dasar: mengenkapsulasi data domain dan logika bisnis domain ke dalam satu layanan mikro. Pendekatan ini menyederhanakan persepsi layanan mikro sebagai komponen integral dalam sistem dan tidak menimbulkan masalah di atas. Ini juga tidak gratis:

  • Standarisasi API diperlukan untuk interaksi dengan logika bisnis (khususnya, untuk menyediakan aktivitas pengguna sebagai bagian dari proses bisnis) dan layanan platform API; perhatian yang lebih hati-hati terhadap perubahan API, diperlukan kompatibilitas maju dan mundur;
  • diperlukan untuk menambahkan perpustakaan runtime tambahan untuk memastikan fungsi logika bisnis sebagai bagian dari setiap layanan mikro tersebut, dan ini menimbulkan persyaratan baru untuk perpustakaan tersebut: ringan dan ketergantungan transitif minimum;
  • pengembang logika bisnis perlu melacak versi perpustakaan: jika layanan mikro belum diselesaikan untuk waktu yang lama, kemungkinan besar layanan itu akan berisi versi lama perpustakaan. Ini bisa menjadi kendala tak terduga untuk menambahkan fitur baru dan mungkin memerlukan logika bisnis lama dari layanan semacam itu untuk dipindahkan ke versi baru pustaka jika ada perubahan yang tidak kompatibel antar versi.

Integrasi gaya BPM

Lapisan layanan platform juga hadir dalam arsitektur seperti itu, tetapi lapisan ini tidak lagi membentuk wadah untuk menjalankan logika bisnis domain, tetapi hanya lingkungannya, yang menyediakan fungsi "platform" tambahan. Lapisan seperti itu diperlukan tidak hanya untuk menjaga keringanan layanan mikro domain, tetapi juga untuk memusatkan manajemen.

Misalnya, aktivitas pengguna dalam proses bisnis menghasilkan tugas. Namun, saat mengerjakan tugas, pengguna harus melihat tugas dari semua domain dalam daftar umum, yang berarti harus ada layanan platform pendaftaran tugas yang sesuai, dibersihkan dari logika bisnis domain. Menjaga enkapsulasi logika bisnis dalam konteks ini cukup bermasalah, dan ini merupakan kompromi lain dari arsitektur ini.

Integrasi proses bisnis melalui mata pengembang aplikasi

Seperti yang telah disebutkan di atas, pengembang aplikasi harus diabstraksi dari fitur teknis dan rekayasa implementasi interaksi beberapa aplikasi agar dapat mengandalkan produktivitas pengembangan yang baik.

Mari kita coba selesaikan masalah integrasi yang agak sulit, yang diciptakan khusus untuk artikel tersebut. Ini akan menjadi tugas "permainan" yang melibatkan tiga aplikasi, di mana masing-masing aplikasi menentukan beberapa nama domain: "app1", "app2", "app3".

Di dalam setiap aplikasi, proses bisnis diluncurkan yang mulai "bermain bola" melalui bus integrasi. Pesan bernama "Bola" akan berfungsi sebagai bola.

Aturan mainnya:

  • pemain pertama adalah inisiator. Dia mengundang pemain lain ke dalam permainan, memulai permainan dan dapat mengakhirinya kapan saja;
  • pemain lain menyatakan partisipasi mereka dalam permainan, "berkenalan" satu sama lain dan pemain pertama;
  • setelah menerima bola, pemain memilih pemain lain yang berpartisipasi dan mengoper bola kepadanya. Jumlah total lintasan dihitung;
  • setiap pemain memiliki "energi", yang berkurang dengan setiap operan bola oleh pemain itu. Saat energi habis, pemain tersingkir dari permainan, mengumumkan pengunduran diri mereka;
  • jika pemain ditinggal sendirian, dia langsung menyatakan kepergiannya;
  • ketika semua pemain tersingkir, pemain pertama menyatakan akhir permainan. Jika dia keluar dari game lebih awal, maka dia tetap mengikuti game tersebut untuk menyelesaikannya.

Untuk mengatasi masalah ini, saya akan menggunakan DSL kami untuk proses bisnis, yang memungkinkan Anda mendeskripsikan logika di Kotlin secara ringkas, dengan minimal boilerplate.

Dalam aplikasi app1, proses bisnis pemain pertama (dia juga penggagas permainan) akan bekerja:

kelas InitialPlayer

import ru.krista.bpm.ProcessInstance
import ru.krista.bpm.runtime.ProcessImpl
import ru.krista.bpm.runtime.constraint.UniqueConstraints
import ru.krista.bpm.runtime.dsl.processModel
import ru.krista.bpm.runtime.dsl.taskOperation
import ru.krista.bpm.runtime.instance.MessageSendInstance

data class PlayerInfo(val name: String, val domain: String, val id: String)

class PlayersList : ArrayList<PlayerInfo>()

// Это класс экземпляра процесса: инкапсулирует его внутреннее состояние
class InitialPlayer : ProcessImpl<InitialPlayer>(initialPlayerModel) {
    var playerName: String by persistent("Player1")
    var energy: Int by persistent(30)
    var players: PlayersList by persistent(PlayersList())
    var shotCounter: Int = 0
}

// Это декларация модели процесса: создается один раз, используется всеми
// экземплярами процесса соответствующего класса
val initialPlayerModel = processModel<InitialPlayer>(name = "InitialPlayer",
                                                     version = 1) {

    // По правилам, первый игрок является инициатором игры и должен быть единственным
    uniqueConstraint = UniqueConstraints.singleton

    // Объявляем активности, из которых состоит бизнес-процесс
    val sendNewGameSignal = signal<String>("NewGame")
    val sendStopGameSignal = signal<String>("StopGame")
    val startTask = humanTask("Start") {
        taskOperation {
            processCondition { players.size > 0 }
            confirmation { "Подключилось ${players.size} игроков. Начинаем?" }
        }
    }
    val stopTask = humanTask("Stop") {
        taskOperation {}
    }
    val waitPlayerJoin = signalWait<String>("PlayerJoin") { signal ->
        players.add(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... join player ${signal.data} ...")
    }
    val waitPlayerOut = signalWait<String>("PlayerOut") { signal ->
        players.remove(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... player ${signal.data} is out ...")
    }
    val sendPlayerOut = signal<String>("PlayerOut") {
        signalData = { playerName }
    }
    val sendHandshake = messageSend<String>("Handshake") {
        messageData = { playerName }
        activation = {
            receiverDomain = process.players.last().domain
            receiverProcessInstanceId = process.players.last().id
        }
    }
    val throwStartBall = messageSend<Int>("Ball") {
        messageData = { 1 }
        activation = { selectNextPlayer() }
    }
    val throwBall = messageSend<Int>("Ball") {
        messageData = { shotCounter + 1 }
        activation = { selectNextPlayer() }
        onEntry { energy -= 1 }
    }
    val waitBall = messageWaitData<Int>("Ball") {
        shotCounter = it
    }

    // Теперь конструируем граф процесса из объявленных активностей
    startFrom(sendNewGameSignal)
            .fork("mainFork") {
                next(startTask)
                next(waitPlayerJoin).next(sendHandshake).next(waitPlayerJoin)
                next(waitPlayerOut)
                        .branch("checkPlayers") {
                            ifTrue { players.isEmpty() }
                                    .next(sendStopGameSignal)
                                    .terminate()
                            ifElse().next(waitPlayerOut)
                        }
            }
    startTask.fork("afterStart") {
        next(throwStartBall)
                .branch("mainLoop") {
                    ifTrue { energy < 5 }.next(sendPlayerOut).next(waitBall)
                    ifElse().next(waitBall).next(throwBall).loop()
                }
        next(stopTask).next(sendStopGameSignal)
    }

    // Навешаем на активности дополнительные обработчики для логирования
    sendNewGameSignal.onExit { println("Let's play!") }
    sendStopGameSignal.onExit { println("Stop!") }
    sendPlayerOut.onExit { println("$playerName: I'm out!") }
}

private fun MessageSendInstance<InitialPlayer, Int>.selectNextPlayer() {
    val player = process.players.random()
    receiverDomain = player.domain
    receiverProcessInstanceId = player.id
    println("Step ${process.shotCounter + 1}: " +
            "${process.playerName} >>> ${player.name}")
}

Selain mengeksekusi logika bisnis, kode di atas dapat menghasilkan model objek dari proses bisnis yang dapat divisualisasikan sebagai diagram. Kami belum mengimplementasikan visualizer, jadi kami harus meluangkan waktu untuk menggambar (di sini saya sedikit menyederhanakan notasi BPMN terkait penggunaan gerbang untuk meningkatkan konsistensi diagram dengan kode di atas):

Integrasi gaya BPM

app2 akan menyertakan proses bisnis pemain lain:

kelas RandomPlayer

import ru.krista.bpm.ProcessInstance
import ru.krista.bpm.runtime.ProcessImpl
import ru.krista.bpm.runtime.dsl.processModel
import ru.krista.bpm.runtime.instance.MessageSendInstance

data class PlayerInfo(val name: String, val domain: String, val id: String)

class PlayersList: ArrayList<PlayerInfo>()

class RandomPlayer : ProcessImpl<RandomPlayer>(randomPlayerModel) {

    var playerName: String by input(persistent = true, 
                                    defaultValue = "RandomPlayer")
    var energy: Int by input(persistent = true, defaultValue = 30)
    var players: PlayersList by persistent(PlayersList())
    var allPlayersOut: Boolean by persistent(false)
    var shotCounter: Int = 0

    val selfPlayer: PlayerInfo
        get() = PlayerInfo(playerName, env.eventDispatcher.domainName, id)
}

val randomPlayerModel = processModel<RandomPlayer>(name = "RandomPlayer", 
                                                   version = 1) {

    val waitNewGameSignal = signalWait<String>("NewGame")
    val waitStopGameSignal = signalWait<String>("StopGame")
    val sendPlayerJoin = signal<String>("PlayerJoin") {
        signalData = { playerName }
    }
    val sendPlayerOut = signal<String>("PlayerOut") {
        signalData = { playerName }
    }
    val waitPlayerJoin = signalWaitCustom<String>("PlayerJoin") {
        eventCondition = { signal ->
            signal.sender.processInstanceId != process.id 
                && !process.players.any { signal.sender.processInstanceId == it.id}
        }
        handler = { signal ->
            players.add(PlayerInfo(
                    signal.data!!,
                    signal.sender.domain,
                    signal.sender.processInstanceId))
        }
    }
    val waitPlayerOut = signalWait<String>("PlayerOut") { signal ->
        players.remove(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        allPlayersOut = players.isEmpty()
    }
    val sendHandshake = messageSend<String>("Handshake") {
        messageData = { playerName }
        activation = {
            receiverDomain = process.players.last().domain
            receiverProcessInstanceId = process.players.last().id
        }
    }
    val receiveHandshake = messageWait<String>("Handshake") { message ->
        if (!players.any { message.sender.processInstanceId == it.id}) {
            players.add(PlayerInfo(
                    message.data!!, 
                    message.sender.domain, 
                    message.sender.processInstanceId))
        }
    }
    val throwBall = messageSend<Int>("Ball") {
        messageData = { shotCounter + 1 }
        activation = { selectNextPlayer() }
        onEntry { energy -= 1 }
    }
    val waitBall = messageWaitData<Int>("Ball") {
        shotCounter = it
    }

    startFrom(waitNewGameSignal)
            .fork("mainFork") {
                next(sendPlayerJoin)
                        .branch("mainLoop") {
                            ifTrue { energy < 5 || allPlayersOut }
                                    .next(sendPlayerOut)
                                    .next(waitBall)
                            ifElse()
                                    .next(waitBall)
                                    .next(throwBall)
                                    .loop()
                        }
                next(waitPlayerJoin).next(sendHandshake).next(waitPlayerJoin)
                next(waitPlayerOut).next(waitPlayerOut)
                next(receiveHandshake).next(receiveHandshake)
                next(waitStopGameSignal).terminate()
            }

    sendPlayerJoin.onExit { println("$playerName: I'm here!") }
    sendPlayerOut.onExit { println("$playerName: I'm out!") }
}

private fun MessageSendInstance<RandomPlayer, Int>.selectNextPlayer() {
    val player = if (process.players.isNotEmpty()) 
        process.players.random() 
    else 
        process.selfPlayer
    receiverDomain = player.domain
    receiverProcessInstanceId = player.id
    println("Step ${process.shotCounter + 1}: " +
            "${process.playerName} >>> ${player.name}")
}

Diagram:

Integrasi gaya BPM

Dalam aplikasi app3, kami akan membuat pemain dengan perilaku yang sedikit berbeda: alih-alih memilih pemain berikutnya secara acak, dia akan bertindak sesuai dengan algoritme round-robin:

kelas RoundRobinPlayer

import ru.krista.bpm.ProcessInstance
import ru.krista.bpm.runtime.ProcessImpl
import ru.krista.bpm.runtime.dsl.processModel
import ru.krista.bpm.runtime.instance.MessageSendInstance

data class PlayerInfo(val name: String, val domain: String, val id: String)

class PlayersList: ArrayList<PlayerInfo>()

class RoundRobinPlayer : ProcessImpl<RoundRobinPlayer>(roundRobinPlayerModel) {

    var playerName: String by input(persistent = true, 
                                    defaultValue = "RoundRobinPlayer")
    var energy: Int by input(persistent = true, defaultValue = 30)
    var players: PlayersList by persistent(PlayersList())
    var nextPlayerIndex: Int by persistent(-1)
    var allPlayersOut: Boolean by persistent(false)
    var shotCounter: Int = 0

    val selfPlayer: PlayerInfo
        get() = PlayerInfo(playerName, env.eventDispatcher.domainName, id)
}

val roundRobinPlayerModel = processModel<RoundRobinPlayer>(
        name = "RoundRobinPlayer", 
        version = 1) {

    val waitNewGameSignal = signalWait<String>("NewGame")
    val waitStopGameSignal = signalWait<String>("StopGame")
    val sendPlayerJoin = signal<String>("PlayerJoin") {
        signalData = { playerName }
    }
    val sendPlayerOut = signal<String>("PlayerOut") {
        signalData = { playerName }
    }
    val waitPlayerJoin = signalWaitCustom<String>("PlayerJoin") {
        eventCondition = { signal ->
            signal.sender.processInstanceId != process.id 
                && !process.players.any { signal.sender.processInstanceId == it.id}
        }
        handler = { signal ->
            players.add(PlayerInfo(
                    signal.data!!, 
                    signal.sender.domain, 
                    signal.sender.processInstanceId))
        }
    }
    val waitPlayerOut = signalWait<String>("PlayerOut") { signal ->
        players.remove(PlayerInfo(
                signal.data!!, 
                signal.sender.domain, 
                signal.sender.processInstanceId))
        allPlayersOut = players.isEmpty()
    }
    val sendHandshake = messageSend<String>("Handshake") {
        messageData = { playerName }
        activation = {
            receiverDomain = process.players.last().domain
            receiverProcessInstanceId = process.players.last().id
        }
    }
    val receiveHandshake = messageWait<String>("Handshake") { message ->
        if (!players.any { message.sender.processInstanceId == it.id}) {
            players.add(PlayerInfo(
                    message.data!!, 
                    message.sender.domain, 
                    message.sender.processInstanceId))
        }
    }
    val throwBall = messageSend<Int>("Ball") {
        messageData = { shotCounter + 1 }
        activation = { selectNextPlayer() }
        onEntry { energy -= 1 }
    }
    val waitBall = messageWaitData<Int>("Ball") {
        shotCounter = it
    }

    startFrom(waitNewGameSignal)
            .fork("mainFork") {
                next(sendPlayerJoin)
                        .branch("mainLoop") {
                            ifTrue { energy < 5 || allPlayersOut }
                                    .next(sendPlayerOut)
                                    .next(waitBall)
                            ifElse()
                                    .next(waitBall)
                                    .next(throwBall)
                                    .loop()
                        }
                next(waitPlayerJoin).next(sendHandshake).next(waitPlayerJoin)
                next(waitPlayerOut).next(waitPlayerOut)
                next(receiveHandshake).next(receiveHandshake)
                next(waitStopGameSignal).terminate()
            }

    sendPlayerJoin.onExit { println("$playerName: I'm here!") }
    sendPlayerOut.onExit { println("$playerName: I'm out!") }
}

private fun MessageSendInstance<RoundRobinPlayer, Int>.selectNextPlayer() {
    var idx = process.nextPlayerIndex + 1
    if (idx >= process.players.size) {
        idx = 0
    }
    process.nextPlayerIndex = idx
    val player = if (process.players.isNotEmpty()) 
        process.players[idx] 
    else 
        process.selfPlayer
    receiverDomain = player.domain
    receiverProcessInstanceId = player.id
    println("Step ${process.shotCounter + 1}: " +
            "${process.playerName} >>> ${player.name}")
}

Jika tidak, perilaku pemain tidak berbeda dari yang sebelumnya, sehingga diagram tidak berubah.

Sekarang kita membutuhkan tes untuk menjalankan semuanya. Saya hanya akan memberikan kode pengujian itu sendiri, agar tidak mengacaukan artikel dengan boilerplate (sebenarnya, saya menggunakan lingkungan pengujian yang dibuat sebelumnya untuk menguji integrasi proses bisnis lainnya):

tesPermainan()

@Test
public void testGame() throws InterruptedException {
    String pl2 = startProcess(app2, "RandomPlayer", playerParams("Player2", 20));
    String pl3 = startProcess(app2, "RandomPlayer", playerParams("Player3", 40));
    String pl4 = startProcess(app3, "RoundRobinPlayer", playerParams("Player4", 25));
    String pl5 = startProcess(app3, "RoundRobinPlayer", playerParams("Player5", 35));
    String pl1 = startProcess(app1, "InitialPlayer");
    // Теперь нужно немного подождать, пока игроки "познакомятся" друг с другом.
    // Ждать через sleep - плохое решение, зато самое простое. 
    // Не делайте так в серьезных тестах!
    Thread.sleep(1000);
    // Запускаем игру, закрывая пользовательскую активность
    assertTrue(closeTask(app1, pl1, "Start"));
    app1.getWaiting().waitProcessFinished(pl1);
    app2.getWaiting().waitProcessFinished(pl2);
    app2.getWaiting().waitProcessFinished(pl3);
    app3.getWaiting().waitProcessFinished(pl4);
    app3.getWaiting().waitProcessFinished(pl5);
}

private Map<String, Object> playerParams(String name, int energy) {
    Map<String, Object> params = new HashMap<>();
    params.put("playerName", name);
    params.put("energy", energy);
    return params;
}

Jalankan tes, lihat log:

keluaran konsol

Взята блокировка ключа lock://app1/process/InitialPlayer
Let's play!
Снята блокировка ключа lock://app1/process/InitialPlayer
Player2: I'm here!
Player3: I'm here!
Player4: I'm here!
Player5: I'm here!
... join player Player2 ...
... join player Player4 ...
... join player Player3 ...
... join player Player5 ...
Step 1: Player1 >>> Player3
Step 2: Player3 >>> Player5
Step 3: Player5 >>> Player3
Step 4: Player3 >>> Player4
Step 5: Player4 >>> Player3
Step 6: Player3 >>> Player4
Step 7: Player4 >>> Player5
Step 8: Player5 >>> Player2
Step 9: Player2 >>> Player5
Step 10: Player5 >>> Player4
Step 11: Player4 >>> Player2
Step 12: Player2 >>> Player4
Step 13: Player4 >>> Player1
Step 14: Player1 >>> Player4
Step 15: Player4 >>> Player3
Step 16: Player3 >>> Player1
Step 17: Player1 >>> Player2
Step 18: Player2 >>> Player3
Step 19: Player3 >>> Player1
Step 20: Player1 >>> Player5
Step 21: Player5 >>> Player1
Step 22: Player1 >>> Player2
Step 23: Player2 >>> Player4
Step 24: Player4 >>> Player5
Step 25: Player5 >>> Player3
Step 26: Player3 >>> Player4
Step 27: Player4 >>> Player2
Step 28: Player2 >>> Player5
Step 29: Player5 >>> Player2
Step 30: Player2 >>> Player1
Step 31: Player1 >>> Player3
Step 32: Player3 >>> Player4
Step 33: Player4 >>> Player1
Step 34: Player1 >>> Player3
Step 35: Player3 >>> Player4
Step 36: Player4 >>> Player3
Step 37: Player3 >>> Player2
Step 38: Player2 >>> Player5
Step 39: Player5 >>> Player4
Step 40: Player4 >>> Player5
Step 41: Player5 >>> Player1
Step 42: Player1 >>> Player5
Step 43: Player5 >>> Player3
Step 44: Player3 >>> Player5
Step 45: Player5 >>> Player2
Step 46: Player2 >>> Player3
Step 47: Player3 >>> Player2
Step 48: Player2 >>> Player5
Step 49: Player5 >>> Player4
Step 50: Player4 >>> Player2
Step 51: Player2 >>> Player5
Step 52: Player5 >>> Player1
Step 53: Player1 >>> Player5
Step 54: Player5 >>> Player3
Step 55: Player3 >>> Player5
Step 56: Player5 >>> Player2
Step 57: Player2 >>> Player1
Step 58: Player1 >>> Player4
Step 59: Player4 >>> Player1
Step 60: Player1 >>> Player4
Step 61: Player4 >>> Player3
Step 62: Player3 >>> Player2
Step 63: Player2 >>> Player5
Step 64: Player5 >>> Player4
Step 65: Player4 >>> Player5
Step 66: Player5 >>> Player1
Step 67: Player1 >>> Player5
Step 68: Player5 >>> Player3
Step 69: Player3 >>> Player4
Step 70: Player4 >>> Player2
Step 71: Player2 >>> Player5
Step 72: Player5 >>> Player2
Step 73: Player2 >>> Player1
Step 74: Player1 >>> Player4
Step 75: Player4 >>> Player1
Step 76: Player1 >>> Player2
Step 77: Player2 >>> Player5
Step 78: Player5 >>> Player4
Step 79: Player4 >>> Player3
Step 80: Player3 >>> Player1
Step 81: Player1 >>> Player5
Step 82: Player5 >>> Player1
Step 83: Player1 >>> Player4
Step 84: Player4 >>> Player5
Step 85: Player5 >>> Player3
Step 86: Player3 >>> Player5
Step 87: Player5 >>> Player2
Step 88: Player2 >>> Player3
Player2: I'm out!
Step 89: Player3 >>> Player4
... player Player2 is out ...
Step 90: Player4 >>> Player1
Step 91: Player1 >>> Player3
Step 92: Player3 >>> Player1
Step 93: Player1 >>> Player4
Step 94: Player4 >>> Player3
Step 95: Player3 >>> Player5
Step 96: Player5 >>> Player1
Step 97: Player1 >>> Player5
Step 98: Player5 >>> Player3
Step 99: Player3 >>> Player5
Step 100: Player5 >>> Player4
Step 101: Player4 >>> Player5
Player4: I'm out!
... player Player4 is out ...
Step 102: Player5 >>> Player1
Step 103: Player1 >>> Player3
Step 104: Player3 >>> Player1
Step 105: Player1 >>> Player3
Step 106: Player3 >>> Player5
Step 107: Player5 >>> Player3
Step 108: Player3 >>> Player1
Step 109: Player1 >>> Player3
Step 110: Player3 >>> Player5
Step 111: Player5 >>> Player1
Step 112: Player1 >>> Player3
Step 113: Player3 >>> Player5
Step 114: Player5 >>> Player3
Step 115: Player3 >>> Player1
Step 116: Player1 >>> Player3
Step 117: Player3 >>> Player5
Step 118: Player5 >>> Player1
Step 119: Player1 >>> Player3
Step 120: Player3 >>> Player5
Step 121: Player5 >>> Player3
Player5: I'm out!
... player Player5 is out ...
Step 122: Player3 >>> Player5
Step 123: Player5 >>> Player1
Player5: I'm out!
Step 124: Player1 >>> Player3
... player Player5 is out ...
Step 125: Player3 >>> Player1
Step 126: Player1 >>> Player3
Player1: I'm out!
... player Player1 is out ...
Step 127: Player3 >>> Player3
Player3: I'm out!
Step 128: Player3 >>> Player3
... player Player3 is out ...
Player3: I'm out!
Stop!
Step 129: Player3 >>> Player3
Player3: I'm out!

Beberapa kesimpulan penting dapat ditarik dari semua ini:

  • jika alat yang diperlukan tersedia, pengembang aplikasi dapat membuat interaksi integrasi antar aplikasi tanpa melepaskan diri dari logika bisnis;
  • kompleksitas (kompleksitas) tugas integrasi yang membutuhkan kompetensi teknik dapat disembunyikan di dalam kerangka kerja jika awalnya diletakkan dalam arsitektur kerangka kerja. Kesulitan tugas (kesulitan) tidak dapat disembunyikan, sehingga solusi untuk tugas sulit dalam kode akan terlihat sesuai;
  • ketika mengembangkan logika integrasi, pada akhirnya perlu memperhitungkan konsistensi dan kurangnya kemampuan linearisasi dari perubahan keadaan semua peserta integrasi. Ini memaksa kita untuk memperumit logika agar tidak peka terhadap urutan terjadinya peristiwa eksternal. Dalam contoh kami, pemain dipaksa untuk mengambil bagian dalam permainan setelah dia mengumumkan keluar dari permainan: pemain lain akan terus mengoper bola kepadanya sampai informasi tentang keluarnya mencapai dan diproses oleh semua peserta. Logika ini tidak mengikuti aturan permainan dan merupakan solusi kompromi dalam kerangka arsitektur yang dipilih.

Selanjutnya, mari kita bicara tentang berbagai seluk-beluk solusi, kompromi, dan poin lainnya.

Semua pesan dalam satu antrian

Semua aplikasi terintegrasi bekerja dengan satu bus integrasi, yang disajikan sebagai broker eksternal, satu BPMQueue untuk pesan dan satu topik BPMTopic untuk sinyal (peristiwa). Meneruskan semua pesan melalui satu antrean itu sendiri merupakan kompromi. Pada tingkat logika bisnis, Anda sekarang dapat memperkenalkan jenis pesan baru sebanyak yang Anda inginkan tanpa mengubah struktur sistem. Ini adalah penyederhanaan yang signifikan, tetapi membawa risiko tertentu, yang, dalam konteks tugas tipikal kami, bagi kami tampaknya tidak begitu signifikan.

Integrasi gaya BPM

Namun, ada satu kehalusan di sini: setiap aplikasi memfilter pesan "miliknya" dari antrian di pintu masuk, dengan nama domainnya. Selain itu, domain dapat ditentukan dalam sinyal, jika Anda perlu membatasi "cakupan" sinyal ke satu aplikasi. Ini harus meningkatkan bandwidth bus, tetapi logika bisnis sekarang harus beroperasi dengan nama domain: wajib untuk menangani pesan, diinginkan untuk sinyal.

Memastikan keandalan bus integrasi

Keandalan terdiri dari beberapa hal:

  • Pialang pesan yang dipilih adalah komponen penting dari arsitektur dan satu titik kegagalan: itu harus cukup toleran terhadap kesalahan. Anda sebaiknya hanya menggunakan implementasi yang telah teruji oleh waktu dengan dukungan yang baik dan komunitas yang besar;
  • perlu untuk memastikan ketersediaan tinggi dari broker pesan, yang secara fisik harus dipisahkan dari aplikasi terintegrasi (ketersediaan tinggi aplikasi dengan logika bisnis terapan jauh lebih sulit dan mahal untuk disediakan);
  • broker wajib memberikan jaminan pengiriman "setidaknya satu kali". Ini adalah persyaratan wajib untuk pengoperasian bus integrasi yang andal. Tidak perlu jaminan tingkat "tepat sekali": proses bisnis biasanya tidak peka terhadap kedatangan berulang pesan atau acara, dan dalam tugas khusus yang penting, lebih mudah untuk menambahkan pemeriksaan tambahan ke logika bisnis daripada terus menggunakan jaminan yang agak "mahal";
  • mengirim pesan dan sinyal harus terlibat dalam transaksi umum dengan perubahan status proses bisnis dan data domain. Opsi yang disukai adalah menggunakan pola Kotak Keluar Transaksional, tetapi akan membutuhkan tabel tambahan di database dan relai. Dalam aplikasi JEE, ini dapat disederhanakan dengan menggunakan manajer JTA lokal, tetapi koneksi ke broker yang dipilih harus dapat berfungsi dalam mode XA;
  • penangan pesan masuk dan acara juga harus bekerja dengan transaksi perubahan status proses bisnis: jika transaksi seperti itu dibatalkan, maka penerimaan pesan juga harus dibatalkan;
  • pesan yang tidak dapat terkirim karena kesalahan harus disimpan di penyimpanan terpisah D.L.Q. (Antrian Surat Mati). Untuk melakukan ini, kami membuat layanan mikro platform terpisah yang menyimpan pesan semacam itu di penyimpanannya, mengindeksnya berdasarkan atribut (untuk pengelompokan dan pencarian cepat), dan memaparkan API untuk melihat, mengirim ulang ke alamat tujuan, dan menghapus pesan. Administrator sistem dapat bekerja dengan layanan ini melalui antarmuka web mereka;
  • dalam pengaturan broker, Anda perlu menyesuaikan jumlah pengiriman ulang dan penundaan antara pengiriman untuk mengurangi kemungkinan pesan masuk ke DLQ (hampir tidak mungkin untuk menghitung parameter optimal, tetapi Anda dapat bertindak secara empiris dan menyesuaikannya selama operasi);
  • penyimpanan DLQ harus terus dipantau, dan sistem pemantauan harus memberi tahu administrator sistem sehingga mereka dapat merespons secepat mungkin saat muncul pesan yang tidak terkirim. Ini akan mengurangi "zona kerusakan" dari kegagalan atau kesalahan logika bisnis;
  • bus integrasi harus peka terhadap ketidakhadiran sementara aplikasi: langganan topik harus tahan lama, dan nama domain aplikasi harus unik sehingga orang lain tidak mencoba memproses pesannya dari antrian selama aplikasi tidak ada.

Memastikan keamanan utas logika bisnis

Instance yang sama dari proses bisnis dapat menerima beberapa pesan dan acara sekaligus, yang pemrosesannya akan dimulai secara paralel. Pada saat yang sama, untuk pengembang aplikasi, semuanya harus sederhana dan aman untuk thread.

Logika bisnis proses memproses setiap peristiwa eksternal yang memengaruhi proses bisnis ini secara individual. Peristiwa ini dapat berupa:

  • meluncurkan contoh proses bisnis;
  • tindakan pengguna yang terkait dengan aktivitas dalam proses bisnis;
  • penerimaan pesan atau sinyal yang berlangganan contoh proses bisnis;
  • kedaluwarsa pengatur waktu yang ditetapkan oleh instans proses bisnis;
  • tindakan kontrol melalui API (mis. proses dibatalkan).

Setiap peristiwa tersebut dapat mengubah status instance proses bisnis: beberapa aktivitas dapat berakhir dan yang lainnya dimulai, nilai properti persisten dapat berubah. Menutup aktivitas apa pun dapat mengakibatkan aktivasi satu atau beberapa aktivitas berikut. Mereka, pada gilirannya, dapat berhenti menunggu acara lain, atau, jika mereka tidak memerlukan data tambahan, mereka dapat menyelesaikan transaksi yang sama. Sebelum menutup transaksi, keadaan baru dari proses bisnis disimpan dalam database, di mana ia akan menunggu kejadian eksternal berikutnya.

Data proses bisnis yang persisten yang disimpan dalam database relasional adalah titik sinkronisasi pemrosesan yang sangat nyaman saat menggunakan SELECT FOR UPDATE. Jika satu transaksi berhasil mendapatkan keadaan proses bisnis dari database untuk mengubahnya, maka tidak ada transaksi lain secara paralel yang bisa mendapatkan keadaan yang sama untuk perubahan lain, dan setelah selesainya transaksi pertama, yang kedua adalah dijamin untuk menerima keadaan yang sudah diubah.

Menggunakan kunci pesimis di sisi DBMS, kami memenuhi semua persyaratan yang diperlukan ACID, dan juga mempertahankan kemampuan untuk menskalakan aplikasi dengan logika bisnis dengan meningkatkan jumlah instans yang berjalan.

Namun, kunci pesimistis mengancam kita dengan kebuntuan, yang berarti bahwa SELECT FOR UPDATE masih harus dibatasi pada batas waktu yang masuk akal jika terjadi kebuntuan pada beberapa kasus mengerikan dalam logika bisnis.

Masalah lainnya adalah sinkronisasi awal proses bisnis. Meskipun tidak ada instance proses bisnis, tidak ada status dalam database juga, sehingga metode yang dijelaskan tidak akan berfungsi. Jika Anda ingin memastikan keunikan instance proses bisnis dalam lingkup tertentu, maka Anda memerlukan semacam objek sinkronisasi yang terkait dengan kelas proses dan cakupan terkait. Untuk mengatasi masalah ini, kami menggunakan mekanisme penguncian berbeda yang memungkinkan kami mengunci sumber daya arbitrer yang ditentukan oleh kunci dalam format URI melalui layanan eksternal.

Dalam contoh kami, proses bisnis InitialPlayer berisi deklarasi

uniqueConstraint = UniqueConstraints.singleton

Oleh karena itu, log berisi pesan tentang pengambilan dan pelepasan kunci dari kunci yang sesuai. Tidak ada pesan seperti itu untuk proses bisnis lainnya: uniqueConstraint is not set.

Masalah proses bisnis dengan status persisten

Terkadang memiliki kondisi yang gigih tidak hanya membantu, tetapi juga sangat menghambat perkembangan.
Masalah dimulai saat Anda perlu melakukan perubahan pada logika bisnis dan/atau model proses bisnis. Tidak ada perubahan seperti itu yang ditemukan kompatibel dengan proses bisnis lama. Jika ada banyak instance "live" di database, membuat perubahan yang tidak kompatibel dapat menyebabkan banyak masalah, yang sering kita temui saat menggunakan jBPM.

Bergantung pada kedalaman perubahan, Anda dapat bertindak dengan dua cara:

  1. buat jenis proses bisnis baru agar tidak membuat perubahan yang tidak kompatibel dengan yang lama, dan gunakan itu sebagai pengganti yang lama saat memulai instance baru. Mesin virtual lama akan terus berfungsi "dengan cara lama";
  2. memigrasi status proses bisnis yang persisten saat memperbarui logika bisnis.

Cara pertama lebih sederhana, namun memiliki keterbatasan dan kekurangan, misalnya:

  • duplikasi logika bisnis dalam banyak model proses bisnis, peningkatan volume logika bisnis;
  • seringkali diperlukan transisi instan ke logika bisnis baru (hampir selalu dalam hal tugas integrasi);
  • pengembang tidak tahu pada titik mana model yang sudah usang dapat dihapus.

Dalam praktiknya, kami menggunakan kedua pendekatan tersebut, tetapi telah membuat sejumlah keputusan untuk menyederhanakan hidup kami:

  • dalam database, status persisten dari proses bisnis disimpan dalam bentuk yang mudah dibaca dan diproses dengan mudah: dalam string format JSON. Ini memungkinkan Anda untuk melakukan migrasi baik di dalam maupun di luar aplikasi. Dalam kasus ekstrim, Anda juga dapat men-tweaknya dengan pegangan (terutama berguna dalam pengembangan selama proses debug);
  • logika bisnis integrasi tidak menggunakan nama proses bisnis, sehingga kapan saja dimungkinkan untuk mengganti implementasi salah satu proses yang berpartisipasi dengan yang baru, dengan nama baru (misalnya, "InitialPlayerV2"). Pengikatan terjadi melalui nama pesan dan sinyal;
  • model proses memiliki nomor versi, yang kami tingkatkan jika kami membuat perubahan yang tidak kompatibel pada model ini, dan nomor ini disimpan bersama status instance proses;
  • status proses yang persisten dibaca dari basis terlebih dahulu menjadi model objek yang mudah digunakan yang dapat digunakan oleh prosedur migrasi jika nomor versi model telah berubah;
  • prosedur migrasi ditempatkan di sebelah logika bisnis dan disebut "malas" untuk setiap contoh proses bisnis pada saat pemulihannya dari database;
  • jika Anda perlu memigrasi status semua instance proses dengan cepat dan sinkron, lebih banyak solusi migrasi database klasik digunakan, tetapi Anda harus bekerja dengan JSON di sana.

Apakah saya memerlukan kerangka kerja lain untuk proses bisnis?

Solusi yang dijelaskan dalam artikel memungkinkan kami untuk menyederhanakan hidup kami secara signifikan, memperluas jangkauan masalah yang diselesaikan pada tingkat pengembangan aplikasi, dan membuat gagasan memisahkan logika bisnis menjadi layanan mikro menjadi lebih menarik. Untuk ini, banyak pekerjaan telah dilakukan, kerangka kerja proses bisnis yang sangat "ringan" telah dibuat, serta komponen layanan untuk memecahkan masalah yang teridentifikasi dalam konteks berbagai tugas yang diterapkan. Kami memiliki keinginan untuk membagikan hasil ini, untuk membawa pengembangan komponen umum ke dalam akses terbuka di bawah lisensi gratis. Ini akan membutuhkan usaha dan waktu. Memahami permintaan akan solusi semacam itu bisa menjadi insentif tambahan bagi kami. Dalam artikel yang diusulkan, sangat sedikit perhatian diberikan pada kemampuan kerangka kerja itu sendiri, tetapi beberapa di antaranya terlihat dari contoh yang disajikan. Namun jika kami menerbitkan kerangka kerja kami, artikel terpisah akan dikhususkan untuk itu. Sementara itu, kami akan berterima kasih jika Anda memberikan sedikit masukan dengan menjawab pertanyaan:

Hanya pengguna terdaftar yang dapat berpartisipasi dalam survei. Masuk, silakan.

Apakah saya memerlukan kerangka kerja lain untuk proses bisnis?

  • 18,8%Ya, saya sudah lama mencari sesuatu seperti ini.

  • 12,5%menarik untuk mempelajari lebih lanjut tentang penerapan Anda, semoga bermanfaat2

  • 6,2%kami menggunakan salah satu kerangka kerja yang ada, tetapi kami berpikir untuk menggantinya1

  • 18,8%kami menggunakan salah satu framework yang ada, semuanya cocok3

  • 18,8%mengatasi tanpa kerangka3

  • 25,0%tulis sendiri4

16 pengguna memilih. 7 pengguna abstain.

Sumber: www.habr.com

Tambah komentar