BPM tarzı entegrasyon

BPM tarzı entegrasyon

Merhaba, Habr!

Şirketimiz, aslan payının büyük miktarda iş mantığına ve EDMS gibi iş akışına sahip işlemsel sistemler tarafından işgal edildiği ERP sınıfı yazılım çözümlerinin geliştirilmesinde uzmanlaşmıştır. Ürünlerimizin modern sürümleri JavaEE teknolojilerine dayanmaktadır, ancak aynı zamanda aktif olarak mikro hizmetler üzerinde deneyler yapıyoruz. Bu tür çözümlerin en sorunlu alanlarından biri, bitişik alan adlarıyla ilgili çeşitli alt sistemlerin entegrasyonudur. Entegrasyon görevleri, kullandığımız mimari stiller, teknoloji yığınları ve çerçeveler ne olursa olsun, başımıza her zaman büyük bir baş ağrısı vermiştir, ancak son zamanlarda bu tür sorunların çözümünde ilerleme kaydedilmiştir.

Dikkatinize sunulan yazıda, NPO Krista'nın belirlenen alandaki deneyiminden ve mimari araştırmalarından bahsedeceğim. Ayrıca bir uygulama geliştiricinin bakış açısından bir entegrasyon sorununa basit bir çözüm örneği ele alacağız ve bu basitliğin arkasında nelerin saklı olduğunu öğreneceğiz.

sorumluluk reddi

Makalede açıklanan mimari ve teknik çözümler, belirli görevler bağlamında kişisel deneyime dayanarak tarafımdan sunulmaktadır. Bu çözümler evrensel olma iddiasında değildir ve diğer kullanım koşulları altında optimal olmayabilir.

BPM'nin bununla ne ilgisi var?

Bu soruyu cevaplamak için, çözümlerimizin uygulanan problemlerinin özelliklerini biraz araştırmamız gerekiyor. Tipik işlem sistemimizde iş mantığının ana kısmı, kullanıcı arayüzleri aracılığıyla veritabanına veri girmek, bu verileri manuel ve otomatik olarak kontrol etmek, bazı iş akışlarından geçirmek, başka bir sisteme/analitik veritabanına/arşive yayınlamak, raporlar oluşturmaktır. Bu nedenle, müşteriler için sistemin temel işlevi, iç iş süreçlerinin otomasyonudur.

Kolaylık sağlamak için, iletişimde "belge" terimini, belirli bir iş akışının "iliştirilebileceği" ortak bir anahtarla birleştirilmiş bir veri kümesinin soyutlaması olarak kullanıyoruz.
Peki ya entegrasyon mantığı? Sonuçta, entegrasyon görevi, müşterinin talebi üzerine DEĞİL, ancak tamamen farklı faktörlerin etkisi altında parçalara "kesilen" sistem mimarisi tarafından üretilir:

  • Conway yasasının etkisi altında;
  • daha önce başka ürünler için geliştirilen alt sistemlerin yeniden kullanılması sonucunda;
  • işlevsel olmayan gereksinimlere dayalı olarak mimar tarafından kararlaştırıldığı gibi.

İş mantığını entegrasyon yapıtlarıyla kirletmemek ve uygulama geliştiriciyi sistemin mimari manzarasının özelliklerini araştırmaktan kurtarmak için entegrasyon mantığını ana iş akışının iş mantığından ayırmanın büyük bir cazibesi var. Bu yaklaşımın bir takım avantajları vardır, ancak uygulama verimsizliğini göstermektedir:

  • entegrasyon problemlerinin çözümü, ana iş akışının uygulanmasındaki sınırlı uzantı noktaları nedeniyle genellikle senkronize çağrılar biçimindeki en basit seçeneklere doğru kayar (aşağıda senkronize entegrasyonun eksiklikleri hakkında daha fazla bilgi);
  • entegrasyon yapıları, başka bir alt sistemden geri bildirim gerektiğinde hala ana iş mantığına nüfuz eder;
  • uygulama geliştiricisi entegrasyonu yok sayar ve iş akışını değiştirerek kolayca bozabilir;
  • sistem, kullanıcının bakış açısından tek bir bütün olmaktan çıkar, alt sistemler arasındaki "dikişler" fark edilir hale gelir, bir alt sistemden diğerine veri aktarımını başlatan gereksiz kullanıcı işlemleri ortaya çıkar.

Başka bir yaklaşım, entegrasyon etkileşimlerini temel iş mantığının ve iş akışının ayrılmaz bir parçası olarak değerlendirmektir. Uygulama geliştiricilerin beceri gereksinimlerinin hızla artmasını önlemek için, yeni entegrasyon etkileşimleri oluşturmak, bir çözüm seçmek için minimum seçenekle kolayca ve doğal bir şekilde yapılmalıdır. Bu göründüğünden daha zordur: Alet, kullanıcıya kullanımı için gerekli çeşitli seçenekleri sağlayacak kadar güçlü olmalı ve aynı zamanda ayağından vurulmasına izin vermemelidir. Bir mühendisin entegrasyon görevleri bağlamında cevaplaması gereken, ancak bir uygulama geliştiricinin günlük işlerinde düşünmemesi gereken birçok soru vardır: işlem sınırları, tutarlılık, atomiklik, güvenlik, ölçeklendirme, yük ve kaynak dağıtımı, yönlendirme, sıralama, yayma ve anahtarlama bağlamları, vb. Uygulama geliştiricilerine, bu tür tüm soruların yanıtlarının zaten gizlendiği oldukça basit karar şablonları sunmak gerekir. Bu kalıplar yeterince güvenli olmalıdır: iş mantığı çok sık değişir, bu da hata verme riskini artırır, hataların maliyeti oldukça düşük bir seviyede kalmalıdır.

Ama yine de, BPM'nin bununla ne ilgisi var? İş akışını uygulamak için pek çok seçenek var...
Gerçekten de, durum geçiş şemasının bildirimsel ayarı ve işleyicileri iş mantığıyla geçişlere bağlama yoluyla, iş süreçlerinin başka bir uygulaması çözümlerimizde çok popülerdir. Aynı zamanda "belgenin" iş sürecindeki mevcut konumunu belirleyen durum, "belgenin" kendisinin bir niteliğidir.

BPM tarzı entegrasyon
Proje başlangıcında süreç böyle görünüyor.

Böyle bir uygulamanın popülaritesi, doğrusal iş süreçleri oluşturmanın göreceli basitliği ve hızından kaynaklanmaktadır. Bununla birlikte, yazılım sistemleri daha karmaşık hale geldikçe, iş sürecinin otomatikleştirilmiş kısmı büyür ve daha karmaşık hale gelir. Her dalın paralel olarak yürütülmesi için ayrışmaya, süreçlerin parçalarının yeniden kullanılmasına ve çatallanma süreçlerine ihtiyaç vardır. Bu koşullar altında, araç elverişsiz hale gelir ve durum geçiş diyagramı bilgi içeriğini kaybeder (entegrasyon etkileşimleri diyagrama hiç yansıtılmaz).

BPM tarzı entegrasyon
Gereksinimleri açıklığa kavuşturmak için birkaç yinelemeden sonra süreç böyle görünüyor

Bu durumdan çıkış yolu motorun entegrasyonuydu. jBPM en karmaşık iş süreçlerine sahip bazı ürünlere dönüştürür. Kısa vadede bu çözüm bir miktar başarılı oldu: Gösterimde oldukça bilgilendirici ve güncel bir diyagramı korurken karmaşık iş süreçlerini uygulamak mümkün hale geldi. BPMN2.

BPM tarzı entegrasyon
Karmaşık bir iş sürecinin küçük bir parçası

Uzun vadede, çözüm beklentileri karşılamadı: görsel araçlar aracılığıyla iş süreçleri oluşturmanın yüksek emek yoğunluğu, kabul edilebilir üretkenlik göstergelerine ulaşılmasına izin vermedi ve aracın kendisi, geliştiriciler arasında en sevilmeyen araçlardan biri oldu. Ayrıca motorun iç yapısıyla ilgili şikayetler vardı ve bu da birçok "yamanın" ve "koltuk değneğinin" ortaya çıkmasına neden oldu.

jBPM kullanmanın ana olumlu yönü, bir iş süreci eşgörünümü için kendi kalıcı durumuna sahip olmanın yararlarının ve zararlarının fark edilmesiydi. Sinyaller ve mesajlar yoluyla eşzamansız etkileşimler kullanarak farklı uygulamalar arasında karmaşık entegrasyon protokollerini uygulamak için bir süreç yaklaşımı kullanma olasılığını da gördük. Kalıcı bir durumun varlığı bunda çok önemli bir rol oynar.

Yukarıdakilere dayanarak, şu sonuca varabiliriz: BPM tarzındaki süreç yaklaşımı, her zamankinden daha karmaşık iş süreçlerini otomatikleştirmek için çok çeşitli görevleri çözmemize, entegrasyon faaliyetlerini bu süreçlere uyumlu bir şekilde sığdırmamıza ve uygulanan süreci görsel olarak uygun bir gösterimde gösterme yeteneğimizi korumamıza olanak tanır.

Bir entegrasyon kalıbı olarak senkronize çağrıların dezavantajları

Eşzamanlı entegrasyon, en basit engelleme çağrısını ifade eder. Bir alt sistem, sunucu tarafı gibi davranır ve API'yi istenen yöntemle kullanıma sunar. Başka bir alt sistem, müşteri tarafı olarak hareket eder ve doğru zamanda bir sonuç beklentisiyle arama yapar. İstemci ve sunucu tarafları, sistemin mimarisine bağlı olarak aynı uygulama ve süreçte veya farklı süreçlerde barındırılabilir. İkinci durumda, bazı RPC uygulamalarını uygulamanız ve parametrelerin sıralanmasını ve aramanın sonucunu sağlamanız gerekir.

BPM tarzı entegrasyon

Bu tür bir entegrasyon modelinin oldukça büyük bir dizi dezavantajı vardır, ancak basitliği nedeniyle pratikte çok yaygın olarak kullanılmaktadır. Uygulama hızı büyülüyor ve son teslim tarihlerini "yakma" koşullarında, çözümü teknik borca ​​​​yazarak tekrar tekrar uygulamanıza neden oluyor. Ancak deneyimsiz geliştiricilerin, olumsuz sonuçları fark etmeden bilinçsizce kullandıkları da olur.

Alt sistemlerin bağlanabilirliğindeki en belirgin artışa ek olarak, "yayma" ve "germe" işlemlerinde daha az belirgin sorunlar vardır. Aslında, iş mantığı herhangi bir değişiklik yaparsa, işlemler vazgeçilmezdir ve işlemler de bu değişikliklerden etkilenen belirli uygulama kaynaklarını kilitler. Yani, bir alt sistem diğerinden yanıt beklemedikçe işlemi tamamlayamaz ve kilitleri serbest bırakamaz. Bu, çeşitli etkilerin riskini önemli ölçüde artırır:

  • sistem yanıtı kaybolur, kullanıcılar isteklere yanıt vermek için uzun süre bekler;
  • sunucu, taşan iş parçacığı havuzu nedeniyle genellikle kullanıcı isteklerine yanıt vermeyi durdurur: iş parçacıklarının çoğu, işlem tarafından kullanılan kaynağın kilidinde "durur";
  • kilitlenmeler ortaya çıkmaya başlar: meydana gelme olasılıkları büyük ölçüde işlemlerin süresine, işlemde yer alan iş mantığının ve kilitlerin miktarına bağlıdır;
  • işlem zaman aşımı sona erme hataları görünür;
  • görev büyük miktarda verinin işlenmesini ve değiştirilmesini gerektiriyorsa sunucu OutOfMemory'ye "düşer" ve eşzamanlı entegrasyonların varlığı, işlemi "daha hafif" işlemlere bölmeyi çok zorlaştırır.

Mimari açıdan bakıldığında, entegrasyon sırasında engelleme çağrılarının kullanılması, bireysel alt sistemlerin kalite kontrolünün kaybedilmesine yol açar: bir alt sistemin kalite hedeflerini, başka bir alt sistemin kalite hedeflerinden ayrı olarak sağlamak imkansızdır. Alt sistemler farklı ekipler tarafından geliştiriliyorsa bu büyük bir problemdir.

Entegre edilmekte olan alt sistemler farklı uygulamalarda ise ve her iki tarafta da senkron değişiklikler yapılması gerekiyorsa işler daha da ilginçleşiyor. Bu değişiklikler işlemsel olarak nasıl yapılır?

Değişiklikler ayrı işlemlerde yapılırsa, sağlam istisna işleme ve tazminatın sağlanması gerekecektir ve bu, senkronize entegrasyonların ana avantajı olan basitliği tamamen ortadan kaldırır.

Dağıtılmış işlemler de akla geliyor, ancak bunları çözümlerimizde kullanmıyoruz: güvenilirliği sağlamak zordur.

İşlem sorununa bir çözüm olarak "Saga"

Mikro hizmetlerin artan popülaritesi ile birlikte, destan desen.

Bu model, yukarıdaki uzun işlemler sorunlarını mükemmel bir şekilde çözer ve aynı zamanda sistemin durumunu iş mantığı açısından yönetme olanaklarını genişletir: başarısız bir işlemden sonra tazminat, sistemi orijinal durumuna geri döndürmeyebilir, ancak bir alternatif sağlayabilir. veri işleme yolu. Ayrıca, süreci “iyi” bir sona getirmeye çalıştığınızda, başarıyla tamamlanan veri işleme adımlarını tekrarlamamanızı da sağlar.

İlginç bir şekilde, yekpare sistemlerde bu model, gevşek bağlı alt sistemlerin entegrasyonu söz konusu olduğunda da geçerlidir ve uzun işlemlerin ve karşılık gelen kaynak kilitlerinin neden olduğu olumsuz etkiler vardır.

BPM tarzındaki iş süreçlerimizle ilgili olarak, Saga'ları uygulamak çok kolay görünüyor: Saga'ların bireysel adımları, iş süreci içindeki faaliyetler olarak ayarlanabilir ve diğer şeylerin yanı sıra iş sürecinin kalıcı durumu belirlenir. , Sagaların iç durumu. Yani herhangi bir ek koordinasyon mekanizmasına ihtiyacımız yok. İhtiyacınız olan tek şey, taşıma olarak "en az bir kez" garantileri destekleyen bir mesaj komisyoncusu.

Ancak böyle bir çözümün de kendi "fiyatı" vardır:

  • iş mantığı daha karmaşık hale gelir: tazminat hesaplamanız gerekir;
  • monolitik sistemler için özellikle hassas olabilecek tam tutarlılıktan vazgeçmek gerekecektir;
  • mimari biraz daha karmaşık hale gelir, bir mesaj simsarı için ek bir ihtiyaç vardır;
  • ek izleme ve yönetim araçları gerekecektir (genel olarak bu bile iyi olsa da: sistem hizmetinin kalitesi artacaktır).

Monolitik sistemler için "Sarkmalar" kullanmanın gerekçesi o kadar açık değildir. Büyük olasılıkla zaten bir aracının olduğu ve projenin başlangıcında tam tutarlılığın feda edildiği mikro hizmetler ve diğer SOA'lar için, bu modeli kullanmanın faydaları, özellikle de uygun bir API varsa, dezavantajlardan önemli ölçüde ağır basabilir. iş mantığı seviyesi.

Mikro hizmetlerde iş mantığını kapsülleme

Mikro hizmetleri denemeye başladığımızda makul bir soru ortaya çıktı: etki alanı iş mantığını, etki alanı verilerinin kalıcılığını sağlayan hizmetle ilgili olarak nereye koymalı?

Çeşitli BPMS'lerin mimarisine bakıldığında, iş mantığını kalıcılıktan ayırmak makul görünebilir: etki alanı iş mantığını yürütmek için ortamı ve kapsayıcıyı oluşturan bir platform ve etki alanından bağımsız mikro hizmetler katmanı oluşturun ve etki alanı verileri kalıcılığını ayrı bir sistem olarak düzenleyin. çok basit ve hafif mikro hizmetler katmanı. Bu durumda iş süreçleri, kalıcılık katmanının hizmetlerini düzenler.

BPM tarzı entegrasyon

Bu yaklaşımın çok büyük bir artısı var: platformun işlevselliğini istediğiniz kadar artırabilirsiniz ve bundan yalnızca ilgili platform mikro hizmetleri katmanı "şişmanlanır". Herhangi bir etki alanındaki iş süreçleri, platformun yeni işlevselliğini güncellenir güncellenmez hemen kullanma fırsatı yakalar.

Daha ayrıntılı bir çalışma, bu yaklaşımın önemli eksikliklerini ortaya çıkardı:

  • birçok alanın iş mantığını aynı anda yürüten bir platform hizmeti, tek hata noktası olarak büyük riskler taşır. İş mantığında sık sık yapılan değişiklikler, hataların sistem genelinde arızalara yol açma riskini artırır;
  • performans sorunları: iş mantığı, verileriyle dar ve yavaş bir arayüz aracılığıyla çalışır:
    • veriler bir kez daha sıralanacak ve ağ yığını boyunca pompalanacak;
    • etki alanı hizmeti, hizmetin harici API düzeyinde yetersiz sorgu parametrelendirme yetenekleri nedeniyle genellikle iş mantığının işlemek için gerektirdiğinden daha fazla veri döndürür;
    • birkaç bağımsız iş mantığı, aynı verileri işlenmek üzere tekrar tekrar talep edebilir (verileri önbelleğe alan oturum çekirdekleri ekleyerek bu sorunu azaltabilirsiniz, ancak bu, mimariyi daha da karmaşık hale getirir ve veri tazeliği ve önbellek geçersiz kılma sorunları yaratır);
  • işlem sorunları:
    • platform hizmeti tarafından depolanan kalıcı duruma sahip iş süreçleri, etki alanı verileriyle tutarsızdır ve bu sorunu çözmenin kolay bir yolu yoktur;
    • etki alanı verilerinin kilidini işlem dışına taşımak: etki alanı iş mantığının değişiklik yapması gerekiyorsa, önce gerçek verilerin doğruluğunu kontrol ettikten sonra, işlenen verilerde rekabetçi bir değişiklik olasılığını dışlamak gerekir. Verilerin harici olarak bloke edilmesi sorunun çözülmesine yardımcı olabilir, ancak böyle bir çözüm ek riskler taşır ve sistemin genel güvenilirliğini azaltır;
  • güncelleme sırasında ek zorluklar: bazı durumlarda, kalıcılık hizmetini ve iş mantığını eşzamanlı olarak veya kesin bir sırayla güncellemeniz gerekir.

Sonunda, temel bilgilere geri dönmek zorunda kaldım: etki alanı verilerini ve etki alanı iş mantığını tek bir mikro hizmette kapsülleyin. Bu yaklaşım, mikro hizmetin sistemin ayrılmaz bir parçası olarak algılanmasını basitleştirir ve yukarıdaki sorunlara yol açmaz. Bu da ücretsiz değil:

  • İş mantığı (özellikle iş süreçlerinin bir parçası olarak kullanıcı etkinlikleri sağlamak için) ve API platform hizmetleri ile etkileşim için API standardizasyonu gereklidir; API değişikliklerine daha dikkatli dikkat edilmelidir; ileri ve geri uyumluluk gereklidir;
  • iş mantığının bu tür her bir mikro hizmetin parçası olarak çalışmasını sağlamak için ek çalışma zamanı kitaplıkları eklemek gerekir ve bu, bu tür kitaplıklar için yeni gereksinimlere yol açar: hafiflik ve minimum geçiş bağımlılıkları;
  • iş mantığı geliştiricilerinin kitaplık sürümlerini takip etmesi gerekir: Bir mikro hizmet uzun süredir sonlandırılmamışsa, büyük olasılıkla kitaplıkların eski bir sürümünü içerecektir. Bu, yeni bir özellik eklemenin önünde beklenmedik bir engel olabilir ve sürümler arasında uyumsuz değişiklikler varsa, böyle bir hizmetin eski iş mantığının kitaplıkların yeni sürümlerine taşınmasını gerektirebilir.

BPM tarzı entegrasyon

Bu tür bir mimaride bir platform hizmetleri katmanı da mevcuttur, ancak bu katman artık etki alanı iş mantığını yürütmek için bir kapsayıcı oluşturmaz, yalnızca yardımcı "platform" işlevleri sağlayan ortamını oluşturur. Böyle bir katman, yalnızca etki alanı mikro hizmetlerinin hafifliğini korumak için değil, aynı zamanda yönetimi merkezileştirmek için de gereklidir.

Örneğin, iş süreçlerindeki kullanıcı aktiviteleri görevler üretir. Bununla birlikte, görevlerle çalışırken, kullanıcının genel listedeki tüm etki alanlarındaki görevleri görmesi gerekir; bu, etki alanı iş mantığından arındırılmış uygun bir görev kayıt platformu hizmeti olması gerektiği anlamına gelir. İş mantığının kapsüllenmesini bu bağlamda tutmak oldukça sorunlu ve bu, bu mimarinin başka bir uzlaşmasıdır.

Bir uygulama geliştiricinin gözünden iş süreçlerinin entegrasyonu

Yukarıda belirtildiği gibi, uygulama geliştiricisinin, iyi bir geliştirme verimliliğine güvenebilmesi için çeşitli uygulamaların etkileşiminin uygulanmasının teknik ve mühendislik özelliklerinden soyutlanması gerekir.

Makale için özel olarak icat edilen oldukça zor bir entegrasyon problemini çözmeye çalışalım. Bu, her birinin bir alan adını tanımladığı üç uygulamayı içeren bir "oyun" görevi olacaktır: "uygulama1", "uygulama2", "uygulama3".

Her uygulamanın içinde, entegrasyon veriyolu aracılığıyla "top oynamaya" başlayan iş süreçleri başlatılır. "Top" adlı mesajlar top olarak hareket edecektir.

Oyunun Kuralları:

  • ilk oyuncu başlatıcıdır. Diğer oyuncuları oyuna davet eder, oyunu başlatır ve her an bitirebilir;
  • diğer oyuncular oyuna katıldıklarını, birbirleriyle ve ilk oyuncuyla "tanıştıklarını" beyan ederler;
  • oyuncu topu aldıktan sonra başka bir katılımcı oyuncu seçer ve topu ona verir. Toplam geçiş sayısı sayılır;
  • her oyuncunun "enerjisi" vardır ve bu, o oyuncu tarafından topa her geçişte azalır. Enerji bittiğinde oyuncu oyundan elenir ve emekli olduğunu duyurur;
  • oyuncu yalnız kalırsa ayrıldığını hemen beyan eder;
  • tüm oyuncular elendiğinde, ilk oyuncu oyunun bittiğini ilan eder. Oyundan daha önce ayrıldıysa, oyunu tamamlamak için oyunu takip etmeye devam eder.

Bu sorunu çözmek için, Kotlin'deki mantığı en az bir kalıpla kompakt bir şekilde tanımlamanıza izin veren iş süreçleri için DSL'imizi kullanacağım.

app1 uygulamasında, ilk oyuncunun (aynı zamanda oyunun başlatıcısıdır) iş süreci çalışacaktır:

sınıf Başlangıç ​​Oyuncusu

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}")
}

İş mantığını yürütmeye ek olarak, yukarıdaki kod bir diyagram olarak görselleştirilebilen bir iş sürecinin nesne modelini üretebilir. Görselleştiriciyi henüz uygulamadık, bu yüzden çizim yapmak için biraz zaman harcamak zorunda kaldık (burada, diyagramın yukarıdaki kodla tutarlılığını artırmak için kapıların kullanımına ilişkin BPMN gösterimini biraz basitleştirdim):

BPM tarzı entegrasyon

app2, başka bir oyuncunun iş sürecini içerecektir:

sınıf 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}")
}

Diyagram:

BPM tarzı entegrasyon

App3 uygulamasında, oyuncuyu biraz farklı bir davranışa sahip hale getireceğiz: Sıradaki oyuncuyu rastgele seçmek yerine, round-robin algoritmasına göre hareket edecek:

sınıf 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}")
}

Aksi takdirde, oyuncunun davranışı bir öncekinden farklı değildir, bu nedenle diyagram değişmez.

Şimdi hepsini çalıştırmak için bir teste ihtiyacımız var. Makaleyi bir şablonla karıştırmamak için yalnızca testin kodunu vereceğim (aslında, diğer iş süreçlerinin entegrasyonunu test etmek için daha önce oluşturulan test ortamını kullandım):

testOyunu()

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

Testi çalıştırın, günlüğe bakın:

konsol çıkışı

Взята блокировка ключа 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!

Bütün bunlardan birkaç önemli sonuç çıkarılabilir:

  • gerekli araçlar mevcutsa, uygulama geliştiriciler iş mantığından kopmadan uygulamalar arasında entegrasyon etkileşimleri oluşturabilir;
  • mühendislik yetkinlikleri gerektiren bir entegrasyon görevinin karmaşıklığı (karmaşıklığı), başlangıçta çerçevenin mimarisinde ortaya konmuşsa çerçevenin içinde gizlenebilir. Görevin zorluğu (zorluğu) gizlenemez, bu nedenle zor bir görevin çözümü kodda buna göre görünecektir;
  • Entegrasyon mantığını geliştirirken, tüm entegrasyon katılımcılarının durum değişikliğinin sonunda tutarlılığını ve lineerleştirilemezliğini hesaba katmak gerekir. Bu, mantığı dış olayların oluş sırasına karşı duyarsız hale getirmek için karmaşıklaştırmaya zorlar bizi. Örneğimizde, oyuncu oyundan çıktığını duyurduktan sonra oyuna katılmaya zorlanır: diğer oyuncular, çıkışıyla ilgili bilgi tüm katılımcılara ulaşana ve onlar tarafından işlenene kadar ona pas vermeye devam edecektir. Bu mantık, oyunun kurallarından kaynaklanmaz ve seçilen mimari çerçevesinde uzlaşmacı bir çözümdür.

Ardından, çözümümüzün çeşitli inceliklerinden, uzlaşmalardan ve diğer noktalardan bahsedelim.

Tüm iletiler bir kuyrukta

Tüm entegre uygulamalar, harici bir aracı, mesajlar için bir BPMQueue ve sinyaller (olaylar) için bir BPMTopic konusu olarak temsil edilen bir entegrasyon veri yolu ile çalışır. Tüm iletileri tek bir kuyruktan geçirmek başlı başına bir tavizdir. İş mantığı seviyesinde artık sistem yapısında değişiklik yapmadan istediğiniz kadar yeni mesaj türü sunabilirsiniz. Bu önemli bir basitleştirmedir, ancak tipik görevlerimiz bağlamında bize çok önemli görünmeyen belirli riskler taşır.

BPM tarzı entegrasyon

Ancak burada bir incelik vardır: her uygulama, "kendi" mesajlarını girişteki kuyruktan etki alanı adına göre filtreler. Ayrıca, sinyalin "kapsamını" tek bir uygulamayla sınırlamanız gerekirse, etki alanı sinyallerde belirtilebilir. Bu, veri yolunun bant genişliğini artırmalıdır, ancak iş mantığı artık etki alanı adlarıyla çalışmalıdır: mesajların adreslenmesi için zorunlu, sinyaller için arzu edilir.

Entegrasyon veri yolunun güvenilirliğini sağlama

Güvenilirlik birkaç şeyden oluşur:

  • Seçilen mesaj aracısı, mimarinin kritik bir bileşenidir ve tek hata noktasıdır: Yeterince hataya dayanıklı olmalıdır. Yalnızca iyi bir desteğe ve geniş bir topluluğa sahip, zaman içinde test edilmiş uygulamaları kullanmalısınız;
  • tümleşik uygulamalardan fiziksel olarak ayrılması gereken mesaj simsarının yüksek kullanılabilirliğini sağlamak gerekir (uygulanan iş mantığına sahip uygulamaların yüksek kullanılabilirliğini sağlamak çok daha zor ve pahalıdır);
  • komisyoncu "en az bir kez" teslimat garantisi vermekle yükümlüdür. Bu, entegrasyon veriyolunun güvenilir çalışması için zorunlu bir gerekliliktir. "Tam olarak bir kez" düzeyindeki garantilere gerek yoktur: iş süreçleri genellikle mesajların veya olayların tekrar tekrar gelmesine duyarlı değildir ve bunun önemli olduğu özel görevlerde iş mantığına ek kontroller eklemek, sürekli kullanmaktan daha kolaydır. oldukça "pahalı" "garantiler;
  • mesajların ve sinyallerin gönderilmesi, iş süreçlerinin ve etki alanı verilerinin durumundaki bir değişiklikle ortak bir işlemde yer almalıdır. Tercih edilen seçenek deseni kullanmak olacaktır. İşlem Giden Kutusu, ancak veritabanında ek bir tablo ve bir geçiş gerektirecektir. JEE uygulamalarında bu, yerel bir JTA yöneticisi kullanılarak basitleştirilebilir, ancak seçilen aracıya bağlantı modunda çalışabilmelidir. XA;
  • gelen mesajların ve olayların işleyicileri, iş sürecinin durumunu değiştirme işlemiyle de çalışmalıdır: böyle bir işlem geri alınırsa, mesajın alınması da iptal edilmelidir;
  • hatalar nedeniyle teslim edilemeyen mesajlar ayrı bir depoda saklanmalıdır D.L.Q. (Ölü Mektup Kuyruğu). Bunu yapmak için, bu tür iletileri deposunda depolayan, özniteliklerine göre dizine ekleyen (hızlı gruplama ve arama için) ve iletileri görüntüleme, hedef adrese yeniden gönderme ve silme için API'yi kullanıma sunan ayrı bir platform mikro hizmeti oluşturduk. Sistem yöneticileri, web arayüzleri aracılığıyla bu hizmetle çalışabilir;
  • Aracı ayarlarında, mesajların DLQ'ya girme olasılığını azaltmak için teslimat yeniden denemelerinin sayısını ve teslimatlar arasındaki gecikmeleri ayarlamanız gerekir (en uygun parametreleri hesaplamak neredeyse imkansızdır, ancak ampirik olarak hareket edebilir ve bunları sırasında ayarlayabilirsiniz. operasyon);
  • DLQ deposu sürekli olarak izlenmeli ve izleme sistemi, teslim edilmeyen mesajlar meydana geldiğinde mümkün olan en kısa sürede yanıt verebilmeleri için sistem yöneticilerini bilgilendirmelidir. Bu, bir arızanın veya iş mantığı hatasının "hasar bölgesini" azaltacaktır;
  • entegrasyon veriyolu, uygulamaların geçici yokluğuna karşı duyarsız olmalıdır: konu abonelikleri dayanıklı olmalı ve uygulamanın yokluğunda başka birinin sıradan mesajını işlemeye çalışmaması için uygulamanın alan adı benzersiz olmalıdır.

İş mantığının iş parçacığı güvenliğini sağlama

Bir iş sürecinin aynı örneği, işlenmesi paralel olarak başlayacak olan birkaç mesajı ve olayı aynı anda alabilir. Aynı zamanda, bir uygulama geliştiricisi için her şey basit ve güvenli olmalıdır.

Süreç iş mantığı, bu iş sürecini etkileyen her bir dış olayı ayrı ayrı işler. Bu olaylar şunlar olabilir:

  • bir iş süreci örneğinin başlatılması;
  • bir iş sürecindeki bir etkinlikle ilgili bir kullanıcı eylemi;
  • bir iş süreci örneğinin abone olduğu bir mesajın veya sinyalin alınması;
  • iş süreci örneği tarafından ayarlanan zamanlayıcının sona ermesi;
  • API aracılığıyla eylemi kontrol edin (ör. işlem iptali).

Bu tür her olay, bir iş süreci örneğinin durumunu değiştirebilir: bazı etkinlikler sona erebilir ve diğerleri başlayabilir, kalıcı özelliklerin değerleri değişebilir. Herhangi bir etkinliğin kapatılması, aşağıdaki etkinliklerden birinin veya daha fazlasının etkinleştirilmesine neden olabilir. Bunlar da diğer olayları beklemeyi bırakabilir veya herhangi bir ek veriye ihtiyaç duymazlarsa aynı işlemde tamamlayabilirler. İşlemi kapatmadan önce, iş sürecinin yeni durumu, bir sonraki harici olayı bekleyeceği veritabanında saklanır.

İlişkisel bir veritabanında saklanan kalıcı iş süreci verileri, GÜNCELLEME İÇİN SEÇ kullanılırken çok uygun bir işleme senkronizasyon noktasıdır. Bir işlem, iş sürecinin durumunu değiştirmek için veritabanından almayı başardıysa, paralel olarak başka hiçbir işlem başka bir değişiklik için aynı durumu alamayacaktır ve ilk işlemin tamamlanmasından sonra ikincisi Halihazırda değiştirilen durumu alma garantisi.

DBMS tarafında karamsar kilitler kullanarak gerekli tüm gereksinimleri yerine getiriyoruz ASİTve çalışan örneklerin sayısını artırarak uygulamayı iş mantığıyla ölçeklendirme yeteneğini de koruyun.

Bununla birlikte, kötümser kilitler bizi kilitlenmelerle tehdit eder; bu, iş mantığındaki bazı korkunç durumlarda kilitlenme olması durumunda GÜNCELLEME İÇİN SEÇ'in yine de makul bir zaman aşımı ile sınırlı olması gerektiği anlamına gelir.

Diğer bir sorun ise iş sürecinin başlangıcındaki senkronizasyondur. İş süreci örneği olmadığı gibi veritabanında da durum yoktur, bu nedenle açıklanan yöntem çalışmaz. Belirli bir kapsamdaki bir iş süreci örneğinin benzersizliğini sağlamak istiyorsanız, süreç sınıfı ve karşılık gelen kapsamla ilişkilendirilmiş bir tür senkronizasyon nesnesine ihtiyacınız vardır. Bu sorunu çözmek için, harici bir hizmet aracılığıyla URI biçiminde bir anahtarla belirtilen keyfi bir kaynağı kilitlememize izin veren farklı bir kilitleme mekanizması kullanıyoruz.

Örneklerimizde, InitialPlayer iş süreci bir bildirim içerir.

uniqueConstraint = UniqueConstraints.singleton

Bu nedenle günlük, ilgili anahtarın kilidinin alınması ve serbest bırakılmasıyla ilgili mesajlar içerir. Diğer iş süreçleri için böyle bir mesaj yok: uniqueConstraint ayarlanmadı.

Kalıcı durumla iş süreci sorunları

Bazen ısrarcı bir duruma sahip olmak sadece yardımcı olmakla kalmaz, aynı zamanda gelişimi gerçekten engeller.
İş mantığı ve/veya iş süreci modelinde değişiklik yapmanız gerektiğinde sorunlar başlar. Bu tür bir değişikliğin iş süreçlerinin eski hali ile uyumlu olduğu görülmemektedir. Veritabanında çok sayıda "canlı" örnek varsa, uyumsuz değişiklikler yapmak, jBPM'yi kullanırken sıklıkla karşılaştığımız birçok soruna neden olabilir.

Değişimin derinliğine bağlı olarak iki şekilde hareket edebilirsiniz:

  1. eskisiyle uyumsuz değişiklikler yapmamak için yeni bir iş süreci türü oluşturun ve yeni örnekleri başlatırken eskisi yerine onu kullanın. Eski örnekler "eski şekilde" çalışmaya devam edecek;
  2. iş mantığını güncellerken iş süreçlerinin kalıcı durumunu taşıyın.

İlk yol daha basittir, ancak sınırlamaları ve dezavantajları vardır, örneğin:

  • birçok iş süreci modelinde iş mantığının tekrarı, iş mantığı hacminde artış;
  • genellikle yeni bir iş mantığına anında geçiş gerekir (neredeyse her zaman entegrasyon görevleri açısından);
  • geliştirici, hangi noktada eski modelleri silmenin mümkün olduğunu bilmiyor.

Uygulamada her iki yaklaşımı da kullanıyoruz, ancak hayatımızı kolaylaştırmak için bir dizi karar aldık:

  • veritabanında, iş sürecinin kalıcı durumu kolayca okunabilir ve kolayca işlenebilir bir biçimde depolanır: bir JSON biçim dizesinde. Bu, hem uygulama içinde hem de dışında geçişler gerçekleştirmenize olanak tanır. Aşırı durumlarda, tutamaçlarla da ince ayar yapabilirsiniz (özellikle hata ayıklama sırasında geliştirmede kullanışlıdır);
  • entegrasyon iş mantığı, iş süreçlerinin adlarını kullanmaz, böylece herhangi bir zamanda katılan süreçlerden birinin uygulanmasını yeni bir adla (örneğin, "InitialPlayerV2") yenisiyle değiştirmek mümkündür. Bağlanma, mesajların ve sinyallerin adları aracılığıyla gerçekleşir;
  • süreç modelinin, bu modelde uyumsuz değişiklikler yaparsak artırdığımız bir sürüm numarası vardır ve bu numara, süreç örneğinin durumu ile birlikte saklanır;
  • sürecin kalıcı durumu, modelin sürüm numarası değiştiyse geçiş prosedürünün birlikte çalışabileceği uygun bir nesne modeline önce tabandan okunur;
  • geçiş prosedürü, iş mantığının yanına yerleştirilir ve veri tabanından geri yükleme sırasında iş sürecinin her bir örneği için "tembel" olarak adlandırılır;
  • tüm işlem örneklerinin durumunu hızlı ve senkronize bir şekilde taşımanız gerekiyorsa, daha klasik veritabanı taşıma çözümleri kullanılır, ancak orada JSON ile çalışmanız gerekir.

İş süreçleri için başka bir çerçeveye ihtiyacım var mı?

Makalede açıklanan çözümler, hayatımızı önemli ölçüde basitleştirmemize, uygulama geliştirme düzeyinde çözülen sorunların kapsamını genişletmemize ve iş mantığını mikro hizmetlere ayırma fikrini daha çekici hale getirmemize olanak sağladı. Bunun için birçok çalışma yapıldı, iş süreçleri için çok "hafif" bir çerçeve ve ayrıca çok çeşitli uygulamalı görevler bağlamında belirlenen sorunları çözmek için hizmet bileşenleri oluşturuldu. Ortak bileşenlerin geliştirilmesini ücretsiz bir lisans altında açık erişime getirmek için bu sonuçları paylaşma arzumuz var. Bu biraz çaba ve zaman gerektirecektir. Bu tür çözümlere olan talebi anlamak bizim için ek bir teşvik olabilir. Önerilen makalede, çerçevenin yeteneklerine çok az dikkat edilir, ancak sunulan örneklerden bazıları görülebilir. Yine de çerçevemizi yayınlarsak, ona ayrı bir makale ayrılacaktır. Bu arada, soruyu yanıtlayarak küçük bir geri bildirim bırakırsanız minnettar oluruz:

Ankete sadece kayıtlı kullanıcılar katılabilir. Giriş yapLütfen.

İş süreçleri için başka bir çerçeveye ihtiyacım var mı?

  • %18,8Evet, uzun zamandır böyle bir şey arıyordum.

  • %12,5uygulamanız hakkında daha fazla bilgi edinmek ilginç olabilir, yararlı olabilir2

  • %6,2mevcut çerçevelerden birini kullanıyoruz, ancak onu değiştirmeyi düşünüyoruz1

  • %18,8mevcut çerçevelerden birini kullanıyoruz, her şey uygun3

  • %18,8çerçevesiz başa çıkma3

  • %25,0kendin yaz4

16 kullanıcı oy kullandı. 7 kişi çekimser kaldı.

Kaynak: habr.com

Yorum ekle