Tích hợp kiểu BPM

Tích hợp kiểu BPM

Hi, Habr!

Công ty chúng tôi chuyên phát triển các giải pháp phần mềm lớp ERP, trong đó phần lớn là do các hệ thống giao dịch với một lượng lớn logic kinh doanh và quy trình làm việc a la EDMS chiếm giữ. Các phiên bản hiện đại của các sản phẩm của chúng tôi dựa trên công nghệ JavaEE, nhưng chúng tôi cũng đang tích cực thử nghiệm với các vi dịch vụ. Một trong những lĩnh vực có vấn đề nhất của các giải pháp như vậy là sự tích hợp của các hệ thống con khác nhau liên quan đến các miền liền kề. Các nhiệm vụ tích hợp luôn khiến chúng ta đau đầu, bất kể kiểu kiến ​​trúc, ngăn xếp công nghệ và khung mà chúng ta sử dụng, nhưng gần đây đã có tiến bộ trong việc giải quyết những vấn đề như vậy.

Trong bài báo thu hút sự chú ý của bạn, tôi sẽ nói về kinh nghiệm và nghiên cứu kiến ​​trúc của NPO Krista trong khu vực được chỉ định. Chúng ta cũng sẽ xem xét một ví dụ về giải pháp đơn giản cho vấn đề tích hợp theo quan điểm của nhà phát triển ứng dụng và tìm hiểu điều gì ẩn sau sự đơn giản này.

Tuyên bố từ chối trách nhiệm

Các giải pháp kiến ​​trúc và kỹ thuật được mô tả trong bài viết được tôi đưa ra dựa trên kinh nghiệm cá nhân trong bối cảnh các nhiệm vụ cụ thể. Các giải pháp này không tuyên bố là phổ biến và có thể không tối ưu trong các điều kiện sử dụng khác.

BPM phải làm gì với nó?

Để trả lời câu hỏi này, chúng ta cần đi sâu một chút vào chi tiết cụ thể của các vấn đề được áp dụng cho các giải pháp của chúng ta. Phần chính của logic nghiệp vụ trong hệ thống giao dịch điển hình của chúng tôi là nhập dữ liệu vào cơ sở dữ liệu thông qua giao diện người dùng, xác minh thủ công và tự động dữ liệu này, chuyển dữ liệu qua một số quy trình công việc, xuất bản dữ liệu đó sang hệ thống/cơ sở dữ liệu phân tích/lưu trữ khác, tạo báo cáo. Do đó, chức năng chính của hệ thống dành cho khách hàng là tự động hóa các quy trình kinh doanh nội bộ của họ.

Để thuận tiện, chúng tôi sử dụng thuật ngữ "tài liệu" trong giao tiếp như một số trừu tượng của một tập dữ liệu, được thống nhất bởi một khóa chung, có thể "đính kèm" một quy trình làm việc cụ thể.
Nhưng còn logic tích hợp thì sao? Rốt cuộc, nhiệm vụ tích hợp được tạo ra bởi kiến ​​​​trúc của hệ thống, được "cưa" thành các phần KHÔNG theo yêu cầu của khách hàng, mà dưới tác động của các yếu tố hoàn toàn khác nhau:

  • chịu ảnh hưởng của định luật Conway;
  • là kết quả của việc sử dụng lại các hệ thống con đã phát triển trước đó cho các sản phẩm khác;
  • theo quyết định của kiến ​​trúc sư, dựa trên các yêu cầu phi chức năng.

Có một sự cám dỗ lớn là tách logic tích hợp khỏi logic nghiệp vụ của quy trình công việc chính để không làm ô nhiễm logic nghiệp vụ với các tạo phẩm tích hợp và giúp nhà phát triển ứng dụng không phải đi sâu vào các đặc thù của cảnh quan kiến ​​trúc của hệ thống. Cách tiếp cận này có một số ưu điểm, nhưng thực tế cho thấy nó không hiệu quả:

  • giải quyết các vấn đề tích hợp thường trượt xuống các tùy chọn đơn giản nhất dưới dạng cuộc gọi đồng bộ do các điểm mở rộng hạn chế trong việc thực hiện quy trình công việc chính (thêm về những thiếu sót của tích hợp đồng bộ bên dưới);
  • các tạo phẩm tích hợp vẫn thâm nhập vào logic kinh doanh chính khi cần có phản hồi từ một hệ thống con khác;
  • nhà phát triển ứng dụng bỏ qua việc tích hợp và có thể dễ dàng phá vỡ nó bằng cách thay đổi quy trình làm việc;
  • hệ thống không còn là một tổng thể duy nhất theo quan điểm của người dùng, các "đường nối" giữa các hệ thống con trở nên đáng chú ý, các hoạt động dư thừa của người dùng xuất hiện bắt đầu chuyển dữ liệu từ hệ thống con này sang hệ thống con khác.

Một cách tiếp cận khác là xem xét các tương tác tích hợp như một phần không thể thiếu trong quy trình làm việc và logic kinh doanh cốt lõi. Để giữ cho các yêu cầu kỹ năng của các nhà phát triển ứng dụng không tăng vọt, việc tạo các tương tác tích hợp mới nên được thực hiện dễ dàng và tự nhiên, với các tùy chọn tối thiểu để chọn giải pháp. Điều này khó hơn vẻ ngoài của nó: công cụ này phải đủ mạnh để cung cấp cho người dùng nhiều tùy chọn cần thiết cho việc sử dụng nó, đồng thời không để mình bị bắn vào chân. Có nhiều câu hỏi mà một kỹ sư nên trả lời trong ngữ cảnh của các nhiệm vụ tích hợp, nhưng một nhà phát triển ứng dụng không nên nghĩ đến trong công việc hàng ngày của họ: ranh giới giao dịch, tính nhất quán, tính nguyên tử, bảo mật, mở rộng quy mô, tải và phân phối tài nguyên, định tuyến, sắp xếp thứ tự, bối cảnh lan truyền và chuyển đổi, v.v. Cần cung cấp cho các nhà phát triển ứng dụng các mẫu quyết định khá đơn giản, trong đó câu trả lời cho tất cả các câu hỏi như vậy đã được ẩn. Các mẫu này phải đủ an toàn: logic nghiệp vụ thay đổi rất thường xuyên, điều này làm tăng nguy cơ gây ra lỗi, chi phí cho lỗi phải duy trì ở mức khá thấp.

Tuy nhiên, BPM phải làm gì với nó? Có nhiều tùy chọn để thực hiện quy trình công việc ...
Thật vậy, một cách triển khai quy trình kinh doanh khác rất phổ biến trong các giải pháp của chúng tôi - thông qua cài đặt khai báo của sơ đồ chuyển đổi trạng thái và kết nối các trình xử lý với logic nghiệp vụ với các quá trình chuyển đổi. Đồng thời, trạng thái xác định vị trí hiện tại của "tài liệu" trong quy trình kinh doanh là một thuộc tính của chính "tài liệu".

Tích hợp kiểu BPM
Đây là cách quy trình diễn ra khi bắt đầu dự án

Sự phổ biến của việc triển khai như vậy là do tính đơn giản và tốc độ tương đối của việc tạo các quy trình kinh doanh tuyến tính. Tuy nhiên, khi các hệ thống phần mềm trở nên phức tạp hơn, phần tự động của quy trình kinh doanh sẽ phát triển và trở nên phức tạp hơn. Cần có sự phân tách, tái sử dụng các phần của quy trình, cũng như phân nhánh các quy trình để mỗi nhánh được thực thi song song. Trong những điều kiện như vậy, công cụ trở nên bất tiện và sơ đồ chuyển đổi trạng thái sẽ mất đi nội dung thông tin của nó (các tương tác tích hợp hoàn toàn không được phản ánh trong sơ đồ).

Tích hợp kiểu BPM
Đây là quy trình trông như thế nào sau nhiều lần lặp lại việc làm rõ các yêu cầu

Cách thoát khỏi tình huống này là sự tích hợp của động cơ jBPM thành một số sản phẩm có quy trình kinh doanh phức tạp nhất. Trong ngắn hạn, giải pháp này đã đạt được một số thành công: có thể triển khai các quy trình kinh doanh phức tạp trong khi vẫn duy trì một biểu đồ khá thông tin và cập nhật trong ký hiệu BPMN2.

Tích hợp kiểu BPM
Một phần nhỏ của quy trình kinh doanh phức tạp

Về lâu dài, giải pháp không đáp ứng được kỳ vọng: cường độ lao động cao trong việc tạo ra các quy trình kinh doanh thông qua các công cụ trực quan không cho phép đạt được các chỉ số năng suất có thể chấp nhận được và bản thân công cụ này đã trở thành một trong những công cụ không được các nhà phát triển yêu thích nhất. Cũng có những lời phàn nàn về cấu trúc bên trong của động cơ dẫn đến việc xuất hiện nhiều “miếng vá” và “nạng”.

Khía cạnh tích cực chính của việc sử dụng jBPM là nhận ra lợi ích và tác hại của việc có trạng thái liên tục của riêng nó đối với một phiên bản quy trình nghiệp vụ. Chúng tôi cũng thấy khả năng sử dụng cách tiếp cận theo quy trình để thực hiện các giao thức tích hợp phức tạp giữa các ứng dụng khác nhau bằng cách sử dụng các tương tác không đồng bộ thông qua tín hiệu và thông báo. Sự hiện diện của một trạng thái liên tục đóng một vai trò quan trọng trong việc này.

Dựa trên những điều trên, chúng ta có thể kết luận: Cách tiếp cận quy trình theo kiểu BPM cho phép chúng tôi giải quyết nhiều nhiệm vụ để tự động hóa các quy trình kinh doanh phức tạp hơn bao giờ hết, các hoạt động tích hợp phù hợp hài hòa với các quy trình này và duy trì khả năng hiển thị trực quan quy trình đã triển khai trong một ký hiệu phù hợp.

Nhược điểm của các cuộc gọi đồng bộ như một mẫu tích hợp

Tích hợp đồng bộ đề cập đến cuộc gọi chặn đơn giản nhất. Một hệ thống con đóng vai trò là phía máy chủ và hiển thị API với phương thức mong muốn. Một hệ thống con khác hoạt động như một phía khách hàng và, vào đúng thời điểm, thực hiện cuộc gọi với mong muốn nhận được kết quả. Tùy thuộc vào kiến ​​trúc của hệ thống, phía máy khách và máy chủ có thể được lưu trữ trong cùng một ứng dụng và quy trình hoặc trong các ứng dụng và quy trình khác nhau. Trong trường hợp thứ hai, bạn cần áp dụng một số triển khai RPC và cung cấp sắp xếp lại các tham số và kết quả của cuộc gọi.

Tích hợp kiểu BPM

Một mô hình tích hợp như vậy có một số nhược điểm khá lớn, nhưng nó được sử dụng rất rộng rãi trong thực tế do tính đơn giản của nó. Tốc độ thực hiện quyến rũ và khiến bạn phải áp dụng đi áp dụng lại trong điều kiện “cháy” deadline, ghi lời giải vào nợ kỹ thuật. Nhưng nó cũng xảy ra rằng các nhà phát triển thiếu kinh nghiệm sử dụng nó một cách vô thức, đơn giản là không nhận ra những hậu quả tiêu cực.

Ngoài sự gia tăng rõ ràng nhất về khả năng kết nối của các hệ thống con, có ít vấn đề rõ ràng hơn với các giao dịch "lan rộng" và "kéo dài". Thật vậy, nếu logic kinh doanh thực hiện bất kỳ thay đổi nào, thì các giao dịch là không thể thiếu và đến lượt các giao dịch, khóa một số tài nguyên ứng dụng bị ảnh hưởng bởi những thay đổi này. Nghĩa là, cho đến khi một hệ thống con chờ phản hồi từ hệ thống khác, nó sẽ không thể hoàn thành giao dịch và giải phóng các khóa. Điều này làm tăng đáng kể nguy cơ của một loạt các hiệu ứng:

  • khả năng phản hồi của hệ thống bị mất, người dùng chờ đợi lâu để có câu trả lời cho các yêu cầu;
  • máy chủ thường ngừng phản hồi các yêu cầu của người dùng do nhóm luồng bị tràn: hầu hết các luồng đều “đứng” trên khóa của tài nguyên do giao dịch chiếm giữ;
  • bế tắc bắt đầu xuất hiện: xác suất xuất hiện của chúng phụ thuộc rất nhiều vào thời lượng giao dịch, số lượng logic nghiệp vụ và khóa liên quan đến giao dịch;
  • xuất hiện lỗi hết thời gian chờ giao dịch;
  • máy chủ “rơi” vào OutOfMemory nếu tác vụ yêu cầu xử lý và thay đổi lượng lớn dữ liệu và sự hiện diện của các tích hợp đồng bộ khiến việc phân chia quá trình xử lý thành các giao dịch “nhẹ nhàng” trở nên rất khó khăn.

Từ quan điểm kiến ​​trúc, việc sử dụng các cuộc gọi chặn trong quá trình tích hợp dẫn đến mất khả năng kiểm soát chất lượng của các hệ thống con riêng lẻ: không thể đảm bảo các mục tiêu chất lượng của một hệ thống con tách biệt với các mục tiêu chất lượng của một hệ thống con khác. Nếu các hệ thống con được phát triển bởi các nhóm khác nhau thì đây là một vấn đề lớn.

Mọi thứ thậm chí còn thú vị hơn nếu các hệ thống con được tích hợp nằm trong các ứng dụng khác nhau và các thay đổi đồng bộ cần được thực hiện ở cả hai phía. Làm cách nào để biến những thay đổi này thành giao dịch?

Nếu các thay đổi được thực hiện trong các giao dịch riêng biệt, thì sẽ cần phải cung cấp khả năng xử lý và bồi thường ngoại lệ mạnh mẽ và điều này loại bỏ hoàn toàn lợi thế chính của tích hợp đồng bộ - tính đơn giản.

Các giao dịch phân tán cũng được lưu ý, nhưng chúng tôi không sử dụng chúng trong các giải pháp của mình: rất khó để đảm bảo độ tin cậy.

"Saga" như một giải pháp cho vấn đề giao dịch

Với sự phổ biến ngày càng tăng của microservice, nhu cầu ngày càng tăng đối với Hoa văn Saga.

Mô hình này giải quyết hoàn hảo các vấn đề trên của các giao dịch dài và cũng mở rộng khả năng quản lý trạng thái của hệ thống từ khía cạnh logic nghiệp vụ: bồi thường sau một giao dịch không thành công có thể không đưa hệ thống trở lại trạng thái ban đầu mà cung cấp một giải pháp thay thế đường xử lý dữ liệu. Nó cũng cho phép bạn không lặp lại các bước xử lý dữ liệu đã hoàn thành thành công khi bạn cố gắng đưa quy trình đến một kết thúc “tốt”.

Thật thú vị, trong các hệ thống nguyên khối, mô hình này cũng có liên quan khi tích hợp các hệ thống con được ghép nối lỏng lẻo và có những tác động tiêu cực gây ra bởi các giao dịch dài và khóa tài nguyên tương ứng.

Đối với các quy trình kinh doanh của chúng tôi theo kiểu BPM, việc triển khai Sagas hóa ra rất dễ dàng: các bước riêng lẻ của Sagas có thể được đặt thành các hoạt động trong quy trình kinh doanh và trạng thái liên tục của quy trình kinh doanh xác định, trong số những thứ khác, trạng thái bên trong của Sagas. Đó là, chúng tôi không cần bất kỳ cơ chế phối hợp bổ sung nào. Tất cả những gì bạn cần là một nhà môi giới tin nhắn có hỗ trợ đảm bảo "ít nhất một lần" dưới dạng vận chuyển.

Nhưng một giải pháp như vậy cũng có "giá" riêng:

  • logic kinh doanh trở nên phức tạp hơn: bạn cần tính toán tiền bồi thường;
  • sẽ cần phải từ bỏ tính nhất quán hoàn toàn, điều này có thể đặc biệt nhạy cảm đối với các hệ thống nguyên khối;
  • kiến trúc trở nên phức tạp hơn một chút, cần có thêm một nhà môi giới tin nhắn;
  • các công cụ giám sát và quản trị bổ sung sẽ được yêu cầu (mặc dù nhìn chung điều này thậm chí còn tốt: chất lượng dịch vụ hệ thống sẽ tăng lên).

Đối với các hệ thống nguyên khối, lý do biện minh cho việc sử dụng "Sags" không quá rõ ràng. Đối với các dịch vụ siêu nhỏ và các SOA khác, trong đó rất có thể đã có một trình trung gian và tính nhất quán hoàn toàn đã bị hy sinh khi bắt đầu dự án, lợi ích của việc sử dụng mẫu này có thể lớn hơn đáng kể so với các nhược điểm, đặc biệt nếu có một API thuận tiện tại mức logic nghiệp vụ.

Đóng gói logic nghiệp vụ trong microservice

Khi chúng tôi bắt đầu thử nghiệm với microservice, một câu hỏi hợp lý đã nảy sinh: đặt logic nghiệp vụ miền ở đâu so với dịch vụ cung cấp tính bền vững của dữ liệu miền?

Khi xem xét kiến ​​trúc của các BPMS khác nhau, có vẻ hợp lý khi tách logic nghiệp vụ khỏi tính bền bỉ: tạo một lớp nền tảng và các dịch vụ siêu nhỏ độc lập với miền tạo thành môi trường và vùng chứa để thực thi logic nghiệp vụ miền và sắp xếp tính bền bỉ của dữ liệu miền thành một phần riêng biệt. lớp vi dịch vụ rất đơn giản và nhẹ. Quy trình kinh doanh trong trường hợp này phối hợp các dịch vụ của lớp kiên trì.

Tích hợp kiểu BPM

Cách tiếp cận này có một điểm cộng rất lớn: bạn có thể tăng chức năng của nền tảng bao nhiêu tùy thích và chỉ lớp vi dịch vụ nền tảng tương ứng mới “phát phì” vì điều này. Các quy trình kinh doanh từ bất kỳ miền nào ngay lập tức có cơ hội sử dụng chức năng mới của nền tảng ngay khi được cập nhật.

Một nghiên cứu chi tiết hơn cho thấy những thiếu sót đáng kể của phương pháp này:

  • một dịch vụ nền tảng thực thi logic kinh doanh của nhiều miền cùng một lúc sẽ tiềm ẩn những rủi ro lớn do một điểm lỗi duy nhất. Những thay đổi thường xuyên đối với logic nghiệp vụ làm tăng nguy cơ lỗi dẫn đến lỗi trên toàn hệ thống;
  • các vấn đề về hiệu suất: logic nghiệp vụ hoạt động với dữ liệu của nó thông qua giao diện hẹp và chậm:
    • dữ liệu một lần nữa sẽ được sắp xếp lại và bơm qua ngăn xếp mạng;
    • dịch vụ miền thường sẽ trả về nhiều dữ liệu hơn logic nghiệp vụ yêu cầu để xử lý, do không đủ khả năng tham số hóa truy vấn ở cấp API bên ngoài của dịch vụ;
    • một số phần logic nghiệp vụ độc lập có thể liên tục yêu cầu lại cùng một dữ liệu để xử lý (bạn có thể giảm thiểu vấn đề này bằng cách thêm đậu phiên lưu trữ dữ liệu vào bộ đệm, nhưng điều này làm phức tạp thêm kiến ​​trúc và tạo ra các vấn đề về làm mới dữ liệu và vô hiệu hóa bộ đệm);
  • Vấn đề giao dịch:
    • các quy trình kinh doanh với trạng thái liên tục được lưu trữ bởi dịch vụ nền tảng không nhất quán với dữ liệu miền và không có cách nào dễ dàng để giải quyết vấn đề này;
    • di chuyển khóa dữ liệu miền ra khỏi giao dịch: nếu logic nghiệp vụ miền cần thực hiện thay đổi, sau khi kiểm tra tính chính xác của dữ liệu thực tế trước tiên, cần loại trừ khả năng thay đổi cạnh tranh trong dữ liệu được xử lý. Chặn dữ liệu bên ngoài có thể giúp giải quyết vấn đề, nhưng giải pháp như vậy mang lại rủi ro bổ sung và làm giảm độ tin cậy tổng thể của hệ thống;
  • các biến chứng bổ sung khi cập nhật: trong một số trường hợp, bạn cần cập nhật dịch vụ liên tục và logic nghiệp vụ một cách đồng bộ hoặc theo trình tự nghiêm ngặt.

Cuối cùng, tôi phải quay lại vấn đề cơ bản: đóng gói dữ liệu miền và logic nghiệp vụ miền vào một dịch vụ siêu nhỏ. Cách tiếp cận này đơn giản hóa nhận thức về microservice như một thành phần không thể thiếu trong hệ thống và không làm phát sinh các vấn đề trên. Điều này cũng không miễn phí:

  • Tiêu chuẩn hóa API là cần thiết để tương tác với logic kinh doanh (đặc biệt là để cung cấp các hoạt động của người dùng như một phần của quy trình kinh doanh) và các dịch vụ nền tảng API; chú ý cẩn thận hơn đến các thay đổi API, cần có khả năng tương thích tiến và lùi;
  • cần phải thêm các thư viện thời gian chạy bổ sung để đảm bảo hoạt động của logic nghiệp vụ như một phần của từng dịch vụ siêu nhỏ đó và điều này dẫn đến các yêu cầu mới đối với các thư viện đó: nhẹ nhàng và tối thiểu các phụ thuộc bắc cầu;
  • các nhà phát triển logic nghiệp vụ cần theo dõi các phiên bản thư viện: nếu một microservice chưa được hoàn thiện trong một thời gian dài, thì rất có thể nó sẽ chứa một phiên bản thư viện đã lỗi thời. Đây có thể là một trở ngại không mong muốn khi thêm một tính năng mới và có thể yêu cầu logic nghiệp vụ cũ của một dịch vụ như vậy phải được di chuyển sang các phiên bản mới của thư viện nếu có những thay đổi không tương thích giữa các phiên bản.

Tích hợp kiểu BPM

Một lớp dịch vụ nền tảng cũng có mặt trong kiến ​​trúc như vậy, nhưng lớp này không còn tạo thành vùng chứa để thực thi logic nghiệp vụ miền mà chỉ là môi trường của nó, cung cấp các chức năng "nền tảng" phụ trợ. Một lớp như vậy không chỉ cần thiết để duy trì sự nhẹ nhàng của các dịch vụ vi mô miền mà còn để quản lý tập trung.

Ví dụ: các hoạt động của người dùng trong các quy trình kinh doanh tạo ra các tác vụ. Tuy nhiên, khi làm việc với các tác vụ, người dùng phải xem các tác vụ từ tất cả các miền trong danh sách chung, điều đó có nghĩa là phải có một dịch vụ nền tảng đăng ký tác vụ phù hợp, đã xóa logic nghiệp vụ của miền. Giữ cho việc đóng gói logic nghiệp vụ trong bối cảnh này là khá khó khăn và đây là một sự thỏa hiệp khác của kiến ​​trúc này.

Tích hợp các quy trình kinh doanh qua con mắt của nhà phát triển ứng dụng

Như đã đề cập ở trên, nhà phát triển ứng dụng phải được trừu tượng hóa khỏi các tính năng kỹ thuật và kỹ thuật của việc thực hiện tương tác của một số ứng dụng để có thể tin tưởng vào năng suất phát triển tốt.

Hãy thử giải một bài toán tích hợp khá khó, được phát minh riêng cho bài báo. Đây sẽ là một nhiệm vụ "trò chơi" liên quan đến ba ứng dụng, trong đó mỗi ứng dụng xác định một số tên miền: "app1", "app2", "app3".

Bên trong mỗi ứng dụng, các quy trình nghiệp vụ được khởi chạy bắt đầu "chơi bóng" thông qua xe buýt tích hợp. Tin nhắn có tên "Ball" sẽ hoạt động như quả bóng.

Nội quy của trò chơi:

  • người chơi đầu tiên là người khởi xướng. Anh ta mời những người chơi khác tham gia trò chơi, bắt đầu trò chơi và có thể kết thúc trò chơi bất cứ lúc nào;
  • những người chơi khác tuyên bố tham gia trò chơi, “làm quen” với nhau và người chơi đầu tiên;
  • sau khi nhận bóng, người chơi chọn một cầu thủ tham gia khác và chuyền bóng cho anh ta. Tổng số đường chuyền được tính;
  • mỗi cầu thủ có "năng lượng", năng lượng này giảm dần theo mỗi đường chuyền bóng của cầu thủ đó. Khi hết năng lượng, người chơi bị loại khỏi trò chơi, tuyên bố nghỉ hưu;
  • nếu người chơi bị bỏ lại một mình, anh ta ngay lập tức tuyên bố rời đi;
  • khi tất cả người chơi bị loại, người chơi đầu tiên tuyên bố kết thúc trò chơi. Nếu anh ta rời trò chơi sớm hơn, thì vẫn phải theo dõi trò chơi để hoàn thành nó.

Để giải quyết vấn đề này, tôi sẽ sử dụng DSL của chúng tôi cho các quy trình kinh doanh, cho phép bạn mô tả logic trong Kotlin một cách cô đọng, với mức tối thiểu là bản soạn sẵn.

Trong ứng dụng app1, quy trình kinh doanh của người chơi đầu tiên (anh ta cũng là người khởi xướng trò chơi) sẽ hoạt động:

lớp Người chơi ban đầu

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

Ngoài việc thực thi logic nghiệp vụ, đoạn mã trên có thể tạo ra một mô hình đối tượng của quy trình nghiệp vụ có thể được trực quan hóa dưới dạng sơ đồ. Chúng tôi chưa triển khai trình hiển thị, vì vậy chúng tôi phải dành thời gian để vẽ (ở đây tôi đã đơn giản hóa một chút ký hiệu BPMN liên quan đến việc sử dụng các cổng để cải thiện tính nhất quán của sơ đồ với đoạn mã trên):

Tích hợp kiểu BPM

app2 sẽ bao gồm quy trình kinh doanh của người chơi khác:

lớp Người chơi ngẫu nhiên

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

Biểu đồ:

Tích hợp kiểu BPM

Trong ứng dụng app3, chúng tôi sẽ làm cho người chơi có hành vi hơi khác: thay vì chọn ngẫu nhiên người chơi tiếp theo, anh ta sẽ hành động theo thuật toán vòng tròn:

lớp 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}")
}

Mặt khác, hành vi của người chơi không khác với hành vi trước đó, vì vậy sơ đồ không thay đổi.

Bây giờ chúng tôi cần một bài kiểm tra để chạy tất cả. Tôi sẽ chỉ cung cấp mã của bài kiểm tra, để không làm lộn xộn bài viết với bản soạn sẵn (thực tế, tôi đã sử dụng môi trường thử nghiệm được tạo trước đó để kiểm tra khả năng tích hợp của các quy trình kinh doanh khác):

trò chơi kiểm tra ()

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

Chạy thử nghiệm, xem nhật ký:

đầu ra bảng điều khiển

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

Một số kết luận quan trọng có thể được rút ra từ tất cả những điều này:

  • nếu có sẵn các công cụ cần thiết, các nhà phát triển ứng dụng có thể tạo ra các tương tác tích hợp giữa các ứng dụng mà không vi phạm logic nghiệp vụ;
  • độ phức tạp (độ phức tạp) của một nhiệm vụ tích hợp đòi hỏi năng lực kỹ thuật có thể ẩn bên trong khung nếu ban đầu nó được đặt trong kiến ​​trúc của khung. Không thể ẩn độ khó của nhiệm vụ (độ khó), do đó, giải pháp cho một nhiệm vụ khó trong mã sẽ tương ứng;
  • khi phát triển logic tích hợp, cần tính đến tính nhất quán cuối cùng và sự thiếu khả năng tuyến tính hóa của sự thay đổi trạng thái của tất cả những người tham gia tích hợp. Điều này buộc chúng ta phải phức tạp hóa logic để làm cho nó không nhạy cảm với thứ tự xảy ra các sự kiện bên ngoài. Trong ví dụ của chúng tôi, người chơi buộc phải tham gia trò chơi sau khi anh ta tuyên bố rời khỏi trò chơi: những người chơi khác sẽ tiếp tục chuyền bóng cho anh ta cho đến khi thông tin về việc anh ta thoát ra được thông báo và được tất cả những người tham gia xử lý. Logic này không tuân theo các quy tắc của trò chơi và là một giải pháp thỏa hiệp trong khuôn khổ của kiến ​​trúc đã chọn.

Tiếp theo, hãy nói về những điểm tinh tế khác nhau trong giải pháp của chúng tôi, những thỏa hiệp và những điểm khác.

Tất cả các tin nhắn trong một hàng đợi

Tất cả các ứng dụng tích hợp đều hoạt động với một bus tích hợp, được trình bày dưới dạng một trình môi giới bên ngoài, một BPMQueue cho tin nhắn và một chủ đề BPMTopic cho các tín hiệu (sự kiện). Bản thân việc chuyển tất cả các tin nhắn qua một hàng đợi đã là một sự thỏa hiệp. Ở cấp độ logic nghiệp vụ, giờ đây bạn có thể giới thiệu bao nhiêu loại thông báo mới tùy thích mà không cần thay đổi cấu trúc hệ thống. Đây là một sự đơn giản hóa đáng kể, nhưng nó mang lại những rủi ro nhất định, trong bối cảnh các nhiệm vụ điển hình của chúng tôi, dường như chúng tôi không quá quan trọng.

Tích hợp kiểu BPM

Tuy nhiên, có một sự tinh tế ở đây: mỗi ứng dụng lọc các tin nhắn "của nó" từ hàng đợi ở lối vào, theo tên miền của nó. Ngoài ra, miền có thể được chỉ định trong các tín hiệu, nếu bạn cần giới hạn "phạm vi" của tín hiệu cho một ứng dụng. Điều này sẽ làm tăng băng thông của bus, nhưng logic nghiệp vụ hiện phải hoạt động với các tên miền: bắt buộc đối với địa chỉ tin nhắn, mong muốn đối với tín hiệu.

Đảm bảo độ tin cậy của bus tích hợp

Độ tin cậy được tạo thành từ một số điều:

  • Trình môi giới thông báo được chọn là một thành phần quan trọng của kiến ​​trúc và là điểm lỗi duy nhất: nó phải có đủ khả năng chịu lỗi. Bạn chỉ nên sử dụng các triển khai đã được thử nghiệm theo thời gian với sự hỗ trợ tốt và cộng đồng lớn;
  • cần phải đảm bảo tính sẵn sàng cao của trình môi giới tin nhắn, mà nó phải được tách biệt về mặt vật lý với các ứng dụng tích hợp (tính sẵn sàng cao của các ứng dụng với logic nghiệp vụ được áp dụng khó cung cấp và tốn kém hơn nhiều);
  • người môi giới có nghĩa vụ cung cấp bảo đảm giao hàng "ít nhất một lần". Đây là yêu cầu bắt buộc đối với hoạt động đáng tin cậy của bus tích hợp. Không cần đảm bảo mức "chính xác một lần": các quy trình nghiệp vụ thường không nhạy cảm với việc nhận các thông báo hoặc sự kiện lặp đi lặp lại và trong các nhiệm vụ đặc biệt khi điều này quan trọng, việc thêm các kiểm tra bổ sung vào logic nghiệp vụ sẽ dễ dàng hơn là sử dụng liên tục thay vì đảm bảo "đắt" ";
  • gửi tin nhắn và tín hiệu phải tham gia vào một giao dịch chung với sự thay đổi trạng thái của quy trình kinh doanh và dữ liệu miền. Tùy chọn ưa thích sẽ là sử dụng mẫu Hộp thư đi giao dịch, nhưng nó sẽ yêu cầu một bảng bổ sung trong cơ sở dữ liệu và một chuyển tiếp. Trong các ứng dụng JEE, điều này có thể được đơn giản hóa bằng cách sử dụng trình quản lý JTA cục bộ, nhưng kết nối với nhà môi giới đã chọn phải có thể hoạt động ở chế độ XA;
  • trình xử lý tin nhắn và sự kiện đến cũng phải làm việc với giao dịch thay đổi trạng thái của quy trình kinh doanh: nếu giao dịch đó bị khôi phục, thì việc nhận tin nhắn cũng phải bị hủy bỏ;
  • các tin nhắn không thể gửi do lỗi nên được lưu trữ trong một cửa hàng riêng ĐLQ (Hàng đợi thư chết). Để làm điều này, chúng tôi đã tạo một vi dịch vụ nền tảng riêng biệt để lưu trữ các thư như vậy trong bộ lưu trữ của nó, lập chỉ mục chúng theo thuộc tính (để nhóm và tìm kiếm nhanh), đồng thời hiển thị API để xem, gửi lại đến địa chỉ đích và xóa thư. Quản trị viên hệ thống có thể làm việc với dịch vụ này thông qua giao diện web của họ;
  • trong cài đặt của nhà môi giới, bạn cần điều chỉnh số lần gửi lại và độ trễ giữa các lần gửi để giảm khả năng tin nhắn vào DLQ (hầu như không thể tính toán các tham số tối ưu, nhưng bạn có thể hành động theo kinh nghiệm và điều chỉnh chúng trong quá trình hoạt động);
  • kho lưu trữ DLQ phải được giám sát liên tục và hệ thống giám sát phải thông báo cho quản trị viên hệ thống để họ có thể phản hồi nhanh nhất có thể khi xảy ra các tin nhắn không được gửi. Điều này sẽ làm giảm “vùng thiệt hại” của lỗi hoặc lỗi logic nghiệp vụ;
  • bus tích hợp phải không nhạy cảm với sự vắng mặt tạm thời của ứng dụng: đăng ký chủ đề phải lâu dài và tên miền của ứng dụng phải là duy nhất để người khác không cố xử lý thông báo của nó từ hàng đợi trong thời gian không có ứng dụng.

Đảm bảo an toàn luồng của logic nghiệp vụ

Cùng một phiên bản của một quy trình công việc có thể nhận được một số thông báo và sự kiện cùng một lúc, quá trình xử lý chúng sẽ bắt đầu song song. Đồng thời, đối với một nhà phát triển ứng dụng, mọi thứ phải đơn giản và an toàn theo luồng.

Logic nghiệp vụ quy trình xử lý từng sự kiện bên ngoài ảnh hưởng đến quy trình nghiệp vụ này một cách riêng lẻ. Những sự kiện này có thể là:

  • khởi chạy một phiên bản quy trình nghiệp vụ;
  • hành động của người dùng liên quan đến một hoạt động trong quy trình kinh doanh;
  • nhận được một tin nhắn hoặc tín hiệu mà một phiên bản quy trình kinh doanh được đăng ký;
  • hết hạn bộ hẹn giờ do phiên bản quy trình công việc đặt;
  • kiểm soát hành động thông qua API (ví dụ: hủy bỏ quy trình).

Mỗi sự kiện như vậy có thể thay đổi trạng thái của một thể hiện quy trình nghiệp vụ: một số hoạt động có thể kết thúc và những hoạt động khác bắt đầu, giá trị của các thuộc tính liên tục có thể thay đổi. Đóng bất kỳ hoạt động nào có thể dẫn đến việc kích hoạt một hoặc nhiều hoạt động sau. Ngược lại, những người đó có thể dừng chờ đợi các sự kiện khác hoặc nếu họ không cần bất kỳ dữ liệu bổ sung nào, họ có thể hoàn thành trong cùng một giao dịch. Trước khi đóng giao dịch, trạng thái mới của quy trình kinh doanh được lưu trữ trong cơ sở dữ liệu, nơi nó sẽ chờ sự kiện bên ngoài tiếp theo.

Dữ liệu quy trình kinh doanh liên tục được lưu trữ trong cơ sở dữ liệu quan hệ là một điểm đồng bộ hóa xử lý rất thuận tiện khi sử dụng CHỌN ĐỂ CẬP NHẬT. Nếu một giao dịch quản lý để có được trạng thái của quy trình kinh doanh từ cơ sở dữ liệu để thay đổi nó, thì không có giao dịch nào khác song song có thể có được trạng thái tương tự cho một thay đổi khác và sau khi hoàn thành giao dịch đầu tiên, giao dịch thứ hai là đảm bảo nhận được trạng thái đã thay đổi.

Sử dụng khóa bi quan ở phía DBMS, chúng tôi đáp ứng tất cả các yêu cầu cần thiết ACID, đồng thời duy trì khả năng thay đổi quy mô ứng dụng logic nghiệp vụ bằng cách tăng số lượng phiên bản đang chạy.

Tuy nhiên, khóa bi quan đe dọa chúng tôi bằng bế tắc, điều đó có nghĩa là CHỌN ĐỂ CẬP NHẬT vẫn nên được giới hạn ở một số thời gian chờ hợp lý trong trường hợp bế tắc đối với một số trường hợp nghiêm trọng trong logic nghiệp vụ.

Một vấn đề khác là việc đồng bộ hóa quá trình bắt đầu kinh doanh. Mặc dù không có phiên bản quy trình nghiệp vụ, nhưng cũng không có trạng thái trong cơ sở dữ liệu, vì vậy phương pháp được mô tả sẽ không hoạt động. Nếu bạn muốn đảm bảo tính duy nhất của một thể hiện quy trình nghiệp vụ trong một phạm vi cụ thể, thì bạn cần một số loại đối tượng đồng bộ hóa được liên kết với lớp quy trình và phạm vi tương ứng. Để giải quyết vấn đề này, chúng tôi sử dụng một cơ chế khóa khác cho phép chúng tôi khóa một tài nguyên tùy ý được chỉ định bởi một khóa ở định dạng URI thông qua một dịch vụ bên ngoài.

Trong các ví dụ của chúng tôi, quy trình công việc của InitialPlayer chứa một khai báo

uniqueConstraint = UniqueConstraints.singleton

Do đó, nhật ký chứa các thông báo về việc lấy và giải phóng khóa của khóa tương ứng. Không có thông báo như vậy cho các quy trình kinh doanh khác: uniqueConstraint không được đặt.

Các vấn đề về quy trình kinh doanh với trạng thái liên tục

Đôi khi có một trạng thái cố chấp không chỉ giúp ích mà còn thực sự cản trở sự phát triển.
Các vấn đề bắt đầu khi bạn cần thay đổi logic nghiệp vụ và/hoặc mô hình quy trình nghiệp vụ. Không có bất kỳ thay đổi nào như vậy được cho là tương thích với trạng thái cũ của quy trình kinh doanh. Nếu có nhiều phiên bản "sống" trong cơ sở dữ liệu, thì việc thực hiện các thay đổi không tương thích có thể gây ra nhiều rắc rối mà chúng ta thường gặp phải khi sử dụng jBPM.

Tùy thuộc vào mức độ thay đổi, bạn có thể hành động theo hai cách:

  1. tạo một loại quy trình công việc mới để không thực hiện các thay đổi không tương thích với loại quy trình cũ và sử dụng loại quy trình đó thay cho loại quy trình cũ khi bắt đầu các phiên bản mới. Các phiên bản cũ sẽ tiếp tục hoạt động "theo cách cũ";
  2. di chuyển trạng thái liên tục của quy trình kinh doanh khi cập nhật logic nghiệp vụ.

Cách đầu tiên đơn giản hơn, nhưng có những hạn chế và nhược điểm, ví dụ:

  • trùng lặp logic nghiệp vụ trong nhiều mô hình quy trình nghiệp vụ, tăng khối lượng logic nghiệp vụ;
  • thường thì cần phải chuyển đổi ngay lập tức sang logic nghiệp vụ mới (hầu như luôn luôn về các nhiệm vụ tích hợp);
  • nhà phát triển không biết tại thời điểm nào có thể xóa các mô hình lỗi thời.

Trên thực tế, chúng tôi sử dụng cả hai cách tiếp cận, nhưng đã đưa ra một số quyết định để đơn giản hóa cuộc sống của mình:

  • trong cơ sở dữ liệu, trạng thái liên tục của quy trình kinh doanh được lưu trữ ở dạng dễ đọc và dễ xử lý: ở dạng chuỗi định dạng JSON. Điều này cho phép bạn thực hiện di chuyển cả bên trong ứng dụng và bên ngoài. Trong trường hợp cực đoan, bạn cũng có thể điều chỉnh nó bằng tay cầm (đặc biệt hữu ích trong quá trình phát triển trong quá trình gỡ lỗi);
  • logic nghiệp vụ tích hợp không sử dụng tên của các quy trình nghiệp vụ, do đó, bất cứ lúc nào cũng có thể thay thế việc triển khai một trong các quy trình tham gia bằng một quy trình mới, có tên mới (ví dụ: "InitialPlayerV2"). Sự ràng buộc xảy ra thông qua tên của thông báo và tín hiệu;
  • mô hình quy trình có số phiên bản, số này sẽ tăng lên nếu chúng tôi thực hiện các thay đổi không tương thích với mô hình này và số này được lưu trữ cùng với trạng thái của phiên bản quy trình;
  • trạng thái liên tục của quy trình được đọc từ cơ sở trước tiên thành một mô hình đối tượng thuận tiện mà quy trình di chuyển có thể hoạt động nếu số phiên bản của mô hình đã thay đổi;
  • quy trình di chuyển được đặt bên cạnh logic nghiệp vụ và được gọi là "lười biếng" đối với từng phiên bản của quy trình nghiệp vụ tại thời điểm khôi phục nó từ cơ sở dữ liệu;
  • nếu bạn cần di chuyển trạng thái của tất cả các phiên bản quy trình một cách nhanh chóng và đồng bộ, thì các giải pháp di chuyển cơ sở dữ liệu cổ điển hơn sẽ được sử dụng, nhưng bạn phải làm việc với JSON ở đó.

Tôi có cần một khuôn khổ khác cho quy trình kinh doanh không?

Các giải pháp được mô tả trong bài báo cho phép chúng tôi đơn giản hóa đáng kể cuộc sống của mình, mở rộng phạm vi các vấn đề được giải quyết ở cấp độ phát triển ứng dụng và làm cho ý tưởng tách logic kinh doanh thành các dịch vụ siêu nhỏ trở nên hấp dẫn hơn. Để làm được điều này, rất nhiều công việc đã được thực hiện, một khuôn khổ rất “nhẹ” cho các quy trình kinh doanh đã được tạo ra, cũng như các thành phần dịch vụ để giải quyết các vấn đề đã xác định trong bối cảnh của một loạt các nhiệm vụ được áp dụng. Chúng tôi có mong muốn chia sẻ những kết quả này, để đưa sự phát triển của các thành phần phổ biến thành truy cập mở theo giấy phép miễn phí. Điều này sẽ đòi hỏi một số nỗ lực và thời gian. Hiểu nhu cầu về các giải pháp như vậy có thể là một động lực bổ sung cho chúng tôi. Trong bài viết được đề xuất, rất ít sự chú ý đến các khả năng của chính khung, nhưng một số trong số chúng có thể nhìn thấy được từ các ví dụ được trình bày. Tuy nhiên, nếu chúng tôi xuất bản khuôn khổ của mình, một bài viết riêng sẽ được dành cho nó. Trong thời gian chờ đợi, chúng tôi sẽ rất biết ơn nếu bạn để lại một chút phản hồi bằng cách trả lời câu hỏi:

Chỉ những người dùng đã đăng ký mới có thể tham gia khảo sát. Đăng nhập, xin vui lòng.

Tôi có cần một khuôn khổ khác cho quy trình kinh doanh không?

  • 18,8%Vâng, tôi đã tìm kiếm một cái gì đó như thế này trong một thời gian dài.

  • 12,5%thật thú vị khi tìm hiểu thêm về cách triển khai của bạn, nó có thể hữu ích2

  • 6,2%chúng tôi sử dụng một trong các khung hiện có, nhưng chúng tôi đang nghĩ đến việc thay thế nó1

  • 18,8%chúng tôi sử dụng một trong các khung hiện có, mọi thứ đều phù hợp3

  • 18,8%đối phó mà không có khuôn khổ3

  • 25,0%viết của riêng bạn4

16 người dùng bình chọn. 7 người dùng bỏ phiếu trắng.

Nguồn: www.habr.com

Thêm một lời nhận xét