diff --git a/404.html b/404.html index cc0dca594..596c32295 100644 --- a/404.html +++ b/404.html @@ -18,8 +18,8 @@ - - + + diff --git a/assets/images/2024-10-28-task-debugging-dc5d52151429b8428cfa326e57cc53d5.png b/assets/images/2024-10-28-task-debugging-dc5d52151429b8428cfa326e57cc53d5.png new file mode 100644 index 000000000..cb0fc439f Binary files /dev/null and b/assets/images/2024-10-28-task-debugging-dc5d52151429b8428cfa326e57cc53d5.png differ diff --git a/assets/images/2024-10-28-workflow-search-6abd5f2dbb3914b3230502f28afab1f2.png b/assets/images/2024-10-28-workflow-search-6abd5f2dbb3914b3230502f28afab1f2.png new file mode 100644 index 000000000..2bc45f473 Binary files /dev/null and b/assets/images/2024-10-28-workflow-search-6abd5f2dbb3914b3230502f28afab1f2.png differ diff --git a/assets/js/0fad6d0a.41f42246.js b/assets/js/0fad6d0a.d22ad18e.js similarity index 86% rename from assets/js/0fad6d0a.41f42246.js rename to assets/js/0fad6d0a.d22ad18e.js index 3471521ad..3173724ff 100644 --- a/assets/js/0fad6d0a.41f42246.js +++ b/assets/js/0fad6d0a.d22ad18e.js @@ -1 +1 @@ -"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[1173],{7700:e=>{e.exports=JSON.parse('{"author":{"name":"Colt McNealy","title":"Managing Member of the LLC","description":"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He\'s a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.","page":{"permalink":"/blog/authors/coltmcnealy"},"socials":{"github":"https://github.com/coltmcnealy-lh","linkedin":"https://www.linkedin.com/in/colt-mcnealy-900b7a148/","x":"https://x.com/coltmcnealy"},"imageURL":"https://avatars.githubusercontent.com/u/100447728","key":"coltmcnealy","count":5},"listMetadata":{"permalink":"/blog/authors/coltmcnealy","page":1,"postsPerPage":20,"totalPages":1,"totalCount":5,"blogDescription":"The latest news and analysis from your favorite workflow engine.","blogTitle":"LittleHorse OSS Blog"}}')}}]); \ No newline at end of file +"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[1173],{7700:e=>{e.exports=JSON.parse('{"author":{"name":"Colt McNealy","title":"Managing Member of the LLC","description":"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He\'s a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.","page":{"permalink":"/blog/authors/coltmcnealy"},"socials":{"github":"https://github.com/coltmcnealy-lh","linkedin":"https://www.linkedin.com/in/colt-mcnealy-900b7a148/","x":"https://x.com/coltmcnealy"},"imageURL":"https://avatars.githubusercontent.com/u/100447728","key":"coltmcnealy","count":6},"listMetadata":{"permalink":"/blog/authors/coltmcnealy","page":1,"postsPerPage":20,"totalPages":1,"totalCount":6,"blogDescription":"The latest news and analysis from your favorite workflow engine.","blogTitle":"LittleHorse OSS Blog"}}')}}]); \ No newline at end of file diff --git a/assets/js/32279f3c.8627ffc2.js b/assets/js/32279f3c.9b0bac4c.js similarity index 97% rename from assets/js/32279f3c.8627ffc2.js rename to assets/js/32279f3c.9b0bac4c.js index c01763aee..c61f5fe78 100644 --- a/assets/js/32279f3c.8627ffc2.js +++ b/assets/js/32279f3c.9b0bac4c.js @@ -1 +1 @@ -"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[1634],{3715:(t,e,a)=>{a.r(e),a.d(e,{assets:()=>l,contentTitle:()=>s,default:()=>p,frontMatter:()=>r,metadata:()=>i,toc:()=>c});var n=a(4848),o=a(8453);const r={slug:"saga-pattern",authors:["coltmcnealy"],tags:["analysis","integration-patterns","littlehorse"]},s="Integration Patterns: Saga Transactions",i={permalink:"/blog/saga-pattern",source:"@site/blog/2024-09-24-saga-pattern.md",title:"Integration Patterns: Saga Transactions",description:"The Saga pattern allows you to defend against data loss, dropped orders, and confused (or grumpy) customers. While useful, the Saga pattern is tricky to get right without an orchestrator.",date:"2024-09-24T00:00:00.000Z",tags:[{inline:!1,label:"Technical Analysis",permalink:"/blog/tags/analysis/",description:"Analysis of the current and future state of Technical Architecture."},{inline:!1,label:"Integration Patterns",permalink:"/blog/tags/integration-patterns/",description:"A 5-part blog series on Integration Patterns that are useful for event-driven systems."},{inline:!1,label:"LittleHorse Orchestrator",permalink:"/blog/tags/littlehorse/",description:"Information about the LittleHorse Orchestrator."}],readingTime:6.235,hasTruncateMarker:!0,authors:[{name:"Colt McNealy",title:"Managing Member of the LLC",description:"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He's a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.",page:{permalink:"/blog/authors/coltmcnealy"},socials:{github:"https://github.com/coltmcnealy-lh",linkedin:"https://www.linkedin.com/in/colt-mcnealy-900b7a148/",x:"https://x.com/coltmcnealy"},imageURL:"https://avatars.githubusercontent.com/u/100447728",key:"coltmcnealy"}],frontMatter:{slug:"saga-pattern",authors:["coltmcnealy"],tags:["analysis","integration-patterns","littlehorse"]},unlisted:!1,prevItem:{title:"Integration Patterns: Transactional Outbox",permalink:"/blog/transactional-outbox"},nextItem:{title:"The Basics of Workflow",permalink:"/blog/basics-of-workflow"}},l={authorsImageUrls:[void 0]},c=[];function u(t){const e={p:"p",...(0,o.R)(),...t.components};return(0,n.jsx)(e.p,{children:"The Saga pattern allows you to defend against data loss, dropped orders, and confused (or grumpy) customers. While useful, the Saga pattern is tricky to get right without an orchestrator."})}function p(t={}){const{wrapper:e}={...(0,o.R)(),...t.components};return e?(0,n.jsx)(e,{...t,children:(0,n.jsx)(u,{...t})}):u(t)}},8453:(t,e,a)=>{a.d(e,{R:()=>s,x:()=>i});var n=a(6540);const o={},r=n.createContext(o);function s(t){const e=n.useContext(r);return n.useMemo((function(){return"function"==typeof t?t(e):{...e,...t}}),[e,t])}function i(t){let e;return e=t.disableParentContext?"function"==typeof t.components?t.components(o):t.components||o:s(t.components),n.createElement(r.Provider,{value:e},t.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[1634],{3715:(t,e,a)=>{a.r(e),a.d(e,{assets:()=>l,contentTitle:()=>s,default:()=>p,frontMatter:()=>r,metadata:()=>i,toc:()=>c});var n=a(4848),o=a(8453);const r={slug:"saga-pattern",authors:["coltmcnealy"],tags:["analysis","integration-patterns","littlehorse"]},s="Integration Patterns: Saga Transactions",i={permalink:"/blog/saga-pattern",source:"@site/blog/2024-09-24-saga-pattern.md",title:"Integration Patterns: Saga Transactions",description:"The Saga pattern allows you to defend against data loss, dropped orders, and confused (or grumpy) customers. While useful, the Saga pattern is tricky to get right without an orchestrator.",date:"2024-09-24T00:00:00.000Z",tags:[{inline:!1,label:"Technical Analysis",permalink:"/blog/tags/analysis/",description:"Analysis of the current and future state of Technical Architecture."},{inline:!1,label:"Integration Patterns",permalink:"/blog/tags/integration-patterns/",description:"A 5-part blog series on Integration Patterns that are useful for event-driven systems."},{inline:!1,label:"LittleHorse Orchestrator",permalink:"/blog/tags/littlehorse/",description:"Information about the LittleHorse Orchestrator."}],readingTime:6.215,hasTruncateMarker:!0,authors:[{name:"Colt McNealy",title:"Managing Member of the LLC",description:"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He's a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.",page:{permalink:"/blog/authors/coltmcnealy"},socials:{github:"https://github.com/coltmcnealy-lh",linkedin:"https://www.linkedin.com/in/colt-mcnealy-900b7a148/",x:"https://x.com/coltmcnealy"},imageURL:"https://avatars.githubusercontent.com/u/100447728",key:"coltmcnealy"}],frontMatter:{slug:"saga-pattern",authors:["coltmcnealy"],tags:["analysis","integration-patterns","littlehorse"]},unlisted:!1,prevItem:{title:"Integration Patterns: Transactional Outbox",permalink:"/blog/transactional-outbox"},nextItem:{title:"The Basics of Workflow",permalink:"/blog/basics-of-workflow"}},l={authorsImageUrls:[void 0]},c=[];function u(t){const e={p:"p",...(0,o.R)(),...t.components};return(0,n.jsx)(e.p,{children:"The Saga pattern allows you to defend against data loss, dropped orders, and confused (or grumpy) customers. While useful, the Saga pattern is tricky to get right without an orchestrator."})}function p(t={}){const{wrapper:e}={...(0,o.R)(),...t.components};return e?(0,n.jsx)(e,{...t,children:(0,n.jsx)(u,{...t})}):u(t)}},8453:(t,e,a)=>{a.d(e,{R:()=>s,x:()=>i});var n=a(6540);const o={},r=n.createContext(o);function s(t){const e=n.useContext(r);return n.useMemo((function(){return"function"==typeof t?t(e):{...e,...t}}),[e,t])}function i(t){let e;return e=t.disableParentContext?"function"==typeof t.components?t.components(o):t.components||o:s(t.components),n.createElement(r.Provider,{value:e},t.children)}}}]); \ No newline at end of file diff --git a/assets/js/3a2db09e.29ad30f3.js b/assets/js/3a2db09e.1baedbe7.js similarity index 81% rename from assets/js/3a2db09e.29ad30f3.js rename to assets/js/3a2db09e.1baedbe7.js index a9579a265..e998b9614 100644 --- a/assets/js/3a2db09e.29ad30f3.js +++ b/assets/js/3a2db09e.1baedbe7.js @@ -1 +1 @@ -"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[8121],{8070:e=>{e.exports=JSON.parse('{"tags":[{"label":"Technical Analysis","permalink":"/blog/tags/analysis/","description":"Analysis of the current and future state of Technical Architecture.","count":6},{"label":"Integration Patterns","permalink":"/blog/tags/integration-patterns/","description":"A 5-part blog series on Integration Patterns that are useful for event-driven systems.","count":2},{"label":"LittleHorse Orchestrator","permalink":"/blog/tags/littlehorse/","description":"Information about the LittleHorse Orchestrator.","count":3},{"label":"Microservices and Workflow","permalink":"/blog/tags/microservice-and-workflow/","description":"A 3-part blog series on the challenges inherent with the microservice architecture, and how Workflow Engines can mitigate those difficulties.","count":3},{"label":"LittleHorse Releases","permalink":"/blog/tags/release/","description":"Release blogs for LittleHorse Orchestrator.","count":7}]}')}}]); \ No newline at end of file +"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[8121],{8070:e=>{e.exports=JSON.parse('{"tags":[{"label":"Technical Analysis","permalink":"/blog/tags/analysis/","description":"Analysis of the current and future state of Technical Architecture.","count":7},{"label":"Integration Patterns","permalink":"/blog/tags/integration-patterns/","description":"A 5-part blog series on Integration Patterns that are useful for event-driven systems.","count":3},{"label":"LittleHorse Orchestrator","permalink":"/blog/tags/littlehorse/","description":"Information about the LittleHorse Orchestrator.","count":4},{"label":"Microservices and Workflow","permalink":"/blog/tags/microservice-and-workflow/","description":"A 3-part blog series on the challenges inherent with the microservice architecture, and how Workflow Engines can mitigate those difficulties.","count":3},{"label":"LittleHorse Releases","permalink":"/blog/tags/release/","description":"Release blogs for LittleHorse Orchestrator.","count":7}]}')}}]); \ No newline at end of file diff --git a/assets/js/4ced973d.db6c90d6.js b/assets/js/4ced973d.db6c90d6.js new file mode 100644 index 000000000..cbd148103 --- /dev/null +++ b/assets/js/4ced973d.db6c90d6.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[8302],{7601:(t,e,n)=>{n.r(e),n.d(e,{assets:()=>l,contentTitle:()=>i,default:()=>h,frontMatter:()=>s,metadata:()=>r,toc:()=>c});var o=n(4848),a=n(8453);const s={slug:"queuing",authors:["coltmcnealy"],tags:["analysis","integration-patterns","littlehorse"]},i="Integration Patterns: Queueing",r={permalink:"/blog/queuing",source:"@site/blog/2024-10-28-queuing.md",title:"Integration Patterns: Queueing",description:"When integrating API's, we sometimes have to tie together steps that can take a long time or might not always be available. If we force the callers of our API's to wait for completion, we find ourselves with some grumpy customers. So what can we do about this?",date:"2024-10-28T00:00:00.000Z",tags:[{inline:!1,label:"Technical Analysis",permalink:"/blog/tags/analysis/",description:"Analysis of the current and future state of Technical Architecture."},{inline:!1,label:"Integration Patterns",permalink:"/blog/tags/integration-patterns/",description:"A 5-part blog series on Integration Patterns that are useful for event-driven systems."},{inline:!1,label:"LittleHorse Orchestrator",permalink:"/blog/tags/littlehorse/",description:"Information about the LittleHorse Orchestrator."}],readingTime:5.745,hasTruncateMarker:!0,authors:[{name:"Colt McNealy",title:"Managing Member of the LLC",description:"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He's a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.",page:{permalink:"/blog/authors/coltmcnealy"},socials:{github:"https://github.com/coltmcnealy-lh",linkedin:"https://www.linkedin.com/in/colt-mcnealy-900b7a148/",x:"https://x.com/coltmcnealy"},imageURL:"https://avatars.githubusercontent.com/u/100447728",key:"coltmcnealy"}],frontMatter:{slug:"queuing",authors:["coltmcnealy"],tags:["analysis","integration-patterns","littlehorse"]},unlisted:!1,nextItem:{title:"Integration Patterns: Transactional Outbox",permalink:"/blog/transactional-outbox"}},l={authorsImageUrls:[void 0]},c=[];function u(t){const e={p:"p",...(0,a.R)(),...t.components};return(0,o.jsx)(e.p,{children:"When integrating API's, we sometimes have to tie together steps that can take a long time or might not always be available. If we force the callers of our API's to wait for completion, we find ourselves with some grumpy customers. So what can we do about this?"})}function h(t={}){const{wrapper:e}={...(0,a.R)(),...t.components};return e?(0,o.jsx)(e,{...t,children:(0,o.jsx)(u,{...t})}):u(t)}},8453:(t,e,n)=>{n.d(e,{R:()=>i,x:()=>r});var o=n(6540);const a={},s=o.createContext(a);function i(t){const e=o.useContext(s);return o.useMemo((function(){return"function"==typeof t?t(e):{...e,...t}}),[e,t])}function r(t){let e;return e=t.disableParentContext?"function"==typeof t.components?t.components(a):t.components||a:i(t.components),o.createElement(s.Provider,{value:e},t.children)}}}]); \ No newline at end of file diff --git a/assets/js/5a5f8fd5.bd685f79.js b/assets/js/5a5f8fd5.46c792b2.js similarity index 76% rename from assets/js/5a5f8fd5.bd685f79.js rename to assets/js/5a5f8fd5.46c792b2.js index 0db7ac1ee..69943142c 100644 --- a/assets/js/5a5f8fd5.bd685f79.js +++ b/assets/js/5a5f8fd5.46c792b2.js @@ -1 +1 @@ -"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[8035],{4884:t=>{t.exports=JSON.parse('{"tag":{"label":"LittleHorse Orchestrator","permalink":"/blog/tags/littlehorse/","description":"Information about the LittleHorse Orchestrator.","allTagsPath":"/blog/tags","count":3,"unlisted":false},"listMetadata":{"permalink":"/blog/tags/littlehorse/","page":1,"postsPerPage":20,"totalPages":1,"totalCount":3,"blogDescription":"The latest news and analysis from your favorite workflow engine.","blogTitle":"LittleHorse OSS Blog"}}')}}]); \ No newline at end of file +"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[8035],{4884:t=>{t.exports=JSON.parse('{"tag":{"label":"LittleHorse Orchestrator","permalink":"/blog/tags/littlehorse/","description":"Information about the LittleHorse Orchestrator.","allTagsPath":"/blog/tags","count":4,"unlisted":false},"listMetadata":{"permalink":"/blog/tags/littlehorse/","page":1,"postsPerPage":20,"totalPages":1,"totalCount":4,"blogDescription":"The latest news and analysis from your favorite workflow engine.","blogTitle":"LittleHorse OSS Blog"}}')}}]); \ No newline at end of file diff --git a/assets/js/5b3e9818.5bdb5f9d.js b/assets/js/5b3e9818.77a6948b.js similarity index 78% rename from assets/js/5b3e9818.5bdb5f9d.js rename to assets/js/5b3e9818.77a6948b.js index 52a4d71a2..b4c2a1662 100644 --- a/assets/js/5b3e9818.5bdb5f9d.js +++ b/assets/js/5b3e9818.77a6948b.js @@ -1 +1 @@ -"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[538],{5002:t=>{t.exports=JSON.parse('{"tag":{"label":"Integration Patterns","permalink":"/blog/tags/integration-patterns/","description":"A 5-part blog series on Integration Patterns that are useful for event-driven systems.","allTagsPath":"/blog/tags","count":2,"unlisted":false},"listMetadata":{"permalink":"/blog/tags/integration-patterns/","page":1,"postsPerPage":20,"totalPages":1,"totalCount":2,"blogDescription":"The latest news and analysis from your favorite workflow engine.","blogTitle":"LittleHorse OSS Blog"}}')}}]); \ No newline at end of file +"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[538],{5002:t=>{t.exports=JSON.parse('{"tag":{"label":"Integration Patterns","permalink":"/blog/tags/integration-patterns/","description":"A 5-part blog series on Integration Patterns that are useful for event-driven systems.","allTagsPath":"/blog/tags","count":3,"unlisted":false},"listMetadata":{"permalink":"/blog/tags/integration-patterns/","page":1,"postsPerPage":20,"totalPages":1,"totalCount":3,"blogDescription":"The latest news and analysis from your favorite workflow engine.","blogTitle":"LittleHorse OSS Blog"}}')}}]); \ No newline at end of file diff --git a/assets/js/6e1334f1.28666ef3.js b/assets/js/6e1334f1.28666ef3.js new file mode 100644 index 000000000..8931ead99 --- /dev/null +++ b/assets/js/6e1334f1.28666ef3.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[798],{8650:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>a,default:()=>d,frontMatter:()=>r,metadata:()=>o,toc:()=>c});var s=n(4848),i=n(8453);const r={slug:"queuing",authors:["coltmcnealy"],tags:["analysis","integration-patterns","littlehorse"]},a="Integration Patterns: Queueing",o={permalink:"/blog/queuing",source:"@site/blog/2024-10-28-queuing.md",title:"Integration Patterns: Queueing",description:"When integrating API's, we sometimes have to tie together steps that can take a long time or might not always be available. If we force the callers of our API's to wait for completion, we find ourselves with some grumpy customers. So what can we do about this?",date:"2024-10-28T00:00:00.000Z",tags:[{inline:!1,label:"Technical Analysis",permalink:"/blog/tags/analysis/",description:"Analysis of the current and future state of Technical Architecture."},{inline:!1,label:"Integration Patterns",permalink:"/blog/tags/integration-patterns/",description:"A 5-part blog series on Integration Patterns that are useful for event-driven systems."},{inline:!1,label:"LittleHorse Orchestrator",permalink:"/blog/tags/littlehorse/",description:"Information about the LittleHorse Orchestrator."}],readingTime:5.745,hasTruncateMarker:!0,authors:[{name:"Colt McNealy",title:"Managing Member of the LLC",description:"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He's a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.",page:{permalink:"/blog/authors/coltmcnealy"},socials:{github:"https://github.com/coltmcnealy-lh",linkedin:"https://www.linkedin.com/in/colt-mcnealy-900b7a148/",x:"https://x.com/coltmcnealy"},imageURL:"https://avatars.githubusercontent.com/u/100447728",key:"coltmcnealy"}],frontMatter:{slug:"queuing",authors:["coltmcnealy"],tags:["analysis","integration-patterns","littlehorse"]},unlisted:!1,nextItem:{title:"Integration Patterns: Transactional Outbox",permalink:"/blog/transactional-outbox"}},l={authorsImageUrls:[void 0]},c=[{value:"Why Queue?",id:"why-queue",level:2},{value:"Example: Reviews Application",id:"example-reviews-application",level:3},{value:"Orchestrators vs. Plain Old Queues",id:"orchestrators-vs-plain-old-queues",level:2},{value:"Monitoring and Debugging",id:"monitoring-and-debugging",level:3},{value:"Multi-Step Processes",id:"multi-step-processes",level:3},{value:"Wrapping Up",id:"wrapping-up",level:2},{value:"Get Involved!",id:"get-involved",level:3}];function h(e){const t={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,i.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(t.p,{children:"When integrating API's, we sometimes have to tie together steps that can take a long time or might not always be available. If we force the callers of our API's to wait for completion, we find ourselves with some grumpy customers. So what can we do about this?"}),"\n",(0,s.jsxs)(t.admonition,{type:"info",children:[(0,s.jsx)(t.p,{children:"This is the third part in a five-part blog series on useful Integration Patterns. This blog series will help you build real-time, responsive applications and microservices that produce predictable results and prevent the Grumpy Customer Problem."}),(0,s.jsxs)(t.ol,{children:["\n",(0,s.jsx)(t.li,{children:(0,s.jsx)(t.a,{href:"/blog/saga-pattern",children:"Saga Transactions"})}),"\n",(0,s.jsx)(t.li,{children:(0,s.jsx)(t.a,{href:"/blog/transactional-outbox",children:"The Transactional Outbox Pattern"})}),"\n",(0,s.jsxs)(t.li,{children:[(0,s.jsx)(t.strong,{children:"[This Post]"})," Queuing and Backpressure"]}),"\n",(0,s.jsx)(t.li,{children:"[Coming soon] Retries and Dead-Letter Queues"}),"\n",(0,s.jsx)(t.li,{children:"[Coming soon] Callbacks and External Events"}),"\n"]})]}),"\n",(0,s.jsx)(t.h2,{id:"why-queue",children:"Why Queue?"}),"\n",(0,s.jsx)(t.p,{children:"In software architecture, simple is almost always better. With fewer moving parts, there are less chances for failure, less things to debug, and fewer pieces of infrastructure. So when and why would you introduce queues to your architecture?"}),"\n",(0,s.jsx)(t.p,{children:"Queues are useful when building services that need to accept a request from a client and then execute some processing which has any of the following characteristics:"}),"\n",(0,s.jsxs)(t.ol,{children:["\n",(0,s.jsx)(t.li,{children:"Slow to execute."}),"\n",(0,s.jsx)(t.li,{children:"Flakey, not always-available, or in need of retries."}),"\n",(0,s.jsx)(t.li,{children:"Have a rate limit or cannot gracefully handle spikey workloads (backpressure)."}),"\n",(0,s.jsx)(t.li,{children:"Have multiple steps that need to all complete before the processing is finalized."}),"\n"]}),"\n",(0,s.jsx)(t.p,{children:"Crucially, if your service enqueues requests, you need to make sure that the caller of your API doesn't need to wait for their entire request to be processed: a simple promise that it will get done should be sufficient. As we will see with a practical example, this is feasible in many business cases."}),"\n",(0,s.jsx)(t.h3,{id:"example-reviews-application",children:"Example: Reviews Application"}),"\n",(0,s.jsx)(t.p,{children:"Consider a product reviews widget on an e-commerce site. In this application, users can submit reviews of a product. However, before a review can be approved to be displayed, it must first be checked for offensive content by a third-party AI service. Sometimes, this third-party service often has response times of over 10 seconds, and sometimes even goes down and is fully unavailable."}),"\n",(0,s.jsx)(t.p,{children:"A naive web app endpoint to handle this use-case might be:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-java",children:'@PostMapping("/review")\npublic ResponseEntity postReview(@RequestBody PostReviewRequest request) {\n\n // Call the third-party AI service, which takes a long time and is flakey\n try {\n ReviewAnalysisResponse reviewAnalysis = thirdPartyService.analyzeReview(request);\n } catch(OffensiveReviewException exn) {\n return ResponseEntity.status(400);\n } catch(Exception exn) {\n return ResponseEntity.status(500);\n }\n\n // If we got here, the review is valid\n reviewService.save(request);\n return ResponseEntity.status(HttpStatus.CREATED);\n}\n'})}),"\n",(0,s.jsxs)(t.p,{children:["As promised ( ","\ud83d\ude09"," ), this endpoint implementation has a sub-optimal user experience. Many times, when the flakey third-party AI service is unavailable, users will simply be unable to post reviews. Even when it is up, users will see the spinning waiting wheel for multiple seconds."]}),"\n",(0,s.jsx)(t.p,{children:"The solution? Enqueue the request for processing later by some external system, and then respond immediately to the client's request. That can be done in two ways:"}),"\n",(0,s.jsxs)(t.ol,{children:["\n",(0,s.jsxs)(t.li,{children:[(0,s.jsx)(t.strong,{children:"Traditional Queuing:"})," simply put a record on some queue, streaming system, or event bus (such as Apache Pulsar, Apache Kafka, or AWS SQS)."]}),"\n",(0,s.jsxs)(t.li,{children:[(0,s.jsx)(t.strong,{children:"Workflow Execution:"})," tell a workflow orchestration engine like LittleHorse to start executing a process!"]}),"\n"]}),"\n",(0,s.jsx)(t.p,{children:"Once the request is enqueued, there will be a system polling the queue to call the third-party analytics API and then either reject or approve the review. This system will be responsible for throttling requests according to the API's service limits, retrying failed messages, and waiting for the API to come back online in the case of an intermittent outage."}),"\n",(0,s.jsx)(t.h2,{id:"orchestrators-vs-plain-old-queues",children:"Orchestrators vs. Plain Old Queues"}),"\n",(0,s.jsxs)(t.p,{children:["Workflow engines ",(0,s.jsx)(t.a,{href:"/blog/basics-of-workflow",children:"internally use message queues"})," on their own! So what's the difference from the user perspective?"]}),"\n",(0,s.jsxs)(t.p,{children:["You can think of a workflow engine as a ",(0,s.jsx)(t.em,{children:"super-smart"})," message queue, with certain clear advantages over message queues including advanced monitoring and better support for multi-step processes."]}),"\n",(0,s.jsx)(t.admonition,{type:"note",children:(0,s.jsx)(t.p,{children:"The next post in this series will take a deep-dive into retries, idempotency, and failure handling, which is another area in which workflow engines shine above and beyond Plain Old Queues."})}),"\n",(0,s.jsx)(t.h3,{id:"monitoring-and-debugging",children:"Monitoring and Debugging"}),"\n",(0,s.jsxs)(t.p,{children:["Workflow engines provide more insight and oversight into your processes than do message queues. In our reviews application, if an angry user (",(0,s.jsx)(t.code,{children:"anakin@jeditemple.com"}),") calls customer support to complain that his review hadn't been processed in over two days, it would be tricky to find the ",(0,s.jsx)(t.em,{children:"exact"})," cause with a pure message queue."]}),"\n",(0,s.jsxs)(t.p,{children:["However, with LittleHorse, you just search for the ",(0,s.jsx)(t.code,{children:"WfRun"})," where ",(0,s.jsx)(t.code,{children:"user-id == anakin@jeditemple.com"}),":"]}),"\n",(0,s.jsx)(t.p,{children:(0,s.jsx)(t.img,{alt:"Dashboard Workflow Search",src:n(1659).A+"",width:"496",height:"294"})}),"\n",(0,s.jsx)(t.p,{children:"and then look on the dashboard to see what went wrong:"}),"\n",(0,s.jsx)(t.p,{children:(0,s.jsx)(t.img,{alt:"Dashboard Task Error Message",src:n(2767).A+"",width:"895",height:"686"})}),"\n",(0,s.jsxs)(t.p,{children:["We are also working on ",(0,s.jsx)(t.em,{children:"workflow metrics"})," that will allow you to use LittleHorse to answer questions such as:"]}),"\n",(0,s.jsxs)(t.ul,{children:["\n",(0,s.jsxs)(t.li,{children:["How long does the ",(0,s.jsx)(t.code,{children:"process-review"})," workflow take on average?"]}),"\n",(0,s.jsxs)(t.li,{children:["How long does each ",(0,s.jsx)(t.code,{children:"analyze-review"})," task attempt take on average, and what percentage of calls fail (i.e. how responsive is the API)?"]}),"\n",(0,s.jsx)(t.li,{children:"What percentage of reviews are approved versus rejected?"}),"\n"]}),"\n",(0,s.jsx)(t.p,{children:"These will likely not be available until March 2025; however, we have nearly finalized the designs for them and have scheduled the implementation to start in January."}),"\n",(0,s.jsx)(t.h3,{id:"multi-step-processes",children:"Multi-Step Processes"}),"\n",(0,s.jsx)(t.p,{children:'So far, the use-case we\'ve discussed involves only two "steps" to be executed:'}),"\n",(0,s.jsxs)(t.ol,{children:["\n",(0,s.jsx)(t.li,{children:"Analyze the product review."}),"\n",(0,s.jsx)(t.li,{children:"Post the review to the site."}),"\n"]}),"\n",(0,s.jsxs)(t.p,{children:["You could arguably execute both steps at once: the only problem we are trying to solve is that we have a flakey API and we don't want our customers to have to wait for it. In theory, the same consumer which calls the external API could also update the visibility of the review to ",(0,s.jsx)(t.code,{children:"APPROVED"}),"."]}),"\n",(0,s.jsx)(t.p,{children:"But what if the business requirements change, and we need to do some post-processing, such as notify a separately-managed (and also flakey) analytics service of what happened? That would require adding another queue:"}),"\n",(0,s.jsxs)(t.ol,{children:["\n",(0,s.jsx)(t.li,{children:"Edit our original consumer to publish to a new queue."}),"\n",(0,s.jsxs)(t.li,{children:["Write a ",(0,s.jsx)(t.em,{children:"new"})," consumer that subscribes to the second queue and notifies the flakey analytics service."]}),"\n",(0,s.jsx)(t.li,{children:"Instrument monitoring for the new queue infrastructure."}),"\n"]}),"\n",(0,s.jsx)(t.p,{children:"This gets especially tricky when we want to handle intermittent availability from the analytics service: we'll have to copy the same boilerplate to handle retries and dead-letter-queues (more on that in the next post)."}),"\n",(0,s.jsxs)(t.p,{children:["However, with the workflow-driven approach, all you need to do is add a single line to your ",(0,s.jsx)(t.code,{children:"WfSpec"}),":"]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-java",children:'wf.execute("notify-analytics-service", userId, review, approvalStatus);\n'})}),"\n",(0,s.jsx)(t.h2,{id:"wrapping-up",children:"Wrapping Up"}),"\n",(0,s.jsxs)(t.p,{children:["Queueing is a great tool to improve the client experience of your API's when you can respond to your callers before all of your processing has been done. Workflow engines like LittleHorse can actually be thought of as a ",(0,s.jsx)(t.em,{children:"super-smart queueing system"}),", which provides all of the advantages of queueing plus better observability and support for multi-step processes."]}),"\n",(0,s.jsx)(t.h3,{id:"get-involved",children:"Get Involved!"}),"\n",(0,s.jsx)(t.p,{children:"Stay tuned for the next post, which will cover retries and dead-letter queues! In the meantime:"}),"\n",(0,s.jsxs)(t.ul,{children:["\n",(0,s.jsxs)(t.li,{children:["Try out our ",(0,s.jsx)(t.a,{href:"https://littlehorse.dev/docs/developer-guide/install",children:"Quickstarts"})]}),"\n",(0,s.jsxs)(t.li,{children:["Join us ",(0,s.jsx)(t.a,{href:"https://launchpass.com/littlehorsecommunity",children:"on Slack"})]}),"\n",(0,s.jsxs)(t.li,{children:["Give us a star ",(0,s.jsx)(t.a,{href:"https://github.com/littlehorse-enterprises/littlehorse",children:"on GitHub"}),"!"]}),"\n"]})]})}function d(e={}){const{wrapper:t}={...(0,i.R)(),...e.components};return t?(0,s.jsx)(t,{...e,children:(0,s.jsx)(h,{...e})}):h(e)}},2767:(e,t,n)=>{n.d(t,{A:()=>s});const s=n.p+"assets/images/2024-10-28-task-debugging-dc5d52151429b8428cfa326e57cc53d5.png"},1659:(e,t,n)=>{n.d(t,{A:()=>s});const s=n.p+"assets/images/2024-10-28-workflow-search-6abd5f2dbb3914b3230502f28afab1f2.png"},8453:(e,t,n)=>{n.d(t,{R:()=>a,x:()=>o});var s=n(6540);const i={},r=s.createContext(i);function a(e){const t=s.useContext(r);return s.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:a(e.components),s.createElement(r.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/814f3328.a5db8ea4.js b/assets/js/814f3328.a5db8ea4.js deleted file mode 100644 index 195a8518a..000000000 --- a/assets/js/814f3328.a5db8ea4.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[7472],{5513:e=>{e.exports=JSON.parse('{"title":"Recent posts","items":[{"title":"Integration Patterns: Transactional Outbox","permalink":"/blog/transactional-outbox","unlisted":false,"date":"2024-09-30T00:00:00.000Z"},{"title":"Integration Patterns: Saga Transactions","permalink":"/blog/saga-pattern","unlisted":false,"date":"2024-09-24T00:00:00.000Z"},{"title":"The Basics of Workflow","permalink":"/blog/basics-of-workflow","unlisted":false,"date":"2024-09-04T00:00:00.000Z"},{"title":"Microservices and Workflow: A Match Made in Heaven","permalink":"/blog/microservices-and-workflow","unlisted":false,"date":"2024-09-02T00:00:00.000Z"},{"title":"Releasing 0.11","permalink":"/blog/littlehorse-0.11-release","unlisted":false,"date":"2024-08-31T00:00:00.000Z"},{"title":"The Challenge of Microservices","permalink":"/blog/challenge-of-microservices","unlisted":false,"date":"2024-08-27T00:00:00.000Z"},{"title":"The Promise of Microservices","permalink":"/blog/promise-of-microservices","unlisted":false,"date":"2024-08-22T00:00:00.000Z"},{"title":"Releasing 0.10","permalink":"/blog/littlehorse-0.10-release","unlisted":false,"date":"2024-07-12T00:00:00.000Z"},{"title":"Releasing 0.9","permalink":"/blog/littlehorse-0.9-release","unlisted":false,"date":"2024-06-24T00:00:00.000Z"},{"title":"Releasing 0.8","permalink":"/blog/littlehorse-0.8-release","unlisted":false,"date":"2024-03-26T00:00:00.000Z"},{"title":"Releasing 0.7","permalink":"/blog/littlehorse-0.7-release","unlisted":false,"date":"2024-01-28T00:00:00.000Z"},{"title":"Releasing 0.5.0","permalink":"/blog/littlehorse-0.5.0-release","unlisted":false,"date":"2023-09-08T00:00:00.000Z"},{"title":"Releasing 0.2.0","permalink":"/blog/littlehorse-0.2.0-release","unlisted":false,"date":"2023-08-30T00:00:00.000Z"}]}')}}]); \ No newline at end of file diff --git a/assets/js/814f3328.f086cd68.js b/assets/js/814f3328.f086cd68.js new file mode 100644 index 000000000..e4e383a48 --- /dev/null +++ b/assets/js/814f3328.f086cd68.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[7472],{5513:e=>{e.exports=JSON.parse('{"title":"Recent posts","items":[{"title":"Integration Patterns: Queueing","permalink":"/blog/queuing","unlisted":false,"date":"2024-10-28T00:00:00.000Z"},{"title":"Integration Patterns: Transactional Outbox","permalink":"/blog/transactional-outbox","unlisted":false,"date":"2024-09-30T00:00:00.000Z"},{"title":"Integration Patterns: Saga Transactions","permalink":"/blog/saga-pattern","unlisted":false,"date":"2024-09-24T00:00:00.000Z"},{"title":"The Basics of Workflow","permalink":"/blog/basics-of-workflow","unlisted":false,"date":"2024-09-04T00:00:00.000Z"},{"title":"Microservices and Workflow: A Match Made in Heaven","permalink":"/blog/microservices-and-workflow","unlisted":false,"date":"2024-09-02T00:00:00.000Z"},{"title":"Releasing 0.11","permalink":"/blog/littlehorse-0.11-release","unlisted":false,"date":"2024-08-31T00:00:00.000Z"},{"title":"The Challenge of Microservices","permalink":"/blog/challenge-of-microservices","unlisted":false,"date":"2024-08-27T00:00:00.000Z"},{"title":"The Promise of Microservices","permalink":"/blog/promise-of-microservices","unlisted":false,"date":"2024-08-22T00:00:00.000Z"},{"title":"Releasing 0.10","permalink":"/blog/littlehorse-0.10-release","unlisted":false,"date":"2024-07-12T00:00:00.000Z"},{"title":"Releasing 0.9","permalink":"/blog/littlehorse-0.9-release","unlisted":false,"date":"2024-06-24T00:00:00.000Z"},{"title":"Releasing 0.8","permalink":"/blog/littlehorse-0.8-release","unlisted":false,"date":"2024-03-26T00:00:00.000Z"},{"title":"Releasing 0.7","permalink":"/blog/littlehorse-0.7-release","unlisted":false,"date":"2024-01-28T00:00:00.000Z"},{"title":"Releasing 0.5.0","permalink":"/blog/littlehorse-0.5.0-release","unlisted":false,"date":"2023-09-08T00:00:00.000Z"},{"title":"Releasing 0.2.0","permalink":"/blog/littlehorse-0.2.0-release","unlisted":false,"date":"2023-08-30T00:00:00.000Z"}]}')}}]); \ No newline at end of file diff --git a/assets/js/b136319f.766bfb69.js b/assets/js/b136319f.766bfb69.js new file mode 100644 index 000000000..a6d59c64f --- /dev/null +++ b/assets/js/b136319f.766bfb69.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[7762],{7393:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>a,default:()=>h,frontMatter:()=>i,metadata:()=>o,toc:()=>c});var s=t(4848),r=t(8453);const i={slug:"saga-pattern",authors:["coltmcnealy"],tags:["analysis","integration-patterns","littlehorse"]},a="Integration Patterns: Saga Transactions",o={permalink:"/blog/saga-pattern",source:"@site/blog/2024-09-24-saga-pattern.md",title:"Integration Patterns: Saga Transactions",description:"The Saga pattern allows you to defend against data loss, dropped orders, and confused (or grumpy) customers. While useful, the Saga pattern is tricky to get right without an orchestrator.",date:"2024-09-24T00:00:00.000Z",tags:[{inline:!1,label:"Technical Analysis",permalink:"/blog/tags/analysis/",description:"Analysis of the current and future state of Technical Architecture."},{inline:!1,label:"Integration Patterns",permalink:"/blog/tags/integration-patterns/",description:"A 5-part blog series on Integration Patterns that are useful for event-driven systems."},{inline:!1,label:"LittleHorse Orchestrator",permalink:"/blog/tags/littlehorse/",description:"Information about the LittleHorse Orchestrator."}],readingTime:6.215,hasTruncateMarker:!0,authors:[{name:"Colt McNealy",title:"Managing Member of the LLC",description:"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He's a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.",page:{permalink:"/blog/authors/coltmcnealy"},socials:{github:"https://github.com/coltmcnealy-lh",linkedin:"https://www.linkedin.com/in/colt-mcnealy-900b7a148/",x:"https://x.com/coltmcnealy"},imageURL:"https://avatars.githubusercontent.com/u/100447728",key:"coltmcnealy"}],frontMatter:{slug:"saga-pattern",authors:["coltmcnealy"],tags:["analysis","integration-patterns","littlehorse"]},unlisted:!1,prevItem:{title:"Integration Patterns: Transactional Outbox",permalink:"/blog/transactional-outbox"},nextItem:{title:"The Basics of Workflow",permalink:"/blog/basics-of-workflow"}},l={authorsImageUrls:[void 0]},c=[{value:"The Saga Pattern",id:"the-saga-pattern",level:2},{value:"Use Cases",id:"use-cases",level:3},{value:"Implementation",id:"implementation",level:3},{value:"Case Study: Order Processing",id:"case-study-order-processing",level:2},{value:"Using Message Queues",id:"using-message-queues",level:3},{value:"Using LittleHorse",id:"using-littlehorse",level:3},{value:"Wrapping Up",id:"wrapping-up",level:2},{value:"Get Involved!",id:"get-involved",level:3}];function d(e){const n={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,r.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.p,{children:"The Saga pattern allows you to defend against data loss, dropped orders, and confused (or grumpy) customers. While useful, the Saga pattern is tricky to get right without an orchestrator."}),"\n",(0,s.jsxs)(n.admonition,{type:"info",children:[(0,s.jsx)(n.p,{children:"This is the first part in a five-part blog series on useful Integration Patterns. This blog series will help you build real-time, responsive applications and microservices that produce predictable results and prevent the Grumpy Customer Problem."}),(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"[This Post]"})," Saga Transactions"]}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.a,{href:"/blog/transactional-outbox",children:"The Transactional Outbox Pattern"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.a,{href:"/blog/queuing",children:"Queuing"})}),"\n",(0,s.jsx)(n.li,{children:"[Coming soon] Retries and Dead-Letter Queues"}),"\n",(0,s.jsx)(n.li,{children:"[Coming soon] Callbacks and External Events"}),"\n"]})]}),"\n",(0,s.jsx)(n.h2,{id:"the-saga-pattern",children:"The Saga Pattern"}),"\n",(0,s.jsxs)(n.p,{children:["At a technical level, the ",(0,s.jsx)(n.a,{href:"https://microservices.io/patterns/data/saga.html",children:"Saga Pattern"})," allows you to perform distributed transactions across multiple disparate systems without 2-phase commit."]}),"\n",(0,s.jsx)(n.p,{children:"In plain English, it is a tool in the belt of a software engineer to prevent half-fulfilled bank transfers, hanging orders, or other failures which would result in a Grumpy Customer."}),"\n",(0,s.jsx)(n.admonition,{type:"info",children:(0,s.jsx)(n.p,{children:'The "Saga" pattern gets its name from literature and film, wherein a "saga" is a series of chronologically-ordered related works. For example, the "Star Wars Saga."'})}),"\n",(0,s.jsx)(n.h3,{id:"use-cases",children:"Use Cases"}),"\n",(0,s.jsx)(n.p,{children:"Business processes often need to perform actions in two separate systems either all at once or not at all. For example, you may need to charge a customer's credit card, reserve inventory, and ship an item to the customer all at once or not at all. If the payment went through but shipping failed, we would see the Grumpy Customer Problem yet again."}),"\n",(0,s.jsx)(n.p,{children:"The Saga pattern is appropriate when:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"A business process must take action across multiple separate systems (legacy monoliths, microservices, external API's, etc),"}),"\n",(0,s.jsx)(n.li,{children:'Each of those actions can be undone via a "compensation task", and'}),"\n",(0,s.jsx)(n.li,{children:"All actions must logically happen together or not at all."}),"\n"]}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsxs)(n.p,{children:["It's also worth noting that a different flavor of the Saga pattern can also be used in ",(0,s.jsx)(n.em,{children:"long-running"})," business processes. In a past job, for example, I worked on a project that implemented the Saga pattern to handle the scheduling of home inspections. In this case, the task of finding an inspector to show up at the home and confirming a time with the homeowner needed to be performed atomically."]})}),"\n",(0,s.jsx)(n.h3,{id:"implementation",children:"Implementation"}),"\n",(0,s.jsx)(n.p,{children:"While Saga is very hard to implement, it's simple to describe:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Try to perform the actions across the multiple systems."}),"\n",(0,s.jsxs)(n.li,{children:["If one of the actions fails, then run a ",(0,s.jsx)(n.em,{children:"compensation"})," for all previously-executed tasks."]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.em,{children:"compensation"}),' is simply an action that "undoes" the previous action. For example, the compensation for a payment task might be to issue a refund.']}),"\n",(0,s.jsx)(n.h2,{id:"case-study-order-processing",children:"Case Study: Order Processing"}),"\n",(0,s.jsxs)(n.p,{children:["Let's take a look at a familiar use-case: an order processing workflow involving the ",(0,s.jsx)(n.code,{children:"inventory"})," service, and the ",(0,s.jsx)(n.code,{children:"payments"})," service. (The ",(0,s.jsx)(n.code,{children:"orders"})," service is involved implicitly.) As they would in a real world scenario, all of our services live on separate physical systems and have their own databases."]}),"\n",(0,s.jsx)(n.p,{children:"In this business process, we first reserve inventory for the ordered item. Next, we charge the customer's credit card."}),"\n",(0,s.jsx)(n.p,{children:"If charging the credit card fails, then we have a problem: we've reserved inventory but not sold it."}),"\n",(0,s.jsxs)(n.p,{children:["Our services need the following functionality. In SOA, these would be endpoints; in LittleHorse, they would be ",(0,s.jsx)(n.code,{children:"TaskDef"}),"s:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"create-order"}),": creates an order in the ",(0,s.jsx)(n.code,{children:"PENDING"})," status."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"reserve-inventory"}),": marks an item as no longer available for sale."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"charge-payment"}),": charges the customer."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"release-inventory"}),": marks an item as available for sale again."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"cancel-order"}),": marks an order as ",(0,s.jsx)(n.code,{children:"CANCELED"}),"."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"complete-order"}),": marks an order as ",(0,s.jsx)(n.code,{children:"COMPLETED"}),"."]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"using-message-queues",children:"Using Message Queues"}),"\n",(0,s.jsx)(n.p,{children:"Using message queues, the happy path looks like the following:"}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"Architecture diagram",src:t(4719).A+"",width:"544",height:"493"})}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsxs)(n.p,{children:["The above image assumes the ",(0,s.jsx)(n.em,{children:"choreography"})," pattern, in contrast to the ",(0,s.jsx)(n.em,{children:"orchestrator"})," pattern. The orchestrator pattern is a ton of work and involves writing something that very much resembles LittleHorse!"]})}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:["Orders service calls ",(0,s.jsx)(n.code,{children:"createOrder()"}),"."]}),"\n",(0,s.jsxs)(n.li,{children:["Orders service publishes to the ",(0,s.jsx)(n.code,{children:"reserve-inventory"})," queue."]}),"\n",(0,s.jsxs)(n.li,{children:["Inventory service reads the message and calls ",(0,s.jsx)(n.code,{children:"reserveInventory()"}),"."]}),"\n",(0,s.jsxs)(n.li,{children:["Inventory service publishes to the ",(0,s.jsx)(n.code,{children:"charge-payment"})," queue."]}),"\n",(0,s.jsx)(n.li,{children:"Payment service charges the credit card."}),"\n",(0,s.jsxs)(n.li,{children:["Payment service publishes to the ",(0,s.jsx)(n.code,{children:"complete-order"})," queue."]}),"\n",(0,s.jsxs)(n.li,{children:["Orders service consumes the record and calls ",(0,s.jsx)(n.code,{children:"completeOrder()"}),"."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"In just the happy path, we have strong coupling already between our services in three places, and we have three message queues to manage."}),"\n",(0,s.jsx)(n.p,{children:"But now we need to release the inventory and cancel the order when the payment doesn't go through. So the flow looks like this:"}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"Architecture Diagram",src:t(8525).A+"",width:"712",height:"493"})}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:["Orders service calls ",(0,s.jsx)(n.code,{children:"createOrder()"}),"."]}),"\n",(0,s.jsxs)(n.li,{children:["Orders service publishes to the ",(0,s.jsx)(n.code,{children:"reserve-inventory"})," queue."]}),"\n",(0,s.jsxs)(n.li,{children:["Inventory service reads the message and calls ",(0,s.jsx)(n.code,{children:"reserveInventory()"}),"."]}),"\n",(0,s.jsxs)(n.li,{children:["Inventory service publishes to the ",(0,s.jsx)(n.code,{children:"charge-payment"})," queue."]}),"\n",(0,s.jsxs)(n.li,{children:["Payment service charges the credit card ",(0,s.jsx)(n.em,{children:"unsuccessfully"}),"."]}),"\n",(0,s.jsxs)(n.li,{children:["Payment service publishes to the ",(0,s.jsx)(n.code,{children:"release-inventory"})," queue."]}),"\n",(0,s.jsxs)(n.li,{children:["Inventory service reads the record and calls ",(0,s.jsx)(n.code,{children:"releaseInventory()"}),"."]}),"\n",(0,s.jsxs)(n.li,{children:["Inventory service publishes to the ",(0,s.jsx)(n.code,{children:"cancel-order"})," queue."]}),"\n",(0,s.jsxs)(n.li,{children:["Orders service consumes the record and calls ",(0,s.jsx)(n.code,{children:"cancelOrder()"}),"."]}),"\n"]}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsxs)(n.p,{children:["We still haven't even considered the case when the ",(0,s.jsx)(n.code,{children:"reserve-inventory"})," step fails and we need to catch that exception and handle the order. For the sake of brevity, we will leave that out."]})}),"\n",(0,s.jsxs)(n.p,{children:["Now, we have ",(0,s.jsx)(n.em,{children:"five"})," different message queues that we have to wrangle with. We can also see that the overall business flow has started to leak across all of our different services."]}),"\n",(0,s.jsx)(n.admonition,{type:"danger",children:(0,s.jsxs)(n.p,{children:["One thing we are ignoring in this blog post is ",(0,s.jsx)(n.em,{children:"reliability"}),": to make this setup production-ready, we would also have to ensure that updates to the internal databases of the services are atomic along with pushing messages to the message queue. We will cover that in next week's post (along with how LittleHorse takes care of that for you)."]})}),"\n",(0,s.jsx)(n.h3,{id:"using-littlehorse",children:"Using LittleHorse"}),"\n",(0,s.jsxs)(n.p,{children:["Using LittleHorse, in java, this whole workflow could look like the following. This is ",(0,s.jsx)(n.em,{children:"real code"})," that does indeed compile and replaces the need for all of the complex queueing logic we had above."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-java",children:'public void sagaExample(WorkflowThread wf) {\n var item = wf.addVariable("item", STR);\n var customer = wf.addVariable("customer", STR);\n var price = wf.addVariable("price", DOUBLE);\n var orderId = wf.addVariable("order-id", STR);\n\n wf.execute("create-order", orderId);\n\n // Saga Here! (We skipped this part in the previous section due to\n // complexity, but LH makes it simple enough.\n NodeOutput inventoryResult = wf.execute("reserve-inventory", item, orderId);\n wf.handleException(inventoryResult, "out-of-stock", handler -> {\n handler.execute("cancel-order", orderId);\n handler.fail("out-of-stock", "Item was out of stock. Order canceled");\n })\n\n NodeOutput paymentResult = wf.execute("charge-payment", customer, price);\n // Saga here again!!\n wf.handleException(paymentResult, "credit-card-declined", handler -> {\n handler.execute("release-inventory", item, orderId);\n handler.execute("cancel-order", orderId);\n handler.fail("credit-card-declined", "Credit card was declined. Order canceled!");\n });\n\n wf.execute("complete-order", orderId);\n}\n'})}),"\n",(0,s.jsxs)(n.p,{children:["Instead of managing five message queues and five strongly-coupled integration points between microservices, all we need to do is register the workflow, define ",(0,s.jsx)(n.em,{children:"truly"})," modular tasks, and let LittleHorse take care of the rest."]}),"\n",(0,s.jsx)(n.h2,{id:"wrapping-up",children:"Wrapping Up"}),"\n",(0,s.jsxs)(n.p,{children:["The Saga Pattern is one of five tools we will cover in this series on avoiding the Grumpy Customer Problem. It's simple to understand but ",(0,s.jsx)(n.em,{children:"painfully complex"})," to implement. Fortunately, LittleHorse makes it easier!"]}),"\n",(0,s.jsxs)(n.admonition,{type:"note",children:[(0,s.jsxs)(n.p,{children:["A careful reader, or anyone who ",(0,s.jsx)(n.a,{href:"https://www.linkedin.com/feed/update/urn:li:activity:7244572885179121664/",children:"reads my rants on LinkedIn"}),", might note that in order to make the order processing workflow truly reliable, we would also need to do something like the Outbox pattern or Event Sourcing."]}),(0,s.jsx)(n.p,{children:"That is true, and we'll cover it in the next post (and you'll see how LittleHorse does that for you automatically!)."})]}),"\n",(0,s.jsx)(n.h3,{id:"get-involved",children:"Get Involved!"}),"\n",(0,s.jsx)(n.p,{children:"Stay tuned for the next post on the Transactional Outbox Pattern! In the meantime:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["Try out our ",(0,s.jsx)(n.a,{href:"https://littlehorse.dev/docs/developer-guide/install",children:"Quickstarts"})]}),"\n",(0,s.jsxs)(n.li,{children:["Join us ",(0,s.jsx)(n.a,{href:"https://launchpass.com/littlehorsecommunity",children:"on Slack"})]}),"\n",(0,s.jsxs)(n.li,{children:["Give us a star ",(0,s.jsx)(n.a,{href:"https://github.com/littlehorse-enterprises/littlehorse",children:"on GitHub"}),"!"]}),"\n"]})]})}function h(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(d,{...e})}):d(e)}},8525:(e,n,t)=>{t.d(n,{A:()=>s});const s=t.p+"assets/images/2024-09-24-choreography-saga-b400a44518ce7213d491df50bad0bb72.png"},4719:(e,n,t)=>{t.d(n,{A:()=>s});const s=t.p+"assets/images/2024-09-24-choreography-simple-830e1fcf682eb3cde8b40210f92b3dd2.png"},8453:(e,n,t)=>{t.d(n,{R:()=>a,x:()=>o});var s=t(6540);const r={},i=s.createContext(r);function a(e){const n=s.useContext(i);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),s.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/b136319f.fb8d8543.js b/assets/js/b136319f.fb8d8543.js deleted file mode 100644 index 9f0b69344..000000000 --- a/assets/js/b136319f.fb8d8543.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[7762],{7393:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>a,default:()=>h,frontMatter:()=>i,metadata:()=>o,toc:()=>c});var s=t(4848),r=t(8453);const i={slug:"saga-pattern",authors:["coltmcnealy"],tags:["analysis","integration-patterns","littlehorse"]},a="Integration Patterns: Saga Transactions",o={permalink:"/blog/saga-pattern",source:"@site/blog/2024-09-24-saga-pattern.md",title:"Integration Patterns: Saga Transactions",description:"The Saga pattern allows you to defend against data loss, dropped orders, and confused (or grumpy) customers. While useful, the Saga pattern is tricky to get right without an orchestrator.",date:"2024-09-24T00:00:00.000Z",tags:[{inline:!1,label:"Technical Analysis",permalink:"/blog/tags/analysis/",description:"Analysis of the current and future state of Technical Architecture."},{inline:!1,label:"Integration Patterns",permalink:"/blog/tags/integration-patterns/",description:"A 5-part blog series on Integration Patterns that are useful for event-driven systems."},{inline:!1,label:"LittleHorse Orchestrator",permalink:"/blog/tags/littlehorse/",description:"Information about the LittleHorse Orchestrator."}],readingTime:6.235,hasTruncateMarker:!0,authors:[{name:"Colt McNealy",title:"Managing Member of the LLC",description:"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He's a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.",page:{permalink:"/blog/authors/coltmcnealy"},socials:{github:"https://github.com/coltmcnealy-lh",linkedin:"https://www.linkedin.com/in/colt-mcnealy-900b7a148/",x:"https://x.com/coltmcnealy"},imageURL:"https://avatars.githubusercontent.com/u/100447728",key:"coltmcnealy"}],frontMatter:{slug:"saga-pattern",authors:["coltmcnealy"],tags:["analysis","integration-patterns","littlehorse"]},unlisted:!1,prevItem:{title:"Integration Patterns: Transactional Outbox",permalink:"/blog/transactional-outbox"},nextItem:{title:"The Basics of Workflow",permalink:"/blog/basics-of-workflow"}},l={authorsImageUrls:[void 0]},c=[{value:"The Saga Pattern",id:"the-saga-pattern",level:2},{value:"Use Cases",id:"use-cases",level:3},{value:"Implementation",id:"implementation",level:3},{value:"Case Study: Order Processing",id:"case-study-order-processing",level:2},{value:"Using Message Queues",id:"using-message-queues",level:3},{value:"Using LittleHorse",id:"using-littlehorse",level:3},{value:"Wrapping Up",id:"wrapping-up",level:2},{value:"Get Involved!",id:"get-involved",level:3}];function d(e){const n={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,r.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.p,{children:"The Saga pattern allows you to defend against data loss, dropped orders, and confused (or grumpy) customers. While useful, the Saga pattern is tricky to get right without an orchestrator."}),"\n",(0,s.jsxs)(n.admonition,{type:"info",children:[(0,s.jsx)(n.p,{children:"This is the first part in a five-part blog series on useful Integration Patterns. This blog series will help you build real-time, responsive applications and microservices that produce predictable results and prevent the Grumpy Customer Problem."}),(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"[This Post]"})," Saga Transactions"]}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.a,{href:"/blog/transactional-outbox",children:"The Transactional Outbox Pattern"})}),"\n",(0,s.jsx)(n.li,{children:"[Coming soon] Queuing and Backpressure"}),"\n",(0,s.jsx)(n.li,{children:"[Coming soon] Retries and Dead-Letter Queues"}),"\n",(0,s.jsx)(n.li,{children:"[Coming soon] Callbacks and External Events"}),"\n"]})]}),"\n",(0,s.jsx)(n.h2,{id:"the-saga-pattern",children:"The Saga Pattern"}),"\n",(0,s.jsxs)(n.p,{children:["At a technical level, the ",(0,s.jsx)(n.a,{href:"https://microservices.io/patterns/data/saga.html",children:"Saga Pattern"})," allows you to perform distributed transactions across multiple disparate systems without 2-phase commit."]}),"\n",(0,s.jsx)(n.p,{children:"In plain English, it is a tool in the belt of a software engineer to prevent half-fulfilled bank transfers, hanging orders, or other failures which would result in a Grumpy Customer."}),"\n",(0,s.jsx)(n.admonition,{type:"info",children:(0,s.jsx)(n.p,{children:'The "Saga" pattern gets its name from literature and film, wherein a "saga" is a series of chronologically-ordered related works. For example, the "Star Wars Saga."'})}),"\n",(0,s.jsx)(n.h3,{id:"use-cases",children:"Use Cases"}),"\n",(0,s.jsx)(n.p,{children:"Business processes often need to perform actions in two separate systems either all at once or not at all. For example, you may need to charge a customer's credit card, reserve inventory, and ship an item to the customer all at once or not at all. If the payment went through but shipping failed, we would see the Grumpy Customer Problem yet again."}),"\n",(0,s.jsx)(n.p,{children:"The Saga pattern is appropriate when:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"A business process must take action across multiple separate systems (legacy monoliths, microservices, external API's, etc),"}),"\n",(0,s.jsx)(n.li,{children:'Each of those actions can be undone via a "compensation task", and'}),"\n",(0,s.jsx)(n.li,{children:"All actions must logically happen together or not at all."}),"\n"]}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsxs)(n.p,{children:["It's also worth noting that a different flavor of the Saga pattern can also be used in ",(0,s.jsx)(n.em,{children:"long-running"})," business processes. In a past job, for example, I worked on a project that implemented the Saga pattern to handle the scheduling of home inspections. In this case, the task of finding an inspector to show up at the home and confirming a time with the homeowner needed to be performed atomically."]})}),"\n",(0,s.jsx)(n.h3,{id:"implementation",children:"Implementation"}),"\n",(0,s.jsx)(n.p,{children:"While Saga is very hard to implement, it's simple to describe:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Try to perform the actions across the multiple systems."}),"\n",(0,s.jsxs)(n.li,{children:["If one of the actions fails, then run a ",(0,s.jsx)(n.em,{children:"compensation"})," for all previously-executed tasks."]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.em,{children:"compensation"}),' is simply an action that "undoes" the previous action. For example, the compensation for a payment task might be to issue a refund.']}),"\n",(0,s.jsx)(n.h2,{id:"case-study-order-processing",children:"Case Study: Order Processing"}),"\n",(0,s.jsxs)(n.p,{children:["Let's take a look at a familiar use-case: an order processing workflow involving the ",(0,s.jsx)(n.code,{children:"inventory"})," service, and the ",(0,s.jsx)(n.code,{children:"payments"})," service. (The ",(0,s.jsx)(n.code,{children:"orders"})," service is involved implicitly.) As they would in a real world scenario, all of our services live on separate physical systems and have their own databases."]}),"\n",(0,s.jsx)(n.p,{children:"In this business process, we first reserve inventory for the ordered item. Next, we charge the customer's credit card."}),"\n",(0,s.jsx)(n.p,{children:"If charging the credit card fails, then we have a problem: we've reserved inventory but not sold it."}),"\n",(0,s.jsxs)(n.p,{children:["Our services need the following functionality. In SOA, these would be endpoints; in LittleHorse, they would be ",(0,s.jsx)(n.code,{children:"TaskDef"}),"s:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"create-order"}),": creates an order in the ",(0,s.jsx)(n.code,{children:"PENDING"})," status."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"reserve-inventory"}),": marks an item as no longer available for sale."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"charge-payment"}),": charges the customer."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"release-inventory"}),": marks an item as available for sale again."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"cancel-order"}),": marks an order as ",(0,s.jsx)(n.code,{children:"CANCELED"}),"."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"complete-order"}),": marks an order as ",(0,s.jsx)(n.code,{children:"COMPLETED"}),"."]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"using-message-queues",children:"Using Message Queues"}),"\n",(0,s.jsx)(n.p,{children:"Using message queues, the happy path looks like the following:"}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"Architecture diagram",src:t(4719).A+"",width:"544",height:"493"})}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsxs)(n.p,{children:["The above image assumes the ",(0,s.jsx)(n.em,{children:"choreography"})," pattern, in contrast to the ",(0,s.jsx)(n.em,{children:"orchestrator"})," pattern. The orchestrator pattern is a ton of work and involves writing something that very much resembles LittleHorse!"]})}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:["Orders service calls ",(0,s.jsx)(n.code,{children:"createOrder()"}),"."]}),"\n",(0,s.jsxs)(n.li,{children:["Orders service publishes to the ",(0,s.jsx)(n.code,{children:"reserve-inventory"})," queue."]}),"\n",(0,s.jsxs)(n.li,{children:["Inventory service reads the message and calls ",(0,s.jsx)(n.code,{children:"reserveInventory()"}),"."]}),"\n",(0,s.jsxs)(n.li,{children:["Inventory service publishes to the ",(0,s.jsx)(n.code,{children:"charge-payment"})," queue."]}),"\n",(0,s.jsx)(n.li,{children:"Payment service charges the credit card."}),"\n",(0,s.jsxs)(n.li,{children:["Payment service publishes to the ",(0,s.jsx)(n.code,{children:"complete-order"})," queue."]}),"\n",(0,s.jsxs)(n.li,{children:["Orders service consumes the record and calls ",(0,s.jsx)(n.code,{children:"completeOrder()"}),"."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"In just the happy path, we have strong coupling already between our services in three places, and we have three message queues to manage."}),"\n",(0,s.jsx)(n.p,{children:"But now we need to release the inventory and cancel the order when the payment doesn't go through. So the flow looks like this:"}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"Architecture Diagram",src:t(8525).A+"",width:"712",height:"493"})}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:["Orders service calls ",(0,s.jsx)(n.code,{children:"createOrder()"}),"."]}),"\n",(0,s.jsxs)(n.li,{children:["Orders service publishes to the ",(0,s.jsx)(n.code,{children:"reserve-inventory"})," queue."]}),"\n",(0,s.jsxs)(n.li,{children:["Inventory service reads the message and calls ",(0,s.jsx)(n.code,{children:"reserveInventory()"}),"."]}),"\n",(0,s.jsxs)(n.li,{children:["Inventory service publishes to the ",(0,s.jsx)(n.code,{children:"charge-payment"})," queue."]}),"\n",(0,s.jsxs)(n.li,{children:["Payment service charges the credit card ",(0,s.jsx)(n.em,{children:"unsuccessfully"}),"."]}),"\n",(0,s.jsxs)(n.li,{children:["Payment service publishes to the ",(0,s.jsx)(n.code,{children:"release-inventory"})," queue."]}),"\n",(0,s.jsxs)(n.li,{children:["Inventory service reads the record and calls ",(0,s.jsx)(n.code,{children:"releaseInventory()"}),"."]}),"\n",(0,s.jsxs)(n.li,{children:["Inventory service publishes to the ",(0,s.jsx)(n.code,{children:"cancel-order"})," queue."]}),"\n",(0,s.jsxs)(n.li,{children:["Orders service consumes the record and calls ",(0,s.jsx)(n.code,{children:"cancelOrder()"}),"."]}),"\n"]}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsxs)(n.p,{children:["We still haven't even considered the case when the ",(0,s.jsx)(n.code,{children:"reserve-inventory"})," step fails and we need to catch that exception and handle the order. For the sake of brevity, we will leave that out."]})}),"\n",(0,s.jsxs)(n.p,{children:["Now, we have ",(0,s.jsx)(n.em,{children:"five"})," different message queues that we have to wrangle with. We can also see that the overall business flow has started to leak across all of our different services."]}),"\n",(0,s.jsx)(n.admonition,{type:"danger",children:(0,s.jsxs)(n.p,{children:["One thing we are ignoring in this blog post is ",(0,s.jsx)(n.em,{children:"reliability"}),": to make this setup production-ready, we would also have to ensure that updates to the internal databases of the services are atomic along with pushing messages to the message queue. We will cover that in next week's post (along with how LittleHorse takes care of that for you)."]})}),"\n",(0,s.jsx)(n.h3,{id:"using-littlehorse",children:"Using LittleHorse"}),"\n",(0,s.jsxs)(n.p,{children:["Using LittleHorse, in java, this whole workflow could look like the following. This is ",(0,s.jsx)(n.em,{children:"real code"})," that does indeed compile and replaces the need for all of the complex queueing logic we had above."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-java",children:'public void sagaExample(WorkflowThread wf) {\n var item = wf.addVariable("item", STR);\n var customer = wf.addVariable("customer", STR);\n var price = wf.addVariable("price", DOUBLE);\n var orderId = wf.addVariable("order-id", STR);\n\n wf.execute("create-order", orderId);\n\n // Saga Here! (We skipped this part in the previous section due to\n // complexity, but LH makes it simple enough.\n NodeOutput inventoryResult = wf.execute("reserve-inventory", item, orderId);\n wf.handleException(inventoryResult, "out-of-stock", handler -> {\n handler.execute("cancel-order", orderId);\n handler.fail("out-of-stock", "Item was out of stock. Order canceled");\n })\n\n NodeOutput paymentResult = wf.execute("charge-payment", customer, price);\n // Saga here again!!\n wf.handleException(paymentResult, "credit-card-declined", handler -> {\n handler.execute("release-inventory", item, orderId);\n handler.execute("cancel-order", orderId);\n handler.fail("credit-card-declined", "Credit card was declined. Order canceled!");\n });\n\n wf.execute("complete-order", orderId);\n}\n'})}),"\n",(0,s.jsxs)(n.p,{children:["Instead of managing five message queues and five strongly-coupled integration points between microservices, all we need to do is register the workflow, define ",(0,s.jsx)(n.em,{children:"truly"})," modular tasks, and let LittleHorse take care of the rest."]}),"\n",(0,s.jsx)(n.h2,{id:"wrapping-up",children:"Wrapping Up"}),"\n",(0,s.jsxs)(n.p,{children:["The Saga Pattern is one of five tools we will cover in this series on avoiding the Grumpy Customer Problem. It's simple to understand but ",(0,s.jsx)(n.em,{children:"painfully complex"})," to implement. Fortunately, LittleHorse makes it easier!"]}),"\n",(0,s.jsxs)(n.admonition,{type:"note",children:[(0,s.jsxs)(n.p,{children:["A careful reader, or anyone who ",(0,s.jsx)(n.a,{href:"https://www.linkedin.com/feed/update/urn:li:activity:7244572885179121664/",children:"reads my rants on LinkedIn"}),", might note that in order to make the order processing workflow truly reliable, we would also need to do something like the Outbox pattern or Event Sourcing."]}),(0,s.jsx)(n.p,{children:"That is true, and we'll cover it in the next post (and you'll see how LittleHorse does that for you automatically!)."})]}),"\n",(0,s.jsx)(n.h3,{id:"get-involved",children:"Get Involved!"}),"\n",(0,s.jsx)(n.p,{children:"Stay tuned for the next post on the Transactional Outbox Pattern! In the meantime:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["Try out our ",(0,s.jsx)(n.a,{href:"https://littlehorse.dev/docs/developer-guide/install",children:"Quickstarts"})]}),"\n",(0,s.jsxs)(n.li,{children:["Join us ",(0,s.jsx)(n.a,{href:"https://launchpass.com/littlehorsecommunity",children:"on Slack"})]}),"\n",(0,s.jsxs)(n.li,{children:["Give us a star ",(0,s.jsx)(n.a,{href:"https://github.com/littlehorse-enterprises/littlehorse",children:"on GitHub"}),"!"]}),"\n"]})]})}function h(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(d,{...e})}):d(e)}},8525:(e,n,t)=>{t.d(n,{A:()=>s});const s=t.p+"assets/images/2024-09-24-choreography-saga-b400a44518ce7213d491df50bad0bb72.png"},4719:(e,n,t)=>{t.d(n,{A:()=>s});const s=t.p+"assets/images/2024-09-24-choreography-simple-830e1fcf682eb3cde8b40210f92b3dd2.png"},8453:(e,n,t)=>{t.d(n,{R:()=>a,x:()=>o});var s=t(6540);const r={},i=s.createContext(r);function a(e){const n=s.useContext(i);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),s.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/b770e739.c86f3b46.js b/assets/js/b770e739.32067987.js similarity index 55% rename from assets/js/b770e739.c86f3b46.js rename to assets/js/b770e739.32067987.js index 9f1ad8ea9..676a17d2b 100644 --- a/assets/js/b770e739.c86f3b46.js +++ b/assets/js/b770e739.32067987.js @@ -1 +1 @@ -"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[9615],{7100:(t,e,n)=>{n.r(e),n.d(e,{assets:()=>l,contentTitle:()=>r,default:()=>h,frontMatter:()=>s,metadata:()=>i,toc:()=>c});var a=n(4848),o=n(8453);const s={slug:"transactional-outbox",authors:["coltmcnealy"],tags:["analysis","integration-patterns","littlehorse"]},r="Integration Patterns: Transactional Outbox",i={permalink:"/blog/transactional-outbox",source:"@site/blog/2024-09-30-transactional-outbox.md",title:"Integration Patterns: Transactional Outbox",description:"Like the Saga Pattern, the Transactional Outbox pattern is tool for defending against data loss in your applications. In this blog we cover how it works and how to do it easier using LittleHorse.",date:"2024-09-30T00:00:00.000Z",tags:[{inline:!1,label:"Technical Analysis",permalink:"/blog/tags/analysis/",description:"Analysis of the current and future state of Technical Architecture."},{inline:!1,label:"Integration Patterns",permalink:"/blog/tags/integration-patterns/",description:"A 5-part blog series on Integration Patterns that are useful for event-driven systems."},{inline:!1,label:"LittleHorse Orchestrator",permalink:"/blog/tags/littlehorse/",description:"Information about the LittleHorse Orchestrator."}],readingTime:5.74,hasTruncateMarker:!0,authors:[{name:"Colt McNealy",title:"Managing Member of the LLC",description:"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He's a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.",page:{permalink:"/blog/authors/coltmcnealy"},socials:{github:"https://github.com/coltmcnealy-lh",linkedin:"https://www.linkedin.com/in/colt-mcnealy-900b7a148/",x:"https://x.com/coltmcnealy"},imageURL:"https://avatars.githubusercontent.com/u/100447728",key:"coltmcnealy"}],frontMatter:{slug:"transactional-outbox",authors:["coltmcnealy"],tags:["analysis","integration-patterns","littlehorse"]},unlisted:!1,nextItem:{title:"Integration Patterns: Saga Transactions",permalink:"/blog/saga-pattern"}},l={authorsImageUrls:[void 0]},c=[];function u(t){const e={a:"a",em:"em",p:"p",...(0,o.R)(),...t.components};return(0,a.jsxs)(e.p,{children:["Like the ",(0,a.jsx)(e.a,{href:"/blog/saga-pattern",children:"Saga Pattern"}),", the Transactional Outbox pattern is tool for defending against data loss in your applications. In this blog we cover how it works and how to do it ",(0,a.jsx)(e.em,{children:"easier"})," using LittleHorse."]})}function h(t={}){const{wrapper:e}={...(0,o.R)(),...t.components};return e?(0,a.jsx)(e,{...t,children:(0,a.jsx)(u,{...t})}):u(t)}},8453:(t,e,n)=>{n.d(e,{R:()=>r,x:()=>i});var a=n(6540);const o={},s=a.createContext(o);function r(t){const e=a.useContext(s);return a.useMemo((function(){return"function"==typeof t?t(e):{...e,...t}}),[e,t])}function i(t){let e;return e=t.disableParentContext?"function"==typeof t.components?t.components(o):t.components||o:r(t.components),a.createElement(s.Provider,{value:e},t.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[9615],{7100:(t,e,n)=>{n.r(e),n.d(e,{assets:()=>l,contentTitle:()=>i,default:()=>g,frontMatter:()=>s,metadata:()=>r,toc:()=>c});var a=n(4848),o=n(8453);const s={slug:"transactional-outbox",authors:["coltmcnealy"],tags:["analysis","integration-patterns","littlehorse"]},i="Integration Patterns: Transactional Outbox",r={permalink:"/blog/transactional-outbox",source:"@site/blog/2024-09-30-transactional-outbox.md",title:"Integration Patterns: Transactional Outbox",description:"Like the Saga Pattern, the Transactional Outbox pattern is tool for defending against data loss in your applications. In this blog we cover how it works and how to do it easier using LittleHorse.",date:"2024-09-30T00:00:00.000Z",tags:[{inline:!1,label:"Technical Analysis",permalink:"/blog/tags/analysis/",description:"Analysis of the current and future state of Technical Architecture."},{inline:!1,label:"Integration Patterns",permalink:"/blog/tags/integration-patterns/",description:"A 5-part blog series on Integration Patterns that are useful for event-driven systems."},{inline:!1,label:"LittleHorse Orchestrator",permalink:"/blog/tags/littlehorse/",description:"Information about the LittleHorse Orchestrator."}],readingTime:5.72,hasTruncateMarker:!0,authors:[{name:"Colt McNealy",title:"Managing Member of the LLC",description:"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He's a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.",page:{permalink:"/blog/authors/coltmcnealy"},socials:{github:"https://github.com/coltmcnealy-lh",linkedin:"https://www.linkedin.com/in/colt-mcnealy-900b7a148/",x:"https://x.com/coltmcnealy"},imageURL:"https://avatars.githubusercontent.com/u/100447728",key:"coltmcnealy"}],frontMatter:{slug:"transactional-outbox",authors:["coltmcnealy"],tags:["analysis","integration-patterns","littlehorse"]},unlisted:!1,prevItem:{title:"Integration Patterns: Queueing",permalink:"/blog/queuing"},nextItem:{title:"Integration Patterns: Saga Transactions",permalink:"/blog/saga-pattern"}},l={authorsImageUrls:[void 0]},c=[];function u(t){const e={a:"a",em:"em",p:"p",...(0,o.R)(),...t.components};return(0,a.jsxs)(e.p,{children:["Like the ",(0,a.jsx)(e.a,{href:"/blog/saga-pattern",children:"Saga Pattern"}),", the Transactional Outbox pattern is tool for defending against data loss in your applications. In this blog we cover how it works and how to do it ",(0,a.jsx)(e.em,{children:"easier"})," using LittleHorse."]})}function g(t={}){const{wrapper:e}={...(0,o.R)(),...t.components};return e?(0,a.jsx)(e,{...t,children:(0,a.jsx)(u,{...t})}):u(t)}},8453:(t,e,n)=>{n.d(e,{R:()=>i,x:()=>r});var a=n(6540);const o={},s=a.createContext(o);function i(t){const e=a.useContext(s);return a.useMemo((function(){return"function"==typeof t?t(e):{...e,...t}}),[e,t])}function r(t){let e;return e=t.disableParentContext?"function"==typeof t.components?t.components(o):t.components||o:i(t.components),a.createElement(s.Provider,{value:e},t.children)}}}]); \ No newline at end of file diff --git a/assets/js/c15d9823.55c527c7.js b/assets/js/c15d9823.7bd0f213.js similarity index 80% rename from assets/js/c15d9823.55c527c7.js rename to assets/js/c15d9823.7bd0f213.js index b6beae1e5..dbe36363d 100644 --- a/assets/js/c15d9823.55c527c7.js +++ b/assets/js/c15d9823.7bd0f213.js @@ -1 +1 @@ -"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[8146],{9328:e=>{e.exports=JSON.parse('{"metadata":{"permalink":"/blog","page":1,"postsPerPage":20,"totalPages":1,"totalCount":13,"blogDescription":"The latest news and analysis from your favorite workflow engine.","blogTitle":"LittleHorse OSS Blog"}}')}}]); \ No newline at end of file +"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[8146],{9328:e=>{e.exports=JSON.parse('{"metadata":{"permalink":"/blog","page":1,"postsPerPage":20,"totalPages":1,"totalCount":14,"blogDescription":"The latest news and analysis from your favorite workflow engine.","blogTitle":"LittleHorse OSS Blog"}}')}}]); \ No newline at end of file diff --git a/assets/js/df0890b4.a0b8ea9e.js b/assets/js/df0890b4.ffde2868.js similarity index 77% rename from assets/js/df0890b4.a0b8ea9e.js rename to assets/js/df0890b4.ffde2868.js index 631859c84..0dff7f95a 100644 --- a/assets/js/df0890b4.a0b8ea9e.js +++ b/assets/js/df0890b4.ffde2868.js @@ -1 +1 @@ -"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[7080],{3362:e=>{e.exports=JSON.parse('{"tag":{"label":"Technical Analysis","permalink":"/blog/tags/analysis/","description":"Analysis of the current and future state of Technical Architecture.","allTagsPath":"/blog/tags","count":6,"unlisted":false},"listMetadata":{"permalink":"/blog/tags/analysis/","page":1,"postsPerPage":20,"totalPages":1,"totalCount":6,"blogDescription":"The latest news and analysis from your favorite workflow engine.","blogTitle":"LittleHorse OSS Blog"}}')}}]); \ No newline at end of file +"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[7080],{3362:e=>{e.exports=JSON.parse('{"tag":{"label":"Technical Analysis","permalink":"/blog/tags/analysis/","description":"Analysis of the current and future state of Technical Architecture.","allTagsPath":"/blog/tags","count":7,"unlisted":false},"listMetadata":{"permalink":"/blog/tags/analysis/","page":1,"postsPerPage":20,"totalPages":1,"totalCount":7,"blogDescription":"The latest news and analysis from your favorite workflow engine.","blogTitle":"LittleHorse OSS Blog"}}')}}]); \ No newline at end of file diff --git a/assets/js/ef8b811a.a4c3f802.js b/assets/js/ef8b811a.0d395909.js similarity index 97% rename from assets/js/ef8b811a.a4c3f802.js rename to assets/js/ef8b811a.0d395909.js index 872febbe0..c8b86bf79 100644 --- a/assets/js/ef8b811a.a4c3f802.js +++ b/assets/js/ef8b811a.0d395909.js @@ -1 +1 @@ -"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[8947],{6600:e=>{e.exports=JSON.parse('{"authors":[{"name":"Colt McNealy","title":"Managing Member of the LLC","description":"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He\'s a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.","page":{"permalink":"/blog/authors/coltmcnealy"},"socials":{"github":"https://github.com/coltmcnealy-lh","linkedin":"https://www.linkedin.com/in/colt-mcnealy-900b7a148/","x":"https://x.com/coltmcnealy"},"imageURL":"https://avatars.githubusercontent.com/u/100447728","key":"coltmcnealy","count":5},{"name":"The LittleHorse Council","title":"The Council of LittleHorse Maintainers","description":"LittleHorse Orchestrator is maintained by LittleHorse Enterprises LLC and available under the SSPL license. The LittleHorse Council is the group of engineers inside LittleHorse Enterprises LLC who are responsible for the stewardship of the open-source Orchestrator project and charged with looking out for the best interests of the LH Community.","url":"https://littlehorse.io","page":{"permalink":"/blog/authors/lh-council"},"socials":{"github":"https://github.com/littlehorse-enterprises","linkedin":"https://www.linkedin.com/company/littlehorse"},"imageURL":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","key":"lh_council","count":7},{"name":"Mitchell Henderson","title":"Principal Technical Architect","description":"Mitch is a software industry veteran experienced with Apache Kafka, Apache Cassandra, and systems modernization across a diverse set of industries including financial services, healthcare, technology, retail, and manufacturing. He prides himself in ensuring that all users of LittleHorse are successful.","page":{"permalink":"/blog/authors/mitchellh"},"socials":{"github":"https://github.com/mitchell-h","linkedin":"https://www.linkedin.com/in/mitchellghenderson/"},"imageURL":"https://avatars.githubusercontent.com/u/6223426","key":"mitchellh","count":1}]}')}}]); \ No newline at end of file +"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[8947],{6600:e=>{e.exports=JSON.parse('{"authors":[{"name":"Colt McNealy","title":"Managing Member of the LLC","description":"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He\'s a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.","page":{"permalink":"/blog/authors/coltmcnealy"},"socials":{"github":"https://github.com/coltmcnealy-lh","linkedin":"https://www.linkedin.com/in/colt-mcnealy-900b7a148/","x":"https://x.com/coltmcnealy"},"imageURL":"https://avatars.githubusercontent.com/u/100447728","key":"coltmcnealy","count":6},{"name":"The LittleHorse Council","title":"The Council of LittleHorse Maintainers","description":"LittleHorse Orchestrator is maintained by LittleHorse Enterprises LLC and available under the SSPL license. The LittleHorse Council is the group of engineers inside LittleHorse Enterprises LLC who are responsible for the stewardship of the open-source Orchestrator project and charged with looking out for the best interests of the LH Community.","url":"https://littlehorse.io","page":{"permalink":"/blog/authors/lh-council"},"socials":{"github":"https://github.com/littlehorse-enterprises","linkedin":"https://www.linkedin.com/company/littlehorse"},"imageURL":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","key":"lh_council","count":7},{"name":"Mitchell Henderson","title":"Principal Technical Architect","description":"Mitch is a software industry veteran experienced with Apache Kafka, Apache Cassandra, and systems modernization across a diverse set of industries including financial services, healthcare, technology, retail, and manufacturing. He prides himself in ensuring that all users of LittleHorse are successful.","page":{"permalink":"/blog/authors/mitchellh"},"socials":{"github":"https://github.com/mitchell-h","linkedin":"https://www.linkedin.com/in/mitchellghenderson/"},"imageURL":"https://avatars.githubusercontent.com/u/6223426","key":"mitchellh","count":1}]}')}}]); \ No newline at end of file diff --git a/assets/js/f21a8f01.939e0c35.js b/assets/js/f21a8f01.939e0c35.js deleted file mode 100644 index 883652313..000000000 --- a/assets/js/f21a8f01.939e0c35.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[2558],{6720:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>d,frontMatter:()=>i,metadata:()=>o,toc:()=>c});var s=n(4848),a=n(8453);const i={slug:"transactional-outbox",authors:["coltmcnealy"],tags:["analysis","integration-patterns","littlehorse"]},r="Integration Patterns: Transactional Outbox",o={permalink:"/blog/transactional-outbox",source:"@site/blog/2024-09-30-transactional-outbox.md",title:"Integration Patterns: Transactional Outbox",description:"Like the Saga Pattern, the Transactional Outbox pattern is tool for defending against data loss in your applications. In this blog we cover how it works and how to do it easier using LittleHorse.",date:"2024-09-30T00:00:00.000Z",tags:[{inline:!1,label:"Technical Analysis",permalink:"/blog/tags/analysis/",description:"Analysis of the current and future state of Technical Architecture."},{inline:!1,label:"Integration Patterns",permalink:"/blog/tags/integration-patterns/",description:"A 5-part blog series on Integration Patterns that are useful for event-driven systems."},{inline:!1,label:"LittleHorse Orchestrator",permalink:"/blog/tags/littlehorse/",description:"Information about the LittleHorse Orchestrator."}],readingTime:5.74,hasTruncateMarker:!0,authors:[{name:"Colt McNealy",title:"Managing Member of the LLC",description:"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He's a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.",page:{permalink:"/blog/authors/coltmcnealy"},socials:{github:"https://github.com/coltmcnealy-lh",linkedin:"https://www.linkedin.com/in/colt-mcnealy-900b7a148/",x:"https://x.com/coltmcnealy"},imageURL:"https://avatars.githubusercontent.com/u/100447728",key:"coltmcnealy"}],frontMatter:{slug:"transactional-outbox",authors:["coltmcnealy"],tags:["analysis","integration-patterns","littlehorse"]},unlisted:!1,nextItem:{title:"Integration Patterns: Saga Transactions",permalink:"/blog/saga-pattern"}},l={authorsImageUrls:[void 0]},c=[{value:"The Transactional Outbox Pattern",id:"the-transactional-outbox-pattern",level:2},{value:"Case Study: Customer Sign-Up",id:"case-study-customer-sign-up",level:2},{value:"Using a Transactional Outbox",id:"using-a-transactional-outbox",level:3},{value:"Using LittleHorse",id:"using-littlehorse",level:3},{value:"Wrapping Up",id:"wrapping-up",level:2},{value:"Additional Use Cases",id:"additional-use-cases",level:3},{value:"Alternative: Log-First Architecture",id:"alternative-log-first-architecture",level:3},{value:"Get Involved!",id:"get-involved",level:3}];function h(e){const t={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,a.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsxs)(t.p,{children:["Like the ",(0,s.jsx)(t.a,{href:"/blog/saga-pattern",children:"Saga Pattern"}),", the Transactional Outbox pattern is tool for defending against data loss in your applications. In this blog we cover how it works and how to do it ",(0,s.jsx)(t.em,{children:"easier"})," using LittleHorse."]}),"\n",(0,s.jsxs)(t.admonition,{type:"info",children:[(0,s.jsx)(t.p,{children:"This is the first part in a five-part blog series on useful Integration Patterns. This blog series will help you build real-time, responsive applications and microservices that produce predictable results and prevent the Grumpy Customer Problem."}),(0,s.jsxs)(t.ol,{children:["\n",(0,s.jsx)(t.li,{children:(0,s.jsx)(t.a,{href:"/blog/saga-pattern",children:"Saga Transactions"})}),"\n",(0,s.jsxs)(t.li,{children:[(0,s.jsx)(t.strong,{children:"[This Post]"})," The Transactional Outbox Pattern"]}),"\n",(0,s.jsx)(t.li,{children:"[Coming soon] Queuing and Backpressure"}),"\n",(0,s.jsx)(t.li,{children:"[Coming soon] Retries and Dead-Letter Queues"}),"\n",(0,s.jsx)(t.li,{children:"[Coming soon] Callbacks and External Events"}),"\n"]})]}),"\n",(0,s.jsx)(t.h2,{id:"the-transactional-outbox-pattern",children:"The Transactional Outbox Pattern"}),"\n",(0,s.jsxs)(t.p,{children:["At the technical level, the ",(0,s.jsx)(t.a,{href:"https://microservices.io/patterns/data/transactional-outbox.html",children:"Transactional Outbox Pattern"})," allows you to atomically:"]}),"\n",(0,s.jsxs)(t.ol,{children:["\n",(0,s.jsx)(t.li,{children:"Update a database, and"}),"\n",(0,s.jsx)(t.li,{children:"Publish a record to a streaming log or message queue (such as Apache Kafka)."}),"\n"]}),"\n",(0,s.jsxs)(t.p,{children:["The ",(0,s.jsx)(t.a,{href:"/blog/saga-pattern",children:"Saga Pattern"})," allows you to make a multi-step business process atomic. However, you can think of the Transactional Outbox pattern as a way to ensure that a process doesn't get dropped halfway through."]}),"\n",(0,s.jsxs)(t.admonition,{type:"tip",children:[(0,s.jsxs)(t.p,{children:["The Transactional Outbox Pattern is often useful ",(0,s.jsx)(t.em,{children:"within"})," a Saga transaction."]}),(0,s.jsx)(t.p,{children:"However, as we'll see later on in this article, LittleHorse removes the need to worry about such difficult technical details."})]}),"\n",(0,s.jsx)(t.h2,{id:"case-study-customer-sign-up",children:"Case Study: Customer Sign-Up"}),"\n",(0,s.jsxs)(t.p,{children:["As an example, let's consider the following Spring Boot REST endpoint (",(0,s.jsx)(t.code,{children:"POST /user"}),"), which must:"]}),"\n",(0,s.jsxs)(t.ol,{children:["\n",(0,s.jsx)(t.li,{children:"Create a customer account in a database."}),"\n",(0,s.jsx)(t.li,{children:"Send a message on a queue which results in a series of account setup actions, including a welcome email being sent to the customer."}),"\n"]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-java",children:'@PostMapping("/user")\npublic ResponseEntity createUser(@RequestBody CreateUserRequest request) {\n database.createUser(request);\n queue.publishUserCreatedEvent(request);\n return ResponseEntity.status(HttpStatus.CREATED);\n}\n'})}),"\n",(0,s.jsxs)(t.p,{children:["A few things can go wrong here which would cause the user to be created in the database but the customer never gets a welcome email, and the account setup fails. First, the queue could be inaccessible (this ",(0,s.jsx)(t.em,{children:"could"})," be saved at the application layer with an exception handler)."]}),"\n",(0,s.jsxs)(t.p,{children:["However, one failure mode which ",(0,s.jsx)(t.em,{children:"cannot"})," be caught at the application layer is if the Spring Boot app crashes during the process of publishing the record to the queue (on or just before the ",(0,s.jsx)(t.code,{children:"queue.publishUserCreatedEvent()"})," line)."]}),"\n",(0,s.jsx)(t.p,{children:"This would definitely cause another case of the Grumpy Customer Problem!"}),"\n",(0,s.jsx)(t.h3,{id:"using-a-transactional-outbox",children:"Using a Transactional Outbox"}),"\n",(0,s.jsx)(t.p,{children:"The core idea of a Transactional Outbox is to make use of transactions within a single database to atomically:"}),"\n",(0,s.jsxs)(t.ol,{children:["\n",(0,s.jsx)(t.li,{children:"Make the database update."}),"\n",(0,s.jsxs)(t.li,{children:["Write the desired queue event to an ",(0,s.jsx)(t.em,{children:"Outbox Table."})]}),"\n"]}),"\n",(0,s.jsx)(t.p,{children:(0,s.jsx)(t.img,{alt:"Transactional Outbox Architecture",src:n(1888).A+"",width:"3631",height:"1623"})}),"\n",(0,s.jsxs)(t.p,{children:["Since items ",(0,s.jsx)(t.code,{children:"1"})," and ",(0,s.jsx)(t.code,{children:"2"})," happen within a single database, it's trivial to wrap them in a transaction. After the queue event is written to the Outbox Table, a separate process eventually reads the new records in the Outbox Table and pushes them to a queue."]}),"\n",(0,s.jsx)(t.p,{children:"We would rewrite our Spring Boot endpoint to only write a transaction to the database. The SQL for that transaction would look something like:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{children:"BEGIN TRANSACTION;\nINSERT INTO user VALUES ...;\nINSERT INTO outbox VALUES ...; # Insert the record for the queue\nCOMMIT;\n"})}),"\n",(0,s.jsxs)(t.p,{children:["The Spring Boot application should have another thread which reads records from the ",(0,s.jsx)(t.code,{children:"outbox"}),' table, publishes them to the queue or streaming system, and updates the record in the database as "read".']}),"\n",(0,s.jsxs)(t.admonition,{type:"warning",children:[(0,s.jsx)(t.p,{children:"The topic of Exactly-Once Semantics is complex; we do not have time in this post to discuss the implications of EOS and a Transactional Outbox."}),(0,s.jsxs)(t.p,{children:['As a hint, you can achieve EOS if you transactionally store the last-written offset inside your message broker. There are many "gotchas" to this depending on your message broker; for example, in Apache Kafka you must use ',(0,s.jsx)(t.code,{children:"read_committed"})," consumers."]})]}),"\n",(0,s.jsx)(t.h3,{id:"using-littlehorse",children:"Using LittleHorse"}),"\n",(0,s.jsx)(t.p,{children:'The Outbox Pattern is necessary to persist outgoing records in the case that we suffer a crash between writing to the database and writing to the record queue. However, what if we could "delegate" persistence and reliability to some other system?'}),"\n",(0,s.jsxs)(t.p,{children:["Enter LittleHorse! What if we had a ",(0,s.jsx)(t.code,{children:"WfSpec"})," that defined our process, as follows:"]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-java",children:'public void wfLogic(WorkflowThread wf) {\n var userRequest = wf.addVariable("create-user-request", JSON_OBJ).required();\n wf.execute("create-user", userRequst);\n wf.execute("send-welcome-email", user);\n}\n'})}),"\n",(0,s.jsx)(t.p,{children:"Now, all our REST endpoint has to do is run the worklfow:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-java",children:'@PostMapping("/user")\npublic ResponseEntity createUser(@RequestBody CreateUserRequest request) {\n // Just run the workflow\n littlehorseClient.runWf(RunWfRequest.newBuilder()\n .setWfSpecName("user-workflow")\n .putVariables("create-user-request", LHLibUtil.objToVarVal(request))\n .build());\n return ResponseEntity.status(HttpStatus.CREATED);\n}\n'})}),"\n",(0,s.jsx)(t.p,{children:(0,s.jsx)(t.img,{alt:"Transactional Outbox Architecture with LittleHorse",src:n(6118).A+"",width:"1970",height:"1485"})}),"\n",(0,s.jsxs)(t.p,{children:["No outbox table needed! If creating the user in the database fails, or if sending the welcome email fails, LittleHorse will patiently retry (according to your retry backoff policy) the ",(0,s.jsx)(t.code,{children:"TaskRun"}),"s until they succeed. In the event that you exhaust your retries, you still haven't lost data:"]}),"\n",(0,s.jsxs)(t.ul,{children:["\n",(0,s.jsx)(t.li,{children:"You can easily search for failed workflows."}),"\n",(0,s.jsxs)(t.li,{children:["You can restart failed workflows with the ",(0,s.jsx)(t.code,{children:"rpc RescueThreadRun"})," once the database incident is resolved."]}),"\n"]}),"\n",(0,s.jsxs)(t.admonition,{type:"tip",children:[(0,s.jsxs)(t.p,{children:["You can add retries using the ",(0,s.jsx)(t.code,{children:"Workflow"})," object:"]}),(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-java",children:'Workflow wf = Workflow.newWorkflow("user-workflow", this::wfLogic);\nwf.setDefaultTaskRetries(10);\nwf.setDefaultTaskExponentialBackoffPolicy(ExponentialBackoffRetryPolicy.newBuilder()\n .setBaseIntervalMs(1000)\n .setMultiplier(3)\n .build());\n\nwf.registerWfSpec(littlehorseClient);\n'})})]}),"\n",(0,s.jsx)(t.h2,{id:"wrapping-up",children:"Wrapping Up"}),"\n",(0,s.jsx)(t.p,{children:"The Transactional Outbox Pattern is a useful and often necessary tool for building reliable integrations between systems. However, it takes time, infrastructure, and deep understanding of distributed systems to get it right. So why spend time solving problems that don't differentiate your business?"}),"\n",(0,s.jsx)(t.p,{children:"Thankfully, LittleHorse offers a workaround to the original problem, removing the need to engage with the complexities of Transactional Outboxes."}),"\n",(0,s.jsx)(t.h3,{id:"additional-use-cases",children:"Additional Use Cases"}),"\n",(0,s.jsxs)(t.p,{children:["The Transactional Outbox pattern is useful anytime you need to update information in a database ",(0,s.jsx)(t.em,{children:"and also"})," publish a record to a streaming log or a message queue."]}),"\n",(0,s.jsx)(t.p,{children:"For example:"}),"\n",(0,s.jsxs)(t.ul,{children:["\n",(0,s.jsxs)(t.li,{children:[(0,s.jsx)(t.strong,{children:"User registration:"})," Save a new user's profile and push a message to a queue in order to trigger a verification email."]}),"\n",(0,s.jsxs)(t.li,{children:[(0,s.jsx)(t.strong,{children:"Appointment scheduling:"})," Save appointment details and notify users via SMS or email."]}),"\n",(0,s.jsxs)(t.li,{children:[(0,s.jsx)(t.strong,{children:"Saga Transactions:"})," Within a Saga transaction (such as the order processing scenario discussed in the ",(0,s.jsx)(t.a,{href:"/blog/saga-pattern#case-study-order-processing",children:"last post"}),"), a service may need to atomically update its database and push a record to a queue."]}),"\n",(0,s.jsxs)(t.li,{children:[(0,s.jsx)(t.strong,{children:"Inventory management:"})," Update stock levels and push updates to warehouses or suppliers."]}),"\n"]}),"\n",(0,s.jsx)(t.h3,{id:"alternative-log-first-architecture",children:"Alternative: Log-First Architecture"}),"\n",(0,s.jsx)(t.p,{children:"Another solution to this specific problem would be to have the request handler (our Spring Boot endpoint) publish directly to an event log like Apache Kafka. Then, there would be two consumer groups for that topic:"}),"\n",(0,s.jsxs)(t.ol,{children:["\n",(0,s.jsxs)(t.li,{children:["A consumer group which creates the ",(0,s.jsx)(t.code,{children:"user"})," record in the database."]}),"\n",(0,s.jsx)(t.li,{children:"A consumer group which sends the welcome email."}),"\n"]}),"\n",(0,s.jsxs)(t.p,{children:["The REST endpoint would return ",(0,s.jsx)(t.code,{children:"201"})," as soon as the record was acknowledged by the streaming platform."]}),"\n",(0,s.jsx)(t.p,{children:"If you squint hard enough, you can see that this is very similar to what happens with LittleHorse; however, using this pattern, you are responsible for wiring together a complex topology of topics and queues, which is much harder than using a workflow!"}),"\n",(0,s.jsx)(t.h3,{id:"get-involved",children:"Get Involved!"}),"\n",(0,s.jsx)(t.p,{children:"Stay tuned for the next post on Queues and Backpressure! In the meantime:"}),"\n",(0,s.jsxs)(t.ul,{children:["\n",(0,s.jsxs)(t.li,{children:["Try out our ",(0,s.jsx)(t.a,{href:"https://littlehorse.dev/docs/developer-guide/install",children:"Quickstarts"})]}),"\n",(0,s.jsxs)(t.li,{children:["Join us ",(0,s.jsx)(t.a,{href:"https://launchpass.com/littlehorsecommunity",children:"on Slack"})]}),"\n",(0,s.jsxs)(t.li,{children:["Give us a star ",(0,s.jsx)(t.a,{href:"https://github.com/littlehorse-enterprises/littlehorse",children:"on GitHub"}),"!"]}),"\n"]})]})}function d(e={}){const{wrapper:t}={...(0,a.R)(),...e.components};return t?(0,s.jsx)(t,{...e,children:(0,s.jsx)(h,{...e})}):h(e)}},6118:(e,t,n)=>{n.d(t,{A:()=>s});const s=n.p+"assets/images/2024-09-30-user-workflow-lh-e7a8ae4a642a0a8d2c4ab2635ce7b445.png"},1888:(e,t,n)=>{n.d(t,{A:()=>s});const s=n.p+"assets/images/2024-09-30user-workflow-outbox-717d91ae3fad27e1d67957580625ab19.png"},8453:(e,t,n)=>{n.d(t,{R:()=>r,x:()=>o});var s=n(6540);const a={},i=s.createContext(a);function r(e){const t=s.useContext(i);return s.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:r(e.components),s.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/f21a8f01.f6b151bf.js b/assets/js/f21a8f01.f6b151bf.js new file mode 100644 index 000000000..e127dc643 --- /dev/null +++ b/assets/js/f21a8f01.f6b151bf.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[2558],{6720:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>d,frontMatter:()=>i,metadata:()=>o,toc:()=>c});var s=n(4848),a=n(8453);const i={slug:"transactional-outbox",authors:["coltmcnealy"],tags:["analysis","integration-patterns","littlehorse"]},r="Integration Patterns: Transactional Outbox",o={permalink:"/blog/transactional-outbox",source:"@site/blog/2024-09-30-transactional-outbox.md",title:"Integration Patterns: Transactional Outbox",description:"Like the Saga Pattern, the Transactional Outbox pattern is tool for defending against data loss in your applications. In this blog we cover how it works and how to do it easier using LittleHorse.",date:"2024-09-30T00:00:00.000Z",tags:[{inline:!1,label:"Technical Analysis",permalink:"/blog/tags/analysis/",description:"Analysis of the current and future state of Technical Architecture."},{inline:!1,label:"Integration Patterns",permalink:"/blog/tags/integration-patterns/",description:"A 5-part blog series on Integration Patterns that are useful for event-driven systems."},{inline:!1,label:"LittleHorse Orchestrator",permalink:"/blog/tags/littlehorse/",description:"Information about the LittleHorse Orchestrator."}],readingTime:5.72,hasTruncateMarker:!0,authors:[{name:"Colt McNealy",title:"Managing Member of the LLC",description:"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He's a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.",page:{permalink:"/blog/authors/coltmcnealy"},socials:{github:"https://github.com/coltmcnealy-lh",linkedin:"https://www.linkedin.com/in/colt-mcnealy-900b7a148/",x:"https://x.com/coltmcnealy"},imageURL:"https://avatars.githubusercontent.com/u/100447728",key:"coltmcnealy"}],frontMatter:{slug:"transactional-outbox",authors:["coltmcnealy"],tags:["analysis","integration-patterns","littlehorse"]},unlisted:!1,prevItem:{title:"Integration Patterns: Queueing",permalink:"/blog/queuing"},nextItem:{title:"Integration Patterns: Saga Transactions",permalink:"/blog/saga-pattern"}},l={authorsImageUrls:[void 0]},c=[{value:"The Transactional Outbox Pattern",id:"the-transactional-outbox-pattern",level:2},{value:"Case Study: Customer Sign-Up",id:"case-study-customer-sign-up",level:2},{value:"Using a Transactional Outbox",id:"using-a-transactional-outbox",level:3},{value:"Using LittleHorse",id:"using-littlehorse",level:3},{value:"Wrapping Up",id:"wrapping-up",level:2},{value:"Additional Use Cases",id:"additional-use-cases",level:3},{value:"Alternative: Log-First Architecture",id:"alternative-log-first-architecture",level:3},{value:"Get Involved!",id:"get-involved",level:3}];function h(e){const t={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,a.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsxs)(t.p,{children:["Like the ",(0,s.jsx)(t.a,{href:"/blog/saga-pattern",children:"Saga Pattern"}),", the Transactional Outbox pattern is tool for defending against data loss in your applications. In this blog we cover how it works and how to do it ",(0,s.jsx)(t.em,{children:"easier"})," using LittleHorse."]}),"\n",(0,s.jsxs)(t.admonition,{type:"info",children:[(0,s.jsx)(t.p,{children:"This is the first part in a five-part blog series on useful Integration Patterns. This blog series will help you build real-time, responsive applications and microservices that produce predictable results and prevent the Grumpy Customer Problem."}),(0,s.jsxs)(t.ol,{children:["\n",(0,s.jsx)(t.li,{children:(0,s.jsx)(t.a,{href:"/blog/saga-pattern",children:"Saga Transactions"})}),"\n",(0,s.jsxs)(t.li,{children:[(0,s.jsx)(t.strong,{children:"[This Post]"})," The Transactional Outbox Pattern"]}),"\n",(0,s.jsx)(t.li,{children:(0,s.jsx)(t.a,{href:"/blog/queuing",children:"Queuing"})}),"\n",(0,s.jsx)(t.li,{children:"[Coming soon] Retries and Dead-Letter Queues"}),"\n",(0,s.jsx)(t.li,{children:"[Coming soon] Callbacks and External Events"}),"\n"]})]}),"\n",(0,s.jsx)(t.h2,{id:"the-transactional-outbox-pattern",children:"The Transactional Outbox Pattern"}),"\n",(0,s.jsxs)(t.p,{children:["At the technical level, the ",(0,s.jsx)(t.a,{href:"https://microservices.io/patterns/data/transactional-outbox.html",children:"Transactional Outbox Pattern"})," allows you to atomically:"]}),"\n",(0,s.jsxs)(t.ol,{children:["\n",(0,s.jsx)(t.li,{children:"Update a database, and"}),"\n",(0,s.jsx)(t.li,{children:"Publish a record to a streaming log or message queue (such as Apache Kafka)."}),"\n"]}),"\n",(0,s.jsxs)(t.p,{children:["The ",(0,s.jsx)(t.a,{href:"/blog/saga-pattern",children:"Saga Pattern"})," allows you to make a multi-step business process atomic. However, you can think of the Transactional Outbox pattern as a way to ensure that a process doesn't get dropped halfway through."]}),"\n",(0,s.jsxs)(t.admonition,{type:"tip",children:[(0,s.jsxs)(t.p,{children:["The Transactional Outbox Pattern is often useful ",(0,s.jsx)(t.em,{children:"within"})," a Saga transaction."]}),(0,s.jsx)(t.p,{children:"However, as we'll see later on in this article, LittleHorse removes the need to worry about such difficult technical details."})]}),"\n",(0,s.jsx)(t.h2,{id:"case-study-customer-sign-up",children:"Case Study: Customer Sign-Up"}),"\n",(0,s.jsxs)(t.p,{children:["As an example, let's consider the following Spring Boot REST endpoint (",(0,s.jsx)(t.code,{children:"POST /user"}),"), which must:"]}),"\n",(0,s.jsxs)(t.ol,{children:["\n",(0,s.jsx)(t.li,{children:"Create a customer account in a database."}),"\n",(0,s.jsx)(t.li,{children:"Send a message on a queue which results in a series of account setup actions, including a welcome email being sent to the customer."}),"\n"]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-java",children:'@PostMapping("/user")\npublic ResponseEntity createUser(@RequestBody CreateUserRequest request) {\n database.createUser(request);\n queue.publishUserCreatedEvent(request);\n return ResponseEntity.status(HttpStatus.CREATED);\n}\n'})}),"\n",(0,s.jsxs)(t.p,{children:["A few things can go wrong here which would cause the user to be created in the database but the customer never gets a welcome email, and the account setup fails. First, the queue could be inaccessible (this ",(0,s.jsx)(t.em,{children:"could"})," be saved at the application layer with an exception handler)."]}),"\n",(0,s.jsxs)(t.p,{children:["However, one failure mode which ",(0,s.jsx)(t.em,{children:"cannot"})," be caught at the application layer is if the Spring Boot app crashes during the process of publishing the record to the queue (on or just before the ",(0,s.jsx)(t.code,{children:"queue.publishUserCreatedEvent()"})," line)."]}),"\n",(0,s.jsx)(t.p,{children:"This would definitely cause another case of the Grumpy Customer Problem!"}),"\n",(0,s.jsx)(t.h3,{id:"using-a-transactional-outbox",children:"Using a Transactional Outbox"}),"\n",(0,s.jsx)(t.p,{children:"The core idea of a Transactional Outbox is to make use of transactions within a single database to atomically:"}),"\n",(0,s.jsxs)(t.ol,{children:["\n",(0,s.jsx)(t.li,{children:"Make the database update."}),"\n",(0,s.jsxs)(t.li,{children:["Write the desired queue event to an ",(0,s.jsx)(t.em,{children:"Outbox Table."})]}),"\n"]}),"\n",(0,s.jsx)(t.p,{children:(0,s.jsx)(t.img,{alt:"Transactional Outbox Architecture",src:n(1888).A+"",width:"3631",height:"1623"})}),"\n",(0,s.jsxs)(t.p,{children:["Since items ",(0,s.jsx)(t.code,{children:"1"})," and ",(0,s.jsx)(t.code,{children:"2"})," happen within a single database, it's trivial to wrap them in a transaction. After the queue event is written to the Outbox Table, a separate process eventually reads the new records in the Outbox Table and pushes them to a queue."]}),"\n",(0,s.jsx)(t.p,{children:"We would rewrite our Spring Boot endpoint to only write a transaction to the database. The SQL for that transaction would look something like:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{children:"BEGIN TRANSACTION;\nINSERT INTO user VALUES ...;\nINSERT INTO outbox VALUES ...; # Insert the record for the queue\nCOMMIT;\n"})}),"\n",(0,s.jsxs)(t.p,{children:["The Spring Boot application should have another thread which reads records from the ",(0,s.jsx)(t.code,{children:"outbox"}),' table, publishes them to the queue or streaming system, and updates the record in the database as "read".']}),"\n",(0,s.jsxs)(t.admonition,{type:"warning",children:[(0,s.jsx)(t.p,{children:"The topic of Exactly-Once Semantics is complex; we do not have time in this post to discuss the implications of EOS and a Transactional Outbox."}),(0,s.jsxs)(t.p,{children:['As a hint, you can achieve EOS if you transactionally store the last-written offset inside your message broker. There are many "gotchas" to this depending on your message broker; for example, in Apache Kafka you must use ',(0,s.jsx)(t.code,{children:"read_committed"})," consumers."]})]}),"\n",(0,s.jsx)(t.h3,{id:"using-littlehorse",children:"Using LittleHorse"}),"\n",(0,s.jsx)(t.p,{children:'The Outbox Pattern is necessary to persist outgoing records in the case that we suffer a crash between writing to the database and writing to the record queue. However, what if we could "delegate" persistence and reliability to some other system?'}),"\n",(0,s.jsxs)(t.p,{children:["Enter LittleHorse! What if we had a ",(0,s.jsx)(t.code,{children:"WfSpec"})," that defined our process, as follows:"]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-java",children:'public void wfLogic(WorkflowThread wf) {\n var userRequest = wf.addVariable("create-user-request", JSON_OBJ).required();\n wf.execute("create-user", userRequst);\n wf.execute("send-welcome-email", user);\n}\n'})}),"\n",(0,s.jsx)(t.p,{children:"Now, all our REST endpoint has to do is run the worklfow:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-java",children:'@PostMapping("/user")\npublic ResponseEntity createUser(@RequestBody CreateUserRequest request) {\n // Just run the workflow\n littlehorseClient.runWf(RunWfRequest.newBuilder()\n .setWfSpecName("user-workflow")\n .putVariables("create-user-request", LHLibUtil.objToVarVal(request))\n .build());\n return ResponseEntity.status(HttpStatus.CREATED);\n}\n'})}),"\n",(0,s.jsx)(t.p,{children:(0,s.jsx)(t.img,{alt:"Transactional Outbox Architecture with LittleHorse",src:n(6118).A+"",width:"1970",height:"1485"})}),"\n",(0,s.jsxs)(t.p,{children:["No outbox table needed! If creating the user in the database fails, or if sending the welcome email fails, LittleHorse will patiently retry (according to your retry backoff policy) the ",(0,s.jsx)(t.code,{children:"TaskRun"}),"s until they succeed. In the event that you exhaust your retries, you still haven't lost data:"]}),"\n",(0,s.jsxs)(t.ul,{children:["\n",(0,s.jsx)(t.li,{children:"You can easily search for failed workflows."}),"\n",(0,s.jsxs)(t.li,{children:["You can restart failed workflows with the ",(0,s.jsx)(t.code,{children:"rpc RescueThreadRun"})," once the database incident is resolved."]}),"\n"]}),"\n",(0,s.jsxs)(t.admonition,{type:"tip",children:[(0,s.jsxs)(t.p,{children:["You can add retries using the ",(0,s.jsx)(t.code,{children:"Workflow"})," object:"]}),(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-java",children:'Workflow wf = Workflow.newWorkflow("user-workflow", this::wfLogic);\nwf.setDefaultTaskRetries(10);\nwf.setDefaultTaskExponentialBackoffPolicy(ExponentialBackoffRetryPolicy.newBuilder()\n .setBaseIntervalMs(1000)\n .setMultiplier(3)\n .build());\n\nwf.registerWfSpec(littlehorseClient);\n'})})]}),"\n",(0,s.jsx)(t.h2,{id:"wrapping-up",children:"Wrapping Up"}),"\n",(0,s.jsx)(t.p,{children:"The Transactional Outbox Pattern is a useful and often necessary tool for building reliable integrations between systems. However, it takes time, infrastructure, and deep understanding of distributed systems to get it right. So why spend time solving problems that don't differentiate your business?"}),"\n",(0,s.jsx)(t.p,{children:"Thankfully, LittleHorse offers a workaround to the original problem, removing the need to engage with the complexities of Transactional Outboxes."}),"\n",(0,s.jsx)(t.h3,{id:"additional-use-cases",children:"Additional Use Cases"}),"\n",(0,s.jsxs)(t.p,{children:["The Transactional Outbox pattern is useful anytime you need to update information in a database ",(0,s.jsx)(t.em,{children:"and also"})," publish a record to a streaming log or a message queue."]}),"\n",(0,s.jsx)(t.p,{children:"For example:"}),"\n",(0,s.jsxs)(t.ul,{children:["\n",(0,s.jsxs)(t.li,{children:[(0,s.jsx)(t.strong,{children:"User registration:"})," Save a new user's profile and push a message to a queue in order to trigger a verification email."]}),"\n",(0,s.jsxs)(t.li,{children:[(0,s.jsx)(t.strong,{children:"Appointment scheduling:"})," Save appointment details and notify users via SMS or email."]}),"\n",(0,s.jsxs)(t.li,{children:[(0,s.jsx)(t.strong,{children:"Saga Transactions:"})," Within a Saga transaction (such as the order processing scenario discussed in the ",(0,s.jsx)(t.a,{href:"/blog/saga-pattern#case-study-order-processing",children:"last post"}),"), a service may need to atomically update its database and push a record to a queue."]}),"\n",(0,s.jsxs)(t.li,{children:[(0,s.jsx)(t.strong,{children:"Inventory management:"})," Update stock levels and push updates to warehouses or suppliers."]}),"\n"]}),"\n",(0,s.jsx)(t.h3,{id:"alternative-log-first-architecture",children:"Alternative: Log-First Architecture"}),"\n",(0,s.jsx)(t.p,{children:"Another solution to this specific problem would be to have the request handler (our Spring Boot endpoint) publish directly to an event log like Apache Kafka. Then, there would be two consumer groups for that topic:"}),"\n",(0,s.jsxs)(t.ol,{children:["\n",(0,s.jsxs)(t.li,{children:["A consumer group which creates the ",(0,s.jsx)(t.code,{children:"user"})," record in the database."]}),"\n",(0,s.jsx)(t.li,{children:"A consumer group which sends the welcome email."}),"\n"]}),"\n",(0,s.jsxs)(t.p,{children:["The REST endpoint would return ",(0,s.jsx)(t.code,{children:"201"})," as soon as the record was acknowledged by the streaming platform."]}),"\n",(0,s.jsx)(t.p,{children:"If you squint hard enough, you can see that this is very similar to what happens with LittleHorse; however, using this pattern, you are responsible for wiring together a complex topology of topics and queues, which is much harder than using a workflow!"}),"\n",(0,s.jsx)(t.h3,{id:"get-involved",children:"Get Involved!"}),"\n",(0,s.jsx)(t.p,{children:"Stay tuned for the next post on Queues and Backpressure! In the meantime:"}),"\n",(0,s.jsxs)(t.ul,{children:["\n",(0,s.jsxs)(t.li,{children:["Try out our ",(0,s.jsx)(t.a,{href:"https://littlehorse.dev/docs/developer-guide/install",children:"Quickstarts"})]}),"\n",(0,s.jsxs)(t.li,{children:["Join us ",(0,s.jsx)(t.a,{href:"https://launchpass.com/littlehorsecommunity",children:"on Slack"})]}),"\n",(0,s.jsxs)(t.li,{children:["Give us a star ",(0,s.jsx)(t.a,{href:"https://github.com/littlehorse-enterprises/littlehorse",children:"on GitHub"}),"!"]}),"\n"]})]})}function d(e={}){const{wrapper:t}={...(0,a.R)(),...e.components};return t?(0,s.jsx)(t,{...e,children:(0,s.jsx)(h,{...e})}):h(e)}},6118:(e,t,n)=>{n.d(t,{A:()=>s});const s=n.p+"assets/images/2024-09-30-user-workflow-lh-e7a8ae4a642a0a8d2c4ab2635ce7b445.png"},1888:(e,t,n)=>{n.d(t,{A:()=>s});const s=n.p+"assets/images/2024-09-30user-workflow-outbox-717d91ae3fad27e1d67957580625ab19.png"},8453:(e,t,n)=>{n.d(t,{R:()=>r,x:()=>o});var s=n(6540);const a={},i=s.createContext(a);function r(e){const t=s.useContext(i);return s.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:r(e.components),s.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/f81c1134.3437bee0.js b/assets/js/f81c1134.3437bee0.js deleted file mode 100644 index 45687f9aa..000000000 --- a/assets/js/f81c1134.3437bee0.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[8130],{7735:e=>{e.exports=JSON.parse('{"archive":{"blogPosts":[{"id":"transactional-outbox","metadata":{"permalink":"/blog/transactional-outbox","source":"@site/blog/2024-09-30-transactional-outbox.md","title":"Integration Patterns: Transactional Outbox","description":"Like the Saga Pattern, the Transactional Outbox pattern is tool for defending against data loss in your applications. In this blog we cover how it works and how to do it easier using LittleHorse.","date":"2024-09-30T00:00:00.000Z","tags":[{"inline":false,"label":"Technical Analysis","permalink":"/blog/tags/analysis/","description":"Analysis of the current and future state of Technical Architecture."},{"inline":false,"label":"Integration Patterns","permalink":"/blog/tags/integration-patterns/","description":"A 5-part blog series on Integration Patterns that are useful for event-driven systems."},{"inline":false,"label":"LittleHorse Orchestrator","permalink":"/blog/tags/littlehorse/","description":"Information about the LittleHorse Orchestrator."}],"readingTime":5.74,"hasTruncateMarker":true,"authors":[{"name":"Colt McNealy","title":"Managing Member of the LLC","description":"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He\'s a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.","page":{"permalink":"/blog/authors/coltmcnealy"},"socials":{"github":"https://github.com/coltmcnealy-lh","linkedin":"https://www.linkedin.com/in/colt-mcnealy-900b7a148/","x":"https://x.com/coltmcnealy"},"imageURL":"https://avatars.githubusercontent.com/u/100447728","key":"coltmcnealy"}],"frontMatter":{"slug":"transactional-outbox","authors":["coltmcnealy"],"tags":["analysis","integration-patterns","littlehorse"]},"unlisted":false,"nextItem":{"title":"Integration Patterns: Saga Transactions","permalink":"/blog/saga-pattern"}},"content":"Like the [Saga Pattern](./2024-09-24-saga-pattern.md), the Transactional Outbox pattern is tool for defending against data loss in your applications. In this blog we cover how it works and how to do it _easier_ using LittleHorse.\\n\\n\x3c!-- truncate --\x3e\\n\\n:::info\\nThis is the first part in a five-part blog series on useful Integration Patterns. This blog series will help you build real-time, responsive applications and microservices that produce predictable results and prevent the Grumpy Customer Problem.\\n\\n1. [Saga Transactions](./2024-09-24-saga-pattern.md)\\n2. **[This Post]** The Transactional Outbox Pattern\\n3. [Coming soon] Queuing and Backpressure\\n4. [Coming soon] Retries and Dead-Letter Queues\\n5. [Coming soon] Callbacks and External Events\\n:::\\n\\n## The Transactional Outbox Pattern\\n\\nAt the technical level, the [Transactional Outbox Pattern](https://microservices.io/patterns/data/transactional-outbox.html) allows you to atomically:\\n\\n1. Update a database, and\\n2. Publish a record to a streaming log or message queue (such as Apache Kafka).\\n\\nThe [Saga Pattern](./2024-09-24-saga-pattern.md) allows you to make a multi-step business process atomic. However, you can think of the Transactional Outbox pattern as a way to ensure that a process doesn\'t get dropped halfway through.\\n\\n:::tip\\nThe Transactional Outbox Pattern is often useful _within_ a Saga transaction.\\n\\nHowever, as we\'ll see later on in this article, LittleHorse removes the need to worry about such difficult technical details.\\n:::\\n\\n## Case Study: Customer Sign-Up\\n\\nAs an example, let\'s consider the following Spring Boot REST endpoint (`POST /user`), which must:\\n\\n1. Create a customer account in a database.\\n2. Send a message on a queue which results in a series of account setup actions, including a welcome email being sent to the customer.\\n\\n```java\\n@PostMapping(\\"/user\\")\\npublic ResponseEntity createUser(@RequestBody CreateUserRequest request) {\\n database.createUser(request);\\n queue.publishUserCreatedEvent(request);\\n return ResponseEntity.status(HttpStatus.CREATED);\\n}\\n```\\n\\nA few things can go wrong here which would cause the user to be created in the database but the customer never gets a welcome email, and the account setup fails. First, the queue could be inaccessible (this _could_ be saved at the application layer with an exception handler).\\n\\nHowever, one failure mode which _cannot_ be caught at the application layer is if the Spring Boot app crashes during the process of publishing the record to the queue (on or just before the `queue.publishUserCreatedEvent()` line).\\n\\nThis would definitely cause another case of the Grumpy Customer Problem!\\n\\n### Using a Transactional Outbox\\n\\nThe core idea of a Transactional Outbox is to make use of transactions within a single database to atomically:\\n\\n1. Make the database update.\\n2. Write the desired queue event to an _Outbox Table._\\n\\n![Transactional Outbox Architecture](./2024-09-30user-workflow-outbox.png)\\n\\nSince items `1` and `2` happen within a single database, it\'s trivial to wrap them in a transaction. After the queue event is written to the Outbox Table, a separate process eventually reads the new records in the Outbox Table and pushes them to a queue.\\n\\nWe would rewrite our Spring Boot endpoint to only write a transaction to the database. The SQL for that transaction would look something like:\\n\\n```\\nBEGIN TRANSACTION;\\nINSERT INTO user VALUES ...;\\nINSERT INTO outbox VALUES ...; # Insert the record for the queue\\nCOMMIT;\\n```\\n\\nThe Spring Boot application should have another thread which reads records from the `outbox` table, publishes them to the queue or streaming system, and updates the record in the database as \\"read\\".\\n\\n:::warning\\nThe topic of Exactly-Once Semantics is complex; we do not have time in this post to discuss the implications of EOS and a Transactional Outbox.\\n\\nAs a hint, you can achieve EOS if you transactionally store the last-written offset inside your message broker. There are many \\"gotchas\\" to this depending on your message broker; for example, in Apache Kafka you must use `read_committed` consumers.\\n:::\\n\\n### Using LittleHorse\\n\\nThe Outbox Pattern is necessary to persist outgoing records in the case that we suffer a crash between writing to the database and writing to the record queue. However, what if we could \\"delegate\\" persistence and reliability to some other system?\\n\\nEnter LittleHorse! What if we had a `WfSpec` that defined our process, as follows:\\n\\n```java\\npublic void wfLogic(WorkflowThread wf) {\\n var userRequest = wf.addVariable(\\"create-user-request\\", JSON_OBJ).required();\\n wf.execute(\\"create-user\\", userRequst);\\n wf.execute(\\"send-welcome-email\\", user);\\n}\\n```\\nNow, all our REST endpoint has to do is run the worklfow:\\n\\n```java\\n@PostMapping(\\"/user\\")\\npublic ResponseEntity createUser(@RequestBody CreateUserRequest request) {\\n // Just run the workflow\\n littlehorseClient.runWf(RunWfRequest.newBuilder()\\n .setWfSpecName(\\"user-workflow\\")\\n .putVariables(\\"create-user-request\\", LHLibUtil.objToVarVal(request))\\n .build());\\n return ResponseEntity.status(HttpStatus.CREATED);\\n}\\n```\\n\\n![Transactional Outbox Architecture with LittleHorse](./2024-09-30-user-workflow-lh.png)\\n\\nNo outbox table needed! If creating the user in the database fails, or if sending the welcome email fails, LittleHorse will patiently retry (according to your retry backoff policy) the `TaskRun`s until they succeed. In the event that you exhaust your retries, you still haven\'t lost data:\\n\\n* You can easily search for failed workflows.\\n* You can restart failed workflows with the `rpc RescueThreadRun` once the database incident is resolved.\\n\\n:::tip\\nYou can add retries using the `Workflow` object:\\n\\n```java\\nWorkflow wf = Workflow.newWorkflow(\\"user-workflow\\", this::wfLogic);\\nwf.setDefaultTaskRetries(10);\\nwf.setDefaultTaskExponentialBackoffPolicy(ExponentialBackoffRetryPolicy.newBuilder()\\n .setBaseIntervalMs(1000)\\n .setMultiplier(3)\\n .build());\\n\\nwf.registerWfSpec(littlehorseClient);\\n```\\n:::\\n\\n## Wrapping Up\\n\\nThe Transactional Outbox Pattern is a useful and often necessary tool for building reliable integrations between systems. However, it takes time, infrastructure, and deep understanding of distributed systems to get it right. So why spend time solving problems that don\'t differentiate your business?\\n\\nThankfully, LittleHorse offers a workaround to the original problem, removing the need to engage with the complexities of Transactional Outboxes.\\n\\n### Additional Use Cases\\n\\nThe Transactional Outbox pattern is useful anytime you need to update information in a database _and also_ publish a record to a streaming log or a message queue.\\n\\nFor example:\\n\\n* **User registration:** Save a new user\'s profile and push a message to a queue in order to trigger a verification email.\\n* **Appointment scheduling:** Save appointment details and notify users via SMS or email.\\n* **Saga Transactions:** Within a Saga transaction (such as the order processing scenario discussed in the [last post](./2024-09-24-saga-pattern.md#case-study-order-processing)), a service may need to atomically update its database and push a record to a queue.\\n* **Inventory management:** Update stock levels and push updates to warehouses or suppliers.\\n\\n### Alternative: Log-First Architecture\\n\\nAnother solution to this specific problem would be to have the request handler (our Spring Boot endpoint) publish directly to an event log like Apache Kafka. Then, there would be two consumer groups for that topic:\\n\\n1. A consumer group which creates the `user` record in the database.\\n2. A consumer group which sends the welcome email.\\n\\nThe REST endpoint would return `201` as soon as the record was acknowledged by the streaming platform.\\n\\nIf you squint hard enough, you can see that this is very similar to what happens with LittleHorse; however, using this pattern, you are responsible for wiring together a complex topology of topics and queues, which is much harder than using a workflow!\\n\\n### Get Involved!\\n\\nStay tuned for the next post on Queues and Backpressure! In the meantime:\\n\\n* Try out our [Quickstarts](https://littlehorse.dev/docs/developer-guide/install)\\n* Join us [on Slack](https://launchpass.com/littlehorsecommunity)\\n* Give us a star [on GitHub](https://github.com/littlehorse-enterprises/littlehorse)!"},{"id":"saga-pattern","metadata":{"permalink":"/blog/saga-pattern","source":"@site/blog/2024-09-24-saga-pattern.md","title":"Integration Patterns: Saga Transactions","description":"The Saga pattern allows you to defend against data loss, dropped orders, and confused (or grumpy) customers. While useful, the Saga pattern is tricky to get right without an orchestrator.","date":"2024-09-24T00:00:00.000Z","tags":[{"inline":false,"label":"Technical Analysis","permalink":"/blog/tags/analysis/","description":"Analysis of the current and future state of Technical Architecture."},{"inline":false,"label":"Integration Patterns","permalink":"/blog/tags/integration-patterns/","description":"A 5-part blog series on Integration Patterns that are useful for event-driven systems."},{"inline":false,"label":"LittleHorse Orchestrator","permalink":"/blog/tags/littlehorse/","description":"Information about the LittleHorse Orchestrator."}],"readingTime":6.235,"hasTruncateMarker":true,"authors":[{"name":"Colt McNealy","title":"Managing Member of the LLC","description":"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He\'s a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.","page":{"permalink":"/blog/authors/coltmcnealy"},"socials":{"github":"https://github.com/coltmcnealy-lh","linkedin":"https://www.linkedin.com/in/colt-mcnealy-900b7a148/","x":"https://x.com/coltmcnealy"},"imageURL":"https://avatars.githubusercontent.com/u/100447728","key":"coltmcnealy"}],"frontMatter":{"slug":"saga-pattern","authors":["coltmcnealy"],"tags":["analysis","integration-patterns","littlehorse"]},"unlisted":false,"prevItem":{"title":"Integration Patterns: Transactional Outbox","permalink":"/blog/transactional-outbox"},"nextItem":{"title":"The Basics of Workflow","permalink":"/blog/basics-of-workflow"}},"content":"The Saga pattern allows you to defend against data loss, dropped orders, and confused (or grumpy) customers. While useful, the Saga pattern is tricky to get right without an orchestrator.\\n\\n\x3c!-- truncate --\x3e\\n\\n:::info\\nThis is the first part in a five-part blog series on useful Integration Patterns. This blog series will help you build real-time, responsive applications and microservices that produce predictable results and prevent the Grumpy Customer Problem.\\n\\n1. **[This Post]** Saga Transactions\\n2. [The Transactional Outbox Pattern](./2024-09-30-transactional-outbox.md)\\n3. [Coming soon] Queuing and Backpressure\\n4. [Coming soon] Retries and Dead-Letter Queues\\n5. [Coming soon] Callbacks and External Events\\n:::\\n\\n## The Saga Pattern\\n\\nAt a technical level, the [Saga Pattern](https://microservices.io/patterns/data/saga.html) allows you to perform distributed transactions across multiple disparate systems without 2-phase commit.\\n\\nIn plain English, it is a tool in the belt of a software engineer to prevent half-fulfilled bank transfers, hanging orders, or other failures which would result in a Grumpy Customer.\\n\\n:::info\\nThe \\"Saga\\" pattern gets its name from literature and film, wherein a \\"saga\\" is a series of chronologically-ordered related works. For example, the \\"Star Wars Saga.\\"\\n:::\\n\\n### Use Cases\\n\\nBusiness processes often need to perform actions in two separate systems either all at once or not at all. For example, you may need to charge a customer\'s credit card, reserve inventory, and ship an item to the customer all at once or not at all. If the payment went through but shipping failed, we would see the Grumpy Customer Problem yet again.\\n\\nThe Saga pattern is appropriate when:\\n* A business process must take action across multiple separate systems (legacy monoliths, microservices, external API\'s, etc),\\n* Each of those actions can be undone via a \\"compensation task\\", and\\n* All actions must logically happen together or not at all.\\n\\n:::tip\\nIt\'s also worth noting that a different flavor of the Saga pattern can also be used in _long-running_ business processes. In a past job, for example, I worked on a project that implemented the Saga pattern to handle the scheduling of home inspections. In this case, the task of finding an inspector to show up at the home and confirming a time with the homeowner needed to be performed atomically.\\n:::\\n\\n### Implementation\\n\\nWhile Saga is very hard to implement, it\'s simple to describe:\\n\\n* Try to perform the actions across the multiple systems.\\n* If one of the actions fails, then run a _compensation_ for all previously-executed tasks.\\n\\nThe _compensation_ is simply an action that \\"undoes\\" the previous action. For example, the compensation for a payment task might be to issue a refund.\\n\\n## Case Study: Order Processing\\n\\nLet\'s take a look at a familiar use-case: an order processing workflow involving the `inventory` service, and the `payments` service. (The `orders` service is involved implicitly.) As they would in a real world scenario, all of our services live on separate physical systems and have their own databases.\\n\\nIn this business process, we first reserve inventory for the ordered item. Next, we charge the customer\'s credit card.\\n\\nIf charging the credit card fails, then we have a problem: we\'ve reserved inventory but not sold it.\\n\\nOur services need the following functionality. In SOA, these would be endpoints; in LittleHorse, they would be `TaskDef`s:\\n* `create-order`: creates an order in the `PENDING` status.\\n* `reserve-inventory`: marks an item as no longer available for sale.\\n* `charge-payment`: charges the customer.\\n* `release-inventory`: marks an item as available for sale again.\\n* `cancel-order`: marks an order as `CANCELED`.\\n* `complete-order`: marks an order as `COMPLETED`.\\n\\n### Using Message Queues\\n\\nUsing message queues, the happy path looks like the following:\\n\\n![Architecture diagram](./2024-09-24-choreography-simple.png)\\n\\n:::note\\nThe above image assumes the _choreography_ pattern, in contrast to the _orchestrator_ pattern. The orchestrator pattern is a ton of work and involves writing something that very much resembles LittleHorse!\\n:::\\n\\n1. Orders service calls `createOrder()`.\\n2. Orders service publishes to the `reserve-inventory` queue.\\n3. Inventory service reads the message and calls `reserveInventory()`.\\n4. Inventory service publishes to the `charge-payment` queue.\\n5. Payment service charges the credit card.\\n6. Payment service publishes to the `complete-order` queue.\\n7. Orders service consumes the record and calls `completeOrder()`.\\n\\nIn just the happy path, we have strong coupling already between our services in three places, and we have three message queues to manage.\\n\\nBut now we need to release the inventory and cancel the order when the payment doesn\'t go through. So the flow looks like this:\\n\\n![Architecture Diagram](./2024-09-24-choreography-saga.png)\\n\\n1. Orders service calls `createOrder()`.\\n2. Orders service publishes to the `reserve-inventory` queue.\\n3. Inventory service reads the message and calls `reserveInventory()`.\\n4. Inventory service publishes to the `charge-payment` queue.\\n5. Payment service charges the credit card _unsuccessfully_.\\n6. Payment service publishes to the `release-inventory` queue.\\n7. Inventory service reads the record and calls `releaseInventory()`.\\n8. Inventory service publishes to the `cancel-order` queue.\\n9. Orders service consumes the record and calls `cancelOrder()`.\\n\\n:::note\\nWe still haven\'t even considered the case when the `reserve-inventory` step fails and we need to catch that exception and handle the order. For the sake of brevity, we will leave that out.\\n:::\\n\\nNow, we have _five_ different message queues that we have to wrangle with. We can also see that the overall business flow has started to leak across all of our different services.\\n\\n:::danger\\nOne thing we are ignoring in this blog post is _reliability_: to make this setup production-ready, we would also have to ensure that updates to the internal databases of the services are atomic along with pushing messages to the message queue. We will cover that in next week\'s post (along with how LittleHorse takes care of that for you).\\n:::\\n\\n### Using LittleHorse\\n\\nUsing LittleHorse, in java, this whole workflow could look like the following. This is _real code_ that does indeed compile and replaces the need for all of the complex queueing logic we had above.\\n\\n```java\\npublic void sagaExample(WorkflowThread wf) {\\n var item = wf.addVariable(\\"item\\", STR);\\n var customer = wf.addVariable(\\"customer\\", STR);\\n var price = wf.addVariable(\\"price\\", DOUBLE);\\n var orderId = wf.addVariable(\\"order-id\\", STR);\\n\\n wf.execute(\\"create-order\\", orderId);\\n\\n // Saga Here! (We skipped this part in the previous section due to\\n // complexity, but LH makes it simple enough.\\n NodeOutput inventoryResult = wf.execute(\\"reserve-inventory\\", item, orderId);\\n wf.handleException(inventoryResult, \\"out-of-stock\\", handler -> {\\n handler.execute(\\"cancel-order\\", orderId);\\n handler.fail(\\"out-of-stock\\", \\"Item was out of stock. Order canceled\\");\\n })\\n\\n NodeOutput paymentResult = wf.execute(\\"charge-payment\\", customer, price);\\n // Saga here again!!\\n wf.handleException(paymentResult, \\"credit-card-declined\\", handler -> {\\n handler.execute(\\"release-inventory\\", item, orderId);\\n handler.execute(\\"cancel-order\\", orderId);\\n handler.fail(\\"credit-card-declined\\", \\"Credit card was declined. Order canceled!\\");\\n });\\n\\n wf.execute(\\"complete-order\\", orderId);\\n}\\n```\\n\\nInstead of managing five message queues and five strongly-coupled integration points between microservices, all we need to do is register the workflow, define _truly_ modular tasks, and let LittleHorse take care of the rest.\\n\\n## Wrapping Up\\n\\nThe Saga Pattern is one of five tools we will cover in this series on avoiding the Grumpy Customer Problem. It\'s simple to understand but _painfully complex_ to implement. Fortunately, LittleHorse makes it easier!\\n\\n:::note\\nA careful reader, or anyone who [reads my rants on LinkedIn](https://www.linkedin.com/feed/update/urn:li:activity:7244572885179121664/), might note that in order to make the order processing workflow truly reliable, we would also need to do something like the Outbox pattern or Event Sourcing.\\n\\nThat is true, and we\'ll cover it in the next post (and you\'ll see how LittleHorse does that for you automatically!).\\n:::\\n\\n### Get Involved!\\n\\nStay tuned for the next post on the Transactional Outbox Pattern! In the meantime:\\n\\n* Try out our [Quickstarts](https://littlehorse.dev/docs/developer-guide/install)\\n* Join us [on Slack](https://launchpass.com/littlehorsecommunity)\\n* Give us a star [on GitHub](https://github.com/littlehorse-enterprises/littlehorse)!"},{"id":"basics-of-workflow","metadata":{"permalink":"/blog/basics-of-workflow","source":"@site/blog/2024-09-04-basics-of-workflows.md","title":"The Basics of Workflow","description":"LittleHorse Enterprises is a workflow engine company. But what is a workflow engine?","date":"2024-09-04T00:00:00.000Z","tags":[{"inline":false,"label":"Technical Analysis","permalink":"/blog/tags/analysis/","description":"Analysis of the current and future state of Technical Architecture."},{"inline":false,"label":"LittleHorse Orchestrator","permalink":"/blog/tags/littlehorse/","description":"Information about the LittleHorse Orchestrator."}],"readingTime":5.675,"hasTruncateMarker":true,"authors":[{"name":"Mitchell Henderson","title":"Principal Technical Architect","description":"Mitch is a software industry veteran experienced with Apache Kafka, Apache Cassandra, and systems modernization across a diverse set of industries including financial services, healthcare, technology, retail, and manufacturing. He prides himself in ensuring that all users of LittleHorse are successful.","page":{"permalink":"/blog/authors/mitchellh"},"socials":{"github":"https://github.com/mitchell-h","linkedin":"https://www.linkedin.com/in/mitchellghenderson/"},"imageURL":"https://avatars.githubusercontent.com/u/6223426","key":"mitchellh"}],"frontMatter":{"slug":"basics-of-workflow","authors":["mitchellh"],"tags":["analysis","littlehorse"]},"unlisted":false,"prevItem":{"title":"Integration Patterns: Saga Transactions","permalink":"/blog/saga-pattern"},"nextItem":{"title":"Microservices and Workflow: A Match Made in Heaven","permalink":"/blog/microservices-and-workflow"}},"content":"LittleHorse Enterprises is a workflow engine company. But what is a workflow engine?\\n\\n\x3c!-- truncate --\x3e\\n\\nIt is a system that allows you to reliably execute a series of steps while being robust to technical failures (network outages, crashes) and business process failures. A step in a workflow can be calling a piece of code on a server, reaching out to an external API, waiting for a callback from a person or external system, or more.\\n\\nA core challenge when automating a business process is **Failure and Exception Handling:** figuring out what to do when something doesn\'t happen, happens with an unexpected outcome, or plain simply fails. This is often difficult to reason about, leaving your applications vulnerable to uncaught exceptions, incomplete business workflows, or data loss.\\n\\nA workflow engine standardizes how to throw an exception, where the exception is logged, and the logic around when/how to retry. This gives you peace of mind that once a workflow run is started, it will reliably complete.\\n\\n## Workflow Architecture\\n\\nAny [workflow-driven application](https://littlehorse.dev/docs/concepts) has three components:\\n\\n1. A really awesome workflow engine like LittleHorse.\\n2. A [Workflow Specification](https://littlehorse.dev/docs/concepts/workflows), which defines the series of steps in your application.\\n3. [Task Workers](https://littlehorse.dev/docs/concepts/tasks), which are computer programs that execute work when the LH Server tells it to.\\n\\n### Workflow Specifications\\n\\nA Workflow Specification (or `WfSpec` in LittleHorse) is the configuration, or metadata object, that tells the engine what Tasks to run,\\nwhat order to run the tasks, **how to handle exceptions or failures,** what variables are to be passed from task to task, and what inputs and outputs are required to run the workflow.\\n\\nIn LittleHorse the `WfSpec` is submitted to and held by the LittleHorse server. Users of LittleHorse can define a `WfSpec` in vanilla code (Java/Go/Python) using the LittleHorse SDK. The SDK will compile your vanilla code into a `WfSpec` that the LH Server understands and keeps inside its data store.\\n\\n:::info\\nTo learn how to write a `WfSpec` in LittleHorse, check out our [`WfSpec` Development docs](https://littlehorse.dev/docs/developer-guide/wfspec-development).\\n:::\\n\\nIn the background LittleHorse server takes the submitted spec from the SDK, and compiles a protobuf object that is submitted to the LittleHorse server.\\n\\nFor example, the following code in Java defines a two-step workflow in which we look up the price of an item, charge a customer\'s credit card, and then ship an item.\\n\\n```java\\npublic class ECommerceWorkflow {\\n\\n public void checkoutWorkflow(WorkflowThread wf) {\\n // Create some Workflow Variables\\n var customerId = wf.addVariable(\\"customer-id\\", VariableType.STR).searchable().required();\\n var itemId = wf.addVariable(\\"item-id\\", VariableType.STR).required();\\n var price = wf.addVariable(\\"price\\", VariableType.INT);\\n\\n // Fetch Price and save it into a variable\\n var priceOutput = wf.execute(\\"calculate-price\\", itemId);\\n wf.mutate(price, VariableMutationType.ASSIGN, priceOutput);\\n\\n // Charge credit card (passing in the output from previous task)\\n wf.execute(\\"charge-credit-card\\", customerId, price);\\n\\n // Ship item\\n wf.execute(\\"ship-item\\", customerId, itemId);\\n }\\n}\\n```\\n\\n:::note\\nJust by using LittleHorse to define the above workflow, you get reliability, observability, retries, and governance out of the box!\\n:::\\n\\n### Tasks and Task Workers\\n\\nTasks are the unit of work that can be executed a workflow engine. It\'s best to think in examples:\\n* Change lower case letters to upper case letters.\\n* Call an API with an input variable and pass along the output.\\n* Fetch data from a database.\\n* Convert a message from HL7 version 2.5 to HL7 version 3.\\n\\nTask workers are programs that use the LittleHorse SDK, connect to LittleHorse, and execute tasks when the workflow says it\'s time to do so.\\n\\n:::tip\\nTo learn how to write a Task Worker, check out our [Task Worker Development Guide](https://littlehorse.dev/docs/developer-guide/task-worker-development).\\n:::\\n\\nYou can also use [External Events](https://littlehorse.dev/docs/concepts/external-events) or [User Tasks](https://littlehorse.dev/docs/concepts/user-tasks) to wait for input from a human user or an external system (like a callback or webhook).\\n\\n\\n### Workflow Clients\\n\\nLastly you need to tell LittleHorse when to run a workflow. You can do it with our CLI (`lhctl`) but in production you\'ll need to use the LittleHorse SDK to kick off a workflow. You can do this with our page on [Running Workflows using grpc](https://littlehorse.dev/docs/developer-guide/grpc/running-workflows)\\n\\nYou\'ll also need to tell LittleHorse about External Events that happen. You can also do this using `lhctl` or [with our SDK\'s](https://littlehorse.dev/docs/developer-guide/grpc/posting-external-events).\\n\\n## LittleHorse Use-Cases\\n\\nThere are many different types of workflow engines, each of which supports different use-cases. For example:\\n\\n* **Batch ETL and Cronjob** workflows are automated by systems like Apache Airflow and Dagster.\\n* **Infrastructure Provisioning and Configuration** workflows can be automated by Ansible, Argo, and Jenkins.\\n* **IT Integration and BPM** workflows may be automated by systems like Camunda and jBPM.\\n\\nHowever, **LittleHorse allows you to orchestrate business processes across your software systems.** Some use-cases are included below.\\n\\n### Microservices\\n\\nAll microservice-based applications are inherently distributed systems with the goal of supporting some business process (because no one writes microservices for the sake of writing code, right?). While often necessary, microservices [present many challenges](https://littlehorse.dev/blog/challenge-of-microservices) to developers due to their distributed nature.\\n\\nOur founder Colt McNealy wrote a [detailed blog](https://littlehorse.dev/blog/microservices-and-workflow) about how a workflow engine\'s reliabile state management and oversight can mitigate some of the problems inherent in microservices. Check it out!\\n\\n### Human-in-the-Loop\\n\\nWorkflows often need to get input from humans:\\n* Approval flows.\\n* Waiting for information from customers.\\n* Handling exceptional scenarios.\\n\\nThat\'s hard to coordinate without a workflow engine. You\'d have to build your own state management system that correlates tasks to workflows. LittleHorse [User Tasks](https://littlehorse.dev/docs/concepts/user-tasks) make this much easier.\\n\\n### RAG and AI\\n\\nAI is only useful when you call it at the right time, with the right inputs, and do something with the outputs. That\'s a workflow. And all sorts of things can go wrong when using LLM\'s, which is why you need to have a robust workflow engine to provide oversight and exception handling.\\n\\n### Legacy System Modernization\\n\\nWhether you are integrating legacy systems that you inherited from the past, or integrating multiple tech stacks accrued through M&A, your customers expect a real-time experience that seamlessly spans all of your systems. Workflow engines are useful for reliably orchestrating actions and moving data across multiple different systems.\\n\\n### API gateway\\n\\nIf we look at the properties of an API gateway and how they are used, a workflow engine makes sense. \\n\x3c!--TODO: insert example API gateway architecture --\x3e\\nThe usage of an API gateway is to have a single layer that abstracts further endpoints. \\nIn practice this most often means calling the same API gateway multiple times, receiving the requested data, and doing some date manipulation or calculations at the application layer.\\nA workflow engine performs all of the most common actions, and includes things like centralized security, possible data obscurity, failure handling, observability and allows for operators to scale compute.\\nAll while still maintaining a central plane that can be shared across an entire orginization. \\nAdditionally a workflow engine still allows for the standard CRUD(Create, Read, Update, Delete) operations that an API gateway provides."},{"id":"microservices-and-workflow","metadata":{"permalink":"/blog/microservices-and-workflow","source":"@site/blog/2024-09-02-microservices-and-workflow.md","title":"Microservices and Workflow: A Match Made in Heaven","description":"While they are often necessary, microservices are a headache. Fortunately, the right workflow engine (such as LittleHorse) can drastically reduce the difficulty of managing microservices.","date":"2024-09-02T00:00:00.000Z","tags":[{"inline":false,"label":"Technical Analysis","permalink":"/blog/tags/analysis/","description":"Analysis of the current and future state of Technical Architecture."},{"inline":false,"label":"Microservices and Workflow","permalink":"/blog/tags/microservice-and-workflow/","description":"A 3-part blog series on the challenges inherent with the microservice architecture, and how Workflow Engines can mitigate those difficulties."}],"readingTime":7.575,"hasTruncateMarker":true,"authors":[{"name":"Colt McNealy","title":"Managing Member of the LLC","description":"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He\'s a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.","page":{"permalink":"/blog/authors/coltmcnealy"},"socials":{"github":"https://github.com/coltmcnealy-lh","linkedin":"https://www.linkedin.com/in/colt-mcnealy-900b7a148/","x":"https://x.com/coltmcnealy"},"imageURL":"https://avatars.githubusercontent.com/u/100447728","key":"coltmcnealy"}],"frontMatter":{"slug":"microservices-and-workflow","title":"Microservices and Workflow: A Match Made in Heaven","authors":["coltmcnealy"],"tags":["analysis","microservice-and-workflow"]},"unlisted":false,"prevItem":{"title":"The Basics of Workflow","permalink":"/blog/basics-of-workflow"},"nextItem":{"title":"Releasing 0.11","permalink":"/blog/littlehorse-0.11-release"}},"content":"While they are often necessary, microservices are a headache. Fortunately, the right workflow engine (such as LittleHorse) can drastically reduce the difficulty of managing microservices.\\n\\n\x3c!-- truncate --\x3e\\n\\n:::info\\nThis is the third and final part of a 3-part blog series:\\n\\n1. [The Promise of Microservices](./2024-08-22-promise-of-microservices.md)\\n2. [The Challenge with Microservices](./2024-08-27-challenges-of-microservices.md)\\n3. **[This Post]** Workflow and Microservices: A Match Made in Heaven\\n:::\\n\\nIf you\'re just joining for the third blog post, we have so far established that microservices are an effective tool for allowing your engineering team to grow beyond just a handful of people working on an enterprise application. However, microservice systems are by nature [**Leaderless**](./2024-08-27-challenges-of-microservices.md#microservices-are-leaderless) and [**Distributed**](./2024-08-27-challenges-of-microservices.md#microservices-are-distributed), which yields challenges in:\\n\\n* [**Observability**](./2024-08-27-challenges-of-microservices.md#observability),\\n* [**Reliability**](./2024-08-27-challenges-of-microservices.md#reliability-and-correctness), and\\n* [**Complexity Management**](./2024-08-27-challenges-of-microservices.md#microservice-coupling).\\n\\nThose challenges inspired me to create [LittleHorse](https://littlehorse.dev/docs/concepts) in the fall of 2021. LittleHorse provides primitives and guardrails out of the box which make it easier to wrangle with distributed systems and coordinate processes/transactions across multiple microservices.\\n\\nIn this post, we\'ll discuss:\\n\\n1. What _workflow_ means.\\n2. How LittleHorse\'s workflow orchestration capabilities make it easier for you to reliably orchestrate complex business processes.\\n\\n:::tip\\nWant to give LittleHorse a try? Get in touch with us!\\n\\n* Join the [**LH Slack Community**](https://launchpass.com/littlehorsecommunity) for the latest news and help from community experts.\\n* Check out our [**Getting Started**](https://littlehorse.dev/docs/developer-guide/install) page.\\n* [**Say hello**](https://docs.google.com/forms/d/e/1FAIpQLScXVvTYy4LQnYoFoRKRQ7ppuxe0KgncsDukvm96qKN0pU5TnQ/viewform) if you\'d like to get in touch with someone from the LittleHorse Enterprises team.\\n:::\\n\\n## What is a Workflow?\\n\\nA workflow is a blueprint that defines a series of tasks to be performed (perhaps conditioned on certain inputs or external events) in order to achieve a business outcome.\\n\\nIf you recall the e-commerce example from the [previous blog post](./2024-08-27-challenges-of-microservices.md#the-nature-of-microservices), you can think of the abstract checkout process as a workflow. This example is interesting because it demonstrates multiple characteristics of common business processes that make microservice development hard.\\n\\n![E-Commerce Checkout Process Diagram](./2024-08-27-complex-checkout.png)\\n\\nFirst, a workflow can be _mission critical_. A customer would be very unhappy if the vendor charged their credit card but failed to ship their order. In technical terms, this means that the state of a workflow needs to be consistent and durable, which is hard to achieve in a distributed system.\\n\\nNext, a workflow can have exceptional cases. Our e-commerce flow has special logic to handle cases when the customer\'s credit card was invalid or when the ordered item was out of stock.\\n\\nFinally, a workflow can be _asynchronous_, meaning that it requires waiting for input from the external world in order to complete. For example, our e-commerce workflow sometimes must wait for a customer to update their credit card information before completing.\\n\\nThe mission-critical nature of workflows, combined with asynchronous events and exceptional cases, places a premium on _consistency._ The results of workflows must be predictable for customers and easy to reason about for business managers and software engineers.\\n\\n:::note\\nA technical or business process does not need to satisfy all three characteristics to be a \\"workflow.\\" In fact, simple processes with just one or two linear steps can benefit from a workflow engine.\\n:::\\n\\n### Workflow Engines\\n\\nA workflow engine is a software system that makes sure the trains run on time in your processes. To use a workflow engine, you must:\\n\\n1. **Define your Tasks**, which are units of work that can be executed in a workflow, and write [Task Workers](https://littlehorse.dev/docs/concepts/tasks) which implement small functions or methods in code to execute those tasks.\\n2. **Register a Workflow Specification** (we call it a [`WfSpec` in LittleHorse](https://littlehorse.dev/docs/concepts/workflows)) which specifies what tasks to execute and when.\\n3. **Run your workflow** so that the workflow engine can orchestrate the process to completion.\\n\\n![LittleHorse Architecture](../static/img/2024-08-28-lh-application.png)\\n\\n[Task Workers](https://littlehorse.dev/docs/developer-guide/task-worker-development) are where a workflow can interface with the outside world. Since a Task in a workflow results in the LittleHorse SDK calling a programming function/method of your choosing, Task Workers allow LittleHorse to integrate with any system. Task Workers can make database queries, call external API\'s, provision infrastructure on AWS, send push notifications to customer mobile apps, perform calculations, call an LLM API, and more.\\n\\nIn LittleHorse, the `WfSpec` is [defined in code](https://littlehorse.dev/docs/developer-guide/wfspec-development) in a language of your choice. Because LittleHorse was written with developers in mind, our DSL\'s have all of the primitives that you\'d expect in a programming language: variables, control flow, exception handling, child threads, interrupts, and awaiting for external events. This allows workflows to be:\\n\\n* Easy to reason about.\\n* Tracked in version control.\\n* Familiar and easy to learn.\\n\\nOnce you tell LittleHorse to [run an instance of your `WfSpec`](https://littlehorse.dev/docs/developer-guide/grpc/running-workflows), LittleHorse will oversee the entire process until it completes. Failed tasks will be retried, every step will be journaled, and the state of your processes will be safely and durably persisted while waiting for external triggers.\\n\\n## Why Workflow?\\n\\nMicroservice applications that are designed as distributed workflows without a workflow engine (like a chain of dominoes falling) present operational challenges because there is no \\"leader\\" providing oversight over the microservice processes. Thankfully, a developer-focused and horizontally-scalable workflow engine like LittleHorse can fill the \\"leader\\" role, thus providing oversight and reliability, and taming the complexity of your business processes.\\n\\nAdditionally, using a workflow engine allows you to develop a set of _reusable_ and _modular_ tasks which can be easily dropped into any business workflow with a common API. Rather than accumulating tech debt, workflow engines allow you to accumulate a set of useful lego bricks.\\n\\nIn most existing organizations there\'s a long list of API calls required to simply _run_ a workflow. Training engineers to use all of the new APIs while securely distributing access and permissions causes confusion and slow development cycles. Workflow engines provide a single API and single system that allows anyone to securely manage, run, and operate complex workflows.\\n\\n### Mission Critical Oversight\\n\\nMission critical business workflows leave no room for technical failures and outages. However, as we discussed [last week](./2024-08-27-challenges-of-microservices.md#reliability-and-correctness), the distributed nature of microservices means that technical failures are not likely but rather certain. LittleHorse provides retries and durable execution capabilities out of the box, removing the need to create complex infrastructure for cross-service transactions (such as dead-letter queues, Outbox tables, and the SAGA pattern).\\n\\nAdditionally, mission-critical processes must be _audited_ and _observed_ in a secure manner with proper access controls. LittleHorse supports this\u2014every step in a workflow is journaled, auditable, and searchable in our dashboard. When humans execute [User Tasks](https://littlehorse.dev/docs/concepts/user-tasks), you can view an audit trail of when and to whom it was assigned and executed; you can see when each `TaskRun` started, completed, and failed (and with what inputs). Our [ACL\'s and Multi-Tenancy](https://littlehorse.dev/docs/concepts/principals-and-tenants) capabilities (and \\"Masked Data\\") ensure that the data remains accessible only to those who must see it.\\n\\n### Simple Asynchronous Processing\\n\\nFor microservice developers, handling asynchronous business processes is challenging because it forces you to persist state, correlate events, and wire together callbacks into a non-linear flow. Developers often need to create database tables for ongoing transactions and maintain complex flow diagrams showing how different services integrate with business events.\\n\\nHowever, LittleHorse provides two primitives to simplify this process:\\n\\n1. [**External Events**](https://littlehorse.dev/docs/concepts/external-events) allow workflows to block until something happens in the outside world, and then resume processing immediately thereafter.\\n2. [**User Tasks**](https://littlehorse.dev/docs/concepts/user-tasks) are like External Events but they model getting input from humans. User Tasks support reminders, assignment, groups, and users.\\n\\nTogether, User Tasks and External Events allow developers to transform complex asynchronous flows (such as our e-commerce example when we wait for a customer to provide a new credit card) into a more manageable linear flow.\\n\\n### Exception Handling\\n\\nFinally, just as processes can fail at the technical level, they can also fail at the business level. As per our ongoing e-commerce example, cards can run out of funds, items go out of stock, customers can cancel orders while they are being processed.\\n\\nHandling any given exceptional case in a business workflow might involve actions in several different microservices. Without a workflow engine, therefore, each exceptional case results in more and more complex interdependencies in your microservices, creating the notoriously feared \\"Distributed Monolith.\\"\\n\\nIn contrast, with LittleHorse as your workflow orchestrator, the dependencies between microservices are mitigated and workflow concepts such as [Failure Handling](https://littlehorse.dev/docs/concepts/workflows#failure-handling) allow you to easily define rollbacks, SAGA patterns, and edge cases without introducing further accidental complexity into your microservices. This allows startups and enterprises alike to implement robust, enterprise-grade business applications without accumulating costly technical debt.\\n\\n## Conclusion\\n\\nFor a variety of reasons, startups and enterprises alike may need to work with microservices despite the challenges they bring. Thankfully, workflow engines like LittleHorse can mitigate those problems by providing oversight into your entire process.\\n\\nAt the LittleHorse Council, we are very excited about the upcoming 1.0 release. Over the next few weeks, we will:\\n* Complete additional load tests, chaos tests, and benchmarks in preparation for 1.0.\\n* Blog about how you can write an e-commerce workflow in LittleHorse with Python.\\n* Do final testing before we release!\\n\\nAnd if you enjoyed this post, give us a star [on GitHub](https://github.com/littlehorse-enterprises/littlehorse) and try out [our quickstarts](https://littlehorse.dev/docs/developer-guide/install) to get going with LittleHorse in under 5 minutes."},{"id":"littlehorse-0.11-release","metadata":{"permalink":"/blog/littlehorse-0.11-release","source":"@site/blog/2024-08-31-0.11-release.md","title":"Releasing 0.11","description":"Releasing LittleHorse `0.11`","date":"2024-08-31T00:00:00.000Z","tags":[{"inline":false,"label":"LittleHorse Releases","permalink":"/blog/tags/release/","description":"Release blogs for LittleHorse Orchestrator."}],"readingTime":2.215,"hasTruncateMarker":true,"authors":[{"name":"The LittleHorse Council","title":"The Council of LittleHorse Maintainers","description":"LittleHorse Orchestrator is maintained by LittleHorse Enterprises LLC and available under the SSPL license. The LittleHorse Council is the group of engineers inside LittleHorse Enterprises LLC who are responsible for the stewardship of the open-source Orchestrator project and charged with looking out for the best interests of the LH Community.","url":"https://littlehorse.io","page":{"permalink":"/blog/authors/lh-council"},"socials":{"github":"https://github.com/littlehorse-enterprises","linkedin":"https://www.linkedin.com/company/littlehorse"},"imageURL":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","key":"lh_council"}],"frontMatter":{"title":"Releasing 0.11","description":"Releasing LittleHorse `0.11`","slug":"littlehorse-0.11-release","authors":["lh_council"],"tags":["release"],"image":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","hide_table_of_contents":false},"unlisted":false,"prevItem":{"title":"Microservices and Workflow: A Match Made in Heaven","permalink":"/blog/microservices-and-workflow"},"nextItem":{"title":"The Challenge of Microservices","permalink":"/blog/challenge-of-microservices"}},"content":"The `0.11` release brings with it the ability to schedule workflows on a cron job, support for secret data, and various dashboard and SDK improvements. \x3c!-- truncate --\x3e\\n\\n## New Features\\n\\nIn addition to several new features, it\'s worth calling out that we upgraded the internal `org.apache.kafka:kafka-streams` dependency to `3.8.0`, which includes several crucial bug fixes (some of which were found by our Grumpy Maintainer, [Eduwer Camacaro](https://github.com/eduwercamacaro)).\\n\\n### Dashboard\\n\\nThe Dashboard saw several enhancements, the most important of which is the `ExternalEventDef` page, which allows users to view `ExternalEvent`s associated with an `ExternalEventDef`.\\n\\n### Scheduled Workflows\\n\\nThe `ScheduledWfRun` feature creates a schedule that runs a `WfSpec` on a cron schedule. This is useful for periodic background tasks.\\n\\n### Secret Variables\\n\\nAs of LittleHorse `0.11`, you may now mark a variable as `masked()`, which means that its value is obscured on the Dashboard and also via `lhctl get variable`.\\n\\nTo make a variable Masked, you can do the following:\\n\\n```\\nWfRunVariable myVar = wf.addVariable(\\"my-var\\", STR).masked();\\n```\\n\\nWe will also release a blog about this feature soon.\\n\\n### Saving User Task Progress\\n\\nWith the `rpc SaveUserTaskRun`, it is now possible to save the results of a `UserTaskRun` without completing it. When you do this, an `event` is added to the audit log showing who saved the `UserTaskRun` and what results were saved.\\n\\n## Release Notes and Artifacts\\n\\nYou can find the release notes and downloads on our GitHub page.\\n\\n* [**`0.11.2`**](https://github.com/littlehorse-enterprises/littlehorse/releases/tag/v0.11.2)\\n* [**`0.11.1`**](https://github.com/littlehorse-enterprises/littlehorse/releases/tag/v0.11.2)\\n* [**`0.11.0`**](https://github.com/littlehorse-enterprises/littlehorse/releases/tag/v0.11.2)\\n\\n## Upgrading\\n\\nJust as since all releases since `0.8`, there were no breaking changes to our protocol buffer API. We do not anticipate any changes with our API in the future, either. This means that old client applications will continue to work with the LH Server `0.11` and beyond.\\n\\nHowever, we refactored the Go SDK to better follow GoLang conventions, which will require code changes (but no changes to the network protocol).\\n\\n### Upgrading the Go SDK\\n\\nNow, instead of having multiple modules to import and use, there are only two:\\n\\n1. The `lhproto` module, with our GRPC clients and protobuf.\\n2. The `littlehorse` module, with everything else.\\n\\nTo add the go SDK to your project, you can run:\\n\\n```\\ngo get github.com/littlehorse-enterprises/littlehorse@v0.11.2\\n```\\n\\nThen, the imports are:\\n\\n```go\\nimport (\\n\\t\\"github.com/littlehorse-enterprises/littlehorse/sdk-go/lhproto\\"\\n\\t\\"github.com/littlehorse-enterprises/littlehorse/sdk-go/littlehorse\\"\\n)\\n```\\n\\n## What\'s Next?\\n\\nBefore committing to [Semantic Versioning](https://semver.org), we will:\\n\\n* Release our release schedule and support plan.\\n* Finish inspecting our SDK\'s for bugs and minor breaking API changes that we want to do before `1.0`.\\n* Finish our benchmarks, chaos tests, and load tests to ensure that our software meets the highest quality standards.\\n\\nWe expect to release `1.0` in early October 2024."},{"id":"challenge-of-microservices","metadata":{"permalink":"/blog/challenge-of-microservices","source":"@site/blog/2024-08-27-challenges-of-microservices.md","title":"The Challenge of Microservices","description":"Microservices are often necessary, but unfortunately they bring with them some baggage.","date":"2024-08-27T00:00:00.000Z","tags":[{"inline":false,"label":"Technical Analysis","permalink":"/blog/tags/analysis/","description":"Analysis of the current and future state of Technical Architecture."},{"inline":false,"label":"Microservices and Workflow","permalink":"/blog/tags/microservice-and-workflow/","description":"A 3-part blog series on the challenges inherent with the microservice architecture, and how Workflow Engines can mitigate those difficulties."}],"readingTime":8.93,"hasTruncateMarker":true,"authors":[{"name":"Colt McNealy","title":"Managing Member of the LLC","description":"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He\'s a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.","page":{"permalink":"/blog/authors/coltmcnealy"},"socials":{"github":"https://github.com/coltmcnealy-lh","linkedin":"https://www.linkedin.com/in/colt-mcnealy-900b7a148/","x":"https://x.com/coltmcnealy"},"imageURL":"https://avatars.githubusercontent.com/u/100447728","key":"coltmcnealy"}],"frontMatter":{"slug":"challenge-of-microservices","title":"The Challenge of Microservices","authors":["coltmcnealy"],"tags":["analysis","microservice-and-workflow"]},"unlisted":false,"prevItem":{"title":"Releasing 0.11","permalink":"/blog/littlehorse-0.11-release"},"nextItem":{"title":"The Promise of Microservices","permalink":"/blog/promise-of-microservices"}},"content":"Microservices are often necessary, but unfortunately they bring with them some baggage. \x3c!-- truncate --\x3e\\n\\n:::info\\nThis is the second part of a 3-part blog series:\\n\\n1. [The Promise of Microservices](./2024-08-22-promise-of-microservices.md)\\n2. **[This Post]** The Challenge with Microservices\\n3. [Workflow and Microservices: A Match Made in Heaven](./2024-09-02-microservices-and-workflow.md)\\n:::\\n\\nLast week, I [blogged](./2024-08-22-promise-of-microservices.md) about the problems that microservices solve, and why they are not only beneficial but necessary in some cases (a good bellwether is the size of your engineering team: beyond 1 or 2 dozen engineers, you will probably start to feel some problems that can be solved with microservices).\\n\\nWhen done correctly, microservices remove several bottlenecks to scaling your business. However, even well-architected microservices bring significant _accidental complexity_.\\n\\nIn particular, microservices are:\\n\\n1. Harder to **observe** and debug.\\n2. Harder to make **reliable** in the case of infrastructure or sofware failures.\\n3. More complex to **maintain** and evolve with changing business practices.\\n\\nIn this article we will explore how the above problems arise from two key facts:\\n* Microservices are **distributed**.\\n* Microservices are **choreographed without a leader**.\\n\\n:::note\\nMicroservices bring with them additional challenges around operationalization and deployment. However, those challenges are out-of-scope for this blog post as we instead choose to focus on the challenges faced by _application development teams_ rather than operations teams.\\n:::\\n\\n## The Nature of Microservices\\n\\nAs I described in [last week\'s blog](./2024-08-22-promise-of-microservices.md):\\n\\n> The term \\"microservices\\" refers to a software architecture wherein an enterprise application comprises a collection of small, loosely coupled, and independently deployable services (these small services are called \\"microservices\\" in contrast to larger monoliths). Each microservice focuses on a specific business capability and communicates with other services over a network, typically through API\'s, streaming platforms, or message queues.\\n\\nCrucially, a single microservice implements technical logic for a specific domain, or bounded context, within the larger company. In contrast, a comprehensive business process requires interacting with technology and people across _many_ business domains. The classic example of microservices architecture, e-commerce checkout, involves at least _shipping_, _billing_, _notifications_, _inventory_, and _orders_.\\n\\nIn the rest of this blog post we will examine microservices through the the lense of e-commerce checkout flow. To start with a simple use-case, the logical flow we will consider is:\\n\\n1. When an order is placed, we create a record in a database in the `orders` service.\\n2. We then reserve inventory (and ensure that the item is in stock) in the `inventory` service.\\n3. We charge the customer using the `payments` service.\\n4. Next, we ship the item using the `shipping` service.\\n5. Finally, the `notifications` service notifies the customer that the parcel is on its way.\\n\\n![Simple e-commerce workflow](./2024-08-27-simple-checkout.png)\\n\\n### Microservices are Distributed\\n\\nRecall that each service (in the workflow diagram above, each box) is its own deployable artifact. That means that the happy-path business process described above will involve five different software systems from start-to-finish.\\n\\nIn the above workflow diagram, each arrow can be accurately interpreted in two ways:\\n1. The logical flow of the business process.\\n2. The physical flow of information between microservices, either through network RPC calls or through a message broker like Apache Kafka.\\n\\nGuess what! This means we have a distributed system by definition. As Splunk [writes in a blog post](https://www.splunk.com/en_us/blog/learn/distributed-systems.html):\\n> A distributed system is simply any environment where multiple computers or devices are working on a variety of tasks and components, all spread across a network.\\n\\nYou need to look no further than the [Fallacies of Distributed Computing](https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing) (written by Sun Microsystems Fellow L. Peter Deutsch in 1994) to see that this means that microservices are no easy task.\\n\\n### Microservices are Leaderless\\n\\nAs we\'ve seen already, any microservice-based application is a distributed system. Some distributed systems have the concept of a _leader_, which is a special node in the system that has special responsibilities.\\n\\n:::info\\nApache Kafka is my favorite distributed system. In Apache Kafka, the _Controller_ is a special Kafka server that is responsible for deciding which partition replicas are hosted on (and led by) which brokers. If the broker who was in charge of a partition goes down, then the Controller chooses a new broker from the ISR to take its place.\\n\\nTherefore, the _Controller_ in Apache Kafka can be thought of as a _leader_.\\n:::\\n\\nWhile systems like Apache Kafka have clear leaders (for example, the _Controller_ may re-assign partition leadership if the cluster becomes too imbalanced), in a microservice-based system there is no central leader to ensure that the chips fall correctly. This is by necessity, because the separation of development concerns and lifecycles across microservices means that microservices cannot and do not have leaders.\\n\\nYou can think of our e-commerce microservice flow as a line of dominoes falling. Once the process starts, no one entity is responsible for ensuring its completion. The business workflow moves from `orders` to `inventory` to `payments` and so on. If `payments` fails for some reason (perhaps a network outage makes the Stripe API unavailable), then it\'s quite possible that the `shipping` service never finds out about the workflow.\\n\\nHowever, in real life such outcomes are not acceptable. This means that every single player in the system must:\\n\\n1. Have built-in reliability mechanisms.\\n2. Understand the preceding and subsequent steps of the business process to route traffic.\\n\\nImplementing the above slows down development, more tightly couples one services to another, increases dependencies, and makes your microservice architecture much more heavyweight.\\n\\n## The Challenges\\n\\nSo far, we have established that there are many players involved in a business process, yet there\'s no one orchestrator involved in ensuring that an ordered item is delievered to the the correct address. This yields three problems:\\n\\n1. **Reliability** in the face of infrastructure failures.\\n2. **Observability** to enable system optimization and debugging.\\n3. **Coupling** of microservices to each other makes it hard to modify the system in response to new business requirements.\\n\\n### Reliability and Correctness\\n\\nProcessing orders is a mission-critical use-case. This means that orders should always complete and never be dropped (for example, we should not charge the customer\'s credit card and not ship the product to them).\\n\\nHowever, asynchronous processing such as that which I outlined above is prone to failures. For example, if you chain microservices together with direct RPC calls, a single network partition can cause an order to get stuck. Even with a reliable message broker such as Apache Kafka or AWS SQS sitting between your microservices, a write to the message broker could fail _after_ the payment went through, still resulting in a stuck order.\\n\\nJust as communication _between_ microservices can fail, the actions performed _by_each microservice can also fail. In many cases actions performed by a microservice depend upon failure-prone external systems and API\'s. If the Stripe API is down, or if the credit card is invalid, we can\'t just stop processing the order there! We must notify the customer of what went wrong and also release the inventory that we reserved.\\n\\nThis means that microservice developers spend countless hours building out infrastructure to support:\\n* Retries\\n* Dead-Letter Queues\\n* Rate-limiting\\n* Timeouts\\n* Transactional Outbox pattern\\n* SAGA Pattern\\n\\nBack to the domino analogy, if one domino misses the next, the entire chain just stops.\\n\\n### Observability\\n\\nThe second problem with microservices is that once a process instance has started (i.e. the dominoes are falling), it is very difficult to observe what happens between steps 2 through 10. This means that multi-step processes with performance issues are hard to optimize, as there are many microservices which could be the bottleneck and it\'s hard to know which. Even worse, when a customer complains about a \\"stuck order,\\" it is difficult to find the point of failure.\\n\\nAs a result, microservice engineers spend time and money:\\n* Slogging through logs on DataDog\\n* Implementing complex distributed tracing such as Zipkin, Jaeger, or Kiali\\n* Saving the state of each process instance (in our case, the `order`) in a DB just for visibility purposes at every step\\n* Coordinating with other teams to manually understand and debug workflows.\\n\\n### Microservice Coupling\\n\\nLastly, because microservices are leaderless, each player in the end-to-end process must have hard-coded integrations with the preceding and subsequent steps. This results in:\\n\\n* **Process coupling**, wherein changing a business process results in significant code updates to rewire the message queues or RPC calls between two steps.\\n* **Schema coupling**, wherein different microservices have strong dependencies on each others\' schemas.\\n\\nMicroservices come with the promise of loose coupling; however, the unfortunate reality is that this is often not the case. As a result, teams often do have to coordinate with each other during deployments.\\n\\nTo see an example of the complexity introduced by coupling of microservices, let\'s consider what happens to our e-commerce checkout workflow when we add a few edge cases to make it more realistic:\\n\\n1. If the credit card is invalid, we request the customer to provide a new one, wait for two days, and either complete or cancel the order.\\n2. If the item is out of stock, we notify the customer who elects either to wait or cancel the order.\\n\\n![Complex Checkout Architecture](./2024-08-27-complex-checkout.png)\\n\\nIn the above diagram, each arrow represents the flow of the business process _and_ information. Each microservice must have custom logic which sends information to the right place. In essence, while we _intended_ to have modular microservices that understand only their own Bounded Context, what we have is tightly-coupled systems which must understand pretty much the entire business workflow.\\n\\nTherefore, when business requirements change, unrelated microservices end up having to change their internal implementation as well.\\n\\n## Looking Forward\\n\\nMicroservices have clear and proven benefits, and are often not just advantageous but _necessary_ in some cases. However, as we discussed today, those benefits do not come without a cost. Because microservices are inherently distributed systems, challenges such as reliability, observability, and coordination are exacerbated.\\n\\nWithout spoiling the punchline of the next blog post, these challenges are why I started LittleHorse almost three years ago. Stay tuned for a description of how a _workflow orchestrator_ can alleviate a good portion of the headaches that come along with microservices.\\n\\n### Business Analytics\\n\\nAstute readers may notice that when discussing the e-commerce checkout example, we didn\'t discuss the problem of _analytics._ We focused exclusively on online transaction processing, or ensuring that the orders are properly fulfilled and processed. However, no attention was paid to business analytics to optimize future sales!\\n\\nThis area is yet another challenge. The LittleHorse Council is working on a major feature (an output Kafka Topic with records for anytime something _interesting_ happens inside a `WfRun`) for the LH Server that will address this. Don\'t worry, we\'ll blog about it soon :wink:."},{"id":"promise-of-microservices","metadata":{"permalink":"/blog/promise-of-microservices","source":"@site/blog/2024-08-22-promise-of-microservices.md","title":"The Promise of Microservices","description":"If microservices add so much complexity, why bother with the hassle?","date":"2024-08-22T00:00:00.000Z","tags":[{"inline":false,"label":"Technical Analysis","permalink":"/blog/tags/analysis/","description":"Analysis of the current and future state of Technical Architecture."},{"inline":false,"label":"Microservices and Workflow","permalink":"/blog/tags/microservice-and-workflow/","description":"A 3-part blog series on the challenges inherent with the microservice architecture, and how Workflow Engines can mitigate those difficulties."}],"readingTime":7.09,"hasTruncateMarker":true,"authors":[{"name":"Colt McNealy","title":"Managing Member of the LLC","description":"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He\'s a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.","page":{"permalink":"/blog/authors/coltmcnealy"},"socials":{"github":"https://github.com/coltmcnealy-lh","linkedin":"https://www.linkedin.com/in/colt-mcnealy-900b7a148/","x":"https://x.com/coltmcnealy"},"imageURL":"https://avatars.githubusercontent.com/u/100447728","key":"coltmcnealy"}],"frontMatter":{"slug":"promise-of-microservices","title":"The Promise of Microservices","authors":["coltmcnealy"],"tags":["analysis","microservice-and-workflow"]},"unlisted":false,"prevItem":{"title":"The Challenge of Microservices","permalink":"/blog/challenge-of-microservices"},"nextItem":{"title":"Releasing 0.10","permalink":"/blog/littlehorse-0.10-release"}},"content":"If microservices add so much complexity, why bother with the hassle? \x3c!-- truncate --\x3e\\n\\n:::info\\nThis is the first part of a 3-part blog series:\\n\\n1. **[This Post]** The Promise of Microservices\\n2. [The Challenge with Microservices](./2024-08-27-challenges-of-microservices.md)\\n3. [Workflow and Microservices: A Match Made in Heaven](./2024-09-02-microservices-and-workflow.md)\\n:::\\n\\n\\nWe\'ve all _heard of_ microservices, but unless you\'ve read copious amounts of Sam Newman and Adam Bellemare\'s writings, you might be wondering whether, when, and why you should adopt them. In this blog post, we will examine the halcyon land promised by microservices.\\n\\nMicroservices have been [deployed widely](https://www.simform.com/blog/microservices-examples/) across many large enterprises, most notably Netflix, Uber, Shopify, PayPal, and others. As we will discover throughout this blog series, a microservice architecture is mandatory once you reach a certain size of company, and it\'s probably overkill for a 12-person startup. The gray area inbetween is the interesting part!\\n\\n## What are Microservices?\\n\\nThe term \\"microservices\\" refers to a software architecture wherein an enterprise application comprises a collection of small, loosely coupled, and independently deployable services (these small services are called \\"microservices\\" in contrast to larger monoliths). Each microservice focuses on a specific business capability and communicates with other services over a network, typically through API\'s, streaming platforms, or message queues.\\n\\nIn practice, this means that a user interaction with an application (such as placing an order) might trigger actions that occur in _many_ small, independently-deployed software systems, such as:\\n\\n* A Notification service\\n* An Inventory Management service\\n* A Payments service\\n* An Order History service\\n\\nFrom the user (client) perspective, one request is made (generally through a Load Balancer, API Gateway, or Ingress Controller) but that request may ping-pong between multiple back-end services and may also result in future actions being scheduled asynchronously:\\n\\n![Microservices Architecture](./2024-08-22-microservices-arch.png)\\n\\nIn contrast to microservices, a _monolithic_ architecture would serve the entire \\"place order\\" request on a single deployable artifact:\\n\\n![Monolithic Architecture](./2024-08-22-monolith-arch.png)\\n\\nIn Domain Driven Design, accidental complexity refers to the unintentional complexity that you introduced to your architecture (deployments, service interactions, third-party dependencies, etc.). Rule #1 of maintaining software systems is to avoid introducing accidental complexity as much as possible.\\n\\nSimply by looking at the visuals above, microservices add a significant dose of accidental complexity to your architecture (more on this in next week\'s post!). Given that, what benefits would make up for the extra complexity introduced by microservices?\\n\\n## Why Now?\\n\\nI would be first to admit that microservices bring with them a series of headaches around cost, observability, maintenance, and ease of evolution (otherwise, I would not have founded LittleHorse Enterprises!). However, microservice architecture plays a vital role in addressing two critical trends reshaping the software development landscape today:\\n\\n* Increased digitization of companies in all business sectors (accelerated by the rise of AI).\\n* Elasticity of cloud computing.\\n\\n### Increased Digitization\\n\\nThe level of digitization expected of businesses in order to compete in the modern market has drastically increased: IT teams must build software that interfaces with an ever-expanding list of external API\'s, legacy systems, user interfaces, internal tools, and SaaS providers.\\n\\nFor example: in the early 2000\'s, it was perfectly acceptable (even _expected_) for a passenger to book airline tickets over the telephone or through a travel agency. However, such an experience would be unheard of today and would immediately hobble an airline who provided such poor digital services.\\n\\nIn addition to using automation to provide better customer services, companies are generating, processing, and analyzing massive amounts of data. For example, grocery stores with razor-thin margins analyze seasonal consumption patterns in order to optimize inventory and prevent costly food waste.\\n\\nThese trends have coincided with (or _caused_, I would argue) a proliferation in the number of 1) software developers, and 2) software tools and API\'s found within companies in all industries, leading to two new problems:\\n\\n1. Allowing large teams of software developers to productively work on an enterprise application in parallel (without stepping on each others\' toes).\\n2. Ensuring that business requirements are effectively communicated to the entire (larger) software engineering team.\\n\\n### Cloud Elasticity\\n\\nAs the importance and quantity of digital software systems exploded over the last two decades, so has the availability of nearly-infinite compute power delivered through cloud infrastructure providers such as AWS.\\n\\nThe promise of _elasticity_, or the ability to quickly spin compute resources up or down according to load and only pay for what you use, is unique to the cloud: for on-prem datacenters, spinning up new compute means buying new machines from Sun Microsystems (hopefully not Microsoft!), and scaling down compute means trying to sell them off on the secondary market. (Ask my father about how that went for a lot of people in 2001.)\\n\\nBeyond scaling up and down, elasticity enables different deployment patterns that did not exist before. Whereas pre-cloud enterprises had dedicated and centralized data-center teams who were in charge of running applications, the accessibility of cloud computing gave rise to the DevOps movement. This has empowered smaller teams of software developers to take on the task of transferring software from \\"it works on my laptop!\\" to \\"it\'s now deployed in production!\\"\\n\\n## Why Microservices?\\n\\nDespite the extra complexity it brings, the microservice architecture can more than pay for itself by ensuring organizational alignment and allowing enterprise architectures to take full advantage of the cloud\'s elasticity.\\n\\n### Organizational Alignment\\n\\nAs discussed earlier, the business problems that software engineering organizations must solve today dwarf those that were solved in the 1990\'s, and so do the software engineering teams that tackle those problems.\\n\\n:::note\\nI am not belittling the engineers of the 90\'s; the problems they solved were arguably _much harder_ than the problems we face today, and there were fewer engineers to face those problems. However, it is a fact that users expect more digital-native experiences today than they did twenty years ago.\\n:::\\n\\nBy breaking applications into smaller services, we can accomplish several important things:\\n* Break up our software engineering team into smaller teams which are each responsible for individual microservices.\\n* Allow different components of a system to be developed with separate tech stacks and released independently.\\n\\nEngineering teams of over a few dozen engineers working on the same deployable piece of software is a recipe for inefficiency. Merge conflicts, arguments over tech stack, slow \\"release trains,\\" and excessive intra-team coordination are just a few problems that arise. However, by breaking your application into smaller microservices, you can also break up your engineering organization into smaller, more efficient teams each in charge of a small number (prefably one!) of microservices.\\n\\nAs an added benefit, properly-designed microservice architectures can follow the principles of Domain Driven Design. Ideally, a single microservice corresponds to a _Bounded Context_ inside the business. This enables a small piece of the technical platform (a microservice) to be managed by a small team of software engineers, who collaborate closely with subject-matter experts and business stakeholders within a very specific domain of the business. Such close collaboration can foster better alignment between business goals and the software produced by engineering teams.\\n\\n### Moving Faster\\n\\nMicroservices can allow developers to move faster by enabling continuous delivery and independent deployment of services. In a monolithic architecture, releasing a new feature or fixing a bug typically requires redeploying the entire application. Since microservices allow smaller pieces of your application to be deployed independently, engineering teams can iterate faster and deliver incremental value to business stakeholders.\\n\\nThese positive effects are amplified by the advent of cloud computing. Since deploying a new application no longer requires buying a physical machine and plugging it into your datacenter but rather just applying a new `Deployment` and `Service` on a Kubernetes cluster, it is now truly feasible for small teams of software engineers to own their application stack from laptop-to-production (obviously, within the guardrails set by the central platform team). Furthermore, cloud computing is a pay-as-you-go (and often even pay-for-what-you-use) expense rather than an up-front cost. Therefore, the dollar cost of infrastructure required to support microservices is much lower today than it would have been before the advent of cloud computing and kubernetes.\\n\\n## Conclusion\\n\\nThe microservice architecture is not just a Twitter-driven buzzword but rather a way of designing system that has several real advantages. For most organizations with over two dozen software engineers, building applications with microservices is not an option but rather a _necessity_. However, those advantages come with a cost.\\n\\nWe will discuss those challenges in next week\'s blog post...in the meantime, though, join our [Community Slack](https://launchpass.com/littlehorsecommunity) to get the latest updates!"},{"id":"littlehorse-0.10-release","metadata":{"permalink":"/blog/littlehorse-0.10-release","source":"@site/blog/2024-07-12-0.10-release.md","title":"Releasing 0.10","description":"Releasing LittleHorse `0.10`","date":"2024-07-12T00:00:00.000Z","tags":[{"inline":false,"label":"LittleHorse Releases","permalink":"/blog/tags/release/","description":"Release blogs for LittleHorse Orchestrator."}],"readingTime":2.005,"hasTruncateMarker":true,"authors":[{"name":"The LittleHorse Council","title":"The Council of LittleHorse Maintainers","description":"LittleHorse Orchestrator is maintained by LittleHorse Enterprises LLC and available under the SSPL license. The LittleHorse Council is the group of engineers inside LittleHorse Enterprises LLC who are responsible for the stewardship of the open-source Orchestrator project and charged with looking out for the best interests of the LH Community.","url":"https://littlehorse.io","page":{"permalink":"/blog/authors/lh-council"},"socials":{"github":"https://github.com/littlehorse-enterprises","linkedin":"https://www.linkedin.com/company/littlehorse"},"imageURL":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","key":"lh_council"}],"frontMatter":{"title":"Releasing 0.10","description":"Releasing LittleHorse `0.10`","slug":"littlehorse-0.10-release","authors":["lh_council"],"tags":["release"],"image":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","hide_table_of_contents":false},"unlisted":false,"prevItem":{"title":"The Promise of Microservices","permalink":"/blog/promise-of-microservices"},"nextItem":{"title":"Releasing 0.9","permalink":"/blog/littlehorse-0.9-release"}},"content":"The `0.10` release brings with it significant performance and reliability improvements. \x3c!-- truncate --\x3e\\n\\n## New Features\\n\\n### `lhctl` Binaries and Release Notes\\n\\nThe `0.10.0` release comes with a new [Release Page](https://github.com/littlehorse-enterprises/littlehorse/releases), including `lhctl` binaries built for ARM, Intel, and Windows.\\n\\n### Reliability during Rebalances\\n\\nPR [#872](https://github.com/littlehorse-enterprises/littlehorse/pull/872) improves the reliability of LittleHorse during Kafka Streams rebalances. Previously, if a write request (eg. `rpc RunWf`) was received just before a rebalance, certain requests would \\"time out\\" from the client perspective and return a `DEADLINE_EXCEEDED` grpc error despite being properly accepted and processed by the server. This PR fixes that issue by redirecting the internal `rpc WaitForCommand` to the new destination for that command.\\n\\n### Rescue Failed Workflows\\n\\nPR [#883](https://github.com/littlehorse-enterprises/littlehorse/pull/883) allows users to restart failed `WfRun`\'s via the `lhctl rescue` command. This is similar to allowing a user to execute mutating SQL queries via a CLI like `psql`.\\n\\nWith this feature, a user can fix a buggy Task Worker implementation and then restart a failed `WfRun` and get it to execute the failed `TaskRun` again via:\\n\\n```\\nlhctl rescue \\n```\\n\\n### mTLS Principals\\n\\nPreviously, only listeners of the type `OAUTH` supported `Principal`s. The `Principal` ID was determined by the OAuth Client ID or User Id. Release `0.10` introduces the ability to infer a `Principal` on an `MTLS` listener, where the `Principal` ID comes from the Common Name on the client certificate.\\n\\nPR [#874](https://github.com/littlehorse-enterprises/littlehorse/pull/874) by one of our newer team members, [Jacob Snarr](https://github.com/snarr), introduced this feature, enabling users that standardize on SSL authentication to continue using that pattern with Littlehorse.\\n\\n### Dashboard Enhancements\\n\\nThe `0.10` release includes multiple enhancements to the Admin Dashboard, including:\\n\\n* Ability to search for `WfRun`\'s by their variables.\\n* Improved `WfRun` search.\\n* Fixed display of `TaskRun`s with the `EXCEPTION` and `ERROR` status.\\n* Showing `VariableMutation`s on the `Edge` in the dashboard.\\n\\n## What\'s Next?\\n\\nWe will need one more minor release before finally releasing `1.0`. We need the following:\\n\\n* Upgrade `org.apache.kafka:kafka-streams` to `3.8.0` to address several critical reliability bugs (we are waiting for the official release).\\n* Conduct new load tests and soak tests against the new version of Kafka Streams.\\n* Review our Go and Python SDK\'s in-depth to ensure proper semantics.\\n\\nAfter that, we will be ready to commit to the backwards compatibility guarantees required by [Semantic Versioning](https://semver.org). We will also release a blog post with our planned release schedule and support schedule."},{"id":"littlehorse-0.9-release","metadata":{"permalink":"/blog/littlehorse-0.9-release","source":"@site/blog/2024-06-24-0.9.2-release.md","title":"Releasing 0.9","description":"Revamping the LittleHorse Dashboard","date":"2024-06-24T00:00:00.000Z","tags":[{"inline":false,"label":"LittleHorse Releases","permalink":"/blog/tags/release/","description":"Release blogs for LittleHorse Orchestrator."}],"readingTime":2.35,"hasTruncateMarker":true,"authors":[{"name":"The LittleHorse Council","title":"The Council of LittleHorse Maintainers","description":"LittleHorse Orchestrator is maintained by LittleHorse Enterprises LLC and available under the SSPL license. The LittleHorse Council is the group of engineers inside LittleHorse Enterprises LLC who are responsible for the stewardship of the open-source Orchestrator project and charged with looking out for the best interests of the LH Community.","url":"https://littlehorse.io","page":{"permalink":"/blog/authors/lh-council"},"socials":{"github":"https://github.com/littlehorse-enterprises","linkedin":"https://www.linkedin.com/company/littlehorse"},"imageURL":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","key":"lh_council"}],"frontMatter":{"title":"Releasing 0.9","description":"Revamping the LittleHorse Dashboard","slug":"littlehorse-0.9-release","authors":["lh_council"],"tags":["release"],"image":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","hide_table_of_contents":false},"unlisted":false,"prevItem":{"title":"Releasing 0.10","permalink":"/blog/littlehorse-0.10-release"},"nextItem":{"title":"Releasing 0.8","permalink":"/blog/littlehorse-0.8-release"}},"content":"The `0.9.2` release is now availble and ready for use. \x3c!-- truncate --\x3e The `0.9.x` releases focused mainly on:\\n\\n* Improving the user experience on the LittleHorse Dashboard\\n* Improving the reliability of the LH Server in the face of rebalances and failures.\\n\\n## New Features\\n\\nWhile the majority of the improvements in the `0.9` release revolve around performance and stability, several of them are highly visible to the user (especially the new dashboard!).\\n\\n### Dashboard Rewrite\\n\\nWith help from [Nelson Jumbo](https://github.com/diablouma), LittleHorse Knight [Mija\xedl Rond\xf3n](https://github.com/mijailrondon) rewrote and revamped our administrative dashboard. It now inclues new features such as:\\n\\n* User Task Detail page\\n* Improved details on `TaskRun` progress\\n* Improved details on `WfRun` progress\\n* A plethora of small bug fixes.\\n\\n### Internal Task Queue Optimizations\\n\\nDeep in the internals of the LittleHorse Server, we implement a Task Queue mechanism to store `ScheduledTask`s before they\'re dispatched to the Task Worker clients. This release included many improvements to stability of the Task Queues.\\n\\nMost importantly, our Grumpy Maintainer (Eduwer Camacaro) put a cap on the memory consumption of a single `TaskDef`. Prior to this release, it was possible for poorly-behaved clients to cause an OOM on the server by running millions of workflows which use a `TaskDef` but not executing the resulting `TaskRun`s. This would cause an un-bounded buildup of `ScheduledTask`s in memory until the server crashed.\\n\\nAfter the `0.9` release, any more than 1,000 `ScheduledTask`s for a certain `TaskDef` are not loaded into memory but left on disk.\\n\\n### Principal Deletion\\n\\nThe `0.9` release includes the ability to delete a `Principal`. The `rpc DeletePrincipal` is smart enough to ensure that there is always at least one Admin `Principal` to prevent a user from locking themselves out of the cluster.\\n\\n### `PollThread` in Java Task Worker\\n\\nWe refactored the internal implementation of the Java Task Worker so that, for each LH Server in the cluster, the Task Worker creates a single `PollThread` object which is responsible for polling and executing `TaskRun`s. The `PollThread`s now poll in parallel, drastically increasing the throughput of a single Java Task Worker.\\n\\nThe `PollThread` was introduced in [#796](https://github.com/littlehorse-enterprises/littlehorse/pull/796).\\n\\n## What\'s Next\\n\\nOur wire protocol (the GRPC API) is quite stable; there have been no major breaking changes since we introduced the alpha version of Multi-Tenancy in `0.7`. We are diligently proceeding through soak tests, load tests, and chaos tests with our server and we have found and addressed several issues.\\n\\nWe continue to look foward to the `1.0` release, and we will reach that milestone once:\\n\\n* We are satisfied with results of load tests and soak tests.\\n* We have had language experts review each of our three main SDK\'s (Java, Go, Python) and we have addressed any change requests.\\n* We approach a year without any breaking changes to our wire protocol."},{"id":"littlehorse-0.8-release","metadata":{"permalink":"/blog/littlehorse-0.8-release","source":"@site/blog/2024-03-26-0.8.1-release.md","title":"Releasing 0.8","description":"Hardening Security","date":"2024-03-26T00:00:00.000Z","tags":[{"inline":false,"label":"LittleHorse Releases","permalink":"/blog/tags/release/","description":"Release blogs for LittleHorse Orchestrator."}],"readingTime":3.955,"hasTruncateMarker":true,"authors":[{"name":"The LittleHorse Council","title":"The Council of LittleHorse Maintainers","description":"LittleHorse Orchestrator is maintained by LittleHorse Enterprises LLC and available under the SSPL license. The LittleHorse Council is the group of engineers inside LittleHorse Enterprises LLC who are responsible for the stewardship of the open-source Orchestrator project and charged with looking out for the best interests of the LH Community.","url":"https://littlehorse.io","page":{"permalink":"/blog/authors/lh-council"},"socials":{"github":"https://github.com/littlehorse-enterprises","linkedin":"https://www.linkedin.com/company/littlehorse"},"imageURL":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","key":"lh_council"}],"frontMatter":{"title":"Releasing 0.8","description":"Hardening Security","slug":"littlehorse-0.8-release","authors":["lh_council"],"tags":["release"],"image":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","hide_table_of_contents":false},"unlisted":false,"prevItem":{"title":"Releasing 0.9","permalink":"/blog/littlehorse-0.9-release"},"nextItem":{"title":"Releasing 0.7","permalink":"/blog/littlehorse-0.7-release"}},"content":"The `0.8` release of LittleHorse is out! This pre-1.0 release contains many new features, security enhancements, and performance improvements.\\n\\n\x3c!-- truncate --\x3e\\n\\n## New Features\\n\\nNew features in this release cover some edge-cases in workflow development which came up from some initial pilots and internal usage of the platform.\\n\\n### Dynamic Task Execution\\n\\nBefore this release, a `TaskNode` had a hard-coded reference to a `TaskDef`. This means that every single `WfRun` that reaches the same `Node` in a `WfSpec` ends up executing the same `TaskDef`.\\n\\nHowever, in LittleHorse Enterprises LLC\'s upcoming Control Plane project (a system for dynamically provisioning LittleHorse clusters as a SaaS service), we anticipate a special use-case (which we will blog about this upcoming fall) wherein we need to _choose_ which `TaskDef` is executed dynamically at runtime.\\n\\nSpecifically, depending on an input variable to a `WfRun` (in this case, the `data-plane-id` variable), we need to execute a different `TaskDef` so that the `TaskRun` is executed by a speciific Task Worker in a specific location. We will blog about that use-case later.\\n\\n### Per-Thread Failure Handlers\\n\\nSince the `0.1.0` release of LittleHorse it has been possible to put a `FailureHandler` on any `Node`, such that if the `NodeRun` fails, then a Failure Handler thread is \\n\\n### Content in `EXCEPTION`s\\n\\n- #714\\n\\n### Multi-Tenancy Improvements\\n\\nMulti-Tenancy has been quietly under development in the LittleHorse Server since the `0.6.0` release introduced a breaking change to allow for it last October. The `0.8` release continues to progress towards making Multi-Tenancy generally-available.\\n\\nThis release includes two new major features for Multi-Tenancy:\\n\\n1. Allowing Python and Go clients to set the `tenant-id` header using `LHC_TENANT_id` ([#704](https://github.com/littlehorse-enterprises/littlehorse/pull/704))\\n2. Allowing administrative `Principal`s with admin privileges over multiple `Tenant`s: ([#679](https://github.com/littlehorse-enterprises/littlehorse/pull/679))\\n\\nMulti-Tenancy and support for authentication + fine-grained ACL\'s via `Principal`s has been a labor of love implemented by [Eduwer Camacaro](https://github.com/eduwercamacaro), who has grown into the role of Grumpy Maintainer of LittleHorse.\\n\\n### Kafka Security Protocol Support\\n\\nPrior to release `0.8`, the LH Server could only access a Kafka cluster with either:\\n* Plaintext access with no security.\\n* TLS with no authentication.\\n* MTLS security.\\n\\nPR [#716](https://github.com/littlehorse-enterprises/littlehorse/pull/716) introduced the following Server configurations:\\n\\n* `LHS_KAFKA_SECURITY_PROTOCOL`\\n* `LHS_KAFKA_SASL_MECHANISM`\\n* `LHS_KAFKA_SASL_JAAS_CONFIG`\\n\\nThis allows for access to any Kafka cluster except those requiring loading custom implementations of callbacks on the client side (for example, using the Strimzi OAuth Plug-in).\\n\\nIt is now possible to run LH with Kafka as:\\n- No security (PLAINTEXT)\\n- TLS on the brokers, no authentication (SSL)\\n- MTLS on the brokers (SSL with TRUSTSTORE set)\\n- SASL with any JAAS config (SASL_SSL)\\n- Confluent Cloud.\\n\\n### LittleHorse Canary\\n\\nThe LittleHorse Canary was released in early access. Inspired by the [Strimzi Canary](https://strimzi.io/blog/2021/11/09/canary/) for Apache Kafka, the LittleHorse Canary is a system that runs workflows on LittleHorse and reports on the health of the cluster(s) that it is monitoring.\\n\\nThe LH Canary system comprises two components:\\n\\n1. The Metronome, which runs workflows and sends metric beats to a Kafka topic.\\n2. The Aggregator, which consumes the metrics beats Kafka topic and aggregates metrics to be exposed to Prometheus and a GRPC API.\\n\\nThe goal of the Canary is to monitor, profile, and benchmark LittleHorse Clusters from the same exact perspective as the clients who use them.\\n\\nThe Canary is the brain child of [Sa\xfal Pi\xf1a](https://github.com/sauljabin), who is also the author of the popular [Kaskade](https://github.com/sauljabin/kaskade) TUI for Apache Kafka.\\n\\n### Exponential Backoff Retry Policy\\n\\nPR ([#707](https://github.com/littlehorse-enterprises/littlehorse/pull/707)) introduced the ability to configure exponential backoff for `TaskRun` retries. Previously, only immediate retries were supported.\\n\\n### JavaScript Client\\n\\nWe published the first version of `littlehorse-client` on NPM [here](https://www.npmjs.com/package/littlehorse-client). This client contains the `LHConfig` in javascript, which provides access to our LittleHorse GRPC API. Note that we do not yet support a JavaScript Task Worker nor a JavaScript `WfSpec` SDK.\\n\\n### Bugfixes\\n\\nIn this release, we fixed several bugs:\\n* Task Worker improperly reported `EXCEPTION`s and `ERROR`s when throwing `LHTaskException` ([#738](https://github.com/littlehorse-enterprises/pull/738))\\n* Fixes task queue rehydration ([#727](https://github.com/littlehorse-enterprises/pull/727))\\n* Fixes the Retention Policy for `ExternalEventDef`\'s ([#724](https://github.com/littlehorse-enterprises/littlehorse/pull/724))\\n* Fixes deadlock in Java task worker ([#723](https://github.com/littlehorse-enterprises/littlehorse/pull/723))\\n* Fixes concurrency bug with the `AsyncWaiter` in the server ([#719](https://github.com/littlehorse-enterprises/littlehorse/pull/719))\\n* Fixes various issues from soak tests ([#706](https://github.com/littlehorse-enterprises/littlehorse/pull/706))\\n* Fixes to `NodeRun` lifecycle ([#665](https://github.com/littlehorse-enterprises/littlehorse/pull/665)).\\n\\n## Looking Forward\\n\\nWe continue to stabilize our API and add features that cover edge cases. Load testing, chaos testing, and soak testing are an ongoing project, and we are working with the Apache Kafka Community on a few bugfixes in the Kafka Streams library which is heavily used in the core of LittleHorse.\\n\\nOnce those action items are resolved, we will make a `1.0` release candidate. However, in the meantime we don\'t expect any massively-breaking API changes at the protocol level. However, certain syntactical changes may occur in our SDK\'s (especially Go and Python)."},{"id":"littlehorse-0.7-release","metadata":{"permalink":"/blog/littlehorse-0.7-release","source":"@site/blog/2024-01-28-0.7-release.md","title":"Releasing 0.7","description":"Approaching a stable `1.0.0` release.","date":"2024-01-28T00:00:00.000Z","tags":[{"inline":false,"label":"LittleHorse Releases","permalink":"/blog/tags/release/","description":"Release blogs for LittleHorse Orchestrator."}],"readingTime":4.535,"hasTruncateMarker":true,"authors":[{"name":"The LittleHorse Council","title":"The Council of LittleHorse Maintainers","description":"LittleHorse Orchestrator is maintained by LittleHorse Enterprises LLC and available under the SSPL license. The LittleHorse Council is the group of engineers inside LittleHorse Enterprises LLC who are responsible for the stewardship of the open-source Orchestrator project and charged with looking out for the best interests of the LH Community.","url":"https://littlehorse.io","page":{"permalink":"/blog/authors/lh-council"},"socials":{"github":"https://github.com/littlehorse-enterprises","linkedin":"https://www.linkedin.com/company/littlehorse"},"imageURL":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","key":"lh_council"}],"frontMatter":{"title":"Releasing 0.7","description":"Approaching a stable `1.0.0` release.","slug":"littlehorse-0.7-release","authors":["lh_council"],"tags":["release"],"image":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","hide_table_of_contents":false},"unlisted":false,"prevItem":{"title":"Releasing 0.8","permalink":"/blog/littlehorse-0.8-release"},"nextItem":{"title":"Releasing 0.5.0","permalink":"/blog/littlehorse-0.5.0-release"}},"content":"We are excited to announce the release of `0.7.2`! \x3c!-- truncate --\x3e This is our last release before we cut `1.0.0`, which will be the first stable and production-ready LittleHorse distribution.\\n\\n## Get Started\\n\\nLittleHorse is free for production use according to the Server-Side Public License!\\n\\nTo get started with LittleHorse OSS, you can:\\n\\n* Visit us on [GitHub](https://github.com/littlehorse-enterprises)\\n* Try our [quickstarts](https://littlehorse.dev/docs/developer-guide/install#installation-and-quickstart) or watch our founder, Colt, go through them in [Java](https://www.youtube.com/watch?v=8Zo_UOStg98&t=6s), [Go](https://www.youtube.com/watch?v=oZQc2ISSZsk), or [Python](https://www.youtube.com/watch?v=l3TZOjfpzTw)\\n* Join our [Slack Community](https://launchpass.com/littlehorse-community) for quick and responsive help!\\n\\nAlso, LittleHorse Enterprises LLC has released its first out our [product-focused website](https://littlehorse.io)! If you\'re still curious and want to learn even more, check out a few of our new in-depth tutorial series on [our YouTube page](https://www.youtube.com/@LittleHorse-ey3vw/featured).\\n\\n## New Features\\n\\nRelease `0.7` introduces many features designed to make your life easier. We plan to write blogs about all of them, so stay tuned!\\n\\n### Administrative Dashboard\\n\\nThe most exciting part of the `0.7.2` release of LittleHorse is the new LH Dashboard, which is an administrative portal into your LittleHorse Cluster. The LH Dashboard lets you check on all of your workflows and tasks and debug everything visually with fine-grained detail. Our quickstarts (see above) have everything you need to get started debugging your workflows with our dashboard.\\n\\nThe LH Dashboard is in the alpha stage, so we appreciate any bug reports or feature requests. Please file them on [our github](https://github.com/littlehorse-enterprises/littlehorse/issues)!\\n\\n### Idempotent Metadata Management\\n\\nManaging your `WfSpec`s and `TaskDef`s just got much easier. Check out our [updated docs](https://littlehorse.dev/docs/developer-guide/grpc/managing-metadata) for tutorials on how to keep your DevOps team happy and seamlessly integrate LittleHorse into your normal application development lifecycle.\\n\\n### Child Workflows\\n\\nWe also added the ability to run a `WfRun` which is a \\"child\\" of another `WfRun`. This allows for some interesting features, most importantly:\\n* Sharing `Variable`s between `WfRun`\'s\\n* Foreign-key relationships between the child and parent `WfRun`\'s.\\n\\nStay tuned for an upcoming blog about _why_ we added that feature. It was guided by our resident Domain-Driven Design expert, Eduwer Camacaro! Here\'s a hint: this feature makes it possible to use LittleHorse Workflows as a native data store for complex business entities. This is a great way to implement the \\"Aggregate Pattern.\\"\\n\\n### Enhanced `SearchWfRun`\\n\\nThe `rpc SearchWfRun` request now has a `repeated VariableMatch variable_filters` field on it. This allows you to filter `WfRun`\'s by the value of one or more `Variable`\'s when searching for them, returning only matching `WfRun`\'s. This is super useful when using a LittleHorse `WfRun` to model a business entity, and you need to do something like \\"find all orders placed by `user-id == john` and `status == OUT_FOR_SHIPPING`\\".\\n\\nIn the past, this was possible using the `rpc SearchVariable` and then back the `WfRunId` out of the `VariableId`; however, that method is a little bit clunky. In reality, our users want to find a `WfRunId` matching certain criteria; they\'re not looking for a `Variable`.\\n\\n## What\'s Next?\\n\\nWe couldn\'t be more excited about what is coming next.\\n\\n### Apache2 Clients\\n\\nSome members of the community have expressed concerns about our clients (SDK\'s + GRPC code) being licensed by the SSPL license. We heard you, and we will update them to the Apache 2.0 License before our `1.0.0` release! The server will remain SSPL.\\n\\n### Tutorials\\n\\nOne of our team members, Sohini, has been hard at work creating video tutorials which will help you get quickly up to speed on advanced LittleHorse concepts. You can find them here on our [YouTube](https://www.youtube.com/@LittleHorse-ey3vw/playlists).\\n\\nAdditionally, our founder has recorded a series of zoom meetings with himself (yes, you read that right...Colt used zoom to record a tutorial video series) going through quickstarts in all of our three SDK\'s. You can find them here in [Java](https://www.youtube.com/watch?v=8Zo_UOStg98&t=6s), [Go](https://www.youtube.com/watch?v=oZQc2ISSZsk), or [Python](https://www.youtube.com/watch?v=l3TZOjfpzTw).\\n\\n### Approaching `1.0.0`\\n\\nWhat\'s missing before `1.0.0`? We have some in-progress features that are already merged to `master` but only partially implemented. If you squint hard enough at our GRPC Api, you might notice that we have support for multi-tenancy and also fine-grained ACL\'s. They are NOT ready for production use as we need to iron out a few wrinkles, but we will have them ready for `1.0.0`. We also are working on an `rpc MigrateWfSpec` which allows you to migrate a running `WfRun` from an older version of a `WfSpec` to a newer version. This is hard work for us but it will be highly useful for our users.\\n\\nAdditionally, we are expanding our end-to-end test coverage to try to shake out as many issues as possible _before_ our users tell us about them. So far, the rate of new bugs that we\'ve discovered has slowed down considerably, which makes us think we are getting close to the quality we expect from our own product.\\n\\nWhat will change when we release `1.0.0`? We will be following [Semantic Versioning](https://semver.org) to the letter, which means we will be paying _super close attention_ to any breaking changes to our API. If we want our users to use us for mission critical workloads, we need to take stability seriously\u2014both in terms of performance and API compatibility.\\n\\nWe will also likely have three minor releases per year, with 12 months of patch support for each minor release. This release schedule is copied from Apache Kafka.\\n\\n### LH Cloud\\n\\nLastly, stay tuned for LittleHorse Cloud! Early access is open. If you would like to sign up for early access to LH Cloud, visit [our website](https://www.littlehorse.io/lh-cloud) or contact `sales@littlehorse.io`."},{"id":"littlehorse-0.5.0-release","metadata":{"permalink":"/blog/littlehorse-0.5.0-release","source":"@site/blog/2023-09-08-0.5.0-release.md","title":"Releasing 0.5.0","description":"Python, For-Each, LH Platform.","date":"2023-09-08T00:00:00.000Z","tags":[{"inline":false,"label":"LittleHorse Releases","permalink":"/blog/tags/release/","description":"Release blogs for LittleHorse Orchestrator."}],"readingTime":5.205,"hasTruncateMarker":true,"authors":[{"name":"The LittleHorse Council","title":"The Council of LittleHorse Maintainers","description":"LittleHorse Orchestrator is maintained by LittleHorse Enterprises LLC and available under the SSPL license. The LittleHorse Council is the group of engineers inside LittleHorse Enterprises LLC who are responsible for the stewardship of the open-source Orchestrator project and charged with looking out for the best interests of the LH Community.","url":"https://littlehorse.io","page":{"permalink":"/blog/authors/lh-council"},"socials":{"github":"https://github.com/littlehorse-enterprises","linkedin":"https://www.linkedin.com/company/littlehorse"},"imageURL":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","key":"lh_council"}],"frontMatter":{"title":"Releasing 0.5.0","description":"Python, For-Each, LH Platform.","slug":"littlehorse-0.5.0-release","authors":["lh_council"],"tags":["release"],"image":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","hide_table_of_contents":false},"unlisted":false,"prevItem":{"title":"Releasing 0.7","permalink":"/blog/littlehorse-0.7-release"},"nextItem":{"title":"Releasing 0.2.0","permalink":"/blog/littlehorse-0.2.0-release"}},"content":"We are excited to announce the minor release `0.5.0`. \x3c!-- truncate --\x3e This release is highlighted by:\\n\\n* Alpha support for building `WfSpec`s in Python.\\n* Improved monitoring and health metrics on the LittleHorse Server.\\n* Support for looping over a `JSON_ARR` and launching threads in parallel for each element.\\n* Improved Exception Handling.\\n* Limited early access for LittleHorse Platform.\\n\\nIn this release, we made great strides towards full Python support, improved monitoring and observability, and added the ability to spawn threads in parallel looping over a `JSON_ARR` variable.\\n\\n## Get Started\\n\\nLittleHorse is free for production use according to the Server-Side Public License!\\n\\nTo get started with LittleHorse OSS, you can:\\n\\n* Try our [quickstarts](https://littlehorse.dev/docs/developer-guide/install)\\n* Visit us on [GitHub](https://github.com/littlehorse-enterprises/littlehorse) and give us a :star:!\\n* Download our [docker images](https://gallery.ecr.aws/littlehorse)\\n\\n## New Features\\n\\nWe\'d like to highlight some of the exciting new features in `0.5.0`.\\n\\n### Python `WfSpec` Support\\n\\nOur Python SDK now has full support for building `WfSpec`s! You can check it out at our [quickstart page](/docs/developer-guide/install).\\n\\n### For-Each Suppport\\n\\nThis is a very exciting feature which allows you to iterate over a list and spawn multiple `ThreadRun`s (like threads in a program).\\n\\nTo see it in action, check out our [example](https://github.com/littlehorse-enterprises/littlehorse/tree/master/examples/spawn-thread-foreach) or read the [documentation](https://littlehorse.dev/docs/developer-guide/wfspec-development/child-threads).\\n\\n### Improved Failure Handling\\n\\nThis release introduces a new status for LittleHorse, called `EXCEPTION`. The `EXCEPTION` status differs from the `ERROR` status in the following ways:\\n\\n* `ERROR` means an unexpected _technical_ failure occurred. For example, a `TaskRun` timed out because a third-party API was down.\\n* `EXCEPTION` means that a failure occurred at the _business process level_. For example, you might use an `EXCEPTION` when a customer has insufficient funds in her account to complete an order.\\n\\nJust like in programming, you can throw and catch `EXCEPTION`s (and you can also catch `ERROR`s). For a blog post that goes in-depth into how LittleHorse makes it easy to handle failures in your workflows, check out our [Failure Handling Docs](/docs/concepts/workflows#failure-handling).\\n\\n### LH Server Monitoring\\n\\nWe added a new path `/status` on the LH Server\'s health endpoint (port `1822` by default) which can be used to inspect the status of all internal Kafka Streams `Task`s on the LH Server. It presents the following information:\\n\\n* All Active Tasks on the host\\n* All Standby Tasks on the host\\n* Any ongoing State Restorations on the host\\n\\nAdditionally, we added a `/diskUsage` endpoint which returns the number of bytes of disk space in use by the LH Server.\\n\\nLittleHorse Platform uses these endpoints to intelligently scale, manage, and operate LittleHorse for you.\\n\\nWe are also in the process of writing and implementing a Kafka Improvement Proposal to improve visibility of Standby Tasks, which will allow the LittleHorse Operator (both in LH Platform and LH Cloud) to safely and smoothly scale LittleHorse clusters down without any downtime. Stay tuned in the Kafka developer mailing list!\\n\\n### LH Platform\\n\\nLittleHorse Platform is a Kubernetes Operator that securely manages a LittleHorse cluster for you in your own environment. It seamlessly integrates with your Kubernetes environment, GitOps workflows, and security strategy (TLS, mTLS, OAuth, Cert Manager, Keycloak).\\n\\nLittleHorse Platform is now available for limited early access, and has been installed in one of the largest health insurance companies in the US.\\n\\nTo get started with LittleHorse Platform, please [contact us](https://docs.google.com/forms/d/e/1FAIpQLScXVvTYy4LQnYoFoRKRQ7ppuxe0KgncsDukvm96qKN0pU5TnQ/viewform?usp=sf_link).\\n\\n### Persistent Variables\\n\\nIn LittleHorse `0.2.0` and later, you can search for `Variable`s by their value. For example, if you have a Workflow Specification that defines a variable `email_address`, you can find all Workflow Run\'s where `email_address == \'obiwan@jedi-council.org` by using the `SearchVariable` rpc call.\\n\\nThe problem with `0.2.0`? You need to provide the `wfSpecVersion` in your search request. That means you can only search for a `Variable` if you know the version of the `WfSpec` it came from.\\n\\nRelease `0.4.0` introduced the ability to mark a `Variable` as `persistent`, which means that:\\n* Every future version of the `WfSpec` must have the same variable definition with the same index type.\\n* You can now search for variables with a certain value across _all versions_ of the `WfSpec`.\\n\\nBe on the lookout for an upcoming blog post about using Persistent Variables and a simple backend-for-frontend to build an end-to-end Approval Workflow Application using only LittleHorse!\\n\\n## What\'s Next\\n\\nOver the next few weeks, we plan to:\\n\\n* Add utilities to make it easier to work with the LittleHorse API.\\n* Allow users to throw a Workflow `EXCEPTION` from within the Task Worker SDK (currently, only `ERROR` is supported).\\n* Continue hardening the LittleHorse Server\'s availability and performance story.\\n* Launch limited early accesss for LittleHorse Cloud and LittleHorse UI.\\n\\nTo get started with LittleHorse, head over to our [installation docs](https://littlehorse.dev/docs/developer-guide/install).\\n\\n### What about `0.3.0` and `0.4.0`?\\n\\nWe also released `0.3.0` and `0.4.0` over the past 5 weeks! (And before `0.3.0`, we had a minor patch bugfix on `0.2.1`).\\n\\nThe only thing missing with `0.3.0` and `0.4.0` is a blog post + announcement. That\'s because a lot of the features we included in this announcement were partially-implemented, implemented in some languages and not others, or in the \\"experimental\\" phase at the time of `0.3.0` and `0.4.0`. We accelerated the release of `0.3.0` and `0.4.0` because certain early-access customers requested certain features on an accelerated timeline.\\n\\nAs our API is mostly stable now, we will slow down our release cadence to likely a new `*.x.*` version (a `minor` release in [Semantic Versioning](https://semver.org)) every two months, with security and bugfix patch releases (`*.*.x`) as needed.\\n\\nAdditionally, as we introduce new features, we will start a release changelog document in which we document the level of stability of the new API\'s introduced. For example:\\n* `STABLE`: Any changes to this API before the next [Major Release](https://semver.org) will be backwards compatible. The feature is covered by our integration tests.\\n* `BETA`: We don\'t anticipate any _large breaking changes_ to the feature/API. It is covered by our integration tests, but it _might_ change before the `1.0.0` release.\\n* `EXPERIMENTAL`: Try it out and give us feedback! But you might want to wait a release or two before putting it into production.\\n\\nThe `0.6.0` release notes will include a table of all of our features and their API Stability Level in all four of our SDK\'s."},{"id":"littlehorse-0.2.0-release","metadata":{"permalink":"/blog/littlehorse-0.2.0-release","source":"@site/blog/2023-08-30-0.2.0-release.md","title":"Releasing 0.2.0","description":"Making workflow development easy again.","date":"2023-08-30T00:00:00.000Z","tags":[{"inline":false,"label":"LittleHorse Releases","permalink":"/blog/tags/release/","description":"Release blogs for LittleHorse Orchestrator."}],"readingTime":3.54,"hasTruncateMarker":true,"authors":[{"name":"The LittleHorse Council","title":"The Council of LittleHorse Maintainers","description":"LittleHorse Orchestrator is maintained by LittleHorse Enterprises LLC and available under the SSPL license. The LittleHorse Council is the group of engineers inside LittleHorse Enterprises LLC who are responsible for the stewardship of the open-source Orchestrator project and charged with looking out for the best interests of the LH Community.","url":"https://littlehorse.io","page":{"permalink":"/blog/authors/lh-council"},"socials":{"github":"https://github.com/littlehorse-enterprises","linkedin":"https://www.linkedin.com/company/littlehorse"},"imageURL":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","key":"lh_council"}],"frontMatter":{"title":"Releasing 0.2.0","description":"Making workflow development easy again.","slug":"littlehorse-0.2.0-release","authors":["lh_council"],"tags":["release"],"image":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","hide_table_of_contents":false},"unlisted":false,"prevItem":{"title":"Releasing 0.5.0","permalink":"/blog/littlehorse-0.5.0-release"}},"content":"We are excited to announce the release of `0.2.0`! \x3c!-- truncate --\x3e In this release, we added several new features, highlighted by User Tasks, security, and Python support.\\n\\n## Get Started\\n\\nLittleHorse is free for production use according to the Server-Side Public License!\\n\\nTo get started with LittleHorse OSS, you can:\\n\\n* Visit us on [GitHub](https://github.com/littlehorse-enterprises)\\n* Try our [quickstarts](https://littlehorse.dev/docs/developer-guide/install#installation-and-quickstart)\\n\\nAdditionally, with version `0.2.0`, we have released our first two Docker Images:\\n\\n* [`lh-server`](https://gallery.ecr.aws/littlehorse/littlehorse-server), the production-ready build of the LittleHorse Server.\\n* [`lh-standalone`](https://gallery.ecr.aws/littlehorse/littlehorse-standalone), a self-contained build of the LittleHorse Server that you can run to get a working LH Installation for local development.\\n\\n## New Features\\n\\nRelease `0.2.0` contains many exciting new features, and we\'ve highlighted a few here.\\n\\n### User Tasks\\n\\n[User Tasks](https://littlehorse.dev/docs/concepts/user-tasks) are a massive new feature released in `0.2.0` which allow you to schedule tasks to be executed by a human user alongside tasks that are executed by computers.\\n\\nIn `0.2.0`, User Tasks have reached stability, meaning that future releases will be backwards-compatible with the current User Tasks API. We currently have the following features:\\n\\n* Assignment of tasks to a User or User Group\\n* Reminder Tasks, or `TaskRun`\'s that are scheduled some time after a `UserTaskRun` is scheduled.\\n* Automatic reassignment of a `UserTaskRun` after some period of inactivity.\\n* Manual reassignment of a `UserTaskRun`.\\n* `UserTaskRun` search.\\n\\n:::note\\nThe public API for User Tasks is stable in all of the grpc clients and in the Java `WfSpec` SDK.\\n\\nThe Go and Python grpc clients both support User Tasks. However, neither Python nor Go yet have support for User Tasks in the `WfSpec` SDK.\\n:::\\n\\n### Workflow Threading\\n\\nRelease `0.2.0` allows you to use a `WAIT_FOR_THREADS` node to wait for more than one child thread at one time. For an example, see our [Parallel Approval Example](https://github.com/littlehorse-enterprises/littlehorse/tree/master/examples/parallel-approval) on our GitHub.\\n\\nFuture releases will provide _backwards-compatible_ enhancements to this\\nfunctionality, allowing various strategies for handling failures of individual child threads.\\n\\n### Python Support\\n\\nWe have released an alpha [Python SDK](https://github.com/littlehorse-enterprises/littlehorse/tree/master/sdk-python)! This release contains:\\n\\n* Python client in grpc\\n* Python Task Worker SDK\\n\\nCurrently, building `WfSpec`\'s in Python is not supported. We aim to move python Task Worker support from alpha to beta, and add alpha support for `WfSpec` development in python, in the `0.3.0` release.\\n\\nTo try out our python task worker client, you can head to [Installation Docs](https://littlehorse.dev/docs/developer-guide/install) and the [Task Worker Development Docs](https://littlehorse.dev/docs/developer-guide/task-worker-development).\\n\\n:::note\\nThe Python SDK is in the alpha stage, meaning that future releases could break backwards compatibility.\\n:::\\n\\n### Security\\n\\nWe added beta support for OAuth, TLS, and mTLS in release `0.2.0`. The following features graduated to \\"beta\\" in this release:\\n\\n* TLS encryption for incoming connections on all listeners, configured on a per-listener basis.\\n* mTLS to authenticate incoming connections on any listeners, configured on a per-listener basis.\\n* OAuth to authenticate incoming connections on any public listener (excluding the inter-server communication port).\\n\\n:::info\\nBeta support means that we will soon add significant functionality, and as such a future release _might_ break backwards compatibility.\\n\\nHowever, future releases of a feature in the _beta_ state will most likely be backwards compatible with `0.2.0` barring exceptional circumstances.\\n:::\\n\\n### Performance\\n\\nWe made several optimizations to our storage management sub-system, reducing the number of put\'s and get\'s into our backing state store by roughly 30%. As a result, a LittleHorse Server running with a single partition is capable of scheduling over 1,100 `TaskRun`\'s per second.\\n\\n### Go Support\\n\\nSupport for the Go client is now beta. Future releases will maintain compatibility for all features on our documentation.\\n\\nRelease `0.3.0` will close the gap between the Java and Go SDK\'s, adding features such as:\\n* Format Strings for Variable Assignments in the `WfSpec` SDK\\n* User Task support in the `WfSpec` SDK\\n* Configuring Indexes on `Variable`s in the `WfSpec` SDK\\n\\n## What\'s Next\\n\\nWe have several exciting features coming soon over the next few releases, including:\\n\\n* Fine-grained access controls\\n* Backward-compatible improvements to [Failure Handling](https://littlehorse.dev/docs/concepts/exception-handling)\\n* C# support\\n* Python support for building `WfSpec`s\\n\\nFor an enterprise-ready distribution of LittleHorse running in your own datacenter, contact `sales@littlehorse.io` to inquire about LittleHorse Platform.\\n\\nFor a pay-as-you-go, serverless Managed Service of LittleHorse in the cloud, fill out the [LH Cloud Waitlist Form](https://docs.google.com/forms/d/e/1FAIpQLScXVvTYy4LQnYoFoRKRQ7ppuxe0KgncsDukvm96qKN0pU5TnQ/viewform)."}]}}')}}]); \ No newline at end of file diff --git a/assets/js/f81c1134.e056e39d.js b/assets/js/f81c1134.e056e39d.js new file mode 100644 index 000000000..0725677cb --- /dev/null +++ b/assets/js/f81c1134.e056e39d.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[8130],{7735:e=>{e.exports=JSON.parse('{"archive":{"blogPosts":[{"id":"queuing","metadata":{"permalink":"/blog/queuing","source":"@site/blog/2024-10-28-queuing.md","title":"Integration Patterns: Queueing","description":"When integrating API\'s, we sometimes have to tie together steps that can take a long time or might not always be available. If we force the callers of our API\'s to wait for completion, we find ourselves with some grumpy customers. So what can we do about this?","date":"2024-10-28T00:00:00.000Z","tags":[{"inline":false,"label":"Technical Analysis","permalink":"/blog/tags/analysis/","description":"Analysis of the current and future state of Technical Architecture."},{"inline":false,"label":"Integration Patterns","permalink":"/blog/tags/integration-patterns/","description":"A 5-part blog series on Integration Patterns that are useful for event-driven systems."},{"inline":false,"label":"LittleHorse Orchestrator","permalink":"/blog/tags/littlehorse/","description":"Information about the LittleHorse Orchestrator."}],"readingTime":5.745,"hasTruncateMarker":true,"authors":[{"name":"Colt McNealy","title":"Managing Member of the LLC","description":"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He\'s a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.","page":{"permalink":"/blog/authors/coltmcnealy"},"socials":{"github":"https://github.com/coltmcnealy-lh","linkedin":"https://www.linkedin.com/in/colt-mcnealy-900b7a148/","x":"https://x.com/coltmcnealy"},"imageURL":"https://avatars.githubusercontent.com/u/100447728","key":"coltmcnealy"}],"frontMatter":{"slug":"queuing","authors":["coltmcnealy"],"tags":["analysis","integration-patterns","littlehorse"]},"unlisted":false,"nextItem":{"title":"Integration Patterns: Transactional Outbox","permalink":"/blog/transactional-outbox"}},"content":"When integrating API\'s, we sometimes have to tie together steps that can take a long time or might not always be available. If we force the callers of our API\'s to wait for completion, we find ourselves with some grumpy customers. So what can we do about this?\\n\\n\x3c!-- truncate --\x3e\\n\\n:::info\\nThis is the third part in a five-part blog series on useful Integration Patterns. This blog series will help you build real-time, responsive applications and microservices that produce predictable results and prevent the Grumpy Customer Problem.\\n\\n1. [Saga Transactions](./2024-09-24-saga-pattern.md)\\n2. [The Transactional Outbox Pattern](./2024-09-30-transactional-outbox.md)\\n3. **[This Post]** Queuing and Backpressure\\n4. [Coming soon] Retries and Dead-Letter Queues\\n5. [Coming soon] Callbacks and External Events\\n:::\\n\\n## Why Queue?\\n\\nIn software architecture, simple is almost always better. With fewer moving parts, there are less chances for failure, less things to debug, and fewer pieces of infrastructure. So when and why would you introduce queues to your architecture?\\n\\nQueues are useful when building services that need to accept a request from a client and then execute some processing which has any of the following characteristics:\\n\\n1. Slow to execute.\\n2. Flakey, not always-available, or in need of retries.\\n3. Have a rate limit or cannot gracefully handle spikey workloads (backpressure).\\n4. Have multiple steps that need to all complete before the processing is finalized.\\n\\nCrucially, if your service enqueues requests, you need to make sure that the caller of your API doesn\'t need to wait for their entire request to be processed: a simple promise that it will get done should be sufficient. As we will see with a practical example, this is feasible in many business cases.\\n\\n### Example: Reviews Application\\n\\nConsider a product reviews widget on an e-commerce site. In this application, users can submit reviews of a product. However, before a review can be approved to be displayed, it must first be checked for offensive content by a third-party AI service. Sometimes, this third-party service often has response times of over 10 seconds, and sometimes even goes down and is fully unavailable.\\n\\nA naive web app endpoint to handle this use-case might be:\\n\\n```java\\n@PostMapping(\\"/review\\")\\npublic ResponseEntity postReview(@RequestBody PostReviewRequest request) {\\n\\n // Call the third-party AI service, which takes a long time and is flakey\\n try {\\n ReviewAnalysisResponse reviewAnalysis = thirdPartyService.analyzeReview(request);\\n } catch(OffensiveReviewException exn) {\\n return ResponseEntity.status(400);\\n } catch(Exception exn) {\\n return ResponseEntity.status(500);\\n }\\n\\n // If we got here, the review is valid\\n reviewService.save(request);\\n return ResponseEntity.status(HttpStatus.CREATED);\\n}\\n```\\n\\nAs promised ( :wink: ), this endpoint implementation has a sub-optimal user experience. Many times, when the flakey third-party AI service is unavailable, users will simply be unable to post reviews. Even when it is up, users will see the spinning waiting wheel for multiple seconds.\\n\\nThe solution? Enqueue the request for processing later by some external system, and then respond immediately to the client\'s request. That can be done in two ways:\\n\\n1. **Traditional Queuing:** simply put a record on some queue, streaming system, or event bus (such as Apache Pulsar, Apache Kafka, or AWS SQS).\\n2. **Workflow Execution:** tell a workflow orchestration engine like LittleHorse to start executing a process!\\n\\nOnce the request is enqueued, there will be a system polling the queue to call the third-party analytics API and then either reject or approve the review. This system will be responsible for throttling requests according to the API\'s service limits, retrying failed messages, and waiting for the API to come back online in the case of an intermittent outage.\\n\\n## Orchestrators vs. Plain Old Queues\\n\\nWorkflow engines [internally use message queues](./2024-09-04-basics-of-workflows.md) on their own! So what\'s the difference from the user perspective?\\n\\nYou can think of a workflow engine as a _super-smart_ message queue, with certain clear advantages over message queues including advanced monitoring and better support for multi-step processes.\\n\\n:::note\\nThe next post in this series will take a deep-dive into retries, idempotency, and failure handling, which is another area in which workflow engines shine above and beyond Plain Old Queues.\\n:::\\n\\n### Monitoring and Debugging\\n\\nWorkflow engines provide more insight and oversight into your processes than do message queues. In our reviews application, if an angry user (`anakin@jeditemple.com`) calls customer support to complain that his review hadn\'t been processed in over two days, it would be tricky to find the _exact_ cause with a pure message queue.\\n\\nHowever, with LittleHorse, you just search for the `WfRun` where `user-id == anakin@jeditemple.com`:\\n\\n![Dashboard Workflow Search](./2024-10-28-workflow-search.png)\\n\\nand then look on the dashboard to see what went wrong:\\n\\n![Dashboard Task Error Message](./2024-10-28-task-debugging.png)\\n\\nWe are also working on _workflow metrics_ that will allow you to use LittleHorse to answer questions such as:\\n\\n* How long does the `process-review` workflow take on average?\\n* How long does each `analyze-review` task attempt take on average, and what percentage of calls fail (i.e. how responsive is the API)?\\n* What percentage of reviews are approved versus rejected?\\n\\nThese will likely not be available until March 2025; however, we have nearly finalized the designs for them and have scheduled the implementation to start in January.\\n\\n### Multi-Step Processes\\n\\nSo far, the use-case we\'ve discussed involves only two \\"steps\\" to be executed:\\n1. Analyze the product review.\\n2. Post the review to the site.\\n\\nYou could arguably execute both steps at once: the only problem we are trying to solve is that we have a flakey API and we don\'t want our customers to have to wait for it. In theory, the same consumer which calls the external API could also update the visibility of the review to `APPROVED`.\\n\\nBut what if the business requirements change, and we need to do some post-processing, such as notify a separately-managed (and also flakey) analytics service of what happened? That would require adding another queue:\\n\\n1. Edit our original consumer to publish to a new queue.\\n2. Write a _new_ consumer that subscribes to the second queue and notifies the flakey analytics service.\\n3. Instrument monitoring for the new queue infrastructure.\\n\\nThis gets especially tricky when we want to handle intermittent availability from the analytics service: we\'ll have to copy the same boilerplate to handle retries and dead-letter-queues (more on that in the next post).\\n\\nHowever, with the workflow-driven approach, all you need to do is add a single line to your `WfSpec`:\\n\\n```java\\nwf.execute(\\"notify-analytics-service\\", userId, review, approvalStatus);\\n```\\n\\n## Wrapping Up\\n\\nQueueing is a great tool to improve the client experience of your API\'s when you can respond to your callers before all of your processing has been done. Workflow engines like LittleHorse can actually be thought of as a _super-smart queueing system_, which provides all of the advantages of queueing plus better observability and support for multi-step processes.\\n\\n### Get Involved!\\n\\nStay tuned for the next post, which will cover retries and dead-letter queues! In the meantime:\\n\\n* Try out our [Quickstarts](https://littlehorse.dev/docs/developer-guide/install)\\n* Join us [on Slack](https://launchpass.com/littlehorsecommunity)\\n* Give us a star [on GitHub](https://github.com/littlehorse-enterprises/littlehorse)!"},{"id":"transactional-outbox","metadata":{"permalink":"/blog/transactional-outbox","source":"@site/blog/2024-09-30-transactional-outbox.md","title":"Integration Patterns: Transactional Outbox","description":"Like the Saga Pattern, the Transactional Outbox pattern is tool for defending against data loss in your applications. In this blog we cover how it works and how to do it easier using LittleHorse.","date":"2024-09-30T00:00:00.000Z","tags":[{"inline":false,"label":"Technical Analysis","permalink":"/blog/tags/analysis/","description":"Analysis of the current and future state of Technical Architecture."},{"inline":false,"label":"Integration Patterns","permalink":"/blog/tags/integration-patterns/","description":"A 5-part blog series on Integration Patterns that are useful for event-driven systems."},{"inline":false,"label":"LittleHorse Orchestrator","permalink":"/blog/tags/littlehorse/","description":"Information about the LittleHorse Orchestrator."}],"readingTime":5.72,"hasTruncateMarker":true,"authors":[{"name":"Colt McNealy","title":"Managing Member of the LLC","description":"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He\'s a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.","page":{"permalink":"/blog/authors/coltmcnealy"},"socials":{"github":"https://github.com/coltmcnealy-lh","linkedin":"https://www.linkedin.com/in/colt-mcnealy-900b7a148/","x":"https://x.com/coltmcnealy"},"imageURL":"https://avatars.githubusercontent.com/u/100447728","key":"coltmcnealy"}],"frontMatter":{"slug":"transactional-outbox","authors":["coltmcnealy"],"tags":["analysis","integration-patterns","littlehorse"]},"unlisted":false,"prevItem":{"title":"Integration Patterns: Queueing","permalink":"/blog/queuing"},"nextItem":{"title":"Integration Patterns: Saga Transactions","permalink":"/blog/saga-pattern"}},"content":"Like the [Saga Pattern](./2024-09-24-saga-pattern.md), the Transactional Outbox pattern is tool for defending against data loss in your applications. In this blog we cover how it works and how to do it _easier_ using LittleHorse.\\n\\n\x3c!-- truncate --\x3e\\n\\n:::info\\nThis is the first part in a five-part blog series on useful Integration Patterns. This blog series will help you build real-time, responsive applications and microservices that produce predictable results and prevent the Grumpy Customer Problem.\\n\\n1. [Saga Transactions](./2024-09-24-saga-pattern.md)\\n2. **[This Post]** The Transactional Outbox Pattern\\n3. [Queuing](./2024-10-28-queuing.md)\\n4. [Coming soon] Retries and Dead-Letter Queues\\n5. [Coming soon] Callbacks and External Events\\n:::\\n\\n## The Transactional Outbox Pattern\\n\\nAt the technical level, the [Transactional Outbox Pattern](https://microservices.io/patterns/data/transactional-outbox.html) allows you to atomically:\\n\\n1. Update a database, and\\n2. Publish a record to a streaming log or message queue (such as Apache Kafka).\\n\\nThe [Saga Pattern](./2024-09-24-saga-pattern.md) allows you to make a multi-step business process atomic. However, you can think of the Transactional Outbox pattern as a way to ensure that a process doesn\'t get dropped halfway through.\\n\\n:::tip\\nThe Transactional Outbox Pattern is often useful _within_ a Saga transaction.\\n\\nHowever, as we\'ll see later on in this article, LittleHorse removes the need to worry about such difficult technical details.\\n:::\\n\\n## Case Study: Customer Sign-Up\\n\\nAs an example, let\'s consider the following Spring Boot REST endpoint (`POST /user`), which must:\\n\\n1. Create a customer account in a database.\\n2. Send a message on a queue which results in a series of account setup actions, including a welcome email being sent to the customer.\\n\\n```java\\n@PostMapping(\\"/user\\")\\npublic ResponseEntity createUser(@RequestBody CreateUserRequest request) {\\n database.createUser(request);\\n queue.publishUserCreatedEvent(request);\\n return ResponseEntity.status(HttpStatus.CREATED);\\n}\\n```\\n\\nA few things can go wrong here which would cause the user to be created in the database but the customer never gets a welcome email, and the account setup fails. First, the queue could be inaccessible (this _could_ be saved at the application layer with an exception handler).\\n\\nHowever, one failure mode which _cannot_ be caught at the application layer is if the Spring Boot app crashes during the process of publishing the record to the queue (on or just before the `queue.publishUserCreatedEvent()` line).\\n\\nThis would definitely cause another case of the Grumpy Customer Problem!\\n\\n### Using a Transactional Outbox\\n\\nThe core idea of a Transactional Outbox is to make use of transactions within a single database to atomically:\\n\\n1. Make the database update.\\n2. Write the desired queue event to an _Outbox Table._\\n\\n![Transactional Outbox Architecture](./2024-09-30user-workflow-outbox.png)\\n\\nSince items `1` and `2` happen within a single database, it\'s trivial to wrap them in a transaction. After the queue event is written to the Outbox Table, a separate process eventually reads the new records in the Outbox Table and pushes them to a queue.\\n\\nWe would rewrite our Spring Boot endpoint to only write a transaction to the database. The SQL for that transaction would look something like:\\n\\n```\\nBEGIN TRANSACTION;\\nINSERT INTO user VALUES ...;\\nINSERT INTO outbox VALUES ...; # Insert the record for the queue\\nCOMMIT;\\n```\\n\\nThe Spring Boot application should have another thread which reads records from the `outbox` table, publishes them to the queue or streaming system, and updates the record in the database as \\"read\\".\\n\\n:::warning\\nThe topic of Exactly-Once Semantics is complex; we do not have time in this post to discuss the implications of EOS and a Transactional Outbox.\\n\\nAs a hint, you can achieve EOS if you transactionally store the last-written offset inside your message broker. There are many \\"gotchas\\" to this depending on your message broker; for example, in Apache Kafka you must use `read_committed` consumers.\\n:::\\n\\n### Using LittleHorse\\n\\nThe Outbox Pattern is necessary to persist outgoing records in the case that we suffer a crash between writing to the database and writing to the record queue. However, what if we could \\"delegate\\" persistence and reliability to some other system?\\n\\nEnter LittleHorse! What if we had a `WfSpec` that defined our process, as follows:\\n\\n```java\\npublic void wfLogic(WorkflowThread wf) {\\n var userRequest = wf.addVariable(\\"create-user-request\\", JSON_OBJ).required();\\n wf.execute(\\"create-user\\", userRequst);\\n wf.execute(\\"send-welcome-email\\", user);\\n}\\n```\\nNow, all our REST endpoint has to do is run the worklfow:\\n\\n```java\\n@PostMapping(\\"/user\\")\\npublic ResponseEntity createUser(@RequestBody CreateUserRequest request) {\\n // Just run the workflow\\n littlehorseClient.runWf(RunWfRequest.newBuilder()\\n .setWfSpecName(\\"user-workflow\\")\\n .putVariables(\\"create-user-request\\", LHLibUtil.objToVarVal(request))\\n .build());\\n return ResponseEntity.status(HttpStatus.CREATED);\\n}\\n```\\n\\n![Transactional Outbox Architecture with LittleHorse](./2024-09-30-user-workflow-lh.png)\\n\\nNo outbox table needed! If creating the user in the database fails, or if sending the welcome email fails, LittleHorse will patiently retry (according to your retry backoff policy) the `TaskRun`s until they succeed. In the event that you exhaust your retries, you still haven\'t lost data:\\n\\n* You can easily search for failed workflows.\\n* You can restart failed workflows with the `rpc RescueThreadRun` once the database incident is resolved.\\n\\n:::tip\\nYou can add retries using the `Workflow` object:\\n\\n```java\\nWorkflow wf = Workflow.newWorkflow(\\"user-workflow\\", this::wfLogic);\\nwf.setDefaultTaskRetries(10);\\nwf.setDefaultTaskExponentialBackoffPolicy(ExponentialBackoffRetryPolicy.newBuilder()\\n .setBaseIntervalMs(1000)\\n .setMultiplier(3)\\n .build());\\n\\nwf.registerWfSpec(littlehorseClient);\\n```\\n:::\\n\\n## Wrapping Up\\n\\nThe Transactional Outbox Pattern is a useful and often necessary tool for building reliable integrations between systems. However, it takes time, infrastructure, and deep understanding of distributed systems to get it right. So why spend time solving problems that don\'t differentiate your business?\\n\\nThankfully, LittleHorse offers a workaround to the original problem, removing the need to engage with the complexities of Transactional Outboxes.\\n\\n### Additional Use Cases\\n\\nThe Transactional Outbox pattern is useful anytime you need to update information in a database _and also_ publish a record to a streaming log or a message queue.\\n\\nFor example:\\n\\n* **User registration:** Save a new user\'s profile and push a message to a queue in order to trigger a verification email.\\n* **Appointment scheduling:** Save appointment details and notify users via SMS or email.\\n* **Saga Transactions:** Within a Saga transaction (such as the order processing scenario discussed in the [last post](./2024-09-24-saga-pattern.md#case-study-order-processing)), a service may need to atomically update its database and push a record to a queue.\\n* **Inventory management:** Update stock levels and push updates to warehouses or suppliers.\\n\\n### Alternative: Log-First Architecture\\n\\nAnother solution to this specific problem would be to have the request handler (our Spring Boot endpoint) publish directly to an event log like Apache Kafka. Then, there would be two consumer groups for that topic:\\n\\n1. A consumer group which creates the `user` record in the database.\\n2. A consumer group which sends the welcome email.\\n\\nThe REST endpoint would return `201` as soon as the record was acknowledged by the streaming platform.\\n\\nIf you squint hard enough, you can see that this is very similar to what happens with LittleHorse; however, using this pattern, you are responsible for wiring together a complex topology of topics and queues, which is much harder than using a workflow!\\n\\n### Get Involved!\\n\\nStay tuned for the next post on Queues and Backpressure! In the meantime:\\n\\n* Try out our [Quickstarts](https://littlehorse.dev/docs/developer-guide/install)\\n* Join us [on Slack](https://launchpass.com/littlehorsecommunity)\\n* Give us a star [on GitHub](https://github.com/littlehorse-enterprises/littlehorse)!"},{"id":"saga-pattern","metadata":{"permalink":"/blog/saga-pattern","source":"@site/blog/2024-09-24-saga-pattern.md","title":"Integration Patterns: Saga Transactions","description":"The Saga pattern allows you to defend against data loss, dropped orders, and confused (or grumpy) customers. While useful, the Saga pattern is tricky to get right without an orchestrator.","date":"2024-09-24T00:00:00.000Z","tags":[{"inline":false,"label":"Technical Analysis","permalink":"/blog/tags/analysis/","description":"Analysis of the current and future state of Technical Architecture."},{"inline":false,"label":"Integration Patterns","permalink":"/blog/tags/integration-patterns/","description":"A 5-part blog series on Integration Patterns that are useful for event-driven systems."},{"inline":false,"label":"LittleHorse Orchestrator","permalink":"/blog/tags/littlehorse/","description":"Information about the LittleHorse Orchestrator."}],"readingTime":6.215,"hasTruncateMarker":true,"authors":[{"name":"Colt McNealy","title":"Managing Member of the LLC","description":"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He\'s a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.","page":{"permalink":"/blog/authors/coltmcnealy"},"socials":{"github":"https://github.com/coltmcnealy-lh","linkedin":"https://www.linkedin.com/in/colt-mcnealy-900b7a148/","x":"https://x.com/coltmcnealy"},"imageURL":"https://avatars.githubusercontent.com/u/100447728","key":"coltmcnealy"}],"frontMatter":{"slug":"saga-pattern","authors":["coltmcnealy"],"tags":["analysis","integration-patterns","littlehorse"]},"unlisted":false,"prevItem":{"title":"Integration Patterns: Transactional Outbox","permalink":"/blog/transactional-outbox"},"nextItem":{"title":"The Basics of Workflow","permalink":"/blog/basics-of-workflow"}},"content":"The Saga pattern allows you to defend against data loss, dropped orders, and confused (or grumpy) customers. While useful, the Saga pattern is tricky to get right without an orchestrator.\\n\\n\x3c!-- truncate --\x3e\\n\\n:::info\\nThis is the first part in a five-part blog series on useful Integration Patterns. This blog series will help you build real-time, responsive applications and microservices that produce predictable results and prevent the Grumpy Customer Problem.\\n\\n1. **[This Post]** Saga Transactions\\n2. [The Transactional Outbox Pattern](./2024-09-30-transactional-outbox.md)\\n3. [Queuing](./2024-10-28-queuing.md)\\n4. [Coming soon] Retries and Dead-Letter Queues\\n5. [Coming soon] Callbacks and External Events\\n:::\\n\\n## The Saga Pattern\\n\\nAt a technical level, the [Saga Pattern](https://microservices.io/patterns/data/saga.html) allows you to perform distributed transactions across multiple disparate systems without 2-phase commit.\\n\\nIn plain English, it is a tool in the belt of a software engineer to prevent half-fulfilled bank transfers, hanging orders, or other failures which would result in a Grumpy Customer.\\n\\n:::info\\nThe \\"Saga\\" pattern gets its name from literature and film, wherein a \\"saga\\" is a series of chronologically-ordered related works. For example, the \\"Star Wars Saga.\\"\\n:::\\n\\n### Use Cases\\n\\nBusiness processes often need to perform actions in two separate systems either all at once or not at all. For example, you may need to charge a customer\'s credit card, reserve inventory, and ship an item to the customer all at once or not at all. If the payment went through but shipping failed, we would see the Grumpy Customer Problem yet again.\\n\\nThe Saga pattern is appropriate when:\\n* A business process must take action across multiple separate systems (legacy monoliths, microservices, external API\'s, etc),\\n* Each of those actions can be undone via a \\"compensation task\\", and\\n* All actions must logically happen together or not at all.\\n\\n:::tip\\nIt\'s also worth noting that a different flavor of the Saga pattern can also be used in _long-running_ business processes. In a past job, for example, I worked on a project that implemented the Saga pattern to handle the scheduling of home inspections. In this case, the task of finding an inspector to show up at the home and confirming a time with the homeowner needed to be performed atomically.\\n:::\\n\\n### Implementation\\n\\nWhile Saga is very hard to implement, it\'s simple to describe:\\n\\n* Try to perform the actions across the multiple systems.\\n* If one of the actions fails, then run a _compensation_ for all previously-executed tasks.\\n\\nThe _compensation_ is simply an action that \\"undoes\\" the previous action. For example, the compensation for a payment task might be to issue a refund.\\n\\n## Case Study: Order Processing\\n\\nLet\'s take a look at a familiar use-case: an order processing workflow involving the `inventory` service, and the `payments` service. (The `orders` service is involved implicitly.) As they would in a real world scenario, all of our services live on separate physical systems and have their own databases.\\n\\nIn this business process, we first reserve inventory for the ordered item. Next, we charge the customer\'s credit card.\\n\\nIf charging the credit card fails, then we have a problem: we\'ve reserved inventory but not sold it.\\n\\nOur services need the following functionality. In SOA, these would be endpoints; in LittleHorse, they would be `TaskDef`s:\\n* `create-order`: creates an order in the `PENDING` status.\\n* `reserve-inventory`: marks an item as no longer available for sale.\\n* `charge-payment`: charges the customer.\\n* `release-inventory`: marks an item as available for sale again.\\n* `cancel-order`: marks an order as `CANCELED`.\\n* `complete-order`: marks an order as `COMPLETED`.\\n\\n### Using Message Queues\\n\\nUsing message queues, the happy path looks like the following:\\n\\n![Architecture diagram](./2024-09-24-choreography-simple.png)\\n\\n:::note\\nThe above image assumes the _choreography_ pattern, in contrast to the _orchestrator_ pattern. The orchestrator pattern is a ton of work and involves writing something that very much resembles LittleHorse!\\n:::\\n\\n1. Orders service calls `createOrder()`.\\n2. Orders service publishes to the `reserve-inventory` queue.\\n3. Inventory service reads the message and calls `reserveInventory()`.\\n4. Inventory service publishes to the `charge-payment` queue.\\n5. Payment service charges the credit card.\\n6. Payment service publishes to the `complete-order` queue.\\n7. Orders service consumes the record and calls `completeOrder()`.\\n\\nIn just the happy path, we have strong coupling already between our services in three places, and we have three message queues to manage.\\n\\nBut now we need to release the inventory and cancel the order when the payment doesn\'t go through. So the flow looks like this:\\n\\n![Architecture Diagram](./2024-09-24-choreography-saga.png)\\n\\n1. Orders service calls `createOrder()`.\\n2. Orders service publishes to the `reserve-inventory` queue.\\n3. Inventory service reads the message and calls `reserveInventory()`.\\n4. Inventory service publishes to the `charge-payment` queue.\\n5. Payment service charges the credit card _unsuccessfully_.\\n6. Payment service publishes to the `release-inventory` queue.\\n7. Inventory service reads the record and calls `releaseInventory()`.\\n8. Inventory service publishes to the `cancel-order` queue.\\n9. Orders service consumes the record and calls `cancelOrder()`.\\n\\n:::note\\nWe still haven\'t even considered the case when the `reserve-inventory` step fails and we need to catch that exception and handle the order. For the sake of brevity, we will leave that out.\\n:::\\n\\nNow, we have _five_ different message queues that we have to wrangle with. We can also see that the overall business flow has started to leak across all of our different services.\\n\\n:::danger\\nOne thing we are ignoring in this blog post is _reliability_: to make this setup production-ready, we would also have to ensure that updates to the internal databases of the services are atomic along with pushing messages to the message queue. We will cover that in next week\'s post (along with how LittleHorse takes care of that for you).\\n:::\\n\\n### Using LittleHorse\\n\\nUsing LittleHorse, in java, this whole workflow could look like the following. This is _real code_ that does indeed compile and replaces the need for all of the complex queueing logic we had above.\\n\\n```java\\npublic void sagaExample(WorkflowThread wf) {\\n var item = wf.addVariable(\\"item\\", STR);\\n var customer = wf.addVariable(\\"customer\\", STR);\\n var price = wf.addVariable(\\"price\\", DOUBLE);\\n var orderId = wf.addVariable(\\"order-id\\", STR);\\n\\n wf.execute(\\"create-order\\", orderId);\\n\\n // Saga Here! (We skipped this part in the previous section due to\\n // complexity, but LH makes it simple enough.\\n NodeOutput inventoryResult = wf.execute(\\"reserve-inventory\\", item, orderId);\\n wf.handleException(inventoryResult, \\"out-of-stock\\", handler -> {\\n handler.execute(\\"cancel-order\\", orderId);\\n handler.fail(\\"out-of-stock\\", \\"Item was out of stock. Order canceled\\");\\n })\\n\\n NodeOutput paymentResult = wf.execute(\\"charge-payment\\", customer, price);\\n // Saga here again!!\\n wf.handleException(paymentResult, \\"credit-card-declined\\", handler -> {\\n handler.execute(\\"release-inventory\\", item, orderId);\\n handler.execute(\\"cancel-order\\", orderId);\\n handler.fail(\\"credit-card-declined\\", \\"Credit card was declined. Order canceled!\\");\\n });\\n\\n wf.execute(\\"complete-order\\", orderId);\\n}\\n```\\n\\nInstead of managing five message queues and five strongly-coupled integration points between microservices, all we need to do is register the workflow, define _truly_ modular tasks, and let LittleHorse take care of the rest.\\n\\n## Wrapping Up\\n\\nThe Saga Pattern is one of five tools we will cover in this series on avoiding the Grumpy Customer Problem. It\'s simple to understand but _painfully complex_ to implement. Fortunately, LittleHorse makes it easier!\\n\\n:::note\\nA careful reader, or anyone who [reads my rants on LinkedIn](https://www.linkedin.com/feed/update/urn:li:activity:7244572885179121664/), might note that in order to make the order processing workflow truly reliable, we would also need to do something like the Outbox pattern or Event Sourcing.\\n\\nThat is true, and we\'ll cover it in the next post (and you\'ll see how LittleHorse does that for you automatically!).\\n:::\\n\\n### Get Involved!\\n\\nStay tuned for the next post on the Transactional Outbox Pattern! In the meantime:\\n\\n* Try out our [Quickstarts](https://littlehorse.dev/docs/developer-guide/install)\\n* Join us [on Slack](https://launchpass.com/littlehorsecommunity)\\n* Give us a star [on GitHub](https://github.com/littlehorse-enterprises/littlehorse)!"},{"id":"basics-of-workflow","metadata":{"permalink":"/blog/basics-of-workflow","source":"@site/blog/2024-09-04-basics-of-workflows.md","title":"The Basics of Workflow","description":"LittleHorse Enterprises is a workflow engine company. But what is a workflow engine?","date":"2024-09-04T00:00:00.000Z","tags":[{"inline":false,"label":"Technical Analysis","permalink":"/blog/tags/analysis/","description":"Analysis of the current and future state of Technical Architecture."},{"inline":false,"label":"LittleHorse Orchestrator","permalink":"/blog/tags/littlehorse/","description":"Information about the LittleHorse Orchestrator."}],"readingTime":5.675,"hasTruncateMarker":true,"authors":[{"name":"Mitchell Henderson","title":"Principal Technical Architect","description":"Mitch is a software industry veteran experienced with Apache Kafka, Apache Cassandra, and systems modernization across a diverse set of industries including financial services, healthcare, technology, retail, and manufacturing. He prides himself in ensuring that all users of LittleHorse are successful.","page":{"permalink":"/blog/authors/mitchellh"},"socials":{"github":"https://github.com/mitchell-h","linkedin":"https://www.linkedin.com/in/mitchellghenderson/"},"imageURL":"https://avatars.githubusercontent.com/u/6223426","key":"mitchellh"}],"frontMatter":{"slug":"basics-of-workflow","authors":["mitchellh"],"tags":["analysis","littlehorse"]},"unlisted":false,"prevItem":{"title":"Integration Patterns: Saga Transactions","permalink":"/blog/saga-pattern"},"nextItem":{"title":"Microservices and Workflow: A Match Made in Heaven","permalink":"/blog/microservices-and-workflow"}},"content":"LittleHorse Enterprises is a workflow engine company. But what is a workflow engine?\\n\\n\x3c!-- truncate --\x3e\\n\\nIt is a system that allows you to reliably execute a series of steps while being robust to technical failures (network outages, crashes) and business process failures. A step in a workflow can be calling a piece of code on a server, reaching out to an external API, waiting for a callback from a person or external system, or more.\\n\\nA core challenge when automating a business process is **Failure and Exception Handling:** figuring out what to do when something doesn\'t happen, happens with an unexpected outcome, or plain simply fails. This is often difficult to reason about, leaving your applications vulnerable to uncaught exceptions, incomplete business workflows, or data loss.\\n\\nA workflow engine standardizes how to throw an exception, where the exception is logged, and the logic around when/how to retry. This gives you peace of mind that once a workflow run is started, it will reliably complete.\\n\\n## Workflow Architecture\\n\\nAny [workflow-driven application](https://littlehorse.dev/docs/concepts) has three components:\\n\\n1. A really awesome workflow engine like LittleHorse.\\n2. A [Workflow Specification](https://littlehorse.dev/docs/concepts/workflows), which defines the series of steps in your application.\\n3. [Task Workers](https://littlehorse.dev/docs/concepts/tasks), which are computer programs that execute work when the LH Server tells it to.\\n\\n### Workflow Specifications\\n\\nA Workflow Specification (or `WfSpec` in LittleHorse) is the configuration, or metadata object, that tells the engine what Tasks to run,\\nwhat order to run the tasks, **how to handle exceptions or failures,** what variables are to be passed from task to task, and what inputs and outputs are required to run the workflow.\\n\\nIn LittleHorse the `WfSpec` is submitted to and held by the LittleHorse server. Users of LittleHorse can define a `WfSpec` in vanilla code (Java/Go/Python) using the LittleHorse SDK. The SDK will compile your vanilla code into a `WfSpec` that the LH Server understands and keeps inside its data store.\\n\\n:::info\\nTo learn how to write a `WfSpec` in LittleHorse, check out our [`WfSpec` Development docs](https://littlehorse.dev/docs/developer-guide/wfspec-development).\\n:::\\n\\nIn the background LittleHorse server takes the submitted spec from the SDK, and compiles a protobuf object that is submitted to the LittleHorse server.\\n\\nFor example, the following code in Java defines a two-step workflow in which we look up the price of an item, charge a customer\'s credit card, and then ship an item.\\n\\n```java\\npublic class ECommerceWorkflow {\\n\\n public void checkoutWorkflow(WorkflowThread wf) {\\n // Create some Workflow Variables\\n var customerId = wf.addVariable(\\"customer-id\\", VariableType.STR).searchable().required();\\n var itemId = wf.addVariable(\\"item-id\\", VariableType.STR).required();\\n var price = wf.addVariable(\\"price\\", VariableType.INT);\\n\\n // Fetch Price and save it into a variable\\n var priceOutput = wf.execute(\\"calculate-price\\", itemId);\\n wf.mutate(price, VariableMutationType.ASSIGN, priceOutput);\\n\\n // Charge credit card (passing in the output from previous task)\\n wf.execute(\\"charge-credit-card\\", customerId, price);\\n\\n // Ship item\\n wf.execute(\\"ship-item\\", customerId, itemId);\\n }\\n}\\n```\\n\\n:::note\\nJust by using LittleHorse to define the above workflow, you get reliability, observability, retries, and governance out of the box!\\n:::\\n\\n### Tasks and Task Workers\\n\\nTasks are the unit of work that can be executed a workflow engine. It\'s best to think in examples:\\n* Change lower case letters to upper case letters.\\n* Call an API with an input variable and pass along the output.\\n* Fetch data from a database.\\n* Convert a message from HL7 version 2.5 to HL7 version 3.\\n\\nTask workers are programs that use the LittleHorse SDK, connect to LittleHorse, and execute tasks when the workflow says it\'s time to do so.\\n\\n:::tip\\nTo learn how to write a Task Worker, check out our [Task Worker Development Guide](https://littlehorse.dev/docs/developer-guide/task-worker-development).\\n:::\\n\\nYou can also use [External Events](https://littlehorse.dev/docs/concepts/external-events) or [User Tasks](https://littlehorse.dev/docs/concepts/user-tasks) to wait for input from a human user or an external system (like a callback or webhook).\\n\\n\\n### Workflow Clients\\n\\nLastly you need to tell LittleHorse when to run a workflow. You can do it with our CLI (`lhctl`) but in production you\'ll need to use the LittleHorse SDK to kick off a workflow. You can do this with our page on [Running Workflows using grpc](https://littlehorse.dev/docs/developer-guide/grpc/running-workflows)\\n\\nYou\'ll also need to tell LittleHorse about External Events that happen. You can also do this using `lhctl` or [with our SDK\'s](https://littlehorse.dev/docs/developer-guide/grpc/posting-external-events).\\n\\n## LittleHorse Use-Cases\\n\\nThere are many different types of workflow engines, each of which supports different use-cases. For example:\\n\\n* **Batch ETL and Cronjob** workflows are automated by systems like Apache Airflow and Dagster.\\n* **Infrastructure Provisioning and Configuration** workflows can be automated by Ansible, Argo, and Jenkins.\\n* **IT Integration and BPM** workflows may be automated by systems like Camunda and jBPM.\\n\\nHowever, **LittleHorse allows you to orchestrate business processes across your software systems.** Some use-cases are included below.\\n\\n### Microservices\\n\\nAll microservice-based applications are inherently distributed systems with the goal of supporting some business process (because no one writes microservices for the sake of writing code, right?). While often necessary, microservices [present many challenges](https://littlehorse.dev/blog/challenge-of-microservices) to developers due to their distributed nature.\\n\\nOur founder Colt McNealy wrote a [detailed blog](https://littlehorse.dev/blog/microservices-and-workflow) about how a workflow engine\'s reliabile state management and oversight can mitigate some of the problems inherent in microservices. Check it out!\\n\\n### Human-in-the-Loop\\n\\nWorkflows often need to get input from humans:\\n* Approval flows.\\n* Waiting for information from customers.\\n* Handling exceptional scenarios.\\n\\nThat\'s hard to coordinate without a workflow engine. You\'d have to build your own state management system that correlates tasks to workflows. LittleHorse [User Tasks](https://littlehorse.dev/docs/concepts/user-tasks) make this much easier.\\n\\n### RAG and AI\\n\\nAI is only useful when you call it at the right time, with the right inputs, and do something with the outputs. That\'s a workflow. And all sorts of things can go wrong when using LLM\'s, which is why you need to have a robust workflow engine to provide oversight and exception handling.\\n\\n### Legacy System Modernization\\n\\nWhether you are integrating legacy systems that you inherited from the past, or integrating multiple tech stacks accrued through M&A, your customers expect a real-time experience that seamlessly spans all of your systems. Workflow engines are useful for reliably orchestrating actions and moving data across multiple different systems.\\n\\n### API gateway\\n\\nIf we look at the properties of an API gateway and how they are used, a workflow engine makes sense. \\n\x3c!--TODO: insert example API gateway architecture --\x3e\\nThe usage of an API gateway is to have a single layer that abstracts further endpoints. \\nIn practice this most often means calling the same API gateway multiple times, receiving the requested data, and doing some date manipulation or calculations at the application layer.\\nA workflow engine performs all of the most common actions, and includes things like centralized security, possible data obscurity, failure handling, observability and allows for operators to scale compute.\\nAll while still maintaining a central plane that can be shared across an entire orginization. \\nAdditionally a workflow engine still allows for the standard CRUD(Create, Read, Update, Delete) operations that an API gateway provides."},{"id":"microservices-and-workflow","metadata":{"permalink":"/blog/microservices-and-workflow","source":"@site/blog/2024-09-02-microservices-and-workflow.md","title":"Microservices and Workflow: A Match Made in Heaven","description":"While they are often necessary, microservices are a headache. Fortunately, the right workflow engine (such as LittleHorse) can drastically reduce the difficulty of managing microservices.","date":"2024-09-02T00:00:00.000Z","tags":[{"inline":false,"label":"Technical Analysis","permalink":"/blog/tags/analysis/","description":"Analysis of the current and future state of Technical Architecture."},{"inline":false,"label":"Microservices and Workflow","permalink":"/blog/tags/microservice-and-workflow/","description":"A 3-part blog series on the challenges inherent with the microservice architecture, and how Workflow Engines can mitigate those difficulties."}],"readingTime":7.575,"hasTruncateMarker":true,"authors":[{"name":"Colt McNealy","title":"Managing Member of the LLC","description":"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He\'s a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.","page":{"permalink":"/blog/authors/coltmcnealy"},"socials":{"github":"https://github.com/coltmcnealy-lh","linkedin":"https://www.linkedin.com/in/colt-mcnealy-900b7a148/","x":"https://x.com/coltmcnealy"},"imageURL":"https://avatars.githubusercontent.com/u/100447728","key":"coltmcnealy"}],"frontMatter":{"slug":"microservices-and-workflow","title":"Microservices and Workflow: A Match Made in Heaven","authors":["coltmcnealy"],"tags":["analysis","microservice-and-workflow"]},"unlisted":false,"prevItem":{"title":"The Basics of Workflow","permalink":"/blog/basics-of-workflow"},"nextItem":{"title":"Releasing 0.11","permalink":"/blog/littlehorse-0.11-release"}},"content":"While they are often necessary, microservices are a headache. Fortunately, the right workflow engine (such as LittleHorse) can drastically reduce the difficulty of managing microservices.\\n\\n\x3c!-- truncate --\x3e\\n\\n:::info\\nThis is the third and final part of a 3-part blog series:\\n\\n1. [The Promise of Microservices](./2024-08-22-promise-of-microservices.md)\\n2. [The Challenge with Microservices](./2024-08-27-challenges-of-microservices.md)\\n3. **[This Post]** Workflow and Microservices: A Match Made in Heaven\\n:::\\n\\nIf you\'re just joining for the third blog post, we have so far established that microservices are an effective tool for allowing your engineering team to grow beyond just a handful of people working on an enterprise application. However, microservice systems are by nature [**Leaderless**](./2024-08-27-challenges-of-microservices.md#microservices-are-leaderless) and [**Distributed**](./2024-08-27-challenges-of-microservices.md#microservices-are-distributed), which yields challenges in:\\n\\n* [**Observability**](./2024-08-27-challenges-of-microservices.md#observability),\\n* [**Reliability**](./2024-08-27-challenges-of-microservices.md#reliability-and-correctness), and\\n* [**Complexity Management**](./2024-08-27-challenges-of-microservices.md#microservice-coupling).\\n\\nThose challenges inspired me to create [LittleHorse](https://littlehorse.dev/docs/concepts) in the fall of 2021. LittleHorse provides primitives and guardrails out of the box which make it easier to wrangle with distributed systems and coordinate processes/transactions across multiple microservices.\\n\\nIn this post, we\'ll discuss:\\n\\n1. What _workflow_ means.\\n2. How LittleHorse\'s workflow orchestration capabilities make it easier for you to reliably orchestrate complex business processes.\\n\\n:::tip\\nWant to give LittleHorse a try? Get in touch with us!\\n\\n* Join the [**LH Slack Community**](https://launchpass.com/littlehorsecommunity) for the latest news and help from community experts.\\n* Check out our [**Getting Started**](https://littlehorse.dev/docs/developer-guide/install) page.\\n* [**Say hello**](https://docs.google.com/forms/d/e/1FAIpQLScXVvTYy4LQnYoFoRKRQ7ppuxe0KgncsDukvm96qKN0pU5TnQ/viewform) if you\'d like to get in touch with someone from the LittleHorse Enterprises team.\\n:::\\n\\n## What is a Workflow?\\n\\nA workflow is a blueprint that defines a series of tasks to be performed (perhaps conditioned on certain inputs or external events) in order to achieve a business outcome.\\n\\nIf you recall the e-commerce example from the [previous blog post](./2024-08-27-challenges-of-microservices.md#the-nature-of-microservices), you can think of the abstract checkout process as a workflow. This example is interesting because it demonstrates multiple characteristics of common business processes that make microservice development hard.\\n\\n![E-Commerce Checkout Process Diagram](./2024-08-27-complex-checkout.png)\\n\\nFirst, a workflow can be _mission critical_. A customer would be very unhappy if the vendor charged their credit card but failed to ship their order. In technical terms, this means that the state of a workflow needs to be consistent and durable, which is hard to achieve in a distributed system.\\n\\nNext, a workflow can have exceptional cases. Our e-commerce flow has special logic to handle cases when the customer\'s credit card was invalid or when the ordered item was out of stock.\\n\\nFinally, a workflow can be _asynchronous_, meaning that it requires waiting for input from the external world in order to complete. For example, our e-commerce workflow sometimes must wait for a customer to update their credit card information before completing.\\n\\nThe mission-critical nature of workflows, combined with asynchronous events and exceptional cases, places a premium on _consistency._ The results of workflows must be predictable for customers and easy to reason about for business managers and software engineers.\\n\\n:::note\\nA technical or business process does not need to satisfy all three characteristics to be a \\"workflow.\\" In fact, simple processes with just one or two linear steps can benefit from a workflow engine.\\n:::\\n\\n### Workflow Engines\\n\\nA workflow engine is a software system that makes sure the trains run on time in your processes. To use a workflow engine, you must:\\n\\n1. **Define your Tasks**, which are units of work that can be executed in a workflow, and write [Task Workers](https://littlehorse.dev/docs/concepts/tasks) which implement small functions or methods in code to execute those tasks.\\n2. **Register a Workflow Specification** (we call it a [`WfSpec` in LittleHorse](https://littlehorse.dev/docs/concepts/workflows)) which specifies what tasks to execute and when.\\n3. **Run your workflow** so that the workflow engine can orchestrate the process to completion.\\n\\n![LittleHorse Architecture](../static/img/2024-08-28-lh-application.png)\\n\\n[Task Workers](https://littlehorse.dev/docs/developer-guide/task-worker-development) are where a workflow can interface with the outside world. Since a Task in a workflow results in the LittleHorse SDK calling a programming function/method of your choosing, Task Workers allow LittleHorse to integrate with any system. Task Workers can make database queries, call external API\'s, provision infrastructure on AWS, send push notifications to customer mobile apps, perform calculations, call an LLM API, and more.\\n\\nIn LittleHorse, the `WfSpec` is [defined in code](https://littlehorse.dev/docs/developer-guide/wfspec-development) in a language of your choice. Because LittleHorse was written with developers in mind, our DSL\'s have all of the primitives that you\'d expect in a programming language: variables, control flow, exception handling, child threads, interrupts, and awaiting for external events. This allows workflows to be:\\n\\n* Easy to reason about.\\n* Tracked in version control.\\n* Familiar and easy to learn.\\n\\nOnce you tell LittleHorse to [run an instance of your `WfSpec`](https://littlehorse.dev/docs/developer-guide/grpc/running-workflows), LittleHorse will oversee the entire process until it completes. Failed tasks will be retried, every step will be journaled, and the state of your processes will be safely and durably persisted while waiting for external triggers.\\n\\n## Why Workflow?\\n\\nMicroservice applications that are designed as distributed workflows without a workflow engine (like a chain of dominoes falling) present operational challenges because there is no \\"leader\\" providing oversight over the microservice processes. Thankfully, a developer-focused and horizontally-scalable workflow engine like LittleHorse can fill the \\"leader\\" role, thus providing oversight and reliability, and taming the complexity of your business processes.\\n\\nAdditionally, using a workflow engine allows you to develop a set of _reusable_ and _modular_ tasks which can be easily dropped into any business workflow with a common API. Rather than accumulating tech debt, workflow engines allow you to accumulate a set of useful lego bricks.\\n\\nIn most existing organizations there\'s a long list of API calls required to simply _run_ a workflow. Training engineers to use all of the new APIs while securely distributing access and permissions causes confusion and slow development cycles. Workflow engines provide a single API and single system that allows anyone to securely manage, run, and operate complex workflows.\\n\\n### Mission Critical Oversight\\n\\nMission critical business workflows leave no room for technical failures and outages. However, as we discussed [last week](./2024-08-27-challenges-of-microservices.md#reliability-and-correctness), the distributed nature of microservices means that technical failures are not likely but rather certain. LittleHorse provides retries and durable execution capabilities out of the box, removing the need to create complex infrastructure for cross-service transactions (such as dead-letter queues, Outbox tables, and the SAGA pattern).\\n\\nAdditionally, mission-critical processes must be _audited_ and _observed_ in a secure manner with proper access controls. LittleHorse supports this\u2014every step in a workflow is journaled, auditable, and searchable in our dashboard. When humans execute [User Tasks](https://littlehorse.dev/docs/concepts/user-tasks), you can view an audit trail of when and to whom it was assigned and executed; you can see when each `TaskRun` started, completed, and failed (and with what inputs). Our [ACL\'s and Multi-Tenancy](https://littlehorse.dev/docs/concepts/principals-and-tenants) capabilities (and \\"Masked Data\\") ensure that the data remains accessible only to those who must see it.\\n\\n### Simple Asynchronous Processing\\n\\nFor microservice developers, handling asynchronous business processes is challenging because it forces you to persist state, correlate events, and wire together callbacks into a non-linear flow. Developers often need to create database tables for ongoing transactions and maintain complex flow diagrams showing how different services integrate with business events.\\n\\nHowever, LittleHorse provides two primitives to simplify this process:\\n\\n1. [**External Events**](https://littlehorse.dev/docs/concepts/external-events) allow workflows to block until something happens in the outside world, and then resume processing immediately thereafter.\\n2. [**User Tasks**](https://littlehorse.dev/docs/concepts/user-tasks) are like External Events but they model getting input from humans. User Tasks support reminders, assignment, groups, and users.\\n\\nTogether, User Tasks and External Events allow developers to transform complex asynchronous flows (such as our e-commerce example when we wait for a customer to provide a new credit card) into a more manageable linear flow.\\n\\n### Exception Handling\\n\\nFinally, just as processes can fail at the technical level, they can also fail at the business level. As per our ongoing e-commerce example, cards can run out of funds, items go out of stock, customers can cancel orders while they are being processed.\\n\\nHandling any given exceptional case in a business workflow might involve actions in several different microservices. Without a workflow engine, therefore, each exceptional case results in more and more complex interdependencies in your microservices, creating the notoriously feared \\"Distributed Monolith.\\"\\n\\nIn contrast, with LittleHorse as your workflow orchestrator, the dependencies between microservices are mitigated and workflow concepts such as [Failure Handling](https://littlehorse.dev/docs/concepts/workflows#failure-handling) allow you to easily define rollbacks, SAGA patterns, and edge cases without introducing further accidental complexity into your microservices. This allows startups and enterprises alike to implement robust, enterprise-grade business applications without accumulating costly technical debt.\\n\\n## Conclusion\\n\\nFor a variety of reasons, startups and enterprises alike may need to work with microservices despite the challenges they bring. Thankfully, workflow engines like LittleHorse can mitigate those problems by providing oversight into your entire process.\\n\\nAt the LittleHorse Council, we are very excited about the upcoming 1.0 release. Over the next few weeks, we will:\\n* Complete additional load tests, chaos tests, and benchmarks in preparation for 1.0.\\n* Blog about how you can write an e-commerce workflow in LittleHorse with Python.\\n* Do final testing before we release!\\n\\nAnd if you enjoyed this post, give us a star [on GitHub](https://github.com/littlehorse-enterprises/littlehorse) and try out [our quickstarts](https://littlehorse.dev/docs/developer-guide/install) to get going with LittleHorse in under 5 minutes."},{"id":"littlehorse-0.11-release","metadata":{"permalink":"/blog/littlehorse-0.11-release","source":"@site/blog/2024-08-31-0.11-release.md","title":"Releasing 0.11","description":"Releasing LittleHorse `0.11`","date":"2024-08-31T00:00:00.000Z","tags":[{"inline":false,"label":"LittleHorse Releases","permalink":"/blog/tags/release/","description":"Release blogs for LittleHorse Orchestrator."}],"readingTime":2.215,"hasTruncateMarker":true,"authors":[{"name":"The LittleHorse Council","title":"The Council of LittleHorse Maintainers","description":"LittleHorse Orchestrator is maintained by LittleHorse Enterprises LLC and available under the SSPL license. The LittleHorse Council is the group of engineers inside LittleHorse Enterprises LLC who are responsible for the stewardship of the open-source Orchestrator project and charged with looking out for the best interests of the LH Community.","url":"https://littlehorse.io","page":{"permalink":"/blog/authors/lh-council"},"socials":{"github":"https://github.com/littlehorse-enterprises","linkedin":"https://www.linkedin.com/company/littlehorse"},"imageURL":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","key":"lh_council"}],"frontMatter":{"title":"Releasing 0.11","description":"Releasing LittleHorse `0.11`","slug":"littlehorse-0.11-release","authors":["lh_council"],"tags":["release"],"image":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","hide_table_of_contents":false},"unlisted":false,"prevItem":{"title":"Microservices and Workflow: A Match Made in Heaven","permalink":"/blog/microservices-and-workflow"},"nextItem":{"title":"The Challenge of Microservices","permalink":"/blog/challenge-of-microservices"}},"content":"The `0.11` release brings with it the ability to schedule workflows on a cron job, support for secret data, and various dashboard and SDK improvements. \x3c!-- truncate --\x3e\\n\\n## New Features\\n\\nIn addition to several new features, it\'s worth calling out that we upgraded the internal `org.apache.kafka:kafka-streams` dependency to `3.8.0`, which includes several crucial bug fixes (some of which were found by our Grumpy Maintainer, [Eduwer Camacaro](https://github.com/eduwercamacaro)).\\n\\n### Dashboard\\n\\nThe Dashboard saw several enhancements, the most important of which is the `ExternalEventDef` page, which allows users to view `ExternalEvent`s associated with an `ExternalEventDef`.\\n\\n### Scheduled Workflows\\n\\nThe `ScheduledWfRun` feature creates a schedule that runs a `WfSpec` on a cron schedule. This is useful for periodic background tasks.\\n\\n### Secret Variables\\n\\nAs of LittleHorse `0.11`, you may now mark a variable as `masked()`, which means that its value is obscured on the Dashboard and also via `lhctl get variable`.\\n\\nTo make a variable Masked, you can do the following:\\n\\n```\\nWfRunVariable myVar = wf.addVariable(\\"my-var\\", STR).masked();\\n```\\n\\nWe will also release a blog about this feature soon.\\n\\n### Saving User Task Progress\\n\\nWith the `rpc SaveUserTaskRun`, it is now possible to save the results of a `UserTaskRun` without completing it. When you do this, an `event` is added to the audit log showing who saved the `UserTaskRun` and what results were saved.\\n\\n## Release Notes and Artifacts\\n\\nYou can find the release notes and downloads on our GitHub page.\\n\\n* [**`0.11.2`**](https://github.com/littlehorse-enterprises/littlehorse/releases/tag/v0.11.2)\\n* [**`0.11.1`**](https://github.com/littlehorse-enterprises/littlehorse/releases/tag/v0.11.2)\\n* [**`0.11.0`**](https://github.com/littlehorse-enterprises/littlehorse/releases/tag/v0.11.2)\\n\\n## Upgrading\\n\\nJust as since all releases since `0.8`, there were no breaking changes to our protocol buffer API. We do not anticipate any changes with our API in the future, either. This means that old client applications will continue to work with the LH Server `0.11` and beyond.\\n\\nHowever, we refactored the Go SDK to better follow GoLang conventions, which will require code changes (but no changes to the network protocol).\\n\\n### Upgrading the Go SDK\\n\\nNow, instead of having multiple modules to import and use, there are only two:\\n\\n1. The `lhproto` module, with our GRPC clients and protobuf.\\n2. The `littlehorse` module, with everything else.\\n\\nTo add the go SDK to your project, you can run:\\n\\n```\\ngo get github.com/littlehorse-enterprises/littlehorse@v0.11.2\\n```\\n\\nThen, the imports are:\\n\\n```go\\nimport (\\n\\t\\"github.com/littlehorse-enterprises/littlehorse/sdk-go/lhproto\\"\\n\\t\\"github.com/littlehorse-enterprises/littlehorse/sdk-go/littlehorse\\"\\n)\\n```\\n\\n## What\'s Next?\\n\\nBefore committing to [Semantic Versioning](https://semver.org), we will:\\n\\n* Release our release schedule and support plan.\\n* Finish inspecting our SDK\'s for bugs and minor breaking API changes that we want to do before `1.0`.\\n* Finish our benchmarks, chaos tests, and load tests to ensure that our software meets the highest quality standards.\\n\\nWe expect to release `1.0` in early October 2024."},{"id":"challenge-of-microservices","metadata":{"permalink":"/blog/challenge-of-microservices","source":"@site/blog/2024-08-27-challenges-of-microservices.md","title":"The Challenge of Microservices","description":"Microservices are often necessary, but unfortunately they bring with them some baggage.","date":"2024-08-27T00:00:00.000Z","tags":[{"inline":false,"label":"Technical Analysis","permalink":"/blog/tags/analysis/","description":"Analysis of the current and future state of Technical Architecture."},{"inline":false,"label":"Microservices and Workflow","permalink":"/blog/tags/microservice-and-workflow/","description":"A 3-part blog series on the challenges inherent with the microservice architecture, and how Workflow Engines can mitigate those difficulties."}],"readingTime":8.93,"hasTruncateMarker":true,"authors":[{"name":"Colt McNealy","title":"Managing Member of the LLC","description":"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He\'s a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.","page":{"permalink":"/blog/authors/coltmcnealy"},"socials":{"github":"https://github.com/coltmcnealy-lh","linkedin":"https://www.linkedin.com/in/colt-mcnealy-900b7a148/","x":"https://x.com/coltmcnealy"},"imageURL":"https://avatars.githubusercontent.com/u/100447728","key":"coltmcnealy"}],"frontMatter":{"slug":"challenge-of-microservices","title":"The Challenge of Microservices","authors":["coltmcnealy"],"tags":["analysis","microservice-and-workflow"]},"unlisted":false,"prevItem":{"title":"Releasing 0.11","permalink":"/blog/littlehorse-0.11-release"},"nextItem":{"title":"The Promise of Microservices","permalink":"/blog/promise-of-microservices"}},"content":"Microservices are often necessary, but unfortunately they bring with them some baggage. \x3c!-- truncate --\x3e\\n\\n:::info\\nThis is the second part of a 3-part blog series:\\n\\n1. [The Promise of Microservices](./2024-08-22-promise-of-microservices.md)\\n2. **[This Post]** The Challenge with Microservices\\n3. [Workflow and Microservices: A Match Made in Heaven](./2024-09-02-microservices-and-workflow.md)\\n:::\\n\\nLast week, I [blogged](./2024-08-22-promise-of-microservices.md) about the problems that microservices solve, and why they are not only beneficial but necessary in some cases (a good bellwether is the size of your engineering team: beyond 1 or 2 dozen engineers, you will probably start to feel some problems that can be solved with microservices).\\n\\nWhen done correctly, microservices remove several bottlenecks to scaling your business. However, even well-architected microservices bring significant _accidental complexity_.\\n\\nIn particular, microservices are:\\n\\n1. Harder to **observe** and debug.\\n2. Harder to make **reliable** in the case of infrastructure or sofware failures.\\n3. More complex to **maintain** and evolve with changing business practices.\\n\\nIn this article we will explore how the above problems arise from two key facts:\\n* Microservices are **distributed**.\\n* Microservices are **choreographed without a leader**.\\n\\n:::note\\nMicroservices bring with them additional challenges around operationalization and deployment. However, those challenges are out-of-scope for this blog post as we instead choose to focus on the challenges faced by _application development teams_ rather than operations teams.\\n:::\\n\\n## The Nature of Microservices\\n\\nAs I described in [last week\'s blog](./2024-08-22-promise-of-microservices.md):\\n\\n> The term \\"microservices\\" refers to a software architecture wherein an enterprise application comprises a collection of small, loosely coupled, and independently deployable services (these small services are called \\"microservices\\" in contrast to larger monoliths). Each microservice focuses on a specific business capability and communicates with other services over a network, typically through API\'s, streaming platforms, or message queues.\\n\\nCrucially, a single microservice implements technical logic for a specific domain, or bounded context, within the larger company. In contrast, a comprehensive business process requires interacting with technology and people across _many_ business domains. The classic example of microservices architecture, e-commerce checkout, involves at least _shipping_, _billing_, _notifications_, _inventory_, and _orders_.\\n\\nIn the rest of this blog post we will examine microservices through the the lense of e-commerce checkout flow. To start with a simple use-case, the logical flow we will consider is:\\n\\n1. When an order is placed, we create a record in a database in the `orders` service.\\n2. We then reserve inventory (and ensure that the item is in stock) in the `inventory` service.\\n3. We charge the customer using the `payments` service.\\n4. Next, we ship the item using the `shipping` service.\\n5. Finally, the `notifications` service notifies the customer that the parcel is on its way.\\n\\n![Simple e-commerce workflow](./2024-08-27-simple-checkout.png)\\n\\n### Microservices are Distributed\\n\\nRecall that each service (in the workflow diagram above, each box) is its own deployable artifact. That means that the happy-path business process described above will involve five different software systems from start-to-finish.\\n\\nIn the above workflow diagram, each arrow can be accurately interpreted in two ways:\\n1. The logical flow of the business process.\\n2. The physical flow of information between microservices, either through network RPC calls or through a message broker like Apache Kafka.\\n\\nGuess what! This means we have a distributed system by definition. As Splunk [writes in a blog post](https://www.splunk.com/en_us/blog/learn/distributed-systems.html):\\n> A distributed system is simply any environment where multiple computers or devices are working on a variety of tasks and components, all spread across a network.\\n\\nYou need to look no further than the [Fallacies of Distributed Computing](https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing) (written by Sun Microsystems Fellow L. Peter Deutsch in 1994) to see that this means that microservices are no easy task.\\n\\n### Microservices are Leaderless\\n\\nAs we\'ve seen already, any microservice-based application is a distributed system. Some distributed systems have the concept of a _leader_, which is a special node in the system that has special responsibilities.\\n\\n:::info\\nApache Kafka is my favorite distributed system. In Apache Kafka, the _Controller_ is a special Kafka server that is responsible for deciding which partition replicas are hosted on (and led by) which brokers. If the broker who was in charge of a partition goes down, then the Controller chooses a new broker from the ISR to take its place.\\n\\nTherefore, the _Controller_ in Apache Kafka can be thought of as a _leader_.\\n:::\\n\\nWhile systems like Apache Kafka have clear leaders (for example, the _Controller_ may re-assign partition leadership if the cluster becomes too imbalanced), in a microservice-based system there is no central leader to ensure that the chips fall correctly. This is by necessity, because the separation of development concerns and lifecycles across microservices means that microservices cannot and do not have leaders.\\n\\nYou can think of our e-commerce microservice flow as a line of dominoes falling. Once the process starts, no one entity is responsible for ensuring its completion. The business workflow moves from `orders` to `inventory` to `payments` and so on. If `payments` fails for some reason (perhaps a network outage makes the Stripe API unavailable), then it\'s quite possible that the `shipping` service never finds out about the workflow.\\n\\nHowever, in real life such outcomes are not acceptable. This means that every single player in the system must:\\n\\n1. Have built-in reliability mechanisms.\\n2. Understand the preceding and subsequent steps of the business process to route traffic.\\n\\nImplementing the above slows down development, more tightly couples one services to another, increases dependencies, and makes your microservice architecture much more heavyweight.\\n\\n## The Challenges\\n\\nSo far, we have established that there are many players involved in a business process, yet there\'s no one orchestrator involved in ensuring that an ordered item is delievered to the the correct address. This yields three problems:\\n\\n1. **Reliability** in the face of infrastructure failures.\\n2. **Observability** to enable system optimization and debugging.\\n3. **Coupling** of microservices to each other makes it hard to modify the system in response to new business requirements.\\n\\n### Reliability and Correctness\\n\\nProcessing orders is a mission-critical use-case. This means that orders should always complete and never be dropped (for example, we should not charge the customer\'s credit card and not ship the product to them).\\n\\nHowever, asynchronous processing such as that which I outlined above is prone to failures. For example, if you chain microservices together with direct RPC calls, a single network partition can cause an order to get stuck. Even with a reliable message broker such as Apache Kafka or AWS SQS sitting between your microservices, a write to the message broker could fail _after_ the payment went through, still resulting in a stuck order.\\n\\nJust as communication _between_ microservices can fail, the actions performed _by_each microservice can also fail. In many cases actions performed by a microservice depend upon failure-prone external systems and API\'s. If the Stripe API is down, or if the credit card is invalid, we can\'t just stop processing the order there! We must notify the customer of what went wrong and also release the inventory that we reserved.\\n\\nThis means that microservice developers spend countless hours building out infrastructure to support:\\n* Retries\\n* Dead-Letter Queues\\n* Rate-limiting\\n* Timeouts\\n* Transactional Outbox pattern\\n* SAGA Pattern\\n\\nBack to the domino analogy, if one domino misses the next, the entire chain just stops.\\n\\n### Observability\\n\\nThe second problem with microservices is that once a process instance has started (i.e. the dominoes are falling), it is very difficult to observe what happens between steps 2 through 10. This means that multi-step processes with performance issues are hard to optimize, as there are many microservices which could be the bottleneck and it\'s hard to know which. Even worse, when a customer complains about a \\"stuck order,\\" it is difficult to find the point of failure.\\n\\nAs a result, microservice engineers spend time and money:\\n* Slogging through logs on DataDog\\n* Implementing complex distributed tracing such as Zipkin, Jaeger, or Kiali\\n* Saving the state of each process instance (in our case, the `order`) in a DB just for visibility purposes at every step\\n* Coordinating with other teams to manually understand and debug workflows.\\n\\n### Microservice Coupling\\n\\nLastly, because microservices are leaderless, each player in the end-to-end process must have hard-coded integrations with the preceding and subsequent steps. This results in:\\n\\n* **Process coupling**, wherein changing a business process results in significant code updates to rewire the message queues or RPC calls between two steps.\\n* **Schema coupling**, wherein different microservices have strong dependencies on each others\' schemas.\\n\\nMicroservices come with the promise of loose coupling; however, the unfortunate reality is that this is often not the case. As a result, teams often do have to coordinate with each other during deployments.\\n\\nTo see an example of the complexity introduced by coupling of microservices, let\'s consider what happens to our e-commerce checkout workflow when we add a few edge cases to make it more realistic:\\n\\n1. If the credit card is invalid, we request the customer to provide a new one, wait for two days, and either complete or cancel the order.\\n2. If the item is out of stock, we notify the customer who elects either to wait or cancel the order.\\n\\n![Complex Checkout Architecture](./2024-08-27-complex-checkout.png)\\n\\nIn the above diagram, each arrow represents the flow of the business process _and_ information. Each microservice must have custom logic which sends information to the right place. In essence, while we _intended_ to have modular microservices that understand only their own Bounded Context, what we have is tightly-coupled systems which must understand pretty much the entire business workflow.\\n\\nTherefore, when business requirements change, unrelated microservices end up having to change their internal implementation as well.\\n\\n## Looking Forward\\n\\nMicroservices have clear and proven benefits, and are often not just advantageous but _necessary_ in some cases. However, as we discussed today, those benefits do not come without a cost. Because microservices are inherently distributed systems, challenges such as reliability, observability, and coordination are exacerbated.\\n\\nWithout spoiling the punchline of the next blog post, these challenges are why I started LittleHorse almost three years ago. Stay tuned for a description of how a _workflow orchestrator_ can alleviate a good portion of the headaches that come along with microservices.\\n\\n### Business Analytics\\n\\nAstute readers may notice that when discussing the e-commerce checkout example, we didn\'t discuss the problem of _analytics._ We focused exclusively on online transaction processing, or ensuring that the orders are properly fulfilled and processed. However, no attention was paid to business analytics to optimize future sales!\\n\\nThis area is yet another challenge. The LittleHorse Council is working on a major feature (an output Kafka Topic with records for anytime something _interesting_ happens inside a `WfRun`) for the LH Server that will address this. Don\'t worry, we\'ll blog about it soon :wink:."},{"id":"promise-of-microservices","metadata":{"permalink":"/blog/promise-of-microservices","source":"@site/blog/2024-08-22-promise-of-microservices.md","title":"The Promise of Microservices","description":"If microservices add so much complexity, why bother with the hassle?","date":"2024-08-22T00:00:00.000Z","tags":[{"inline":false,"label":"Technical Analysis","permalink":"/blog/tags/analysis/","description":"Analysis of the current and future state of Technical Architecture."},{"inline":false,"label":"Microservices and Workflow","permalink":"/blog/tags/microservice-and-workflow/","description":"A 3-part blog series on the challenges inherent with the microservice architecture, and how Workflow Engines can mitigate those difficulties."}],"readingTime":7.09,"hasTruncateMarker":true,"authors":[{"name":"Colt McNealy","title":"Managing Member of the LLC","description":"Colt is the founder of LittleHorse Enterprises and the original author of the LittleHorse Orchestrator. He\'s a passionate Apache Kafka fan and loves hockey, golf, piano, cooking, and Taekwondo.","page":{"permalink":"/blog/authors/coltmcnealy"},"socials":{"github":"https://github.com/coltmcnealy-lh","linkedin":"https://www.linkedin.com/in/colt-mcnealy-900b7a148/","x":"https://x.com/coltmcnealy"},"imageURL":"https://avatars.githubusercontent.com/u/100447728","key":"coltmcnealy"}],"frontMatter":{"slug":"promise-of-microservices","title":"The Promise of Microservices","authors":["coltmcnealy"],"tags":["analysis","microservice-and-workflow"]},"unlisted":false,"prevItem":{"title":"The Challenge of Microservices","permalink":"/blog/challenge-of-microservices"},"nextItem":{"title":"Releasing 0.10","permalink":"/blog/littlehorse-0.10-release"}},"content":"If microservices add so much complexity, why bother with the hassle? \x3c!-- truncate --\x3e\\n\\n:::info\\nThis is the first part of a 3-part blog series:\\n\\n1. **[This Post]** The Promise of Microservices\\n2. [The Challenge with Microservices](./2024-08-27-challenges-of-microservices.md)\\n3. [Workflow and Microservices: A Match Made in Heaven](./2024-09-02-microservices-and-workflow.md)\\n:::\\n\\n\\nWe\'ve all _heard of_ microservices, but unless you\'ve read copious amounts of Sam Newman and Adam Bellemare\'s writings, you might be wondering whether, when, and why you should adopt them. In this blog post, we will examine the halcyon land promised by microservices.\\n\\nMicroservices have been [deployed widely](https://www.simform.com/blog/microservices-examples/) across many large enterprises, most notably Netflix, Uber, Shopify, PayPal, and others. As we will discover throughout this blog series, a microservice architecture is mandatory once you reach a certain size of company, and it\'s probably overkill for a 12-person startup. The gray area inbetween is the interesting part!\\n\\n## What are Microservices?\\n\\nThe term \\"microservices\\" refers to a software architecture wherein an enterprise application comprises a collection of small, loosely coupled, and independently deployable services (these small services are called \\"microservices\\" in contrast to larger monoliths). Each microservice focuses on a specific business capability and communicates with other services over a network, typically through API\'s, streaming platforms, or message queues.\\n\\nIn practice, this means that a user interaction with an application (such as placing an order) might trigger actions that occur in _many_ small, independently-deployed software systems, such as:\\n\\n* A Notification service\\n* An Inventory Management service\\n* A Payments service\\n* An Order History service\\n\\nFrom the user (client) perspective, one request is made (generally through a Load Balancer, API Gateway, or Ingress Controller) but that request may ping-pong between multiple back-end services and may also result in future actions being scheduled asynchronously:\\n\\n![Microservices Architecture](./2024-08-22-microservices-arch.png)\\n\\nIn contrast to microservices, a _monolithic_ architecture would serve the entire \\"place order\\" request on a single deployable artifact:\\n\\n![Monolithic Architecture](./2024-08-22-monolith-arch.png)\\n\\nIn Domain Driven Design, accidental complexity refers to the unintentional complexity that you introduced to your architecture (deployments, service interactions, third-party dependencies, etc.). Rule #1 of maintaining software systems is to avoid introducing accidental complexity as much as possible.\\n\\nSimply by looking at the visuals above, microservices add a significant dose of accidental complexity to your architecture (more on this in next week\'s post!). Given that, what benefits would make up for the extra complexity introduced by microservices?\\n\\n## Why Now?\\n\\nI would be first to admit that microservices bring with them a series of headaches around cost, observability, maintenance, and ease of evolution (otherwise, I would not have founded LittleHorse Enterprises!). However, microservice architecture plays a vital role in addressing two critical trends reshaping the software development landscape today:\\n\\n* Increased digitization of companies in all business sectors (accelerated by the rise of AI).\\n* Elasticity of cloud computing.\\n\\n### Increased Digitization\\n\\nThe level of digitization expected of businesses in order to compete in the modern market has drastically increased: IT teams must build software that interfaces with an ever-expanding list of external API\'s, legacy systems, user interfaces, internal tools, and SaaS providers.\\n\\nFor example: in the early 2000\'s, it was perfectly acceptable (even _expected_) for a passenger to book airline tickets over the telephone or through a travel agency. However, such an experience would be unheard of today and would immediately hobble an airline who provided such poor digital services.\\n\\nIn addition to using automation to provide better customer services, companies are generating, processing, and analyzing massive amounts of data. For example, grocery stores with razor-thin margins analyze seasonal consumption patterns in order to optimize inventory and prevent costly food waste.\\n\\nThese trends have coincided with (or _caused_, I would argue) a proliferation in the number of 1) software developers, and 2) software tools and API\'s found within companies in all industries, leading to two new problems:\\n\\n1. Allowing large teams of software developers to productively work on an enterprise application in parallel (without stepping on each others\' toes).\\n2. Ensuring that business requirements are effectively communicated to the entire (larger) software engineering team.\\n\\n### Cloud Elasticity\\n\\nAs the importance and quantity of digital software systems exploded over the last two decades, so has the availability of nearly-infinite compute power delivered through cloud infrastructure providers such as AWS.\\n\\nThe promise of _elasticity_, or the ability to quickly spin compute resources up or down according to load and only pay for what you use, is unique to the cloud: for on-prem datacenters, spinning up new compute means buying new machines from Sun Microsystems (hopefully not Microsoft!), and scaling down compute means trying to sell them off on the secondary market. (Ask my father about how that went for a lot of people in 2001.)\\n\\nBeyond scaling up and down, elasticity enables different deployment patterns that did not exist before. Whereas pre-cloud enterprises had dedicated and centralized data-center teams who were in charge of running applications, the accessibility of cloud computing gave rise to the DevOps movement. This has empowered smaller teams of software developers to take on the task of transferring software from \\"it works on my laptop!\\" to \\"it\'s now deployed in production!\\"\\n\\n## Why Microservices?\\n\\nDespite the extra complexity it brings, the microservice architecture can more than pay for itself by ensuring organizational alignment and allowing enterprise architectures to take full advantage of the cloud\'s elasticity.\\n\\n### Organizational Alignment\\n\\nAs discussed earlier, the business problems that software engineering organizations must solve today dwarf those that were solved in the 1990\'s, and so do the software engineering teams that tackle those problems.\\n\\n:::note\\nI am not belittling the engineers of the 90\'s; the problems they solved were arguably _much harder_ than the problems we face today, and there were fewer engineers to face those problems. However, it is a fact that users expect more digital-native experiences today than they did twenty years ago.\\n:::\\n\\nBy breaking applications into smaller services, we can accomplish several important things:\\n* Break up our software engineering team into smaller teams which are each responsible for individual microservices.\\n* Allow different components of a system to be developed with separate tech stacks and released independently.\\n\\nEngineering teams of over a few dozen engineers working on the same deployable piece of software is a recipe for inefficiency. Merge conflicts, arguments over tech stack, slow \\"release trains,\\" and excessive intra-team coordination are just a few problems that arise. However, by breaking your application into smaller microservices, you can also break up your engineering organization into smaller, more efficient teams each in charge of a small number (prefably one!) of microservices.\\n\\nAs an added benefit, properly-designed microservice architectures can follow the principles of Domain Driven Design. Ideally, a single microservice corresponds to a _Bounded Context_ inside the business. This enables a small piece of the technical platform (a microservice) to be managed by a small team of software engineers, who collaborate closely with subject-matter experts and business stakeholders within a very specific domain of the business. Such close collaboration can foster better alignment between business goals and the software produced by engineering teams.\\n\\n### Moving Faster\\n\\nMicroservices can allow developers to move faster by enabling continuous delivery and independent deployment of services. In a monolithic architecture, releasing a new feature or fixing a bug typically requires redeploying the entire application. Since microservices allow smaller pieces of your application to be deployed independently, engineering teams can iterate faster and deliver incremental value to business stakeholders.\\n\\nThese positive effects are amplified by the advent of cloud computing. Since deploying a new application no longer requires buying a physical machine and plugging it into your datacenter but rather just applying a new `Deployment` and `Service` on a Kubernetes cluster, it is now truly feasible for small teams of software engineers to own their application stack from laptop-to-production (obviously, within the guardrails set by the central platform team). Furthermore, cloud computing is a pay-as-you-go (and often even pay-for-what-you-use) expense rather than an up-front cost. Therefore, the dollar cost of infrastructure required to support microservices is much lower today than it would have been before the advent of cloud computing and kubernetes.\\n\\n## Conclusion\\n\\nThe microservice architecture is not just a Twitter-driven buzzword but rather a way of designing system that has several real advantages. For most organizations with over two dozen software engineers, building applications with microservices is not an option but rather a _necessity_. However, those advantages come with a cost.\\n\\nWe will discuss those challenges in next week\'s blog post...in the meantime, though, join our [Community Slack](https://launchpass.com/littlehorsecommunity) to get the latest updates!"},{"id":"littlehorse-0.10-release","metadata":{"permalink":"/blog/littlehorse-0.10-release","source":"@site/blog/2024-07-12-0.10-release.md","title":"Releasing 0.10","description":"Releasing LittleHorse `0.10`","date":"2024-07-12T00:00:00.000Z","tags":[{"inline":false,"label":"LittleHorse Releases","permalink":"/blog/tags/release/","description":"Release blogs for LittleHorse Orchestrator."}],"readingTime":2.005,"hasTruncateMarker":true,"authors":[{"name":"The LittleHorse Council","title":"The Council of LittleHorse Maintainers","description":"LittleHorse Orchestrator is maintained by LittleHorse Enterprises LLC and available under the SSPL license. The LittleHorse Council is the group of engineers inside LittleHorse Enterprises LLC who are responsible for the stewardship of the open-source Orchestrator project and charged with looking out for the best interests of the LH Community.","url":"https://littlehorse.io","page":{"permalink":"/blog/authors/lh-council"},"socials":{"github":"https://github.com/littlehorse-enterprises","linkedin":"https://www.linkedin.com/company/littlehorse"},"imageURL":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","key":"lh_council"}],"frontMatter":{"title":"Releasing 0.10","description":"Releasing LittleHorse `0.10`","slug":"littlehorse-0.10-release","authors":["lh_council"],"tags":["release"],"image":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","hide_table_of_contents":false},"unlisted":false,"prevItem":{"title":"The Promise of Microservices","permalink":"/blog/promise-of-microservices"},"nextItem":{"title":"Releasing 0.9","permalink":"/blog/littlehorse-0.9-release"}},"content":"The `0.10` release brings with it significant performance and reliability improvements. \x3c!-- truncate --\x3e\\n\\n## New Features\\n\\n### `lhctl` Binaries and Release Notes\\n\\nThe `0.10.0` release comes with a new [Release Page](https://github.com/littlehorse-enterprises/littlehorse/releases), including `lhctl` binaries built for ARM, Intel, and Windows.\\n\\n### Reliability during Rebalances\\n\\nPR [#872](https://github.com/littlehorse-enterprises/littlehorse/pull/872) improves the reliability of LittleHorse during Kafka Streams rebalances. Previously, if a write request (eg. `rpc RunWf`) was received just before a rebalance, certain requests would \\"time out\\" from the client perspective and return a `DEADLINE_EXCEEDED` grpc error despite being properly accepted and processed by the server. This PR fixes that issue by redirecting the internal `rpc WaitForCommand` to the new destination for that command.\\n\\n### Rescue Failed Workflows\\n\\nPR [#883](https://github.com/littlehorse-enterprises/littlehorse/pull/883) allows users to restart failed `WfRun`\'s via the `lhctl rescue` command. This is similar to allowing a user to execute mutating SQL queries via a CLI like `psql`.\\n\\nWith this feature, a user can fix a buggy Task Worker implementation and then restart a failed `WfRun` and get it to execute the failed `TaskRun` again via:\\n\\n```\\nlhctl rescue \\n```\\n\\n### mTLS Principals\\n\\nPreviously, only listeners of the type `OAUTH` supported `Principal`s. The `Principal` ID was determined by the OAuth Client ID or User Id. Release `0.10` introduces the ability to infer a `Principal` on an `MTLS` listener, where the `Principal` ID comes from the Common Name on the client certificate.\\n\\nPR [#874](https://github.com/littlehorse-enterprises/littlehorse/pull/874) by one of our newer team members, [Jacob Snarr](https://github.com/snarr), introduced this feature, enabling users that standardize on SSL authentication to continue using that pattern with Littlehorse.\\n\\n### Dashboard Enhancements\\n\\nThe `0.10` release includes multiple enhancements to the Admin Dashboard, including:\\n\\n* Ability to search for `WfRun`\'s by their variables.\\n* Improved `WfRun` search.\\n* Fixed display of `TaskRun`s with the `EXCEPTION` and `ERROR` status.\\n* Showing `VariableMutation`s on the `Edge` in the dashboard.\\n\\n## What\'s Next?\\n\\nWe will need one more minor release before finally releasing `1.0`. We need the following:\\n\\n* Upgrade `org.apache.kafka:kafka-streams` to `3.8.0` to address several critical reliability bugs (we are waiting for the official release).\\n* Conduct new load tests and soak tests against the new version of Kafka Streams.\\n* Review our Go and Python SDK\'s in-depth to ensure proper semantics.\\n\\nAfter that, we will be ready to commit to the backwards compatibility guarantees required by [Semantic Versioning](https://semver.org). We will also release a blog post with our planned release schedule and support schedule."},{"id":"littlehorse-0.9-release","metadata":{"permalink":"/blog/littlehorse-0.9-release","source":"@site/blog/2024-06-24-0.9.2-release.md","title":"Releasing 0.9","description":"Revamping the LittleHorse Dashboard","date":"2024-06-24T00:00:00.000Z","tags":[{"inline":false,"label":"LittleHorse Releases","permalink":"/blog/tags/release/","description":"Release blogs for LittleHorse Orchestrator."}],"readingTime":2.35,"hasTruncateMarker":true,"authors":[{"name":"The LittleHorse Council","title":"The Council of LittleHorse Maintainers","description":"LittleHorse Orchestrator is maintained by LittleHorse Enterprises LLC and available under the SSPL license. The LittleHorse Council is the group of engineers inside LittleHorse Enterprises LLC who are responsible for the stewardship of the open-source Orchestrator project and charged with looking out for the best interests of the LH Community.","url":"https://littlehorse.io","page":{"permalink":"/blog/authors/lh-council"},"socials":{"github":"https://github.com/littlehorse-enterprises","linkedin":"https://www.linkedin.com/company/littlehorse"},"imageURL":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","key":"lh_council"}],"frontMatter":{"title":"Releasing 0.9","description":"Revamping the LittleHorse Dashboard","slug":"littlehorse-0.9-release","authors":["lh_council"],"tags":["release"],"image":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","hide_table_of_contents":false},"unlisted":false,"prevItem":{"title":"Releasing 0.10","permalink":"/blog/littlehorse-0.10-release"},"nextItem":{"title":"Releasing 0.8","permalink":"/blog/littlehorse-0.8-release"}},"content":"The `0.9.2` release is now availble and ready for use. \x3c!-- truncate --\x3e The `0.9.x` releases focused mainly on:\\n\\n* Improving the user experience on the LittleHorse Dashboard\\n* Improving the reliability of the LH Server in the face of rebalances and failures.\\n\\n## New Features\\n\\nWhile the majority of the improvements in the `0.9` release revolve around performance and stability, several of them are highly visible to the user (especially the new dashboard!).\\n\\n### Dashboard Rewrite\\n\\nWith help from [Nelson Jumbo](https://github.com/diablouma), LittleHorse Knight [Mija\xedl Rond\xf3n](https://github.com/mijailrondon) rewrote and revamped our administrative dashboard. It now inclues new features such as:\\n\\n* User Task Detail page\\n* Improved details on `TaskRun` progress\\n* Improved details on `WfRun` progress\\n* A plethora of small bug fixes.\\n\\n### Internal Task Queue Optimizations\\n\\nDeep in the internals of the LittleHorse Server, we implement a Task Queue mechanism to store `ScheduledTask`s before they\'re dispatched to the Task Worker clients. This release included many improvements to stability of the Task Queues.\\n\\nMost importantly, our Grumpy Maintainer (Eduwer Camacaro) put a cap on the memory consumption of a single `TaskDef`. Prior to this release, it was possible for poorly-behaved clients to cause an OOM on the server by running millions of workflows which use a `TaskDef` but not executing the resulting `TaskRun`s. This would cause an un-bounded buildup of `ScheduledTask`s in memory until the server crashed.\\n\\nAfter the `0.9` release, any more than 1,000 `ScheduledTask`s for a certain `TaskDef` are not loaded into memory but left on disk.\\n\\n### Principal Deletion\\n\\nThe `0.9` release includes the ability to delete a `Principal`. The `rpc DeletePrincipal` is smart enough to ensure that there is always at least one Admin `Principal` to prevent a user from locking themselves out of the cluster.\\n\\n### `PollThread` in Java Task Worker\\n\\nWe refactored the internal implementation of the Java Task Worker so that, for each LH Server in the cluster, the Task Worker creates a single `PollThread` object which is responsible for polling and executing `TaskRun`s. The `PollThread`s now poll in parallel, drastically increasing the throughput of a single Java Task Worker.\\n\\nThe `PollThread` was introduced in [#796](https://github.com/littlehorse-enterprises/littlehorse/pull/796).\\n\\n## What\'s Next\\n\\nOur wire protocol (the GRPC API) is quite stable; there have been no major breaking changes since we introduced the alpha version of Multi-Tenancy in `0.7`. We are diligently proceeding through soak tests, load tests, and chaos tests with our server and we have found and addressed several issues.\\n\\nWe continue to look foward to the `1.0` release, and we will reach that milestone once:\\n\\n* We are satisfied with results of load tests and soak tests.\\n* We have had language experts review each of our three main SDK\'s (Java, Go, Python) and we have addressed any change requests.\\n* We approach a year without any breaking changes to our wire protocol."},{"id":"littlehorse-0.8-release","metadata":{"permalink":"/blog/littlehorse-0.8-release","source":"@site/blog/2024-03-26-0.8.1-release.md","title":"Releasing 0.8","description":"Hardening Security","date":"2024-03-26T00:00:00.000Z","tags":[{"inline":false,"label":"LittleHorse Releases","permalink":"/blog/tags/release/","description":"Release blogs for LittleHorse Orchestrator."}],"readingTime":3.955,"hasTruncateMarker":true,"authors":[{"name":"The LittleHorse Council","title":"The Council of LittleHorse Maintainers","description":"LittleHorse Orchestrator is maintained by LittleHorse Enterprises LLC and available under the SSPL license. The LittleHorse Council is the group of engineers inside LittleHorse Enterprises LLC who are responsible for the stewardship of the open-source Orchestrator project and charged with looking out for the best interests of the LH Community.","url":"https://littlehorse.io","page":{"permalink":"/blog/authors/lh-council"},"socials":{"github":"https://github.com/littlehorse-enterprises","linkedin":"https://www.linkedin.com/company/littlehorse"},"imageURL":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","key":"lh_council"}],"frontMatter":{"title":"Releasing 0.8","description":"Hardening Security","slug":"littlehorse-0.8-release","authors":["lh_council"],"tags":["release"],"image":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","hide_table_of_contents":false},"unlisted":false,"prevItem":{"title":"Releasing 0.9","permalink":"/blog/littlehorse-0.9-release"},"nextItem":{"title":"Releasing 0.7","permalink":"/blog/littlehorse-0.7-release"}},"content":"The `0.8` release of LittleHorse is out! This pre-1.0 release contains many new features, security enhancements, and performance improvements.\\n\\n\x3c!-- truncate --\x3e\\n\\n## New Features\\n\\nNew features in this release cover some edge-cases in workflow development which came up from some initial pilots and internal usage of the platform.\\n\\n### Dynamic Task Execution\\n\\nBefore this release, a `TaskNode` had a hard-coded reference to a `TaskDef`. This means that every single `WfRun` that reaches the same `Node` in a `WfSpec` ends up executing the same `TaskDef`.\\n\\nHowever, in LittleHorse Enterprises LLC\'s upcoming Control Plane project (a system for dynamically provisioning LittleHorse clusters as a SaaS service), we anticipate a special use-case (which we will blog about this upcoming fall) wherein we need to _choose_ which `TaskDef` is executed dynamically at runtime.\\n\\nSpecifically, depending on an input variable to a `WfRun` (in this case, the `data-plane-id` variable), we need to execute a different `TaskDef` so that the `TaskRun` is executed by a speciific Task Worker in a specific location. We will blog about that use-case later.\\n\\n### Per-Thread Failure Handlers\\n\\nSince the `0.1.0` release of LittleHorse it has been possible to put a `FailureHandler` on any `Node`, such that if the `NodeRun` fails, then a Failure Handler thread is \\n\\n### Content in `EXCEPTION`s\\n\\n- #714\\n\\n### Multi-Tenancy Improvements\\n\\nMulti-Tenancy has been quietly under development in the LittleHorse Server since the `0.6.0` release introduced a breaking change to allow for it last October. The `0.8` release continues to progress towards making Multi-Tenancy generally-available.\\n\\nThis release includes two new major features for Multi-Tenancy:\\n\\n1. Allowing Python and Go clients to set the `tenant-id` header using `LHC_TENANT_id` ([#704](https://github.com/littlehorse-enterprises/littlehorse/pull/704))\\n2. Allowing administrative `Principal`s with admin privileges over multiple `Tenant`s: ([#679](https://github.com/littlehorse-enterprises/littlehorse/pull/679))\\n\\nMulti-Tenancy and support for authentication + fine-grained ACL\'s via `Principal`s has been a labor of love implemented by [Eduwer Camacaro](https://github.com/eduwercamacaro), who has grown into the role of Grumpy Maintainer of LittleHorse.\\n\\n### Kafka Security Protocol Support\\n\\nPrior to release `0.8`, the LH Server could only access a Kafka cluster with either:\\n* Plaintext access with no security.\\n* TLS with no authentication.\\n* MTLS security.\\n\\nPR [#716](https://github.com/littlehorse-enterprises/littlehorse/pull/716) introduced the following Server configurations:\\n\\n* `LHS_KAFKA_SECURITY_PROTOCOL`\\n* `LHS_KAFKA_SASL_MECHANISM`\\n* `LHS_KAFKA_SASL_JAAS_CONFIG`\\n\\nThis allows for access to any Kafka cluster except those requiring loading custom implementations of callbacks on the client side (for example, using the Strimzi OAuth Plug-in).\\n\\nIt is now possible to run LH with Kafka as:\\n- No security (PLAINTEXT)\\n- TLS on the brokers, no authentication (SSL)\\n- MTLS on the brokers (SSL with TRUSTSTORE set)\\n- SASL with any JAAS config (SASL_SSL)\\n- Confluent Cloud.\\n\\n### LittleHorse Canary\\n\\nThe LittleHorse Canary was released in early access. Inspired by the [Strimzi Canary](https://strimzi.io/blog/2021/11/09/canary/) for Apache Kafka, the LittleHorse Canary is a system that runs workflows on LittleHorse and reports on the health of the cluster(s) that it is monitoring.\\n\\nThe LH Canary system comprises two components:\\n\\n1. The Metronome, which runs workflows and sends metric beats to a Kafka topic.\\n2. The Aggregator, which consumes the metrics beats Kafka topic and aggregates metrics to be exposed to Prometheus and a GRPC API.\\n\\nThe goal of the Canary is to monitor, profile, and benchmark LittleHorse Clusters from the same exact perspective as the clients who use them.\\n\\nThe Canary is the brain child of [Sa\xfal Pi\xf1a](https://github.com/sauljabin), who is also the author of the popular [Kaskade](https://github.com/sauljabin/kaskade) TUI for Apache Kafka.\\n\\n### Exponential Backoff Retry Policy\\n\\nPR ([#707](https://github.com/littlehorse-enterprises/littlehorse/pull/707)) introduced the ability to configure exponential backoff for `TaskRun` retries. Previously, only immediate retries were supported.\\n\\n### JavaScript Client\\n\\nWe published the first version of `littlehorse-client` on NPM [here](https://www.npmjs.com/package/littlehorse-client). This client contains the `LHConfig` in javascript, which provides access to our LittleHorse GRPC API. Note that we do not yet support a JavaScript Task Worker nor a JavaScript `WfSpec` SDK.\\n\\n### Bugfixes\\n\\nIn this release, we fixed several bugs:\\n* Task Worker improperly reported `EXCEPTION`s and `ERROR`s when throwing `LHTaskException` ([#738](https://github.com/littlehorse-enterprises/pull/738))\\n* Fixes task queue rehydration ([#727](https://github.com/littlehorse-enterprises/pull/727))\\n* Fixes the Retention Policy for `ExternalEventDef`\'s ([#724](https://github.com/littlehorse-enterprises/littlehorse/pull/724))\\n* Fixes deadlock in Java task worker ([#723](https://github.com/littlehorse-enterprises/littlehorse/pull/723))\\n* Fixes concurrency bug with the `AsyncWaiter` in the server ([#719](https://github.com/littlehorse-enterprises/littlehorse/pull/719))\\n* Fixes various issues from soak tests ([#706](https://github.com/littlehorse-enterprises/littlehorse/pull/706))\\n* Fixes to `NodeRun` lifecycle ([#665](https://github.com/littlehorse-enterprises/littlehorse/pull/665)).\\n\\n## Looking Forward\\n\\nWe continue to stabilize our API and add features that cover edge cases. Load testing, chaos testing, and soak testing are an ongoing project, and we are working with the Apache Kafka Community on a few bugfixes in the Kafka Streams library which is heavily used in the core of LittleHorse.\\n\\nOnce those action items are resolved, we will make a `1.0` release candidate. However, in the meantime we don\'t expect any massively-breaking API changes at the protocol level. However, certain syntactical changes may occur in our SDK\'s (especially Go and Python)."},{"id":"littlehorse-0.7-release","metadata":{"permalink":"/blog/littlehorse-0.7-release","source":"@site/blog/2024-01-28-0.7-release.md","title":"Releasing 0.7","description":"Approaching a stable `1.0.0` release.","date":"2024-01-28T00:00:00.000Z","tags":[{"inline":false,"label":"LittleHorse Releases","permalink":"/blog/tags/release/","description":"Release blogs for LittleHorse Orchestrator."}],"readingTime":4.535,"hasTruncateMarker":true,"authors":[{"name":"The LittleHorse Council","title":"The Council of LittleHorse Maintainers","description":"LittleHorse Orchestrator is maintained by LittleHorse Enterprises LLC and available under the SSPL license. The LittleHorse Council is the group of engineers inside LittleHorse Enterprises LLC who are responsible for the stewardship of the open-source Orchestrator project and charged with looking out for the best interests of the LH Community.","url":"https://littlehorse.io","page":{"permalink":"/blog/authors/lh-council"},"socials":{"github":"https://github.com/littlehorse-enterprises","linkedin":"https://www.linkedin.com/company/littlehorse"},"imageURL":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","key":"lh_council"}],"frontMatter":{"title":"Releasing 0.7","description":"Approaching a stable `1.0.0` release.","slug":"littlehorse-0.7-release","authors":["lh_council"],"tags":["release"],"image":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","hide_table_of_contents":false},"unlisted":false,"prevItem":{"title":"Releasing 0.8","permalink":"/blog/littlehorse-0.8-release"},"nextItem":{"title":"Releasing 0.5.0","permalink":"/blog/littlehorse-0.5.0-release"}},"content":"We are excited to announce the release of `0.7.2`! \x3c!-- truncate --\x3e This is our last release before we cut `1.0.0`, which will be the first stable and production-ready LittleHorse distribution.\\n\\n## Get Started\\n\\nLittleHorse is free for production use according to the Server-Side Public License!\\n\\nTo get started with LittleHorse OSS, you can:\\n\\n* Visit us on [GitHub](https://github.com/littlehorse-enterprises)\\n* Try our [quickstarts](https://littlehorse.dev/docs/developer-guide/install#installation-and-quickstart) or watch our founder, Colt, go through them in [Java](https://www.youtube.com/watch?v=8Zo_UOStg98&t=6s), [Go](https://www.youtube.com/watch?v=oZQc2ISSZsk), or [Python](https://www.youtube.com/watch?v=l3TZOjfpzTw)\\n* Join our [Slack Community](https://launchpass.com/littlehorse-community) for quick and responsive help!\\n\\nAlso, LittleHorse Enterprises LLC has released its first out our [product-focused website](https://littlehorse.io)! If you\'re still curious and want to learn even more, check out a few of our new in-depth tutorial series on [our YouTube page](https://www.youtube.com/@LittleHorse-ey3vw/featured).\\n\\n## New Features\\n\\nRelease `0.7` introduces many features designed to make your life easier. We plan to write blogs about all of them, so stay tuned!\\n\\n### Administrative Dashboard\\n\\nThe most exciting part of the `0.7.2` release of LittleHorse is the new LH Dashboard, which is an administrative portal into your LittleHorse Cluster. The LH Dashboard lets you check on all of your workflows and tasks and debug everything visually with fine-grained detail. Our quickstarts (see above) have everything you need to get started debugging your workflows with our dashboard.\\n\\nThe LH Dashboard is in the alpha stage, so we appreciate any bug reports or feature requests. Please file them on [our github](https://github.com/littlehorse-enterprises/littlehorse/issues)!\\n\\n### Idempotent Metadata Management\\n\\nManaging your `WfSpec`s and `TaskDef`s just got much easier. Check out our [updated docs](https://littlehorse.dev/docs/developer-guide/grpc/managing-metadata) for tutorials on how to keep your DevOps team happy and seamlessly integrate LittleHorse into your normal application development lifecycle.\\n\\n### Child Workflows\\n\\nWe also added the ability to run a `WfRun` which is a \\"child\\" of another `WfRun`. This allows for some interesting features, most importantly:\\n* Sharing `Variable`s between `WfRun`\'s\\n* Foreign-key relationships between the child and parent `WfRun`\'s.\\n\\nStay tuned for an upcoming blog about _why_ we added that feature. It was guided by our resident Domain-Driven Design expert, Eduwer Camacaro! Here\'s a hint: this feature makes it possible to use LittleHorse Workflows as a native data store for complex business entities. This is a great way to implement the \\"Aggregate Pattern.\\"\\n\\n### Enhanced `SearchWfRun`\\n\\nThe `rpc SearchWfRun` request now has a `repeated VariableMatch variable_filters` field on it. This allows you to filter `WfRun`\'s by the value of one or more `Variable`\'s when searching for them, returning only matching `WfRun`\'s. This is super useful when using a LittleHorse `WfRun` to model a business entity, and you need to do something like \\"find all orders placed by `user-id == john` and `status == OUT_FOR_SHIPPING`\\".\\n\\nIn the past, this was possible using the `rpc SearchVariable` and then back the `WfRunId` out of the `VariableId`; however, that method is a little bit clunky. In reality, our users want to find a `WfRunId` matching certain criteria; they\'re not looking for a `Variable`.\\n\\n## What\'s Next?\\n\\nWe couldn\'t be more excited about what is coming next.\\n\\n### Apache2 Clients\\n\\nSome members of the community have expressed concerns about our clients (SDK\'s + GRPC code) being licensed by the SSPL license. We heard you, and we will update them to the Apache 2.0 License before our `1.0.0` release! The server will remain SSPL.\\n\\n### Tutorials\\n\\nOne of our team members, Sohini, has been hard at work creating video tutorials which will help you get quickly up to speed on advanced LittleHorse concepts. You can find them here on our [YouTube](https://www.youtube.com/@LittleHorse-ey3vw/playlists).\\n\\nAdditionally, our founder has recorded a series of zoom meetings with himself (yes, you read that right...Colt used zoom to record a tutorial video series) going through quickstarts in all of our three SDK\'s. You can find them here in [Java](https://www.youtube.com/watch?v=8Zo_UOStg98&t=6s), [Go](https://www.youtube.com/watch?v=oZQc2ISSZsk), or [Python](https://www.youtube.com/watch?v=l3TZOjfpzTw).\\n\\n### Approaching `1.0.0`\\n\\nWhat\'s missing before `1.0.0`? We have some in-progress features that are already merged to `master` but only partially implemented. If you squint hard enough at our GRPC Api, you might notice that we have support for multi-tenancy and also fine-grained ACL\'s. They are NOT ready for production use as we need to iron out a few wrinkles, but we will have them ready for `1.0.0`. We also are working on an `rpc MigrateWfSpec` which allows you to migrate a running `WfRun` from an older version of a `WfSpec` to a newer version. This is hard work for us but it will be highly useful for our users.\\n\\nAdditionally, we are expanding our end-to-end test coverage to try to shake out as many issues as possible _before_ our users tell us about them. So far, the rate of new bugs that we\'ve discovered has slowed down considerably, which makes us think we are getting close to the quality we expect from our own product.\\n\\nWhat will change when we release `1.0.0`? We will be following [Semantic Versioning](https://semver.org) to the letter, which means we will be paying _super close attention_ to any breaking changes to our API. If we want our users to use us for mission critical workloads, we need to take stability seriously\u2014both in terms of performance and API compatibility.\\n\\nWe will also likely have three minor releases per year, with 12 months of patch support for each minor release. This release schedule is copied from Apache Kafka.\\n\\n### LH Cloud\\n\\nLastly, stay tuned for LittleHorse Cloud! Early access is open. If you would like to sign up for early access to LH Cloud, visit [our website](https://www.littlehorse.io/lh-cloud) or contact `sales@littlehorse.io`."},{"id":"littlehorse-0.5.0-release","metadata":{"permalink":"/blog/littlehorse-0.5.0-release","source":"@site/blog/2023-09-08-0.5.0-release.md","title":"Releasing 0.5.0","description":"Python, For-Each, LH Platform.","date":"2023-09-08T00:00:00.000Z","tags":[{"inline":false,"label":"LittleHorse Releases","permalink":"/blog/tags/release/","description":"Release blogs for LittleHorse Orchestrator."}],"readingTime":5.205,"hasTruncateMarker":true,"authors":[{"name":"The LittleHorse Council","title":"The Council of LittleHorse Maintainers","description":"LittleHorse Orchestrator is maintained by LittleHorse Enterprises LLC and available under the SSPL license. The LittleHorse Council is the group of engineers inside LittleHorse Enterprises LLC who are responsible for the stewardship of the open-source Orchestrator project and charged with looking out for the best interests of the LH Community.","url":"https://littlehorse.io","page":{"permalink":"/blog/authors/lh-council"},"socials":{"github":"https://github.com/littlehorse-enterprises","linkedin":"https://www.linkedin.com/company/littlehorse"},"imageURL":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","key":"lh_council"}],"frontMatter":{"title":"Releasing 0.5.0","description":"Python, For-Each, LH Platform.","slug":"littlehorse-0.5.0-release","authors":["lh_council"],"tags":["release"],"image":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","hide_table_of_contents":false},"unlisted":false,"prevItem":{"title":"Releasing 0.7","permalink":"/blog/littlehorse-0.7-release"},"nextItem":{"title":"Releasing 0.2.0","permalink":"/blog/littlehorse-0.2.0-release"}},"content":"We are excited to announce the minor release `0.5.0`. \x3c!-- truncate --\x3e This release is highlighted by:\\n\\n* Alpha support for building `WfSpec`s in Python.\\n* Improved monitoring and health metrics on the LittleHorse Server.\\n* Support for looping over a `JSON_ARR` and launching threads in parallel for each element.\\n* Improved Exception Handling.\\n* Limited early access for LittleHorse Platform.\\n\\nIn this release, we made great strides towards full Python support, improved monitoring and observability, and added the ability to spawn threads in parallel looping over a `JSON_ARR` variable.\\n\\n## Get Started\\n\\nLittleHorse is free for production use according to the Server-Side Public License!\\n\\nTo get started with LittleHorse OSS, you can:\\n\\n* Try our [quickstarts](https://littlehorse.dev/docs/developer-guide/install)\\n* Visit us on [GitHub](https://github.com/littlehorse-enterprises/littlehorse) and give us a :star:!\\n* Download our [docker images](https://gallery.ecr.aws/littlehorse)\\n\\n## New Features\\n\\nWe\'d like to highlight some of the exciting new features in `0.5.0`.\\n\\n### Python `WfSpec` Support\\n\\nOur Python SDK now has full support for building `WfSpec`s! You can check it out at our [quickstart page](/docs/developer-guide/install).\\n\\n### For-Each Suppport\\n\\nThis is a very exciting feature which allows you to iterate over a list and spawn multiple `ThreadRun`s (like threads in a program).\\n\\nTo see it in action, check out our [example](https://github.com/littlehorse-enterprises/littlehorse/tree/master/examples/spawn-thread-foreach) or read the [documentation](https://littlehorse.dev/docs/developer-guide/wfspec-development/child-threads).\\n\\n### Improved Failure Handling\\n\\nThis release introduces a new status for LittleHorse, called `EXCEPTION`. The `EXCEPTION` status differs from the `ERROR` status in the following ways:\\n\\n* `ERROR` means an unexpected _technical_ failure occurred. For example, a `TaskRun` timed out because a third-party API was down.\\n* `EXCEPTION` means that a failure occurred at the _business process level_. For example, you might use an `EXCEPTION` when a customer has insufficient funds in her account to complete an order.\\n\\nJust like in programming, you can throw and catch `EXCEPTION`s (and you can also catch `ERROR`s). For a blog post that goes in-depth into how LittleHorse makes it easy to handle failures in your workflows, check out our [Failure Handling Docs](/docs/concepts/workflows#failure-handling).\\n\\n### LH Server Monitoring\\n\\nWe added a new path `/status` on the LH Server\'s health endpoint (port `1822` by default) which can be used to inspect the status of all internal Kafka Streams `Task`s on the LH Server. It presents the following information:\\n\\n* All Active Tasks on the host\\n* All Standby Tasks on the host\\n* Any ongoing State Restorations on the host\\n\\nAdditionally, we added a `/diskUsage` endpoint which returns the number of bytes of disk space in use by the LH Server.\\n\\nLittleHorse Platform uses these endpoints to intelligently scale, manage, and operate LittleHorse for you.\\n\\nWe are also in the process of writing and implementing a Kafka Improvement Proposal to improve visibility of Standby Tasks, which will allow the LittleHorse Operator (both in LH Platform and LH Cloud) to safely and smoothly scale LittleHorse clusters down without any downtime. Stay tuned in the Kafka developer mailing list!\\n\\n### LH Platform\\n\\nLittleHorse Platform is a Kubernetes Operator that securely manages a LittleHorse cluster for you in your own environment. It seamlessly integrates with your Kubernetes environment, GitOps workflows, and security strategy (TLS, mTLS, OAuth, Cert Manager, Keycloak).\\n\\nLittleHorse Platform is now available for limited early access, and has been installed in one of the largest health insurance companies in the US.\\n\\nTo get started with LittleHorse Platform, please [contact us](https://docs.google.com/forms/d/e/1FAIpQLScXVvTYy4LQnYoFoRKRQ7ppuxe0KgncsDukvm96qKN0pU5TnQ/viewform?usp=sf_link).\\n\\n### Persistent Variables\\n\\nIn LittleHorse `0.2.0` and later, you can search for `Variable`s by their value. For example, if you have a Workflow Specification that defines a variable `email_address`, you can find all Workflow Run\'s where `email_address == \'obiwan@jedi-council.org` by using the `SearchVariable` rpc call.\\n\\nThe problem with `0.2.0`? You need to provide the `wfSpecVersion` in your search request. That means you can only search for a `Variable` if you know the version of the `WfSpec` it came from.\\n\\nRelease `0.4.0` introduced the ability to mark a `Variable` as `persistent`, which means that:\\n* Every future version of the `WfSpec` must have the same variable definition with the same index type.\\n* You can now search for variables with a certain value across _all versions_ of the `WfSpec`.\\n\\nBe on the lookout for an upcoming blog post about using Persistent Variables and a simple backend-for-frontend to build an end-to-end Approval Workflow Application using only LittleHorse!\\n\\n## What\'s Next\\n\\nOver the next few weeks, we plan to:\\n\\n* Add utilities to make it easier to work with the LittleHorse API.\\n* Allow users to throw a Workflow `EXCEPTION` from within the Task Worker SDK (currently, only `ERROR` is supported).\\n* Continue hardening the LittleHorse Server\'s availability and performance story.\\n* Launch limited early accesss for LittleHorse Cloud and LittleHorse UI.\\n\\nTo get started with LittleHorse, head over to our [installation docs](https://littlehorse.dev/docs/developer-guide/install).\\n\\n### What about `0.3.0` and `0.4.0`?\\n\\nWe also released `0.3.0` and `0.4.0` over the past 5 weeks! (And before `0.3.0`, we had a minor patch bugfix on `0.2.1`).\\n\\nThe only thing missing with `0.3.0` and `0.4.0` is a blog post + announcement. That\'s because a lot of the features we included in this announcement were partially-implemented, implemented in some languages and not others, or in the \\"experimental\\" phase at the time of `0.3.0` and `0.4.0`. We accelerated the release of `0.3.0` and `0.4.0` because certain early-access customers requested certain features on an accelerated timeline.\\n\\nAs our API is mostly stable now, we will slow down our release cadence to likely a new `*.x.*` version (a `minor` release in [Semantic Versioning](https://semver.org)) every two months, with security and bugfix patch releases (`*.*.x`) as needed.\\n\\nAdditionally, as we introduce new features, we will start a release changelog document in which we document the level of stability of the new API\'s introduced. For example:\\n* `STABLE`: Any changes to this API before the next [Major Release](https://semver.org) will be backwards compatible. The feature is covered by our integration tests.\\n* `BETA`: We don\'t anticipate any _large breaking changes_ to the feature/API. It is covered by our integration tests, but it _might_ change before the `1.0.0` release.\\n* `EXPERIMENTAL`: Try it out and give us feedback! But you might want to wait a release or two before putting it into production.\\n\\nThe `0.6.0` release notes will include a table of all of our features and their API Stability Level in all four of our SDK\'s."},{"id":"littlehorse-0.2.0-release","metadata":{"permalink":"/blog/littlehorse-0.2.0-release","source":"@site/blog/2023-08-30-0.2.0-release.md","title":"Releasing 0.2.0","description":"Making workflow development easy again.","date":"2023-08-30T00:00:00.000Z","tags":[{"inline":false,"label":"LittleHorse Releases","permalink":"/blog/tags/release/","description":"Release blogs for LittleHorse Orchestrator."}],"readingTime":3.54,"hasTruncateMarker":true,"authors":[{"name":"The LittleHorse Council","title":"The Council of LittleHorse Maintainers","description":"LittleHorse Orchestrator is maintained by LittleHorse Enterprises LLC and available under the SSPL license. The LittleHorse Council is the group of engineers inside LittleHorse Enterprises LLC who are responsible for the stewardship of the open-source Orchestrator project and charged with looking out for the best interests of the LH Community.","url":"https://littlehorse.io","page":{"permalink":"/blog/authors/lh-council"},"socials":{"github":"https://github.com/littlehorse-enterprises","linkedin":"https://www.linkedin.com/company/littlehorse"},"imageURL":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","key":"lh_council"}],"frontMatter":{"title":"Releasing 0.2.0","description":"Making workflow development easy again.","slug":"littlehorse-0.2.0-release","authors":["lh_council"],"tags":["release"],"image":"https://avatars.githubusercontent.com/u/140006313?s=400&u=7bf4c91d92dfe590ac71bb6b4821e1a81aa5b712&v=4","hide_table_of_contents":false},"unlisted":false,"prevItem":{"title":"Releasing 0.5.0","permalink":"/blog/littlehorse-0.5.0-release"}},"content":"We are excited to announce the release of `0.2.0`! \x3c!-- truncate --\x3e In this release, we added several new features, highlighted by User Tasks, security, and Python support.\\n\\n## Get Started\\n\\nLittleHorse is free for production use according to the Server-Side Public License!\\n\\nTo get started with LittleHorse OSS, you can:\\n\\n* Visit us on [GitHub](https://github.com/littlehorse-enterprises)\\n* Try our [quickstarts](https://littlehorse.dev/docs/developer-guide/install#installation-and-quickstart)\\n\\nAdditionally, with version `0.2.0`, we have released our first two Docker Images:\\n\\n* [`lh-server`](https://gallery.ecr.aws/littlehorse/littlehorse-server), the production-ready build of the LittleHorse Server.\\n* [`lh-standalone`](https://gallery.ecr.aws/littlehorse/littlehorse-standalone), a self-contained build of the LittleHorse Server that you can run to get a working LH Installation for local development.\\n\\n## New Features\\n\\nRelease `0.2.0` contains many exciting new features, and we\'ve highlighted a few here.\\n\\n### User Tasks\\n\\n[User Tasks](https://littlehorse.dev/docs/concepts/user-tasks) are a massive new feature released in `0.2.0` which allow you to schedule tasks to be executed by a human user alongside tasks that are executed by computers.\\n\\nIn `0.2.0`, User Tasks have reached stability, meaning that future releases will be backwards-compatible with the current User Tasks API. We currently have the following features:\\n\\n* Assignment of tasks to a User or User Group\\n* Reminder Tasks, or `TaskRun`\'s that are scheduled some time after a `UserTaskRun` is scheduled.\\n* Automatic reassignment of a `UserTaskRun` after some period of inactivity.\\n* Manual reassignment of a `UserTaskRun`.\\n* `UserTaskRun` search.\\n\\n:::note\\nThe public API for User Tasks is stable in all of the grpc clients and in the Java `WfSpec` SDK.\\n\\nThe Go and Python grpc clients both support User Tasks. However, neither Python nor Go yet have support for User Tasks in the `WfSpec` SDK.\\n:::\\n\\n### Workflow Threading\\n\\nRelease `0.2.0` allows you to use a `WAIT_FOR_THREADS` node to wait for more than one child thread at one time. For an example, see our [Parallel Approval Example](https://github.com/littlehorse-enterprises/littlehorse/tree/master/examples/parallel-approval) on our GitHub.\\n\\nFuture releases will provide _backwards-compatible_ enhancements to this\\nfunctionality, allowing various strategies for handling failures of individual child threads.\\n\\n### Python Support\\n\\nWe have released an alpha [Python SDK](https://github.com/littlehorse-enterprises/littlehorse/tree/master/sdk-python)! This release contains:\\n\\n* Python client in grpc\\n* Python Task Worker SDK\\n\\nCurrently, building `WfSpec`\'s in Python is not supported. We aim to move python Task Worker support from alpha to beta, and add alpha support for `WfSpec` development in python, in the `0.3.0` release.\\n\\nTo try out our python task worker client, you can head to [Installation Docs](https://littlehorse.dev/docs/developer-guide/install) and the [Task Worker Development Docs](https://littlehorse.dev/docs/developer-guide/task-worker-development).\\n\\n:::note\\nThe Python SDK is in the alpha stage, meaning that future releases could break backwards compatibility.\\n:::\\n\\n### Security\\n\\nWe added beta support for OAuth, TLS, and mTLS in release `0.2.0`. The following features graduated to \\"beta\\" in this release:\\n\\n* TLS encryption for incoming connections on all listeners, configured on a per-listener basis.\\n* mTLS to authenticate incoming connections on any listeners, configured on a per-listener basis.\\n* OAuth to authenticate incoming connections on any public listener (excluding the inter-server communication port).\\n\\n:::info\\nBeta support means that we will soon add significant functionality, and as such a future release _might_ break backwards compatibility.\\n\\nHowever, future releases of a feature in the _beta_ state will most likely be backwards compatible with `0.2.0` barring exceptional circumstances.\\n:::\\n\\n### Performance\\n\\nWe made several optimizations to our storage management sub-system, reducing the number of put\'s and get\'s into our backing state store by roughly 30%. As a result, a LittleHorse Server running with a single partition is capable of scheduling over 1,100 `TaskRun`\'s per second.\\n\\n### Go Support\\n\\nSupport for the Go client is now beta. Future releases will maintain compatibility for all features on our documentation.\\n\\nRelease `0.3.0` will close the gap between the Java and Go SDK\'s, adding features such as:\\n* Format Strings for Variable Assignments in the `WfSpec` SDK\\n* User Task support in the `WfSpec` SDK\\n* Configuring Indexes on `Variable`s in the `WfSpec` SDK\\n\\n## What\'s Next\\n\\nWe have several exciting features coming soon over the next few releases, including:\\n\\n* Fine-grained access controls\\n* Backward-compatible improvements to [Failure Handling](https://littlehorse.dev/docs/concepts/exception-handling)\\n* C# support\\n* Python support for building `WfSpec`s\\n\\nFor an enterprise-ready distribution of LittleHorse running in your own datacenter, contact `sales@littlehorse.io` to inquire about LittleHorse Platform.\\n\\nFor a pay-as-you-go, serverless Managed Service of LittleHorse in the cloud, fill out the [LH Cloud Waitlist Form](https://docs.google.com/forms/d/e/1FAIpQLScXVvTYy4LQnYoFoRKRQ7ppuxe0KgncsDukvm96qKN0pU5TnQ/viewform)."}]}}')}}]); \ No newline at end of file diff --git a/assets/js/main.ba7e9ffe.js b/assets/js/main.ba7e9ffe.js new file mode 100644 index 000000000..4ae7cfad3 --- /dev/null +++ b/assets/js/main.ba7e9ffe.js @@ -0,0 +1,2 @@ +/*! For license information please see main.ba7e9ffe.js.LICENSE.txt */ +(self.webpackChunklh_site=self.webpackChunklh_site||[]).push([[8792],{3219:(e,t,n)=>{"use strict";n.d(t,{Bc:()=>g,E8:()=>Hn,a1:()=>Un});var r=n(6540);n(961);function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function a(e){for(var t=1;t=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function c(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=n){var r,o,a=[],i=!0,l=!1;try{for(n=n.call(e);!(i=(r=n.next()).done)&&(a.push(r.value),!t||a.length!==t);i=!0);}catch(e){l=!0,o=e}finally{try{i||null==n.return||n.return()}finally{if(l)throw o}}return a}}(e,t)||d(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function u(e){return function(e){if(Array.isArray(e))return p(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||d(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function d(e,t){if(e){if("string"==typeof e)return p(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?p(e,t):void 0}}function p(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);ne.length)&&(t=e.length);for(var n=0,r=new Array(t);ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function L(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function R(e){for(var t=1;t=3||2===n&&r>=4||1===n&&r>=10);function a(t,n,r){if(o&&void 0!==r){var a=r[0].__autocomplete_algoliaCredentials,i={"X-Algolia-Application-Id":a.appId,"X-Algolia-API-Key":a.apiKey};e.apply(void 0,[t].concat(P(n),[{headers:i}]))}else e.apply(void 0,[t].concat(P(n)))}return{init:function(t,n){e("init",{appId:t,apiKey:n})},setUserToken:function(t){e("setUserToken",t)},clickedObjectIDsAfterSearch:function(){for(var e=arguments.length,t=new Array(e),n=0;n0&&a("clickedObjectIDsAfterSearch",M(t),t[0].items)},clickedObjectIDs:function(){for(var e=arguments.length,t=new Array(e),n=0;n0&&a("clickedObjectIDs",M(t),t[0].items)},clickedFilters:function(){for(var t=arguments.length,n=new Array(t),r=0;r0&&e.apply(void 0,["clickedFilters"].concat(n))},convertedObjectIDsAfterSearch:function(){for(var e=arguments.length,t=new Array(e),n=0;n0&&a("convertedObjectIDsAfterSearch",M(t),t[0].items)},convertedObjectIDs:function(){for(var e=arguments.length,t=new Array(e),n=0;n0&&a("convertedObjectIDs",M(t),t[0].items)},convertedFilters:function(){for(var t=arguments.length,n=new Array(t),r=0;r0&&e.apply(void 0,["convertedFilters"].concat(n))},viewedObjectIDs:function(){for(var e=arguments.length,t=new Array(e),n=0;n0&&t.reduce((function(e,t){var n=t.items,r=N(t,j);return[].concat(P(e),P(function(e){for(var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:20,n=[],r=0;r0&&e.apply(void 0,["viewedFilters"].concat(n))}}}function B(e){var t=e.items.reduce((function(e,t){var n;return e[t.__autocomplete_indexName]=(null!==(n=e[t.__autocomplete_indexName])&&void 0!==n?n:[]).concat(t),e}),{});return Object.keys(t).map((function(e){return{index:e,items:t[e],algoliaSource:["autocomplete"]}}))}function z(e){return e.objectID&&e.__autocomplete_indexName&&e.__autocomplete_queryID}function $(e){return $="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},$(e)}function U(e){return function(e){if(Array.isArray(e))return H(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(e){if("string"==typeof e)return H(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?H(e,t):void 0}}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function H(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n0&&G({onItemsChange:r,items:n,insights:l,state:t}))}}),0);return{name:"aa.algoliaInsightsPlugin",subscribe:function(e){var t=e.setContext,n=e.onSelect,r=e.onActive;i("addAlgoliaAgent","insights-plugin"),t({algoliaInsightsPlugin:{__algoliaSearchParameters:{clickAnalytics:!0},insights:l}}),n((function(e){var t=e.item,n=e.state,r=e.event;z(t)&&o({state:n,event:r,insights:l,item:t,insightsEvents:[V({eventName:"Item Selected"},O({item:t,items:s.current}))]})})),r((function(e){var t=e.item,n=e.state,r=e.event;z(t)&&a({state:n,event:r,insights:l,item:t,insightsEvents:[V({eventName:"Item Active"},O({item:t,items:s.current}))]})}))},onStateChange:function(e){var t=e.state;c({state:t})},__autocomplete_pluginOptions:e}}function Y(e,t){var n=t;return{then:function(t,r){return Y(e.then(X(t,n,e),X(r,n,e)),n)},catch:function(t){return Y(e.catch(X(t,n,e)),n)},finally:function(t){return t&&n.onCancelList.push(t),Y(e.finally(X(t&&function(){return n.onCancelList=[],t()},n,e)),n)},cancel:function(){n.isCanceled=!0;var e=n.onCancelList;n.onCancelList=[],e.forEach((function(e){e()}))},isCanceled:function(){return!0===n.isCanceled}}}function Z(e){return Y(e,{isCanceled:!1,onCancelList:[]})}function X(e,t,n){return e?function(n){return t.isCanceled?n:e(n)}:n}function J(e,t,n,r){if(!n)return null;if(e<0&&(null===t||null!==r&&0===t))return n+e;var o=(null===t?-1:t)+e;return o<=-1||o>=n?null===r?null:0:o}function ee(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function te(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n0},reshape:function(e){return e.sources}},e),{},{id:null!==(n=e.id)&&void 0!==n?n:"autocomplete-".concat(w++),plugins:o,initialState:ge({activeItemId:null,query:"",completion:null,collections:[],isOpen:!1,status:"idle",context:{}},e.initialState),onStateChange:function(t){var n;null===(n=e.onStateChange)||void 0===n||n.call(e,t),o.forEach((function(e){var n;return null===(n=e.onStateChange)||void 0===n?void 0:n.call(e,t)}))},onSubmit:function(t){var n;null===(n=e.onSubmit)||void 0===n||n.call(e,t),o.forEach((function(e){var n;return null===(n=e.onSubmit)||void 0===n?void 0:n.call(e,t)}))},onReset:function(t){var n;null===(n=e.onReset)||void 0===n||n.call(e,t),o.forEach((function(e){var n;return null===(n=e.onReset)||void 0===n?void 0:n.call(e,t)}))},getSources:function(n){return Promise.all([].concat(function(e){return function(e){if(Array.isArray(e))return me(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(e){if("string"==typeof e)return me(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?me(e,t):void 0}}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}(o.map((function(e){return e.getSources}))),[e.getSources]).filter(Boolean).map((function(e){return function(e,t){var n=[];return Promise.resolve(e(t)).then((function(e){return Promise.all(e.filter((function(e){return Boolean(e)})).map((function(e){if(e.sourceId,n.includes(e.sourceId))throw new Error("[Autocomplete] The `sourceId` ".concat(JSON.stringify(e.sourceId)," is not unique."));n.push(e.sourceId);var t={getItemInputValue:function(e){return e.state.query},getItemUrl:function(){},onSelect:function(e){(0,e.setIsOpen)(!1)},onActive:E,onResolve:E};Object.keys(t).forEach((function(e){t[e].__default=!0}));var r=te(te({},t),e);return Promise.resolve(r)})))}))}(e,n)}))).then((function(e){return y(e)})).then((function(e){return e.map((function(e){return ge(ge({},e),{},{onSelect:function(n){e.onSelect(n),t.forEach((function(e){var t;return null===(t=e.onSelect)||void 0===t?void 0:t.call(e,n)}))},onActive:function(n){e.onActive(n),t.forEach((function(e){var t;return null===(t=e.onActive)||void 0===t?void 0:t.call(e,n)}))},onResolve:function(n){e.onResolve(n),t.forEach((function(e){var t;return null===(t=e.onResolve)||void 0===t?void 0:t.call(e,n)}))}})}))}))},navigator:ge({navigate:function(e){var t=e.itemUrl;r.location.assign(t)},navigateNewTab:function(e){var t=e.itemUrl,n=r.open(t,"_blank","noopener");null==n||n.focus()},navigateNewWindow:function(e){var t=e.itemUrl;r.open(t,"_blank","noopener")}},e.navigator)})}function ye(e){return ye="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},ye(e)}function we(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function Se(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}(e,Ie);Be&&o.environment.clearTimeout(Be);var c=s.setCollections,u=s.setIsOpen,d=s.setQuery,p=s.setActiveItemId,f=s.setStatus;if(d(a),p(o.defaultActiveItemId),!a&&!1===o.openOnFocus){var m,h=l.getState().collections.map((function(e){return Le(Le({},e),{},{items:[]})}));f("idle"),c(h),u(null!==(m=r.isOpen)&&void 0!==m?m:o.shouldPanelOpen({state:l.getState()}));var g=Z(ze(h).then((function(){return Promise.resolve()})));return l.pendingRequests.add(g)}f("loading"),Be=o.environment.setTimeout((function(){f("stalled")}),o.stallThreshold);var b=Z(ze(o.getSources(Le({query:a,refresh:i,state:l.getState()},s)).then((function(e){return Promise.all(e.map((function(e){return Promise.resolve(e.getItems(Le({query:a,refresh:i,state:l.getState()},s))).then((function(t){return function(e,t,n){if(o=e,Boolean(null==o?void 0:o.execute)){var r="algolia"===e.requesterId?Object.assign.apply(Object,[{}].concat(Ce(Object.keys(n.context).map((function(e){var t;return null===(t=n.context[e])||void 0===t?void 0:t.__algoliaSearchParameters}))))):{};return _e(_e({},e),{},{requests:e.queries.map((function(n){return{query:"algolia"===e.requesterId?_e(_e({},n),{},{params:_e(_e({},r),n.params)}):n,sourceId:t,transformResponse:e.transformResponse}}))})}var o;return{items:e,sourceId:t}}(t,e.sourceId,l.getState())}))}))).then(Te).then((function(t){return function(e,t,n){return t.map((function(t){var r,o=e.filter((function(e){return e.sourceId===t.sourceId})),a=o.map((function(e){return e.items})),i=o[0].transformResponse,l=i?i({results:r=a,hits:r.map((function(e){return e.hits})).filter(Boolean),facetHits:r.map((function(e){var t;return null===(t=e.facetHits)||void 0===t?void 0:t.map((function(e){return{label:e.value,count:e.count,_highlightResult:{label:{value:e.highlighted}}}}))})).filter(Boolean)}):a;return t.onResolve({source:t,results:a,items:l,state:n.getState()}),l.every(Boolean),'The `getItems` function from source "'.concat(t.sourceId,'" must return an array of items but returned ').concat(JSON.stringify(void 0),".\n\nDid you forget to return items?\n\nSee: https://www.algolia.com/doc/ui-libraries/autocomplete/core-concepts/sources/#param-getitems"),{source:t,items:l}}))}(t,e,l)})).then((function(e){return function(e){var t=e.props,n=e.state,r=e.collections.reduce((function(e,t){return Se(Se({},e),{},ke({},t.source.sourceId,Se(Se({},t.source),{},{getItems:function(){return y(t.items)}})))}),{}),o=t.plugins.reduce((function(e,t){return t.reshape?t.reshape(e):e}),{sourcesBySourceId:r,state:n}).sourcesBySourceId;return y(t.reshape({sourcesBySourceId:o,sources:Object.values(o),state:n})).filter(Boolean).map((function(e){return{source:e,items:e.getItems()}}))}({collections:e,props:o,state:l.getState()})}))})))).then((function(e){var n;f("idle"),c(e);var d=o.shouldPanelOpen({state:l.getState()});u(null!==(n=r.isOpen)&&void 0!==n?n:o.openOnFocus&&!a&&d||d);var p=oe(l.getState());if(null!==l.getState().activeItemId&&p){var m=p.item,h=p.itemInputValue,g=p.itemUrl,b=p.source;b.onActive(Le({event:t,item:m,itemInputValue:h,itemUrl:g,refresh:i,source:b,state:l.getState()},s))}})).finally((function(){f("idle"),Be&&o.environment.clearTimeout(Be)}));return l.pendingRequests.add(b)}function Ue(e){return Ue="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Ue(e)}var He=["event","props","refresh","store"];function qe(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function Ve(e){for(var t=1;t=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function at(e){var t=e.props,n=e.refresh,r=e.store,o=ot(e,Ge),a=function(e,t){return void 0!==t?"".concat(e,"-").concat(t):e};return{getEnvironmentProps:function(e){var n=e.inputElement,o=e.formElement,a=e.panelElement;function i(e){!r.getState().isOpen&&r.pendingRequests.isEmpty()||e.target===n||!1===[o,a].some((function(t){return(n=t)===(r=e.target)||n.contains(r);var n,r}))&&(r.dispatch("blur",null),t.debug||r.pendingRequests.cancelAll())}return nt({onTouchStart:i,onMouseDown:i,onTouchMove:function(e){!1!==r.getState().isOpen&&n===t.environment.document.activeElement&&e.target!==n&&n.blur()}},ot(e,Qe))},getRootProps:function(e){return nt({role:"combobox","aria-expanded":r.getState().isOpen,"aria-haspopup":"listbox","aria-owns":r.getState().isOpen?"".concat(t.id,"-list"):void 0,"aria-labelledby":"".concat(t.id,"-label")},e)},getFormProps:function(e){return e.inputElement,nt({action:"",noValidate:!0,role:"search",onSubmit:function(a){var i;a.preventDefault(),t.onSubmit(nt({event:a,refresh:n,state:r.getState()},o)),r.dispatch("submit",null),null===(i=e.inputElement)||void 0===i||i.blur()},onReset:function(a){var i;a.preventDefault(),t.onReset(nt({event:a,refresh:n,state:r.getState()},o)),r.dispatch("reset",null),null===(i=e.inputElement)||void 0===i||i.focus()}},ot(e,Ye))},getLabelProps:function(e){var n=e||{},r=n.sourceIndex,o=ot(n,Xe);return nt({htmlFor:"".concat(a(t.id,r),"-input"),id:"".concat(a(t.id,r),"-label")},o)},getInputProps:function(e){var a;function i(e){(t.openOnFocus||Boolean(r.getState().query))&&$e(nt({event:e,props:t,query:r.getState().completion||r.getState().query,refresh:n,store:r},o)),r.dispatch("focus",null)}var l=e||{},s=(l.inputElement,l.maxLength),c=void 0===s?512:s,u=ot(l,Ze),d=oe(r.getState()),p=function(e){return Boolean(e&&e.match(ae))}((null===(a=t.environment.navigator)||void 0===a?void 0:a.userAgent)||""),f=null!=d&&d.itemUrl&&!p?"go":"search";return nt({"aria-autocomplete":"both","aria-activedescendant":r.getState().isOpen&&null!==r.getState().activeItemId?"".concat(t.id,"-item-").concat(r.getState().activeItemId):void 0,"aria-controls":r.getState().isOpen?"".concat(t.id,"-list"):void 0,"aria-labelledby":"".concat(t.id,"-label"),value:r.getState().completion||r.getState().query,id:"".concat(t.id,"-input"),autoComplete:"off",autoCorrect:"off",autoCapitalize:"off",enterKeyHint:f,spellCheck:"false",autoFocus:t.autoFocus,placeholder:t.placeholder,maxLength:c,type:"search",onChange:function(e){$e(nt({event:e,props:t,query:e.currentTarget.value.slice(0,c),refresh:n,store:r},o))},onKeyDown:function(e){!function(e){var t=e.event,n=e.props,r=e.refresh,o=e.store,a=function(e,t){if(null==e)return{};var n,r,o=function(e,t){if(null==e)return{};var n,r,o={},a=Object.keys(e);for(r=0;r=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}(e,He);if("ArrowUp"===t.key||"ArrowDown"===t.key){var i=function(){var e=n.environment.document.getElementById("".concat(n.id,"-item-").concat(o.getState().activeItemId));e&&(e.scrollIntoViewIfNeeded?e.scrollIntoViewIfNeeded(!1):e.scrollIntoView(!1))},l=function(){var e=oe(o.getState());if(null!==o.getState().activeItemId&&e){var n=e.item,i=e.itemInputValue,l=e.itemUrl,s=e.source;s.onActive(Ve({event:t,item:n,itemInputValue:i,itemUrl:l,refresh:r,source:s,state:o.getState()},a))}};t.preventDefault(),!1===o.getState().isOpen&&(n.openOnFocus||Boolean(o.getState().query))?$e(Ve({event:t,props:n,query:o.getState().query,refresh:r,store:o},a)).then((function(){o.dispatch(t.key,{nextActiveItemId:n.defaultActiveItemId}),l(),setTimeout(i,0)})):(o.dispatch(t.key,{}),l(),i())}else if("Escape"===t.key)t.preventDefault(),o.dispatch(t.key,null),o.pendingRequests.cancelAll();else if("Tab"===t.key)o.dispatch("blur",null),o.pendingRequests.cancelAll();else if("Enter"===t.key){if(null===o.getState().activeItemId||o.getState().collections.every((function(e){return 0===e.items.length})))return void(n.debug||o.pendingRequests.cancelAll());t.preventDefault();var s=oe(o.getState()),c=s.item,u=s.itemInputValue,d=s.itemUrl,p=s.source;if(t.metaKey||t.ctrlKey)void 0!==d&&(p.onSelect(Ve({event:t,item:c,itemInputValue:u,itemUrl:d,refresh:r,source:p,state:o.getState()},a)),n.navigator.navigateNewTab({itemUrl:d,item:c,state:o.getState()}));else if(t.shiftKey)void 0!==d&&(p.onSelect(Ve({event:t,item:c,itemInputValue:u,itemUrl:d,refresh:r,source:p,state:o.getState()},a)),n.navigator.navigateNewWindow({itemUrl:d,item:c,state:o.getState()}));else if(t.altKey);else{if(void 0!==d)return p.onSelect(Ve({event:t,item:c,itemInputValue:u,itemUrl:d,refresh:r,source:p,state:o.getState()},a)),void n.navigator.navigate({itemUrl:d,item:c,state:o.getState()});$e(Ve({event:t,nextState:{isOpen:!1},props:n,query:u,refresh:r,store:o},a)).then((function(){p.onSelect(Ve({event:t,item:c,itemInputValue:u,itemUrl:d,refresh:r,source:p,state:o.getState()},a))}))}}}(nt({event:e,props:t,refresh:n,store:r},o))},onFocus:i,onBlur:E,onClick:function(n){e.inputElement!==t.environment.document.activeElement||r.getState().isOpen||i(n)}},u)},getPanelProps:function(e){return nt({onMouseDown:function(e){e.preventDefault()},onMouseLeave:function(){r.dispatch("mouseleave",null)}},e)},getListProps:function(e){var n=e||{},r=n.sourceIndex,o=ot(n,Je);return nt({role:"listbox","aria-labelledby":"".concat(a(t.id,r),"-label"),id:"".concat(a(t.id,r),"-list")},o)},getItemProps:function(e){var i=e.item,l=e.source,s=e.sourceIndex,c=ot(e,et);return nt({id:"".concat(a(t.id,s),"-item-").concat(i.__autocomplete_id),role:"option","aria-selected":r.getState().activeItemId===i.__autocomplete_id,onMouseMove:function(e){if(i.__autocomplete_id!==r.getState().activeItemId){r.dispatch("mousemove",i.__autocomplete_id);var t=oe(r.getState());if(null!==r.getState().activeItemId&&t){var a=t.item,l=t.itemInputValue,s=t.itemUrl,c=t.source;c.onActive(nt({event:e,item:a,itemInputValue:l,itemUrl:s,refresh:n,source:c,state:r.getState()},o))}}},onMouseDown:function(e){e.preventDefault()},onClick:function(e){var a=l.getItemInputValue({item:i,state:r.getState()}),s=l.getItemUrl({item:i,state:r.getState()});(s?Promise.resolve():$e(nt({event:e,nextState:{isOpen:!1},props:t,query:a,refresh:n,store:r},o))).then((function(){l.onSelect(nt({event:e,item:i,itemInputValue:a,itemUrl:s,refresh:n,source:l,state:r.getState()},o))}))}},c)}}}function it(e){return it="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},it(e)}function lt(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function st(e){for(var t=1;t0&&r.createElement("div",{className:"DocSearch-NoResults-Prefill-List"},r.createElement("p",{className:"DocSearch-Help"},c,":"),r.createElement("ul",null,h.slice(0,3).reduce((function(e,t){return[].concat(u(e),[r.createElement("li",{key:t},r.createElement("button",{className:"DocSearch-Prefill",key:t,type:"button",onClick:function(){o.setQuery(t.toLowerCase()+" "),o.refresh(),o.inputRef.current.focus()}},t))])}),[]))),o.getMissingResultsUrl&&r.createElement("p",{className:"DocSearch-Help"},"".concat(p," "),r.createElement("a",{href:o.getMissingResultsUrl({query:o.state.query}),target:"_blank",rel:"noopener noreferrer"},m)))}var zt=["hit","attribute","tagName"];function $t(e,t){return t.split(".").reduce((function(e,t){return null!=e&&e[t]?e[t]:null}),e)}function Ut(e){var t=e.hit,n=e.attribute,o=e.tagName,i=void 0===o?"span":o,l=s(e,zt);return(0,r.createElement)(i,a(a({},l),{},{dangerouslySetInnerHTML:{__html:$t(t,"_snippetResult.".concat(n,".value"))||$t(t,n)}}))}function Ht(e){return e.collection&&0!==e.collection.items.length?r.createElement("section",{className:"DocSearch-Hits"},r.createElement("div",{className:"DocSearch-Hit-source"},e.title),r.createElement("ul",e.getListProps(),e.collection.items.map((function(t,n){return r.createElement(qt,l({key:[e.title,t.objectID].join(":"),item:t,index:n},e))})))):null}function qt(e){var t=e.item,n=e.index,o=e.renderIcon,a=e.renderAction,i=e.getItemProps,s=e.onItemClick,u=e.collection,d=e.hitComponent,p=c(r.useState(!1),2),f=p[0],m=p[1],h=c(r.useState(!1),2),g=h[0],b=h[1],v=r.useRef(null),y=d;return r.createElement("li",l({className:["DocSearch-Hit",t.__docsearch_parent&&"DocSearch-Hit--Child",f&&"DocSearch-Hit--deleting",g&&"DocSearch-Hit--favoriting"].filter(Boolean).join(" "),onTransitionEnd:function(){v.current&&v.current()}},i({item:t,source:u.source,onClick:function(e){s(t,e)}})),r.createElement(y,{hit:t},r.createElement("div",{className:"DocSearch-Hit-Container"},o({item:t,index:n}),t.hierarchy[t.type]&&"lvl1"===t.type&&r.createElement("div",{className:"DocSearch-Hit-content-wrapper"},r.createElement(Ut,{className:"DocSearch-Hit-title",hit:t,attribute:"hierarchy.lvl1"}),t.content&&r.createElement(Ut,{className:"DocSearch-Hit-path",hit:t,attribute:"content"})),t.hierarchy[t.type]&&("lvl2"===t.type||"lvl3"===t.type||"lvl4"===t.type||"lvl5"===t.type||"lvl6"===t.type)&&r.createElement("div",{className:"DocSearch-Hit-content-wrapper"},r.createElement(Ut,{className:"DocSearch-Hit-title",hit:t,attribute:"hierarchy.".concat(t.type)}),r.createElement(Ut,{className:"DocSearch-Hit-path",hit:t,attribute:"hierarchy.lvl1"})),"content"===t.type&&r.createElement("div",{className:"DocSearch-Hit-content-wrapper"},r.createElement(Ut,{className:"DocSearch-Hit-title",hit:t,attribute:"content"}),r.createElement(Ut,{className:"DocSearch-Hit-path",hit:t,attribute:"hierarchy.lvl1"})),a({item:t,runDeleteTransition:function(e){m(!0),v.current=e},runFavoriteTransition:function(e){b(!0),v.current=e}}))))}function Vt(e,t,n){return e.reduce((function(e,r){var o=t(r);return e.hasOwnProperty(o)||(e[o]=[]),e[o].length<(n||5)&&e[o].push(r),e}),{})}function Wt(e){return e}function Kt(e){return 1===e.button||e.altKey||e.ctrlKey||e.metaKey||e.shiftKey}function Gt(){}var Qt=/(|<\/mark>)/g,Yt=RegExp(Qt.source);function Zt(e){var t,n,r=e;if(!r.__docsearch_parent&&!e._highlightResult)return e.hierarchy.lvl0;var o=((r.__docsearch_parent?null===(t=r.__docsearch_parent)||void 0===t||null===(t=t._highlightResult)||void 0===t||null===(t=t.hierarchy)||void 0===t?void 0:t.lvl0:null===(n=e._highlightResult)||void 0===n||null===(n=n.hierarchy)||void 0===n?void 0:n.lvl0)||{}).value;return o&&Yt.test(o)?o.replace(Qt,""):o}function Xt(e){return r.createElement("div",{className:"DocSearch-Dropdown-Container"},e.state.collections.map((function(t){if(0===t.items.length)return null;var n=Zt(t.items[0]);return r.createElement(Ht,l({},e,{key:t.source.sourceId,title:n,collection:t,renderIcon:function(e){var n,o=e.item,a=e.index;return r.createElement(r.Fragment,null,o.__docsearch_parent&&r.createElement("svg",{className:"DocSearch-Hit-Tree",viewBox:"0 0 24 54"},r.createElement("g",{stroke:"currentColor",fill:"none",fillRule:"evenodd",strokeLinecap:"round",strokeLinejoin:"round"},o.__docsearch_parent!==(null===(n=t.items[a+1])||void 0===n?void 0:n.__docsearch_parent)?r.createElement("path",{d:"M8 6v21M20 27H8.3"}):r.createElement("path",{d:"M8 6v42M20 27H8.3"}))),r.createElement("div",{className:"DocSearch-Hit-icon"},r.createElement(Pt,{type:o.type})))},renderAction:function(){return r.createElement("div",{className:"DocSearch-Hit-action"},r.createElement(jt,null))}}))})),e.resultsFooterComponent&&r.createElement("section",{className:"DocSearch-HitsFooter"},r.createElement(e.resultsFooterComponent,{state:e.state})))}var Jt=["translations"];function en(e){var t=e.translations,n=void 0===t?{}:t,o=s(e,Jt),a=n.recentSearchesTitle,i=void 0===a?"Recent":a,c=n.noRecentSearchesText,u=void 0===c?"No recent searches":c,d=n.saveRecentSearchButtonTitle,p=void 0===d?"Save this search":d,f=n.removeRecentSearchButtonTitle,m=void 0===f?"Remove this search from history":f,h=n.favoriteSearchesTitle,g=void 0===h?"Favorite":h,b=n.removeFavoriteSearchButtonTitle,v=void 0===b?"Remove this search from favorites":b;return"idle"===o.state.status&&!1===o.hasCollections?o.disableUserPersonalization?null:r.createElement("div",{className:"DocSearch-StartScreen"},r.createElement("p",{className:"DocSearch-Help"},u)):!1===o.hasCollections?null:r.createElement("div",{className:"DocSearch-Dropdown-Container"},r.createElement(Ht,l({},o,{title:i,collection:o.state.collections[0],renderIcon:function(){return r.createElement("div",{className:"DocSearch-Hit-icon"},r.createElement(Ct,null))},renderAction:function(e){var t=e.item,n=e.runFavoriteTransition,a=e.runDeleteTransition;return r.createElement(r.Fragment,null,r.createElement("div",{className:"DocSearch-Hit-action"},r.createElement("button",{className:"DocSearch-Hit-action-button",title:p,type:"submit",onClick:function(e){e.preventDefault(),e.stopPropagation(),n((function(){o.favoriteSearches.add(t),o.recentSearches.remove(t),o.refresh()}))}},r.createElement(Lt,null))),r.createElement("div",{className:"DocSearch-Hit-action"},r.createElement("button",{className:"DocSearch-Hit-action-button",title:m,type:"submit",onClick:function(e){e.preventDefault(),e.stopPropagation(),a((function(){o.recentSearches.remove(t),o.refresh()}))}},r.createElement(At,null))))}})),r.createElement(Ht,l({},o,{title:g,collection:o.state.collections[1],renderIcon:function(){return r.createElement("div",{className:"DocSearch-Hit-icon"},r.createElement(Lt,null))},renderAction:function(e){var t=e.item,n=e.runDeleteTransition;return r.createElement("div",{className:"DocSearch-Hit-action"},r.createElement("button",{className:"DocSearch-Hit-action-button",title:v,type:"submit",onClick:function(e){e.preventDefault(),e.stopPropagation(),n((function(){o.favoriteSearches.remove(t),o.refresh()}))}},r.createElement(At,null)))}})))}var tn=["translations"],nn=r.memo((function(e){var t=e.translations,n=void 0===t?{}:t,o=s(e,tn);if("error"===o.state.status)return r.createElement(Mt,{translations:null==n?void 0:n.errorScreen});var a=o.state.collections.some((function(e){return e.items.length>0}));return o.state.query?!1===a?r.createElement(Bt,l({},o,{translations:null==n?void 0:n.noResultsScreen})):r.createElement(Xt,o):r.createElement(en,l({},o,{hasCollections:a,translations:null==n?void 0:n.startScreen}))}),(function(e,t){return"loading"===t.state.status||"stalled"===t.state.status})),rn=["translations"];function on(e){var t=e.translations,n=void 0===t?{}:t,o=s(e,rn),a=n.resetButtonTitle,i=void 0===a?"Clear the query":a,c=n.resetButtonAriaLabel,u=void 0===c?"Clear the query":c,d=n.cancelButtonText,p=void 0===d?"Cancel":d,f=n.cancelButtonAriaLabel,h=void 0===f?"Cancel":f,g=n.searchInputLabel,b=void 0===g?"Search":g,v=o.getFormProps({inputElement:o.inputRef.current}).onReset;return r.useEffect((function(){o.autoFocus&&o.inputRef.current&&o.inputRef.current.focus()}),[o.autoFocus,o.inputRef]),r.useEffect((function(){o.isFromSelection&&o.inputRef.current&&o.inputRef.current.select()}),[o.isFromSelection,o.inputRef]),r.createElement(r.Fragment,null,r.createElement("form",{className:"DocSearch-Form",onSubmit:function(e){e.preventDefault()},onReset:v},r.createElement("label",l({className:"DocSearch-MagnifierLabel"},o.getLabelProps()),r.createElement(m,null),r.createElement("span",{className:"DocSearch-VisuallyHiddenForAccessibility"},b)),r.createElement("div",{className:"DocSearch-LoadingIndicator"},r.createElement(Ot,null)),r.createElement("input",l({className:"DocSearch-Input",ref:o.inputRef},o.getInputProps({inputElement:o.inputRef.current,autoFocus:o.autoFocus,maxLength:64}))),r.createElement("button",{type:"reset",title:i,className:"DocSearch-Reset","aria-label":u,hidden:!o.state.query},r.createElement(At,null))),r.createElement("button",{className:"DocSearch-Cancel",type:"reset","aria-label":h,onClick:o.onClose},p))}var an=["_highlightResult","_snippetResult"];function ln(e){var t=e.key,n=e.limit,r=void 0===n?5:n,o=function(e){return!1===function(){var e="__TEST_KEY__";try{return localStorage.setItem(e,""),localStorage.removeItem(e),!0}catch(e){return!1}}()?{setItem:function(){},getItem:function(){return[]}}:{setItem:function(t){return window.localStorage.setItem(e,JSON.stringify(t))},getItem:function(){var t=window.localStorage.getItem(e);return t?JSON.parse(t):[]}}}(t),a=o.getItem().slice(0,r);return{add:function(e){var t=e,n=(t._highlightResult,t._snippetResult,s(t,an)),i=a.findIndex((function(e){return e.objectID===n.objectID}));i>-1&&a.splice(i,1),a.unshift(n),a=a.slice(0,r),o.setItem(a)},remove:function(e){a=a.filter((function(t){return t.objectID!==e.objectID})),o.setItem(a)},getAll:function(){return a}}}function sn(e){const t=`algoliasearch-client-js-${e.key}`;let n;const r=()=>(void 0===n&&(n=e.localStorage||window.localStorage),n),o=()=>JSON.parse(r().getItem(t)||"{}"),a=e=>{r().setItem(t,JSON.stringify(e))};return{get:(t,n,r={miss:()=>Promise.resolve()})=>Promise.resolve().then((()=>{(()=>{const t=e.timeToLive?1e3*e.timeToLive:null,n=o(),r=Object.fromEntries(Object.entries(n).filter((([,e])=>void 0!==e.timestamp)));if(a(r),!t)return;const i=Object.fromEntries(Object.entries(r).filter((([,e])=>{const n=(new Date).getTime();return!(e.timestamp+tPromise.all([e?e.value:n(),void 0!==e]))).then((([e,t])=>Promise.all([e,t||r.miss(e)]))).then((([e])=>e)),set:(e,n)=>Promise.resolve().then((()=>{const a=o();return a[JSON.stringify(e)]={timestamp:(new Date).getTime(),value:n},r().setItem(t,JSON.stringify(a)),n})),delete:e=>Promise.resolve().then((()=>{const n=o();delete n[JSON.stringify(e)],r().setItem(t,JSON.stringify(n))})),clear:()=>Promise.resolve().then((()=>{r().removeItem(t)}))}}function cn(e){const t=[...e.caches],n=t.shift();return void 0===n?{get:(e,t,n={miss:()=>Promise.resolve()})=>t().then((e=>Promise.all([e,n.miss(e)]))).then((([e])=>e)),set:(e,t)=>Promise.resolve(t),delete:e=>Promise.resolve(),clear:()=>Promise.resolve()}:{get:(e,r,o={miss:()=>Promise.resolve()})=>n.get(e,r,o).catch((()=>cn({caches:t}).get(e,r,o))),set:(e,r)=>n.set(e,r).catch((()=>cn({caches:t}).set(e,r))),delete:e=>n.delete(e).catch((()=>cn({caches:t}).delete(e))),clear:()=>n.clear().catch((()=>cn({caches:t}).clear()))}}function un(e={serializable:!0}){let t={};return{get(n,r,o={miss:()=>Promise.resolve()}){const a=JSON.stringify(n);if(a in t)return Promise.resolve(e.serializable?JSON.parse(t[a]):t[a]);const i=r(),l=o&&o.miss||(()=>Promise.resolve());return i.then((e=>l(e))).then((()=>i))},set:(n,r)=>(t[JSON.stringify(n)]=e.serializable?JSON.stringify(r):r,Promise.resolve(r)),delete:e=>(delete t[JSON.stringify(e)],Promise.resolve()),clear:()=>(t={},Promise.resolve())}}function dn(e){let t=e.length-1;for(;t>0;t--){const n=Math.floor(Math.random()*(t+1)),r=e[t];e[t]=e[n],e[n]=r}return e}function pn(e,t){return t?(Object.keys(t).forEach((n=>{e[n]=t[n](e)})),e):e}function fn(e,...t){let n=0;return e.replace(/%s/g,(()=>encodeURIComponent(t[n++])))}const mn={WithinQueryParameters:0,WithinHeaders:1};function hn(e,t){const n=e||{},r=n.data||{};return Object.keys(n).forEach((e=>{-1===["timeout","headers","queryParameters","data","cacheable"].indexOf(e)&&(r[e]=n[e])})),{data:Object.entries(r).length>0?r:void 0,timeout:n.timeout||t,headers:n.headers||{},queryParameters:n.queryParameters||{},cacheable:n.cacheable}}const gn={Read:1,Write:2,Any:3},bn=1,vn=3;function yn(e,t=bn){return{...e,status:t,lastUpdate:Date.now()}}function wn(e){return"string"==typeof e?{protocol:"https",url:e,accept:gn.Any}:{protocol:e.protocol||"https",url:e.url,accept:e.accept||gn.Any}}const Sn="GET",kn="POST";function xn(e,t,n,r){const o=[],a=function(e,t){if(e.method===Sn||void 0===e.data&&void 0===t.data)return;const n=Array.isArray(e.data)?e.data:{...e.data,...t.data};return JSON.stringify(n)}(n,r),i=function(e,t){const n={...e.headers,...t.headers},r={};return Object.keys(n).forEach((e=>{const t=n[e];r[e.toLowerCase()]=t})),r}(e,r),l=n.method,s=n.method!==Sn?{}:{...n.data,...r.data},c={"x-algolia-agent":e.userAgent.value,...e.queryParameters,...s,...r.queryParameters};let u=0;const d=(t,s)=>{const p=t.pop();if(void 0===p)throw{name:"RetryError",message:"Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.",transporterStackTrace:Cn(o)};const f={data:a,headers:i,method:l,url:_n(p,n.path,c),connectTimeout:s(u,e.timeouts.connect),responseTimeout:s(u,r.timeout)},m=e=>{const n={request:f,response:e,host:p,triesLeft:t.length};return o.push(n),n},h={onSuccess:e=>function(e){try{return JSON.parse(e.content)}catch(t){throw function(e,t){return{name:"DeserializationError",message:e,response:t}}(t.message,e)}}(e),onRetry(n){const r=m(n);return n.isTimedOut&&u++,Promise.all([e.logger.info("Retryable failure",An(r)),e.hostsCache.set(p,yn(p,n.isTimedOut?vn:2))]).then((()=>d(t,s)))},onFail(e){throw m(e),function({content:e,status:t},n){let r=e;try{r=JSON.parse(e).message}catch(e){}return function(e,t,n){return{name:"ApiError",message:e,status:t,transporterStackTrace:n}}(r,t,n)}(e,Cn(o))}};return e.requester.send(f).then((e=>((e,t)=>(e=>{const t=e.status;return e.isTimedOut||(({isTimedOut:e,status:t})=>!e&&!~~t)(e)||2!=~~(t/100)&&4!=~~(t/100)})(e)?t.onRetry(e):(({status:e})=>2==~~(e/100))(e)?t.onSuccess(e):t.onFail(e))(e,h)))};return function(e,t){return Promise.all(t.map((t=>e.get(t,(()=>Promise.resolve(yn(t))))))).then((e=>{const n=e.filter((e=>function(e){return e.status===bn||Date.now()-e.lastUpdate>12e4}(e))),r=e.filter((e=>function(e){return e.status===vn&&Date.now()-e.lastUpdate<=12e4}(e))),o=[...n,...r];return{getTimeout:(e,t)=>(0===r.length&&0===e?1:r.length+3+e)*t,statelessHosts:o.length>0?o.map((e=>wn(e))):t}}))}(e.hostsCache,t).then((e=>d([...e.statelessHosts].reverse(),e.getTimeout)))}function En(e){const t={value:`Algolia for JavaScript (${e})`,add(e){const n=`; ${e.segment}${void 0!==e.version?` (${e.version})`:""}`;return-1===t.value.indexOf(n)&&(t.value=`${t.value}${n}`),t}};return t}function _n(e,t,n){const r=On(n);let o=`${e.protocol}://${e.url}/${"/"===t.charAt(0)?t.substr(1):t}`;return r.length&&(o+=`?${r}`),o}function On(e){return Object.keys(e).map((t=>{return fn("%s=%s",t,(n=e[t],"[object Object]"===Object.prototype.toString.call(n)||"[object Array]"===Object.prototype.toString.call(n)?JSON.stringify(e[t]):e[t]));var n})).join("&")}function Cn(e){return e.map((e=>An(e)))}function An(e){const t=e.request.headers["x-algolia-api-key"]?{"x-algolia-api-key":"*****"}:{};return{...e,request:{...e.request,headers:{...e.request.headers,...t}}}}const jn=e=>{const t=e.appId,n=function(e,t,n){const r={"x-algolia-api-key":n,"x-algolia-application-id":t};return{headers:()=>e===mn.WithinHeaders?r:{},queryParameters:()=>e===mn.WithinQueryParameters?r:{}}}(void 0!==e.authMode?e.authMode:mn.WithinHeaders,t,e.apiKey),r=function(e){const{hostsCache:t,logger:n,requester:r,requestsCache:o,responsesCache:a,timeouts:i,userAgent:l,hosts:s,queryParameters:c,headers:u}=e,d={hostsCache:t,logger:n,requester:r,requestsCache:o,responsesCache:a,timeouts:i,userAgent:l,headers:u,queryParameters:c,hosts:s.map((e=>wn(e))),read(e,t){const n=hn(t,d.timeouts.read),r=()=>xn(d,d.hosts.filter((e=>!!(e.accept&gn.Read))),e,n);if(!0!==(void 0!==n.cacheable?n.cacheable:e.cacheable))return r();const o={request:e,mappedRequestOptions:n,transporter:{queryParameters:d.queryParameters,headers:d.headers}};return d.responsesCache.get(o,(()=>d.requestsCache.get(o,(()=>d.requestsCache.set(o,r()).then((e=>Promise.all([d.requestsCache.delete(o),e])),(e=>Promise.all([d.requestsCache.delete(o),Promise.reject(e)]))).then((([e,t])=>t))))),{miss:e=>d.responsesCache.set(o,e)})},write:(e,t)=>xn(d,d.hosts.filter((e=>!!(e.accept&gn.Write))),e,hn(t,d.timeouts.write))};return d}({hosts:[{url:`${t}-dsn.algolia.net`,accept:gn.Read},{url:`${t}.algolia.net`,accept:gn.Write}].concat(dn([{url:`${t}-1.algolianet.com`},{url:`${t}-2.algolianet.com`},{url:`${t}-3.algolianet.com`}])),...e,headers:{...n.headers(),"content-type":"application/x-www-form-urlencoded",...e.headers},queryParameters:{...n.queryParameters(),...e.queryParameters}}),o={transporter:r,appId:t,addAlgoliaAgent(e,t){r.userAgent.add({segment:e,version:t})},clearCache:()=>Promise.all([r.requestsCache.clear(),r.responsesCache.clear()]).then((()=>{}))};return pn(o,e.methods)},Tn=e=>(t,n)=>t.method===Sn?e.transporter.read(t,n):e.transporter.write(t,n),Pn=e=>(t,n={})=>pn({transporter:e.transporter,appId:e.appId,indexName:t},n.methods),In=e=>(t,n)=>{const r=t.map((e=>({...e,params:On(e.params||{})})));return e.transporter.read({method:kn,path:"1/indexes/*/queries",data:{requests:r},cacheable:!0},n)},Nn=e=>(t,n)=>Promise.all(t.map((t=>{const{facetName:r,facetQuery:o,...a}=t.params;return Pn(e)(t.indexName,{methods:{searchForFacetValues:Dn}}).searchForFacetValues(r,o,{...n,...a})}))),Ln=e=>(t,n,r)=>e.transporter.read({method:kn,path:fn("1/answers/%s/prediction",e.indexName),data:{query:t,queryLanguages:n},cacheable:!0},r),Rn=e=>(t,n)=>e.transporter.read({method:kn,path:fn("1/indexes/%s/query",e.indexName),data:{query:t},cacheable:!0},n),Dn=e=>(t,n,r)=>e.transporter.read({method:kn,path:fn("1/indexes/%s/facets/%s/query",e.indexName,t),data:{facetQuery:n},cacheable:!0},r),Mn=1,Fn=2,Bn=3;function zn(e,t,n){const r={appId:e,apiKey:t,timeouts:{connect:1,read:2,write:30},requester:{send:e=>new Promise((t=>{const n=new XMLHttpRequest;n.open(e.method,e.url,!0),Object.keys(e.headers).forEach((t=>n.setRequestHeader(t,e.headers[t])));const r=(e,r)=>setTimeout((()=>{n.abort(),t({status:0,content:r,isTimedOut:!0})}),1e3*e),o=r(e.connectTimeout,"Connection timeout");let a;n.onreadystatechange=()=>{n.readyState>n.OPENED&&void 0===a&&(clearTimeout(o),a=r(e.responseTimeout,"Socket timeout"))},n.onerror=()=>{0===n.status&&(clearTimeout(o),clearTimeout(a),t({content:n.responseText||"Network request failed",status:n.status,isTimedOut:!1}))},n.onload=()=>{clearTimeout(o),clearTimeout(a),t({content:n.responseText,status:n.status,isTimedOut:!1})},n.send(e.data)}))},logger:(o=Bn,{debug:(e,t)=>(Mn>=o&&console.debug(e,t),Promise.resolve()),info:(e,t)=>(Fn>=o&&console.info(e,t),Promise.resolve()),error:(e,t)=>(console.error(e,t),Promise.resolve())}),responsesCache:un(),requestsCache:un({serializable:!1}),hostsCache:cn({caches:[sn({key:`4.19.1-${e}`}),un()]}),userAgent:En("4.19.1").add({segment:"Browser",version:"lite"}),authMode:mn.WithinQueryParameters};var o;return jn({...r,...n,methods:{search:In,searchForFacetValues:Nn,multipleQueries:In,multipleSearchForFacetValues:Nn,customRequest:Tn,initIndex:e=>t=>Pn(e)(t,{methods:{search:Rn,searchForFacetValues:Dn,findAnswers:Ln}})}})}zn.version="4.19.1";var $n=["footer","searchBox"];function Un(e){var t=e.appId,n=e.apiKey,o=e.indexName,i=e.placeholder,u=void 0===i?"Search docs":i,d=e.searchParameters,p=e.maxResultsPerGroup,f=e.onClose,m=void 0===f?Gt:f,h=e.transformItems,g=void 0===h?Wt:h,b=e.hitComponent,v=void 0===b?_t:b,y=e.resultsFooterComponent,w=void 0===y?function(){return null}:y,S=e.navigator,k=e.initialScrollY,x=void 0===k?0:k,E=e.transformSearchClient,_=void 0===E?Wt:E,O=e.disableUserPersonalization,C=void 0!==O&&O,A=e.initialQuery,j=void 0===A?"":A,T=e.translations,P=void 0===T?{}:T,I=e.getMissingResultsUrl,N=e.insights,L=void 0!==N&&N,R=P.footer,D=P.searchBox,M=s(P,$n),F=c(r.useState({query:"",collections:[],completion:null,context:{},isOpen:!1,activeItemId:null,status:"idle"}),2),B=F[0],z=F[1],$=r.useRef(null),U=r.useRef(null),H=r.useRef(null),q=r.useRef(null),V=r.useRef(null),W=r.useRef(10),K=r.useRef("undefined"!=typeof window?window.getSelection().toString().slice(0,64):"").current,G=r.useRef(j||K).current,Q=function(e,t,n){return r.useMemo((function(){var r=zn(e,t);return r.addAlgoliaAgent("docsearch","3.6.1"),!1===/docsearch.js \(.*\)/.test(r.transporter.userAgent.value)&&r.addAlgoliaAgent("docsearch-react","3.6.1"),n(r)}),[e,t,n])}(t,n,_),Y=r.useRef(ln({key:"__DOCSEARCH_FAVORITE_SEARCHES__".concat(o),limit:10})).current,Z=r.useRef(ln({key:"__DOCSEARCH_RECENT_SEARCHES__".concat(o),limit:0===Y.getAll().length?7:4})).current,X=r.useCallback((function(e){if(!C){var t="content"===e.type?e.__docsearch_parent:e;t&&-1===Y.getAll().findIndex((function(e){return e.objectID===t.objectID}))&&Z.add(t)}}),[Y,Z,C]),J=r.useCallback((function(e){if(B.context.algoliaInsightsPlugin&&e.__autocomplete_id){var t=e,n={eventName:"Item Selected",index:t.__autocomplete_indexName,items:[t],positions:[e.__autocomplete_id],queryID:t.__autocomplete_queryID};B.context.algoliaInsightsPlugin.insights.clickedObjectIDsAfterSearch(n)}}),[B.context.algoliaInsightsPlugin]),ee=r.useMemo((function(){return St({id:"docsearch",defaultActiveItemId:0,placeholder:u,openOnFocus:!0,initialState:{query:G,context:{searchSuggestions:[]}},insights:L,navigator:S,onStateChange:function(e){z(e.state)},getSources:function(e){var r=e.query,i=e.state,l=e.setContext,s=e.setStatus;if(!r)return C?[]:[{sourceId:"recentSearches",onSelect:function(e){var t=e.item,n=e.event;X(t),Kt(n)||m()},getItemUrl:function(e){return e.item.url},getItems:function(){return Z.getAll()}},{sourceId:"favoriteSearches",onSelect:function(e){var t=e.item,n=e.event;X(t),Kt(n)||m()},getItemUrl:function(e){return e.item.url},getItems:function(){return Y.getAll()}}];var c=Boolean(L);return Q.search([{query:r,indexName:o,params:a({attributesToRetrieve:["hierarchy.lvl0","hierarchy.lvl1","hierarchy.lvl2","hierarchy.lvl3","hierarchy.lvl4","hierarchy.lvl5","hierarchy.lvl6","content","type","url"],attributesToSnippet:["hierarchy.lvl1:".concat(W.current),"hierarchy.lvl2:".concat(W.current),"hierarchy.lvl3:".concat(W.current),"hierarchy.lvl4:".concat(W.current),"hierarchy.lvl5:".concat(W.current),"hierarchy.lvl6:".concat(W.current),"content:".concat(W.current)],snippetEllipsisText:"\u2026",highlightPreTag:"",highlightPostTag:"",hitsPerPage:20,clickAnalytics:c},d)}]).catch((function(e){throw"RetryError"===e.name&&s("error"),e})).then((function(e){var r=e.results[0],s=r.hits,u=r.nbHits,d=Vt(s,(function(e){return Zt(e)}),p);i.context.searchSuggestions.length0&&(re(),V.current&&V.current.focus())}),[G,re]),r.useEffect((function(){function e(){if(U.current){var e=.01*window.innerHeight;U.current.style.setProperty("--docsearch-vh","".concat(e,"px"))}}return e(),window.addEventListener("resize",e),function(){window.removeEventListener("resize",e)}}),[]),r.createElement("div",l({ref:$},ne({"aria-expanded":!0}),{className:["DocSearch","DocSearch-Container","stalled"===B.status&&"DocSearch-Container--Stalled","error"===B.status&&"DocSearch-Container--Errored"].filter(Boolean).join(" "),role:"button",tabIndex:0,onMouseDown:function(e){e.target===e.currentTarget&&m()}}),r.createElement("div",{className:"DocSearch-Modal",ref:U},r.createElement("header",{className:"DocSearch-SearchBar",ref:H},r.createElement(on,l({},ee,{state:B,autoFocus:0===G.length,inputRef:V,isFromSelection:Boolean(G)&&G===K,translations:D,onClose:m}))),r.createElement("div",{className:"DocSearch-Dropdown",ref:q},r.createElement(nn,l({},ee,{indexName:o,state:B,hitComponent:v,resultsFooterComponent:w,disableUserPersonalization:C,recentSearches:Z,favoriteSearches:Y,inputRef:V,translations:M,getMissingResultsUrl:I,onItemClick:function(e,t){J(e),X(e),Kt(t)||m()}}))),r.createElement("footer",{className:"DocSearch-Footer"},r.createElement(Et,{translations:R}))))}function Hn(e){var t=e.isOpen,n=e.onOpen,o=e.onClose,a=e.onInput,i=e.searchButtonRef;r.useEffect((function(){function e(e){var r;(27===e.keyCode&&t||"k"===(null===(r=e.key)||void 0===r?void 0:r.toLowerCase())&&(e.metaKey||e.ctrlKey)||!function(e){var t=e.target,n=t.tagName;return t.isContentEditable||"INPUT"===n||"SELECT"===n||"TEXTAREA"===n}(e)&&"/"===e.key&&!t)&&(e.preventDefault(),t?o():document.body.classList.contains("DocSearch--active")||document.body.classList.contains("DocSearch--active")||n()),i&&i.current===document.activeElement&&a&&/[a-zA-Z0-9]/.test(String.fromCharCode(e.keyCode))&&a(e)}return window.addEventListener("keydown",e),function(){window.removeEventListener("keydown",e)}}),[t,n,o,a,i])}},8328:(e,t,n)=>{"use strict";n.d(t,{A:()=>p});n(6540);var r=n(3259),o=n.n(r),a=n(4054);const i={"0058b4c6":[()=>n.e(849).then(n.t.bind(n,6164,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-175.json",6164],"015df829":[()=>n.e(4372).then(n.bind(n,1393)),"@site/blog/2024-08-27-challenges-of-microservices.md",1393],"01a85c17":[()=>Promise.all([n.e(1869),n.e(8209)]).then(n.bind(n,9158)),"@theme/BlogTagsListPage",9158],"03cca2c9":[()=>Promise.all([n.e(1869),n.e(8087)]).then(n.bind(n,2889)),"@site/docs/05-developer-guide/09-grpc/40-advanced/00-await-workflow-events.md",2889],"0bd106d9":[()=>Promise.all([n.e(1869),n.e(9371)]).then(n.bind(n,3193)),"@site/docs/05-developer-guide/08-wfspec-development/02-conditionals.md",3193],"0fad6d0a":[()=>n.e(1173).then(n.t.bind(n,7700,19)),"@generated/docusaurus-plugin-content-blog/default/p/blog-authors-coltmcnealy-4ff.json",7700],"141346fb":[()=>n.e(4098).then(n.bind(n,3910)),"@site/blog/2024-09-04-basics-of-workflows.md",3910],"150c26e9":[()=>Promise.all([n.e(1869),n.e(4792)]).then(n.bind(n,4591)),"@site/docs/05-developer-guide/09-grpc/05-managing-metadata.md",4591],17896441:[()=>Promise.all([n.e(1869),n.e(8498),n.e(8401)]).then(n.bind(n,575)),"@theme/DocItem",575],"1a4e3797":[()=>Promise.all([n.e(1869),n.e(2138)]).then(n.bind(n,673)),"@theme/SearchPage",673],"1d85cfc3":[()=>n.e(9964).then(n.bind(n,6172)),"@site/blog/2024-07-12-0.10-release.md?truncated=true",6172],"1e599e21":[()=>n.e(4198).then(n.bind(n,8289)),"@site/docs/05-developer-guide/09-grpc/40-advanced/40-advanced.md",8289],"1f391b9e":[()=>Promise.all([n.e(1869),n.e(8498),n.e(6061)]).then(n.bind(n,7973)),"@theme/MDXPage",7973],"209570fd":[()=>n.e(312).then(n.bind(n,5321)),"@site/blog/2024-08-31-0.11-release.md?truncated=true",5321],"23f588f5":[()=>n.e(7434).then(n.bind(n,4299)),"@site/blog/2023-08-30-0.2.0-release.md",4299],"2494aaaa":[()=>n.e(6958).then(n.bind(n,1511)),"@site/blog/2024-08-22-promise-of-microservices.md",1511],"24a81a13":[()=>n.e(6848).then(n.bind(n,2509)),"@site/docs/06-operations/03-client-configuration.md",2509],"2a162317":[()=>n.e(7828).then(n.bind(n,4620)),"@site/docs/08-api.md",4620],"32279f3c":[()=>n.e(1634).then(n.bind(n,3715)),"@site/blog/2024-09-24-saga-pattern.md?truncated=true",3715],"33fc5bb8":[()=>Promise.all([n.e(1869),n.e(8498),n.e(3347),n.e(867)]).then(n.bind(n,778)),"@theme/Blog/Pages/BlogAuthorsPostsPage",778],"366d7c2c":[()=>n.e(4325).then(n.t.bind(n,6552,19)),"@generated/docusaurus-plugin-content-blog/default/p/blog-authors-mitchellh-454.json",6552],"36994c47":[()=>n.e(9858).then(n.t.bind(n,5516,19)),"@generated/docusaurus-plugin-content-blog/default/__plugin.json",5516],"393be207":[()=>n.e(4134).then(n.bind(n,633)),"@site/src/pages/markdown-page.md",633],"3a2db09e":[()=>n.e(8121).then(n.t.bind(n,8070,19)),"@generated/docusaurus-plugin-content-blog/default/p/blog-tags-df9.json",8070],"477ea5a4":[()=>n.e(9993).then(n.bind(n,7945)),"@site/docs/06-operations/02-dashboard-configuration.md",7945],"4781d3d2":[()=>n.e(8892).then(n.bind(n,8969)),"@site/blog/2024-08-22-promise-of-microservices.md?truncated=true",8969],"49cd91b3":[()=>n.e(5808).then(n.bind(n,6422)),"@site/docs/05-developer-guide/05-developer-guide.md",6422],"4bc002f8":[()=>Promise.all([n.e(1869),n.e(8488)]).then(n.bind(n,6603)),"@site/docs/05-developer-guide/08-wfspec-development/20-advanced/10-throwing-events.md",6603],"4ced973d":[()=>n.e(8302).then(n.bind(n,7601)),"@site/blog/2024-10-28-queuing.md?truncated=true",7601],"524a0089":[()=>n.e(4982).then(n.bind(n,8588)),"@site/docs/04-concepts/04-external-events.md",8588],"5263bac3":[()=>n.e(7962).then(n.bind(n,240)),"@site/docs/05-developer-guide/08-wfspec-development/08-wfspec-development.md",240],"57cb429f":[()=>n.e(2775).then(n.bind(n,2879)),"@site/blog/2023-09-08-0.5.0-release.md?truncated=true",2879],"59f5fc7c":[()=>n.e(9905).then(n.bind(n,5283)),"@site/docs/06-operations/01-server-configuration.md",5283],"5a5f8fd5":[()=>n.e(8035).then(n.t.bind(n,4884,19)),"@generated/docusaurus-plugin-content-blog/default/p/blog-tags-littlehorse-67f.json",4884],"5a6d38a2":[()=>Promise.all([n.e(1869),n.e(9291)]).then(n.bind(n,3972)),"@site/docs/05-developer-guide/08-wfspec-development/03-mutating-variables.md",3972],"5af1e9f6":[()=>n.e(8648).then(n.bind(n,9451)),"@site/blog/2024-03-26-0.8.1-release.md",9451],"5b3e9818":[()=>n.e(538).then(n.t.bind(n,5002,19)),"@generated/docusaurus-plugin-content-blog/default/p/blog-tags-integration-patterns-c03.json",5002],"5b77cc54":[()=>n.e(3457).then(n.t.bind(n,517,19)),"@generated/docusaurus-plugin-content-blog/default/p/blog-tags-microservice-and-workflow-c14.json",517],"5d0b0e70":[()=>n.e(1946).then(n.bind(n,4102)),"@site/docs/04-concepts/04-concepts.md",4102],"5e95c892":[()=>n.e(9647).then(n.bind(n,7121)),"@theme/DocsRoot",7121],"5e9f5e1a":[()=>Promise.resolve().then(n.bind(n,4784)),"@generated/docusaurus.config",4784],"621db11d":[()=>Promise.all([n.e(1869),n.e(3347),n.e(4212)]).then(n.bind(n,3250)),"@theme/Blog/Pages/BlogAuthorsListPage",3250],62796615:[()=>Promise.all([n.e(1869),n.e(9445)]).then(n.bind(n,1296)),"@site/docs/05-developer-guide/09-grpc/10-running-workflows.md",1296],"66680c26":[()=>n.e(1642).then(n.bind(n,9399)),"@site/docs/04-concepts/30-advanced/00-wfspec-versioning.md",9399],"6875c492":[()=>Promise.all([n.e(1869),n.e(8498),n.e(3347),n.e(4813)]).then(n.bind(n,3069)),"@theme/BlogTagsPostsPage",3069],"6ddfedc5":[()=>Promise.all([n.e(1869),n.e(6928)]).then(n.bind(n,5638)),"@site/docs/05-developer-guide/08-wfspec-development/08-user-tasks.md",5638],"6e1334f1":[()=>n.e(798).then(n.bind(n,8650)),"@site/blog/2024-10-28-queuing.md",8650],"6ed84113":[()=>Promise.all([n.e(1869),n.e(6417)]).then(n.bind(n,7194)),"@site/docs/05-developer-guide/02-client-configuration.md",7194],"74974c25":[()=>n.e(8090).then(n.bind(n,8805)),"@site/docs/07-faq.md",8805],"7e3e0d30":[()=>n.e(7746).then(n.bind(n,7697)),"@site/docs/06-operations/10-docker-compose/15-three-servers.md",7697],"7f375326":[()=>n.e(1886).then(n.bind(n,1777)),"@site/blog/2024-01-28-0.7-release.md?truncated=true",1777],"7f97f38f":[()=>n.e(5739).then(n.bind(n,7027)),"@site/blog/2024-08-27-challenges-of-microservices.md?truncated=true",7027],"80b6e621":[()=>n.e(6309).then(n.bind(n,3308)),"@site/docs/04-concepts/03-tasks.md",3308],"814f3328":[()=>n.e(7472).then(n.t.bind(n,5513,19)),"~blog/default/blog-post-list-prop-default.json",5513],"81a46808":[()=>n.e(1715).then(n.bind(n,6112)),"@site/blog/2024-07-12-0.10-release.md",6112],83879120:[()=>Promise.all([n.e(1869),n.e(4723)]).then(n.bind(n,4327)),"@site/docs/05-developer-guide/08-wfspec-development/05-interrupts.md",4327],"845dc8ad":[()=>n.e(7506).then(n.bind(n,8156)),"@site/docs/04-concepts/13-principals-and-tenants.md",8156],84954295:[()=>n.e(7757).then(n.bind(n,6010)),"@site/blog/2024-09-04-basics-of-workflows.md?truncated=true",6010],"8bb7c884":[()=>n.e(3133).then(n.bind(n,3046)),"@site/docs/04-concepts/01-workflows.md",3046],"8bc86172":[()=>n.e(3828).then(n.bind(n,4298)),"@site/blog/2024-06-24-0.9.2-release.md",4298],"8dbae9ea":[()=>n.e(7316).then(n.bind(n,7693)),"@site/blog/2024-03-26-0.8.1-release.md?truncated=true",7693],"9215fec6":[()=>n.e(536).then(n.bind(n,8842)),"@site/docs/05-developer-guide/08-wfspec-development/20-advanced/20-advanced.md",8842],"971c8a60":[()=>n.e(3896).then(n.bind(n,1994)),"@site/docs/06-operations/10-docker-compose/10-docker-compose.md",1994],"9a24bfa0":[()=>Promise.all([n.e(1869),n.e(3372)]).then(n.bind(n,1867)),"@site/docs/05-developer-guide/08-wfspec-development/07-child-threads.md",1867],"9b53f530":[()=>n.e(4120).then(n.bind(n,7783)),"@site/docs/06-operations/10-docker-compose/05-confluent-cloud.md",7783],"9e4087bc":[()=>n.e(2711).then(n.bind(n,9331)),"@theme/BlogArchivePage",9331],"9e75eaed":[()=>Promise.all([n.e(1869),n.e(456)]).then(n.bind(n,4261)),"@site/docs/05-developer-guide/08-wfspec-development/20-advanced/00-wait-for-condition.md",4261],a06c0eb2:[()=>n.e(300).then(n.bind(n,4898)),"@site/docs/05-developer-guide/03-lhctl.md",4898],a1824316:[()=>Promise.all([n.e(1869),n.e(9045)]).then(n.bind(n,3307)),"@site/docs/05-developer-guide/08-wfspec-development/04-external-events.md",3307],a6aa9e1f:[()=>Promise.all([n.e(1869),n.e(8498),n.e(3347),n.e(7643)]).then(n.bind(n,5124)),"@theme/BlogListPage",5124],a7456010:[()=>n.e(1235).then(n.t.bind(n,8552,19)),"@generated/docusaurus-plugin-content-pages/default/__plugin.json",8552],a7bd4aaa:[()=>n.e(7098).then(n.bind(n,4532)),"@theme/DocVersionRoot",4532],a86a7ed2:[()=>n.e(7173).then(n.bind(n,3773)),"@site/blog/2023-08-30-0.2.0-release.md?truncated=true",3773],a94703ab:[()=>Promise.all([n.e(1869),n.e(9048)]).then(n.bind(n,1377)),"@theme/DocRoot",1377],a957f15c:[()=>n.e(5174).then(n.bind(n,488)),"@site/blog/2024-09-02-microservices-and-workflow.md?truncated=true",488],aba21aa0:[()=>n.e(5742).then(n.t.bind(n,7093,19)),"@generated/docusaurus-plugin-content-docs/default/__plugin.json",7093],acecf23e:[()=>n.e(1903).then(n.t.bind(n,1912,19)),"~blog/default/blogMetadata-default.json",1912],b136319f:[()=>n.e(7762).then(n.bind(n,7393)),"@site/blog/2024-09-24-saga-pattern.md",7393],b1ad0a29:[()=>n.e(9685).then(n.bind(n,3095)),"@site/blog/2024-08-31-0.11-release.md",3095],b23d33c8:[()=>n.e(1561).then(n.bind(n,998)),"@site/docs/04-concepts/05-user-tasks.md",998],b770e739:[()=>n.e(9615).then(n.bind(n,7100)),"@site/blog/2024-09-30-transactional-outbox.md?truncated=true",7100],bd28587a:[()=>n.e(3408).then(n.bind(n,2702)),"@site/blog/2024-06-24-0.9.2-release.md?truncated=true",2702],be5744c7:[()=>n.e(8046).then(n.bind(n,7915)),"@site/docs/04-concepts/30-advanced/30-advanced.md",7915],bead5408:[()=>n.e(3484).then(n.bind(n,5076)),"@site/docs/05-developer-guide/09-grpc/09-grpc.md",5076],c141421f:[()=>n.e(957).then(n.t.bind(n,936,19)),"@generated/docusaurus-theme-search-algolia/default/__plugin.json",936],c15d9823:[()=>n.e(8146).then(n.t.bind(n,9328,19)),"@generated/docusaurus-plugin-content-blog/default/p/blog-bd9.json",9328],c4f5d8e4:[()=>Promise.all([n.e(1869),n.e(2634)]).then(n.bind(n,2083)),"@site/src/pages/index.js",2083],c69aaf89:[()=>Promise.all([n.e(1869),n.e(3361)]).then(n.bind(n,2495)),"@site/docs/05-developer-guide/09-grpc/20-user-tasks.md",2495],c9736c35:[()=>Promise.all([n.e(1869),n.e(1872)]).then(n.bind(n,8023)),"@site/docs/05-developer-guide/05-task-worker-development.md",8023],ccc49370:[()=>Promise.all([n.e(1869),n.e(8498),n.e(3347),n.e(3249)]).then(n.bind(n,3858)),"@theme/BlogPostPage",3858],d0f4e7d1:[()=>n.e(2296).then(n.t.bind(n,2343,19)),"@generated/docusaurus-plugin-content-blog/default/p/blog-tags-release-bae.json",2343],d52af3ba:[()=>n.e(9403).then(n.bind(n,9333)),"@site/docs/06-operations/00-overview.md",9333],d5e335f6:[()=>n.e(7130).then(n.bind(n,6e3)),"@site/blog/2023-09-08-0.5.0-release.md",6e3],d61070d0:[()=>n.e(6583).then(n.bind(n,3535)),"@site/blog/2024-01-28-0.7-release.md",3535],d9c5bba9:[()=>n.e(7784).then(n.bind(n,9548)),"@site/docs/06-operations/10-docker-compose/00-basic.md",9548],db1cb5ce:[()=>n.e(2186).then(n.bind(n,1500)),"@site/docs/04-concepts/06-workflow-events.md",1500],dd97924f:[()=>Promise.all([n.e(1869),n.e(832)]).then(n.bind(n,8162)),"@site/docs/05-developer-guide/09-grpc/00-basics.md",8162],ddb4e1f1:[()=>n.e(7948).then(n.bind(n,7936)),"@site/docs/01-overview.md",7936],df0890b4:[()=>n.e(7080).then(n.t.bind(n,3362,19)),"@generated/docusaurus-plugin-content-blog/default/p/blog-tags-analysis-2d6.json",3362],e33a6d67:[()=>Promise.all([n.e(1869),n.e(3324)]).then(n.bind(n,7720)),"@site/docs/05-developer-guide/08-wfspec-development/01-basics.md",7720],e359e6f5:[()=>Promise.all([n.e(1869),n.e(3983)]).then(n.bind(n,3157)),"@site/docs/05-developer-guide/08-wfspec-development/06-exception-handling.md",3157],e3618154:[()=>n.e(826).then(n.bind(n,9100)),"@site/blog/2024-09-02-microservices-and-workflow.md",9100],eed23361:[()=>Promise.all([n.e(1869),n.e(3234)]).then(n.bind(n,3039)),"@site/docs/02-architecture-and-guarantees.md",3039],ef8b811a:[()=>n.e(8947).then(n.t.bind(n,6600,19)),"@generated/docusaurus-plugin-content-blog/default/p/blog-authors-790.json",6600],f13b4cb1:[()=>n.e(6051).then(n.t.bind(n,2349,19)),"@generated/docusaurus-plugin-content-blog/default/p/blog-authors-lh-council-1ba.json",2349],f21a8f01:[()=>n.e(2558).then(n.bind(n,6720)),"@site/blog/2024-09-30-transactional-outbox.md",6720],f23abd89:[()=>Promise.all([n.e(1869),n.e(216)]).then(n.bind(n,4689)),"@site/docs/05-developer-guide/00-install.md",4689],f447dd38:[()=>Promise.all([n.e(1869),n.e(4635)]).then(n.bind(n,323)),"@site/docs/05-developer-guide/09-grpc/15-posting-external-events.md",323],f81c1134:[()=>n.e(8130).then(n.t.bind(n,7735,19)),"@generated/docusaurus-plugin-content-blog/default/p/blog-archive-f05.json",7735],fb7e2344:[()=>n.e(7142).then(n.bind(n,8066)),"@site/docs/06-operations/06-operations.md",8066]};var l=n(4848);function s(e){let{error:t,retry:n,pastDelay:r}=e;return t?(0,l.jsxs)("div",{style:{textAlign:"center",color:"#fff",backgroundColor:"#fa383e",borderColor:"#fa383e",borderStyle:"solid",borderRadius:"0.25rem",borderWidth:"1px",boxSizing:"border-box",display:"block",padding:"1rem",flex:"0 0 50%",marginLeft:"25%",marginRight:"25%",marginTop:"5rem",maxWidth:"50%",width:"100%"},children:[(0,l.jsx)("p",{children:String(t)}),(0,l.jsx)("div",{children:(0,l.jsx)("button",{type:"button",onClick:n,children:"Retry"})})]}):r?(0,l.jsx)("div",{style:{display:"flex",justifyContent:"center",alignItems:"center",height:"100vh"},children:(0,l.jsx)("svg",{id:"loader",style:{width:128,height:110,position:"absolute",top:"calc(100vh - 64%)"},viewBox:"0 0 45 45",xmlns:"http://www.w3.org/2000/svg",stroke:"#61dafb",children:(0,l.jsxs)("g",{fill:"none",fillRule:"evenodd",transform:"translate(1 1)",strokeWidth:"2",children:[(0,l.jsxs)("circle",{cx:"22",cy:"22",r:"6",strokeOpacity:"0",children:[(0,l.jsx)("animate",{attributeName:"r",begin:"1.5s",dur:"3s",values:"6;22",calcMode:"linear",repeatCount:"indefinite"}),(0,l.jsx)("animate",{attributeName:"stroke-opacity",begin:"1.5s",dur:"3s",values:"1;0",calcMode:"linear",repeatCount:"indefinite"}),(0,l.jsx)("animate",{attributeName:"stroke-width",begin:"1.5s",dur:"3s",values:"2;0",calcMode:"linear",repeatCount:"indefinite"})]}),(0,l.jsxs)("circle",{cx:"22",cy:"22",r:"6",strokeOpacity:"0",children:[(0,l.jsx)("animate",{attributeName:"r",begin:"3s",dur:"3s",values:"6;22",calcMode:"linear",repeatCount:"indefinite"}),(0,l.jsx)("animate",{attributeName:"stroke-opacity",begin:"3s",dur:"3s",values:"1;0",calcMode:"linear",repeatCount:"indefinite"}),(0,l.jsx)("animate",{attributeName:"stroke-width",begin:"3s",dur:"3s",values:"2;0",calcMode:"linear",repeatCount:"indefinite"})]}),(0,l.jsx)("circle",{cx:"22",cy:"22",r:"8",children:(0,l.jsx)("animate",{attributeName:"r",begin:"0s",dur:"1.5s",values:"6;1;2;3;4;5;6",calcMode:"linear",repeatCount:"indefinite"})})]})})}):null}var c=n(6921),u=n(3102);function d(e,t){if("*"===e)return o()({loading:s,loader:()=>n.e(2237).then(n.bind(n,2237)),modules:["@theme/NotFound"],webpack:()=>[2237],render(e,t){const n=e.default;return(0,l.jsx)(u.W,{value:{plugin:{name:"native",id:"default"}},children:(0,l.jsx)(n,{...t})})}});const r=a[`${e}-${t}`],d={},p=[],f=[],m=(0,c.A)(r);return Object.entries(m).forEach((e=>{let[t,n]=e;const r=i[n];r&&(d[t]=r[0],p.push(r[1]),f.push(r[2]))})),o().Map({loading:s,loader:d,modules:p,webpack:()=>f,render(t,n){const o=JSON.parse(JSON.stringify(r));Object.entries(t).forEach((t=>{let[n,r]=t;const a=r.default;if(!a)throw new Error(`The page component at ${e} doesn't have a default export. This makes it impossible to render anything. Consider default-exporting a React component.`);"object"!=typeof a&&"function"!=typeof a||Object.keys(r).filter((e=>"default"!==e)).forEach((e=>{a[e]=r[e]}));let i=o;const l=n.split(".");l.slice(0,-1).forEach((e=>{i=i[e]})),i[l[l.length-1]]=a}));const a=o.__comp;delete o.__comp;const i=o.__context;delete o.__context;const s=o.__props;return delete o.__props,(0,l.jsx)(u.W,{value:i,children:(0,l.jsx)(a,{...o,...s,...n})})}})}const p=[{path:"/blog",component:d("/blog","e53"),exact:!0},{path:"/blog/archive",component:d("/blog/archive","182"),exact:!0},{path:"/blog/authors",component:d("/blog/authors","0b7"),exact:!0},{path:"/blog/authors/coltmcnealy",component:d("/blog/authors/coltmcnealy","d1c"),exact:!0},{path:"/blog/authors/lh-council",component:d("/blog/authors/lh-council","42e"),exact:!0},{path:"/blog/authors/mitchellh",component:d("/blog/authors/mitchellh","602"),exact:!0},{path:"/blog/basics-of-workflow",component:d("/blog/basics-of-workflow","828"),exact:!0},{path:"/blog/challenge-of-microservices",component:d("/blog/challenge-of-microservices","0e6"),exact:!0},{path:"/blog/littlehorse-0.10-release",component:d("/blog/littlehorse-0.10-release","309"),exact:!0},{path:"/blog/littlehorse-0.11-release",component:d("/blog/littlehorse-0.11-release","ff6"),exact:!0},{path:"/blog/littlehorse-0.2.0-release",component:d("/blog/littlehorse-0.2.0-release","e1a"),exact:!0},{path:"/blog/littlehorse-0.5.0-release",component:d("/blog/littlehorse-0.5.0-release","19d"),exact:!0},{path:"/blog/littlehorse-0.7-release",component:d("/blog/littlehorse-0.7-release","ca0"),exact:!0},{path:"/blog/littlehorse-0.8-release",component:d("/blog/littlehorse-0.8-release","4e9"),exact:!0},{path:"/blog/littlehorse-0.9-release",component:d("/blog/littlehorse-0.9-release","288"),exact:!0},{path:"/blog/microservices-and-workflow",component:d("/blog/microservices-and-workflow","bf7"),exact:!0},{path:"/blog/promise-of-microservices",component:d("/blog/promise-of-microservices","920"),exact:!0},{path:"/blog/queuing",component:d("/blog/queuing","fc7"),exact:!0},{path:"/blog/saga-pattern",component:d("/blog/saga-pattern","5e0"),exact:!0},{path:"/blog/tags",component:d("/blog/tags","287"),exact:!0},{path:"/blog/tags/analysis/",component:d("/blog/tags/analysis/","f53"),exact:!0},{path:"/blog/tags/integration-patterns/",component:d("/blog/tags/integration-patterns/","166"),exact:!0},{path:"/blog/tags/littlehorse/",component:d("/blog/tags/littlehorse/","6b3"),exact:!0},{path:"/blog/tags/microservice-and-workflow/",component:d("/blog/tags/microservice-and-workflow/","ca4"),exact:!0},{path:"/blog/tags/release/",component:d("/blog/tags/release/","42a"),exact:!0},{path:"/blog/transactional-outbox",component:d("/blog/transactional-outbox","6e7"),exact:!0},{path:"/markdown-page",component:d("/markdown-page","3d7"),exact:!0},{path:"/search",component:d("/search","5de"),exact:!0},{path:"/docs",component:d("/docs","f97"),routes:[{path:"/docs",component:d("/docs","6bf"),routes:[{path:"/docs",component:d("/docs","ae7"),routes:[{path:"/docs/api",component:d("/docs/api","8dc"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/architecture-and-guarantees",component:d("/docs/architecture-and-guarantees","a57"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/concepts/",component:d("/docs/concepts/","05c"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/concepts/advanced/",component:d("/docs/concepts/advanced/","1aa"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/concepts/advanced/wfspec-versioning",component:d("/docs/concepts/advanced/wfspec-versioning","c97"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/concepts/external-events",component:d("/docs/concepts/external-events","0bd"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/concepts/principals-and-tenants",component:d("/docs/concepts/principals-and-tenants","18d"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/concepts/tasks",component:d("/docs/concepts/tasks","6d5"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/concepts/user-tasks",component:d("/docs/concepts/user-tasks","648"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/concepts/workflow-events",component:d("/docs/concepts/workflow-events","483"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/concepts/workflows",component:d("/docs/concepts/workflows","2dd"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/",component:d("/docs/developer-guide/","30e"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/client-configuration",component:d("/docs/developer-guide/client-configuration","238"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/grpc/",component:d("/docs/developer-guide/grpc/","297"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/grpc/advanced/",component:d("/docs/developer-guide/grpc/advanced/","1cf"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/grpc/advanced/await-workflow-events",component:d("/docs/developer-guide/grpc/advanced/await-workflow-events","b92"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/grpc/basics",component:d("/docs/developer-guide/grpc/basics","1e2"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/grpc/managing-metadata",component:d("/docs/developer-guide/grpc/managing-metadata","d01"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/grpc/posting-external-events",component:d("/docs/developer-guide/grpc/posting-external-events","57f"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/grpc/running-workflows",component:d("/docs/developer-guide/grpc/running-workflows","dc9"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/grpc/user-tasks",component:d("/docs/developer-guide/grpc/user-tasks","be7"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/install",component:d("/docs/developer-guide/install","d47"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/lhctl",component:d("/docs/developer-guide/lhctl","809"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/task-worker-development",component:d("/docs/developer-guide/task-worker-development","168"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/wfspec-development/",component:d("/docs/developer-guide/wfspec-development/","db2"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/wfspec-development/advanced/",component:d("/docs/developer-guide/wfspec-development/advanced/","b4b"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/wfspec-development/advanced/throwing-events",component:d("/docs/developer-guide/wfspec-development/advanced/throwing-events","420"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/wfspec-development/advanced/wait-for-condition",component:d("/docs/developer-guide/wfspec-development/advanced/wait-for-condition","442"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/wfspec-development/basics",component:d("/docs/developer-guide/wfspec-development/basics","801"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/wfspec-development/child-threads",component:d("/docs/developer-guide/wfspec-development/child-threads","663"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/wfspec-development/conditionals",component:d("/docs/developer-guide/wfspec-development/conditionals","576"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/wfspec-development/exception-handling",component:d("/docs/developer-guide/wfspec-development/exception-handling","fde"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/wfspec-development/external-events",component:d("/docs/developer-guide/wfspec-development/external-events","302"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/wfspec-development/interrupts",component:d("/docs/developer-guide/wfspec-development/interrupts","d53"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/wfspec-development/mutating-variables",component:d("/docs/developer-guide/wfspec-development/mutating-variables","da1"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/developer-guide/wfspec-development/user-tasks",component:d("/docs/developer-guide/wfspec-development/user-tasks","8ee"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/faq",component:d("/docs/faq","47d"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/operations/",component:d("/docs/operations/","99e"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/operations/client-configuration",component:d("/docs/operations/client-configuration","6ce"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/operations/dashboard-configuration",component:d("/docs/operations/dashboard-configuration","138"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/operations/docker-compose/",component:d("/docs/operations/docker-compose/","691"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/operations/docker-compose/basic",component:d("/docs/operations/docker-compose/basic","cbb"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/operations/docker-compose/confluent-cloud",component:d("/docs/operations/docker-compose/confluent-cloud","7a0"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/operations/docker-compose/three-servers",component:d("/docs/operations/docker-compose/three-servers","813"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/operations/overview",component:d("/docs/operations/overview","2ba"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/operations/server-configuration",component:d("/docs/operations/server-configuration","43c"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/overview",component:d("/docs/overview","904"),exact:!0,sidebar:"tutorialSidebar"}]}]}]},{path:"/",component:d("/","2e1"),exact:!0},{path:"*",component:d("*")}]},6125:(e,t,n)=>{"use strict";n.d(t,{o:()=>a,x:()=>i});var r=n(6540),o=n(4848);const a=r.createContext(!1);function i(e){let{children:t}=e;const[n,i]=(0,r.useState)(!1);return(0,r.useEffect)((()=>{i(!0)}),[]),(0,o.jsx)(a.Provider,{value:n,children:t})}},8536:(e,t,n)=>{"use strict";var r=n(6540),o=n(5338),a=n(545),i=n(4625),l=n(4784),s=n(8193);const c=[n(1911),n(119),n(6134),n(6294),n(1043)];var u=n(8328),d=n(6347),p=n(2831),f=n(4848);function m(e){let{children:t}=e;return(0,f.jsx)(f.Fragment,{children:t})}var h=n(5260),g=n(4586),b=n(6025),v=n(6342),y=n(5500),w=n(2131),S=n(4090),k=n(2967),x=n(440),E=n(1463);function _(){const{i18n:{currentLocale:e,defaultLocale:t,localeConfigs:n}}=(0,g.A)(),r=(0,w.o)(),o=n[e].htmlLang,a=e=>e.replace("-","_");return(0,f.jsxs)(h.A,{children:[Object.entries(n).map((e=>{let[t,{htmlLang:n}]=e;return(0,f.jsx)("link",{rel:"alternate",href:r.createUrl({locale:t,fullyQualified:!0}),hrefLang:n},t)})),(0,f.jsx)("link",{rel:"alternate",href:r.createUrl({locale:t,fullyQualified:!0}),hrefLang:"x-default"}),(0,f.jsx)("meta",{property:"og:locale",content:a(o)}),Object.values(n).filter((e=>o!==e.htmlLang)).map((e=>(0,f.jsx)("meta",{property:"og:locale:alternate",content:a(e.htmlLang)},`meta-og-${e.htmlLang}`)))]})}function O(e){let{permalink:t}=e;const{siteConfig:{url:n}}=(0,g.A)(),r=function(){const{siteConfig:{url:e,baseUrl:t,trailingSlash:n}}=(0,g.A)(),{pathname:r}=(0,d.zy)();return e+(0,x.Ks)((0,b.Ay)(r),{trailingSlash:n,baseUrl:t})}(),o=t?`${n}${t}`:r;return(0,f.jsxs)(h.A,{children:[(0,f.jsx)("meta",{property:"og:url",content:o}),(0,f.jsx)("link",{rel:"canonical",href:o})]})}function C(){const{i18n:{currentLocale:e}}=(0,g.A)(),{metadata:t,image:n}=(0,v.p)();return(0,f.jsxs)(f.Fragment,{children:[(0,f.jsxs)(h.A,{children:[(0,f.jsx)("meta",{name:"twitter:card",content:"summary_large_image"}),(0,f.jsx)("body",{className:S.w})]}),n&&(0,f.jsx)(y.be,{image:n}),(0,f.jsx)(O,{}),(0,f.jsx)(_,{}),(0,f.jsx)(E.A,{tag:k.C,locale:e}),(0,f.jsx)(h.A,{children:t.map(((e,t)=>(0,f.jsx)("meta",{...e},t)))})]})}const A=new Map;var j=n(6125),T=n(6988),P=n(205);function I(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r{const r=t.default?.[e]??t[e];return r?.(...n)}));return()=>o.forEach((e=>e?.()))}const N=function(e){let{children:t,location:n,previousLocation:r}=e;return(0,P.A)((()=>{r!==n&&(!function(e){let{location:t,previousLocation:n}=e;if(!n)return;const r=t.pathname===n.pathname,o=t.hash===n.hash,a=t.search===n.search;if(r&&o&&!a)return;const{hash:i}=t;if(i){const e=decodeURIComponent(i.substring(1)),t=document.getElementById(e);t?.scrollIntoView()}else window.scrollTo(0,0)}({location:n,previousLocation:r}),I("onRouteDidUpdate",{previousLocation:r,location:n}))}),[r,n]),t};function L(e){const t=Array.from(new Set([e,decodeURI(e)])).map((e=>(0,p.u)(u.A,e))).flat();return Promise.all(t.map((e=>e.route.component.preload?.())))}class R extends r.Component{previousLocation;routeUpdateCleanupCb;constructor(e){super(e),this.previousLocation=null,this.routeUpdateCleanupCb=s.A.canUseDOM?I("onRouteUpdate",{previousLocation:null,location:this.props.location}):()=>{},this.state={nextRouteHasLoaded:!0}}shouldComponentUpdate(e,t){if(e.location===this.props.location)return t.nextRouteHasLoaded;const n=e.location;return this.previousLocation=this.props.location,this.setState({nextRouteHasLoaded:!1}),this.routeUpdateCleanupCb=I("onRouteUpdate",{previousLocation:this.previousLocation,location:n}),L(n.pathname).then((()=>{this.routeUpdateCleanupCb(),this.setState({nextRouteHasLoaded:!0})})).catch((e=>{console.warn(e),window.location.reload()})),!1}render(){const{children:e,location:t}=this.props;return(0,f.jsx)(N,{previousLocation:this.previousLocation,location:t,children:(0,f.jsx)(d.qh,{location:t,render:()=>e})})}}const D=R,M="__docusaurus-base-url-issue-banner-container",F="__docusaurus-base-url-issue-banner",B="__docusaurus-base-url-issue-banner-suggestion-container";function z(e){return`\ndocument.addEventListener('DOMContentLoaded', function maybeInsertBanner() {\n var shouldInsert = typeof window['docusaurus'] === 'undefined';\n shouldInsert && insertBanner();\n});\n\nfunction insertBanner() {\n var bannerContainer = document.createElement('div');\n bannerContainer.id = '${M}';\n var bannerHtml = ${JSON.stringify(function(e){return`\n
\n

Your Docusaurus site did not load properly.

\n

A very common reason is a wrong site baseUrl configuration.

\n

Current configured baseUrl = ${e} ${"/"===e?" (default value)":""}

\n

We suggest trying baseUrl =

\n
\n`}(e)).replace(/{let{route:t}=e;return!0===t.exact})))return A.set(e.pathname,e.pathname),e;const t=e.pathname.trim().replace(/(?:\/index)?\.html$/,"")||"/";return A.set(e.pathname,t),{...e,pathname:t}}((0,d.zy)());return(0,f.jsx)(D,{location:e,children:K})}function Q(){return(0,f.jsx)(q.A,{children:(0,f.jsx)(T.l,{children:(0,f.jsxs)(j.x,{children:[(0,f.jsxs)(m,{children:[(0,f.jsx)(H,{}),(0,f.jsx)(C,{}),(0,f.jsx)(U,{}),(0,f.jsx)(G,{})]}),(0,f.jsx)(W,{})]})})})}var Y=n(4054);const Z=function(e){try{return document.createElement("link").relList.supports(e)}catch{return!1}}("prefetch")?function(e){return new Promise(((t,n)=>{if("undefined"==typeof document)return void n();const r=document.createElement("link");r.setAttribute("rel","prefetch"),r.setAttribute("href",e),r.onload=()=>t(),r.onerror=()=>n();const o=document.getElementsByTagName("head")[0]??document.getElementsByName("script")[0]?.parentNode;o?.appendChild(r)}))}:function(e){return new Promise(((t,n)=>{const r=new XMLHttpRequest;r.open("GET",e,!0),r.withCredentials=!0,r.onload=()=>{200===r.status?t():n()},r.send(null)}))};var X=n(6921);const J=new Set,ee=new Set,te=()=>navigator.connection?.effectiveType.includes("2g")||navigator.connection?.saveData,ne={prefetch:e=>{if(!(e=>!te()&&!ee.has(e)&&!J.has(e))(e))return!1;J.add(e);const t=(0,p.u)(u.A,e).flatMap((e=>{return t=e.route.path,Object.entries(Y).filter((e=>{let[n]=e;return n.replace(/-[^-]+$/,"")===t})).flatMap((e=>{let[,t]=e;return Object.values((0,X.A)(t))}));var t}));return Promise.all(t.map((e=>{const t=n.gca(e);return t&&!t.includes("undefined")?Z(t).catch((()=>{})):Promise.resolve()})))},preload:e=>!!(e=>!te()&&!ee.has(e))(e)&&(ee.add(e),L(e))},re=Object.freeze(ne);function oe(e){let{children:t}=e;return"hash"===l.default.future.experimental_router?(0,f.jsx)(i.I9,{children:t}):(0,f.jsx)(i.Kd,{children:t})}const ae=Boolean(!0);if(s.A.canUseDOM){window.docusaurus=re;const e=document.getElementById("__docusaurus"),t=(0,f.jsx)(a.vd,{children:(0,f.jsx)(oe,{children:(0,f.jsx)(Q,{})})}),n=(e,t)=>{console.error("Docusaurus React Root onRecoverableError:",e,t)},i=()=>{if(window.docusaurusRoot)window.docusaurusRoot.render(t);else if(ae)window.docusaurusRoot=o.hydrateRoot(e,t,{onRecoverableError:n});else{const r=o.createRoot(e,{onRecoverableError:n});r.render(t),window.docusaurusRoot=r}};L(window.location.pathname).then((()=>{(0,r.startTransition)(i)}))}},6988:(e,t,n)=>{"use strict";n.d(t,{o:()=>d,l:()=>p});var r=n(6540),o=n(4784);const a=JSON.parse('{"docusaurus-plugin-content-docs":{"default":{"path":"/docs","versions":[{"name":"current","label":"Next","isLast":true,"path":"/docs","mainDocId":"overview","docs":[{"id":"api","path":"/docs/api","sidebar":"tutorialSidebar"},{"id":"architecture-and-guarantees","path":"/docs/architecture-and-guarantees","sidebar":"tutorialSidebar"},{"id":"concepts/advanced/advanced","path":"/docs/concepts/advanced/","sidebar":"tutorialSidebar"},{"id":"concepts/advanced/wfspec-versioning","path":"/docs/concepts/advanced/wfspec-versioning","sidebar":"tutorialSidebar"},{"id":"concepts/concepts","path":"/docs/concepts/","sidebar":"tutorialSidebar"},{"id":"concepts/external-events","path":"/docs/concepts/external-events","sidebar":"tutorialSidebar"},{"id":"concepts/principals-and-tenants","path":"/docs/concepts/principals-and-tenants","sidebar":"tutorialSidebar"},{"id":"concepts/tasks","path":"/docs/concepts/tasks","sidebar":"tutorialSidebar"},{"id":"concepts/user-tasks","path":"/docs/concepts/user-tasks","sidebar":"tutorialSidebar"},{"id":"concepts/workflow-events","path":"/docs/concepts/workflow-events","sidebar":"tutorialSidebar"},{"id":"concepts/workflows","path":"/docs/concepts/workflows","sidebar":"tutorialSidebar"},{"id":"developer-guide/client-configuration","path":"/docs/developer-guide/client-configuration","sidebar":"tutorialSidebar"},{"id":"developer-guide/developer-guide","path":"/docs/developer-guide/","sidebar":"tutorialSidebar"},{"id":"developer-guide/grpc/advanced/advanced","path":"/docs/developer-guide/grpc/advanced/","sidebar":"tutorialSidebar"},{"id":"developer-guide/grpc/advanced/await-workflow-events","path":"/docs/developer-guide/grpc/advanced/await-workflow-events","sidebar":"tutorialSidebar"},{"id":"developer-guide/grpc/basics","path":"/docs/developer-guide/grpc/basics","sidebar":"tutorialSidebar"},{"id":"developer-guide/grpc/grpc","path":"/docs/developer-guide/grpc/","sidebar":"tutorialSidebar"},{"id":"developer-guide/grpc/managing-metadata","path":"/docs/developer-guide/grpc/managing-metadata","sidebar":"tutorialSidebar"},{"id":"developer-guide/grpc/posting-external-events","path":"/docs/developer-guide/grpc/posting-external-events","sidebar":"tutorialSidebar"},{"id":"developer-guide/grpc/running-workflows","path":"/docs/developer-guide/grpc/running-workflows","sidebar":"tutorialSidebar"},{"id":"developer-guide/grpc/user-tasks","path":"/docs/developer-guide/grpc/user-tasks","sidebar":"tutorialSidebar"},{"id":"developer-guide/install","path":"/docs/developer-guide/install","sidebar":"tutorialSidebar"},{"id":"developer-guide/lhctl","path":"/docs/developer-guide/lhctl","sidebar":"tutorialSidebar"},{"id":"developer-guide/task-worker-development","path":"/docs/developer-guide/task-worker-development","sidebar":"tutorialSidebar"},{"id":"developer-guide/wfspec-development/advanced/advanced","path":"/docs/developer-guide/wfspec-development/advanced/","sidebar":"tutorialSidebar"},{"id":"developer-guide/wfspec-development/advanced/throwing-events","path":"/docs/developer-guide/wfspec-development/advanced/throwing-events","sidebar":"tutorialSidebar"},{"id":"developer-guide/wfspec-development/advanced/wait-for-condition","path":"/docs/developer-guide/wfspec-development/advanced/wait-for-condition","sidebar":"tutorialSidebar"},{"id":"developer-guide/wfspec-development/basics","path":"/docs/developer-guide/wfspec-development/basics","sidebar":"tutorialSidebar"},{"id":"developer-guide/wfspec-development/child-threads","path":"/docs/developer-guide/wfspec-development/child-threads","sidebar":"tutorialSidebar"},{"id":"developer-guide/wfspec-development/conditionals","path":"/docs/developer-guide/wfspec-development/conditionals","sidebar":"tutorialSidebar"},{"id":"developer-guide/wfspec-development/exception-handling","path":"/docs/developer-guide/wfspec-development/exception-handling","sidebar":"tutorialSidebar"},{"id":"developer-guide/wfspec-development/external-events","path":"/docs/developer-guide/wfspec-development/external-events","sidebar":"tutorialSidebar"},{"id":"developer-guide/wfspec-development/interrupts","path":"/docs/developer-guide/wfspec-development/interrupts","sidebar":"tutorialSidebar"},{"id":"developer-guide/wfspec-development/mutating-variables","path":"/docs/developer-guide/wfspec-development/mutating-variables","sidebar":"tutorialSidebar"},{"id":"developer-guide/wfspec-development/user-tasks","path":"/docs/developer-guide/wfspec-development/user-tasks","sidebar":"tutorialSidebar"},{"id":"developer-guide/wfspec-development/wfspec-development","path":"/docs/developer-guide/wfspec-development/","sidebar":"tutorialSidebar"},{"id":"faq","path":"/docs/faq","sidebar":"tutorialSidebar"},{"id":"operations/client-configuration","path":"/docs/operations/client-configuration","sidebar":"tutorialSidebar"},{"id":"operations/dashboard-configuration","path":"/docs/operations/dashboard-configuration","sidebar":"tutorialSidebar"},{"id":"operations/docker-compose/basic","path":"/docs/operations/docker-compose/basic","sidebar":"tutorialSidebar"},{"id":"operations/docker-compose/confluent-cloud","path":"/docs/operations/docker-compose/confluent-cloud","sidebar":"tutorialSidebar"},{"id":"operations/docker-compose/docker-compose","path":"/docs/operations/docker-compose/","sidebar":"tutorialSidebar"},{"id":"operations/docker-compose/three-servers","path":"/docs/operations/docker-compose/three-servers","sidebar":"tutorialSidebar"},{"id":"operations/operations","path":"/docs/operations/","sidebar":"tutorialSidebar"},{"id":"operations/overview","path":"/docs/operations/overview","sidebar":"tutorialSidebar"},{"id":"operations/server-configuration","path":"/docs/operations/server-configuration","sidebar":"tutorialSidebar"},{"id":"overview","path":"/docs/overview","sidebar":"tutorialSidebar"}],"draftIds":[],"sidebars":{"tutorialSidebar":{"link":{"path":"/docs/overview","label":"Overview"}}}}],"breadcrumbs":true}},"docusaurus-plugin-google-gtag":{"default":{"trackingID":["G-1DL56CH5SS"],"anonymizeIP":false,"id":"default"}},"docusaurus-plugin-google-tag-manager":{"default":{"containerId":"GTM-NCK3N2PC","id":"default"}}}'),i=JSON.parse('{"defaultLocale":"en","locales":["en"],"path":"i18n","currentLocale":"en","localeConfigs":{"en":{"label":"English","direction":"ltr","htmlLang":"en","calendar":"gregory","path":"en"}}}');var l=n(2654);const s=JSON.parse('{"docusaurusVersion":"3.5.2","siteVersion":"0.0.0","pluginVersions":{"docusaurus-plugin-content-docs":{"type":"package","name":"@docusaurus/plugin-content-docs","version":"3.5.2"},"docusaurus-plugin-content-blog":{"type":"package","name":"@docusaurus/plugin-content-blog","version":"3.5.2"},"docusaurus-plugin-content-pages":{"type":"package","name":"@docusaurus/plugin-content-pages","version":"3.5.2"},"docusaurus-plugin-google-gtag":{"type":"package","name":"@docusaurus/plugin-google-gtag","version":"3.5.2"},"docusaurus-plugin-google-tag-manager":{"type":"package","name":"@docusaurus/plugin-google-tag-manager","version":"3.5.2"},"docusaurus-plugin-sitemap":{"type":"package","name":"@docusaurus/plugin-sitemap","version":"3.5.2"},"docusaurus-theme-classic":{"type":"package","name":"@docusaurus/theme-classic","version":"3.5.2"},"docusaurus-theme-search-algolia":{"type":"package","name":"@docusaurus/theme-search-algolia","version":"3.5.2"}}}');var c=n(4848);const u={siteConfig:o.default,siteMetadata:s,globalData:a,i18n:i,codeTranslations:l},d=r.createContext(u);function p(e){let{children:t}=e;return(0,c.jsx)(d.Provider,{value:u,children:t})}},7489:(e,t,n)=>{"use strict";n.d(t,{A:()=>h});var r=n(6540),o=n(8193),a=n(5260),i=n(440),l=n(1957),s=n(3102),c=n(4848);function u(e){let{error:t,tryAgain:n}=e;return(0,c.jsxs)("div",{style:{display:"flex",flexDirection:"column",justifyContent:"center",alignItems:"flex-start",minHeight:"100vh",width:"100%",maxWidth:"80ch",fontSize:"20px",margin:"0 auto",padding:"1rem"},children:[(0,c.jsx)("h1",{style:{fontSize:"3rem"},children:"This page crashed"}),(0,c.jsx)("button",{type:"button",onClick:n,style:{margin:"1rem 0",fontSize:"2rem",cursor:"pointer",borderRadius:20,padding:"1rem"},children:"Try again"}),(0,c.jsx)(d,{error:t})]})}function d(e){let{error:t}=e;const n=(0,i.rA)(t).map((e=>e.message)).join("\n\nCause:\n");return(0,c.jsx)("p",{style:{whiteSpace:"pre-wrap"},children:n})}function p(e){let{children:t}=e;return(0,c.jsx)(s.W,{value:{plugin:{name:"docusaurus-core-error-boundary",id:"default"}},children:t})}function f(e){let{error:t,tryAgain:n}=e;return(0,c.jsx)(p,{children:(0,c.jsxs)(h,{fallback:()=>(0,c.jsx)(u,{error:t,tryAgain:n}),children:[(0,c.jsx)(a.A,{children:(0,c.jsx)("title",{children:"Page Error"})}),(0,c.jsx)(l.A,{children:(0,c.jsx)(u,{error:t,tryAgain:n})})]})})}const m=e=>(0,c.jsx)(f,{...e});class h extends r.Component{constructor(e){super(e),this.state={error:null}}componentDidCatch(e){o.A.canUseDOM&&this.setState({error:e})}render(){const{children:e}=this.props,{error:t}=this.state;if(t){const e={error:t,tryAgain:()=>this.setState({error:null})};return(this.props.fallback??m)(e)}return e??null}}},8193:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});const r="undefined"!=typeof window&&"document"in window&&"createElement"in window.document,o={canUseDOM:r,canUseEventListeners:r&&("addEventListener"in window||"attachEvent"in window),canUseIntersectionObserver:r&&"IntersectionObserver"in window,canUseViewport:r&&"screen"in window}},5260:(e,t,n)=>{"use strict";n.d(t,{A:()=>a});n(6540);var r=n(545),o=n(4848);function a(e){return(0,o.jsx)(r.mg,{...e})}},8774:(e,t,n)=>{"use strict";n.d(t,{A:()=>f});var r=n(6540),o=n(4625),a=n(440),i=n(4586),l=n(6654),s=n(8193),c=n(3427),u=n(6025),d=n(4848);function p(e,t){let{isNavLink:n,to:p,href:f,activeClassName:m,isActive:h,"data-noBrokenLinkCheck":g,autoAddBaseUrl:b=!0,...v}=e;const{siteConfig:y}=(0,i.A)(),{trailingSlash:w,baseUrl:S}=y,k=y.future.experimental_router,{withBaseUrl:x}=(0,u.hH)(),E=(0,c.A)(),_=(0,r.useRef)(null);(0,r.useImperativeHandle)(t,(()=>_.current));const O=p||f;const C=(0,l.A)(O),A=O?.replace("pathname://","");let j=void 0!==A?(T=A,b&&(e=>e.startsWith("/"))(T)?x(T):T):void 0;var T;"hash"===k&&j?.startsWith("./")&&(j=j?.slice(1)),j&&C&&(j=(0,a.Ks)(j,{trailingSlash:w,baseUrl:S}));const P=(0,r.useRef)(!1),I=n?o.k2:o.N_,N=s.A.canUseIntersectionObserver,L=(0,r.useRef)(),R=()=>{P.current||null==j||(window.docusaurus.preload(j),P.current=!0)};(0,r.useEffect)((()=>(!N&&C&&s.A.canUseDOM&&null!=j&&window.docusaurus.prefetch(j),()=>{N&&L.current&&L.current.disconnect()})),[L,j,N,C]);const D=j?.startsWith("#")??!1,M=!v.target||"_self"===v.target,F=!j||!C||!M||D&&"hash"!==k;g||!D&&F||E.collectLink(j),v.id&&E.collectAnchor(v.id);const B={};return F?(0,d.jsx)("a",{ref:_,href:j,...O&&!C&&{target:"_blank",rel:"noopener noreferrer"},...v,...B}):(0,d.jsx)(I,{...v,onMouseEnter:R,onTouchStart:R,innerRef:e=>{_.current=e,N&&e&&C&&(L.current=new window.IntersectionObserver((t=>{t.forEach((t=>{e===t.target&&(t.isIntersecting||t.intersectionRatio>0)&&(L.current.unobserve(e),L.current.disconnect(),null!=j&&window.docusaurus.prefetch(j))}))})),L.current.observe(e))},to:j,...n&&{isActive:h,activeClassName:m},...B})}const f=r.forwardRef(p)},1312:(e,t,n)=>{"use strict";n.d(t,{A:()=>c,T:()=>s});var r=n(6540),o=n(4848);function a(e,t){const n=e.split(/(\{\w+\})/).map(((e,n)=>{if(n%2==1){const n=t?.[e.slice(1,-1)];if(void 0!==n)return n}return e}));return n.some((e=>(0,r.isValidElement)(e)))?n.map(((e,t)=>(0,r.isValidElement)(e)?r.cloneElement(e,{key:t}):e)).filter((e=>""!==e)):n.join("")}var i=n(2654);function l(e){let{id:t,message:n}=e;if(void 0===t&&void 0===n)throw new Error("Docusaurus translation declarations must have at least a translation id or a default translation message");return i[t??n]??n??t}function s(e,t){let{message:n,id:r}=e;return a(l({message:n,id:r}),t)}function c(e){let{children:t,id:n,values:r}=e;if(t&&"string"!=typeof t)throw console.warn("Illegal children",t),new Error("The Docusaurus component only accept simple string values");const i=l({message:t,id:n});return(0,o.jsx)(o.Fragment,{children:a(i,r)})}},7065:(e,t,n)=>{"use strict";n.d(t,{W:()=>r});const r="default"},6654:(e,t,n)=>{"use strict";function r(e){return/^(?:\w*:|\/\/)/.test(e)}function o(e){return void 0!==e&&!r(e)}n.d(t,{A:()=>o,z:()=>r})},6025:(e,t,n)=>{"use strict";n.d(t,{Ay:()=>l,hH:()=>i});var r=n(6540),o=n(4586),a=n(6654);function i(){const{siteConfig:e}=(0,o.A)(),{baseUrl:t,url:n}=e,i=e.future.experimental_router,l=(0,r.useCallback)(((e,r)=>function(e){let{siteUrl:t,baseUrl:n,url:r,options:{forcePrependBaseUrl:o=!1,absolute:i=!1}={},router:l}=e;if(!r||r.startsWith("#")||(0,a.z)(r))return r;if("hash"===l)return r.startsWith("/")?`.${r}`:`./${r}`;if(o)return n+r.replace(/^\//,"");if(r===n.replace(/\/$/,""))return n;const s=r.startsWith(n)?r:n+r.replace(/^\//,"");return i?t+s:s}({siteUrl:n,baseUrl:t,url:e,options:r,router:i})),[n,t,i]);return{withBaseUrl:l}}function l(e,t){void 0===t&&(t={});const{withBaseUrl:n}=i();return n(e,t)}},3427:(e,t,n)=>{"use strict";n.d(t,{A:()=>i});var r=n(6540);n(4848);const o=r.createContext({collectAnchor:()=>{},collectLink:()=>{}}),a=()=>(0,r.useContext)(o);function i(){return a()}},4586:(e,t,n)=>{"use strict";n.d(t,{A:()=>a});var r=n(6540),o=n(6988);function a(){return(0,r.useContext)(o.o)}},2303:(e,t,n)=>{"use strict";n.d(t,{A:()=>a});var r=n(6540),o=n(6125);function a(){return(0,r.useContext)(o.o)}},205:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});var r=n(6540);const o=n(8193).A.canUseDOM?r.useLayoutEffect:r.useEffect},6803:(e,t,n)=>{"use strict";n.d(t,{A:()=>a});var r=n(6540),o=n(3102);function a(){const e=r.useContext(o.o);if(!e)throw new Error("Unexpected: no Docusaurus route context found");return e}},6921:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});const r=e=>"object"==typeof e&&!!e&&Object.keys(e).length>0;function o(e){const t={};return function e(n,o){Object.entries(n).forEach((n=>{let[a,i]=n;const l=o?`${o}.${a}`:a;r(i)?e(i,l):t[l]=i}))}(e),t}},3102:(e,t,n)=>{"use strict";n.d(t,{W:()=>i,o:()=>a});var r=n(6540),o=n(4848);const a=r.createContext(null);function i(e){let{children:t,value:n}=e;const i=r.useContext(a),l=(0,r.useMemo)((()=>function(e){let{parent:t,value:n}=e;if(!t){if(!n)throw new Error("Unexpected: no Docusaurus route context found");if(!("plugin"in n))throw new Error("Unexpected: Docusaurus topmost route context has no `plugin` attribute");return n}const r={...t.data,...n?.data};return{plugin:t.plugin,data:r}}({parent:i,value:n})),[i,n]);return(0,o.jsx)(a.Provider,{value:l,children:t})}},3886:(e,t,n)=>{"use strict";n.d(t,{VQ:()=>g,XK:()=>y,g1:()=>v});var r=n(6540),o=n(4070),a=n(7065),i=n(6342),l=n(679),s=n(9532),c=n(4848);const u=e=>`docs-preferred-version-${e}`,d={save:(e,t,n)=>{(0,l.Wf)(u(e),{persistence:t}).set(n)},read:(e,t)=>(0,l.Wf)(u(e),{persistence:t}).get(),clear:(e,t)=>{(0,l.Wf)(u(e),{persistence:t}).del()}},p=e=>Object.fromEntries(e.map((e=>[e,{preferredVersionName:null}])));const f=r.createContext(null);function m(){const e=(0,o.Gy)(),t=(0,i.p)().docs.versionPersistence,n=(0,r.useMemo)((()=>Object.keys(e)),[e]),[a,l]=(0,r.useState)((()=>p(n)));(0,r.useEffect)((()=>{l(function(e){let{pluginIds:t,versionPersistence:n,allDocsData:r}=e;function o(e){const t=d.read(e,n);return r[e].versions.some((e=>e.name===t))?{preferredVersionName:t}:(d.clear(e,n),{preferredVersionName:null})}return Object.fromEntries(t.map((e=>[e,o(e)])))}({allDocsData:e,versionPersistence:t,pluginIds:n}))}),[e,t,n]);return[a,(0,r.useMemo)((()=>({savePreferredVersion:function(e,n){d.save(e,t,n),l((t=>({...t,[e]:{preferredVersionName:n}})))}})),[t])]}function h(e){let{children:t}=e;const n=m();return(0,c.jsx)(f.Provider,{value:n,children:t})}function g(e){let{children:t}=e;return(0,c.jsx)(h,{children:t})}function b(){const e=(0,r.useContext)(f);if(!e)throw new s.dV("DocsPreferredVersionContextProvider");return e}function v(e){void 0===e&&(e=a.W);const t=(0,o.ht)(e),[n,i]=b(),{preferredVersionName:l}=n[e];return{preferredVersion:t.versions.find((e=>e.name===l))??null,savePreferredVersionName:(0,r.useCallback)((t=>{i.savePreferredVersion(e,t)}),[i,e])}}function y(){const e=(0,o.Gy)(),[t]=b();function n(n){const r=e[n],{preferredVersionName:o}=t[n];return r.versions.find((e=>e.name===o))??null}const r=Object.keys(e);return Object.fromEntries(r.map((e=>[e,n(e)])))}},2565:(e,t,n)=>{"use strict";n.d(t,{k:()=>a,v:()=>i});var r=n(4070),o=n(3886);function a(e,t){return`docs-${e}-${t}`}function i(){const e=(0,r.Gy)(),t=(0,r.gk)(),n=(0,o.XK)();return[...Object.keys(e).map((function(r){const o=t?.activePlugin.pluginId===r?t.activeVersion:void 0,i=n[r],l=e[r].versions.find((e=>e.isLast));return a(r,(o??i??l).name)}))]}},609:(e,t,n)=>{"use strict";n.d(t,{V:()=>s,t:()=>c});var r=n(6540),o=n(9532),a=n(4848);const i=Symbol("EmptyContext"),l=r.createContext(i);function s(e){let{children:t,name:n,items:o}=e;const i=(0,r.useMemo)((()=>n&&o?{name:n,items:o}:null),[n,o]);return(0,a.jsx)(l.Provider,{value:i,children:t})}function c(){const e=(0,r.useContext)(l);if(e===i)throw new o.dV("DocsSidebarProvider");return e}},6972:(e,t,n)=>{"use strict";n.d(t,{B5:()=>x,Nr:()=>p,OF:()=>y,QB:()=>k,Vd:()=>w,Y:()=>b,fW:()=>S,w8:()=>h});var r=n(6540),o=n(6347),a=n(2831),i=n(4070),l=n(9169),s=n(1682),c=n(3886),u=n(3025),d=n(609);function p(e){return"link"!==e.type||e.unlisted?"category"===e.type?function(e){if(e.href&&!e.linkUnlisted)return e.href;for(const t of e.items){const e=p(t);if(e)return e}}(e):void 0:e.href}const f=(e,t)=>void 0!==e&&(0,l.ys)(e,t),m=(e,t)=>e.some((e=>h(e,t)));function h(e,t){return"link"===e.type?f(e.href,t):"category"===e.type&&(f(e.href,t)||m(e.items,t))}function g(e,t){switch(e.type){case"category":return h(e,t)||e.items.some((e=>g(e,t)));case"link":return!e.unlisted||h(e,t);default:return!0}}function b(e,t){return(0,r.useMemo)((()=>e.filter((e=>g(e,t)))),[e,t])}function v(e){let{sidebarItems:t,pathname:n,onlyCategories:r=!1}=e;const o=[];return function e(t){for(const a of t)if("category"===a.type&&((0,l.ys)(a.href,n)||e(a.items))||"link"===a.type&&(0,l.ys)(a.href,n)){return r&&"category"!==a.type||o.unshift(a),!0}return!1}(t),o}function y(){const e=(0,d.t)(),{pathname:t}=(0,o.zy)(),n=(0,i.vT)()?.pluginData.breadcrumbs;return!1!==n&&e?v({sidebarItems:e.items,pathname:t}):null}function w(e){const{activeVersion:t}=(0,i.zK)(e),{preferredVersion:n}=(0,c.g1)(e),o=(0,i.r7)(e);return(0,r.useMemo)((()=>(0,s.sb)([t,n,o].filter(Boolean))),[t,n,o])}function S(e,t){const n=w(t);return(0,r.useMemo)((()=>{const t=n.flatMap((e=>e.sidebars?Object.entries(e.sidebars):[])),r=t.find((t=>t[0]===e));if(!r)throw new Error(`Can't find any sidebar with id "${e}" in version${n.length>1?"s":""} ${n.map((e=>e.name)).join(", ")}".\nAvailable sidebar ids are:\n- ${t.map((e=>e[0])).join("\n- ")}`);return r[1]}),[e,n])}function k(e,t){const n=w(t);return(0,r.useMemo)((()=>{const t=n.flatMap((e=>e.docs)),r=t.find((t=>t.id===e));if(!r){if(n.flatMap((e=>e.draftIds)).includes(e))return null;throw new Error(`Couldn't find any doc with id "${e}" in version${n.length>1?"s":""} "${n.map((e=>e.name)).join(", ")}".\nAvailable doc ids are:\n- ${(0,s.sb)(t.map((e=>e.id))).join("\n- ")}`)}return r}),[e,n])}function x(e){let{route:t}=e;const n=(0,o.zy)(),r=(0,u.r)(),i=t.routes,l=i.find((e=>(0,o.B6)(n.pathname,e)));if(!l)return null;const s=l.sidebar,c=s?r.docsSidebars[s]:void 0;return{docElement:(0,a.v)(i),sidebarName:s,sidebarItems:c}}},3025:(e,t,n)=>{"use strict";n.d(t,{n:()=>l,r:()=>s});var r=n(6540),o=n(9532),a=n(4848);const i=r.createContext(null);function l(e){let{children:t,version:n}=e;return(0,a.jsx)(i.Provider,{value:n,children:t})}function s(){const e=(0,r.useContext)(i);if(null===e)throw new o.dV("DocsVersionProvider");return e}},4070:(e,t,n)=>{"use strict";n.d(t,{zK:()=>b,vT:()=>f,gk:()=>m,Gy:()=>d,HW:()=>v,ht:()=>p,r7:()=>g,jh:()=>h});var r=n(6347),o=n(4586),a=n(7065);function i(e,t){void 0===t&&(t={});const n=function(){const{globalData:e}=(0,o.A)();return e}()[e];if(!n&&t.failfast)throw new Error(`Docusaurus plugin global data not found for "${e}" plugin.`);return n}const l=e=>e.versions.find((e=>e.isLast));function s(e,t){return[...e.versions].sort(((e,t)=>e.path===t.path?0:e.path.includes(t.path)?-1:t.path.includes(e.path)?1:0)).find((e=>!!(0,r.B6)(t,{path:e.path,exact:!1,strict:!1})))}function c(e,t){const n=s(e,t),o=n?.docs.find((e=>!!(0,r.B6)(t,{path:e.path,exact:!0,strict:!1})));return{activeVersion:n,activeDoc:o,alternateDocVersions:o?function(t){const n={};return e.versions.forEach((e=>{e.docs.forEach((r=>{r.id===t&&(n[e.name]=r)}))})),n}(o.id):{}}}const u={},d=()=>i("docusaurus-plugin-content-docs")??u,p=e=>{try{return function(e,t,n){void 0===t&&(t=a.W),void 0===n&&(n={});const r=i(e),o=r?.[t];if(!o&&n.failfast)throw new Error(`Docusaurus plugin global data not found for "${e}" plugin with id "${t}".`);return o}("docusaurus-plugin-content-docs",e,{failfast:!0})}catch(t){throw new Error("You are using a feature of the Docusaurus docs plugin, but this plugin does not seem to be enabled"+("Default"===e?"":` (pluginId=${e}`),{cause:t})}};function f(e){void 0===e&&(e={});const t=d(),{pathname:n}=(0,r.zy)();return function(e,t,n){void 0===n&&(n={});const o=Object.entries(e).sort(((e,t)=>t[1].path.localeCompare(e[1].path))).find((e=>{let[,n]=e;return!!(0,r.B6)(t,{path:n.path,exact:!1,strict:!1})})),a=o?{pluginId:o[0],pluginData:o[1]}:void 0;if(!a&&n.failfast)throw new Error(`Can't find active docs plugin for "${t}" pathname, while it was expected to be found. Maybe you tried to use a docs feature that can only be used on a docs-related page? Existing docs plugin paths are: ${Object.values(e).map((e=>e.path)).join(", ")}`);return a}(t,n,e)}function m(e){void 0===e&&(e={});const t=f(e),{pathname:n}=(0,r.zy)();if(!t)return;return{activePlugin:t,activeVersion:s(t.pluginData,n)}}function h(e){return p(e).versions}function g(e){const t=p(e);return l(t)}function b(e){const t=p(e),{pathname:n}=(0,r.zy)();return c(t,n)}function v(e){const t=p(e),{pathname:n}=(0,r.zy)();return function(e,t){const n=l(e);return{latestDocSuggestion:c(e,t).alternateDocVersions[n.name],latestVersionSuggestion:n}}(t,n)}},1911:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r={onRouteDidUpdate(e){let{location:t,previousLocation:n}=e;!n||t.pathname===n.pathname&&t.search===n.search&&t.hash===n.hash||setTimeout((()=>{window.gtag("set","page_path",t.pathname+t.search+t.hash),window.gtag("event","page_view")}))}}},6294:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>a});var r=n(5947),o=n.n(r);o().configure({showSpinner:!1});const a={onRouteUpdate(e){let{location:t,previousLocation:n}=e;if(n&&t.pathname!==n.pathname){const e=window.setTimeout((()=>{o().start()}),200);return()=>window.clearTimeout(e)}},onRouteDidUpdate(){o().done()}}},6134:(e,t,n)=>{"use strict";var r=n(4876),o=n(4784);!function(e){const{themeConfig:{prism:t}}=o.default,{additionalLanguages:r}=t;globalThis.Prism=e,r.forEach((e=>{"php"===e&&n(9700),n(3524)(`./prism-${e}`)})),delete globalThis.Prism}(r.My)},1107:(e,t,n)=>{"use strict";n.d(t,{A:()=>u});n(6540);var r=n(8215),o=n(1312),a=n(6342),i=n(8774),l=n(3427);const s={anchorWithStickyNavbar:"anchorWithStickyNavbar_LWe7",anchorWithHideOnScrollNavbar:"anchorWithHideOnScrollNavbar_WYt5"};var c=n(4848);function u(e){let{as:t,id:n,...u}=e;const d=(0,l.A)(),{navbar:{hideOnScroll:p}}=(0,a.p)();if("h1"===t||!n)return(0,c.jsx)(t,{...u,id:void 0});d.collectAnchor(n);const f=(0,o.T)({id:"theme.common.headingLinkTitle",message:"Direct link to {heading}",description:"Title for link to heading"},{heading:"string"==typeof u.children?u.children:n});return(0,c.jsxs)(t,{...u,className:(0,r.A)("anchor",p?s.anchorWithHideOnScrollNavbar:s.anchorWithStickyNavbar,u.className),id:n,children:[u.children,(0,c.jsx)(i.A,{className:"hash-link",to:`#${n}`,"aria-label":f,title:f,children:"\u200b"})]})}},3186:(e,t,n)=>{"use strict";n.d(t,{A:()=>a});n(6540);const r={iconExternalLink:"iconExternalLink_nPIU"};var o=n(4848);function a(e){let{width:t=13.5,height:n=13.5}=e;return(0,o.jsx)("svg",{width:t,height:n,"aria-hidden":"true",viewBox:"0 0 24 24",className:r.iconExternalLink,children:(0,o.jsx)("path",{fill:"currentColor",d:"M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"})})}},1957:(e,t,n)=>{"use strict";n.d(t,{A:()=>Ot});var r=n(6540),o=n(8215),a=n(7489),i=n(5500),l=n(6347),s=n(1312),c=n(5062),u=n(4848);const d="__docusaurus_skipToContent_fallback";function p(e){e.setAttribute("tabindex","-1"),e.focus(),e.removeAttribute("tabindex")}function f(){const e=(0,r.useRef)(null),{action:t}=(0,l.W6)(),n=(0,r.useCallback)((e=>{e.preventDefault();const t=document.querySelector("main:first-of-type")??document.getElementById(d);t&&p(t)}),[]);return(0,c.$)((n=>{let{location:r}=n;e.current&&!r.hash&&"PUSH"===t&&p(e.current)})),{containerRef:e,onClick:n}}const m=(0,s.T)({id:"theme.common.skipToMainContent",description:"The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation",message:"Skip to main content"});function h(e){const t=e.children??m,{containerRef:n,onClick:r}=f();return(0,u.jsx)("div",{ref:n,role:"region","aria-label":m,children:(0,u.jsx)("a",{...e,href:`#${d}`,onClick:r,children:t})})}var g=n(7559),b=n(4090);const v={skipToContent:"skipToContent_fXgn"};function y(){return(0,u.jsx)(h,{className:v.skipToContent})}var w=n(6342),S=n(5041);function k(e){let{width:t=21,height:n=21,color:r="currentColor",strokeWidth:o=1.2,className:a,...i}=e;return(0,u.jsx)("svg",{viewBox:"0 0 15 15",width:t,height:n,...i,children:(0,u.jsx)("g",{stroke:r,strokeWidth:o,children:(0,u.jsx)("path",{d:"M.75.75l13.5 13.5M14.25.75L.75 14.25"})})})}const x={closeButton:"closeButton_CVFx"};function E(e){return(0,u.jsx)("button",{type:"button","aria-label":(0,s.T)({id:"theme.AnnouncementBar.closeButtonAriaLabel",message:"Close",description:"The ARIA label for close button of announcement bar"}),...e,className:(0,o.A)("clean-btn close",x.closeButton,e.className),children:(0,u.jsx)(k,{width:14,height:14,strokeWidth:3.1})})}const _={content:"content_knG7"};function O(e){const{announcementBar:t}=(0,w.p)(),{content:n}=t;return(0,u.jsx)("div",{...e,className:(0,o.A)(_.content,e.className),dangerouslySetInnerHTML:{__html:n}})}const C={announcementBar:"announcementBar_mb4j",announcementBarPlaceholder:"announcementBarPlaceholder_vyr4",announcementBarClose:"announcementBarClose_gvF7",announcementBarContent:"announcementBarContent_xLdY"};function A(){const{announcementBar:e}=(0,w.p)(),{isActive:t,close:n}=(0,S.M)();if(!t)return null;const{backgroundColor:r,textColor:o,isCloseable:a}=e;return(0,u.jsxs)("div",{className:C.announcementBar,style:{backgroundColor:r,color:o},role:"banner",children:[a&&(0,u.jsx)("div",{className:C.announcementBarPlaceholder}),(0,u.jsx)(O,{className:C.announcementBarContent}),a&&(0,u.jsx)(E,{onClick:n,className:C.announcementBarClose})]})}var j=n(2069),T=n(3104);var P=n(9532),I=n(5600);const N=r.createContext(null);function L(e){let{children:t}=e;const n=function(){const e=(0,j.M)(),t=(0,I.YL)(),[n,o]=(0,r.useState)(!1),a=null!==t.component,i=(0,P.ZC)(a);return(0,r.useEffect)((()=>{a&&!i&&o(!0)}),[a,i]),(0,r.useEffect)((()=>{a?e.shown||o(!0):o(!1)}),[e.shown,a]),(0,r.useMemo)((()=>[n,o]),[n])}();return(0,u.jsx)(N.Provider,{value:n,children:t})}function R(e){if(e.component){const t=e.component;return(0,u.jsx)(t,{...e.props})}}function D(){const e=(0,r.useContext)(N);if(!e)throw new P.dV("NavbarSecondaryMenuDisplayProvider");const[t,n]=e,o=(0,r.useCallback)((()=>n(!1)),[n]),a=(0,I.YL)();return(0,r.useMemo)((()=>({shown:t,hide:o,content:R(a)})),[o,a,t])}function M(e){let{header:t,primaryMenu:n,secondaryMenu:r}=e;const{shown:a}=D();return(0,u.jsxs)("div",{className:"navbar-sidebar",children:[t,(0,u.jsxs)("div",{className:(0,o.A)("navbar-sidebar__items",{"navbar-sidebar__items--show-secondary":a}),children:[(0,u.jsx)("div",{className:"navbar-sidebar__item menu",children:n}),(0,u.jsx)("div",{className:"navbar-sidebar__item menu",children:r})]})]})}var F=n(5293),B=n(2303);function z(e){return(0,u.jsx)("svg",{viewBox:"0 0 24 24",width:24,height:24,...e,children:(0,u.jsx)("path",{fill:"currentColor",d:"M12,9c1.65,0,3,1.35,3,3s-1.35,3-3,3s-3-1.35-3-3S10.35,9,12,9 M12,7c-2.76,0-5,2.24-5,5s2.24,5,5,5s5-2.24,5-5 S14.76,7,12,7L12,7z M2,13l2,0c0.55,0,1-0.45,1-1s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S1.45,13,2,13z M20,13l2,0c0.55,0,1-0.45,1-1 s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S19.45,13,20,13z M11,2v2c0,0.55,0.45,1,1,1s1-0.45,1-1V2c0-0.55-0.45-1-1-1S11,1.45,11,2z M11,20v2c0,0.55,0.45,1,1,1s1-0.45,1-1v-2c0-0.55-0.45-1-1-1C11.45,19,11,19.45,11,20z M5.99,4.58c-0.39-0.39-1.03-0.39-1.41,0 c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0s0.39-1.03,0-1.41L5.99,4.58z M18.36,16.95 c-0.39-0.39-1.03-0.39-1.41,0c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0c0.39-0.39,0.39-1.03,0-1.41 L18.36,16.95z M19.42,5.99c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06c-0.39,0.39-0.39,1.03,0,1.41 s1.03,0.39,1.41,0L19.42,5.99z M7.05,18.36c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06 c-0.39,0.39-0.39,1.03,0,1.41s1.03,0.39,1.41,0L7.05,18.36z"})})}function $(e){return(0,u.jsx)("svg",{viewBox:"0 0 24 24",width:24,height:24,...e,children:(0,u.jsx)("path",{fill:"currentColor",d:"M9.37,5.51C9.19,6.15,9.1,6.82,9.1,7.5c0,4.08,3.32,7.4,7.4,7.4c0.68,0,1.35-0.09,1.99-0.27C17.45,17.19,14.93,19,12,19 c-3.86,0-7-3.14-7-7C5,9.07,6.81,6.55,9.37,5.51z M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36 c-0.98,1.37-2.58,2.26-4.4,2.26c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"})})}const U={toggle:"toggle_vylO",toggleButton:"toggleButton_gllP",darkToggleIcon:"darkToggleIcon_wfgR",lightToggleIcon:"lightToggleIcon_pyhR",toggleButtonDisabled:"toggleButtonDisabled_aARS"};function H(e){let{className:t,buttonClassName:n,value:r,onChange:a}=e;const i=(0,B.A)(),l=(0,s.T)({message:"Switch between dark and light mode (currently {mode})",id:"theme.colorToggle.ariaLabel",description:"The ARIA label for the navbar color mode toggle"},{mode:"dark"===r?(0,s.T)({message:"dark mode",id:"theme.colorToggle.ariaLabel.mode.dark",description:"The name for the dark color mode"}):(0,s.T)({message:"light mode",id:"theme.colorToggle.ariaLabel.mode.light",description:"The name for the light color mode"})});return(0,u.jsx)("div",{className:(0,o.A)(U.toggle,t),children:(0,u.jsxs)("button",{className:(0,o.A)("clean-btn",U.toggleButton,!i&&U.toggleButtonDisabled,n),type:"button",onClick:()=>a("dark"===r?"light":"dark"),disabled:!i,title:l,"aria-label":l,"aria-live":"polite",children:[(0,u.jsx)(z,{className:(0,o.A)(U.toggleIcon,U.lightToggleIcon)}),(0,u.jsx)($,{className:(0,o.A)(U.toggleIcon,U.darkToggleIcon)})]})})}const q=r.memo(H),V={darkNavbarColorModeToggle:"darkNavbarColorModeToggle_X3D1"};function W(e){let{className:t}=e;const n=(0,w.p)().navbar.style,r=(0,w.p)().colorMode.disableSwitch,{colorMode:o,setColorMode:a}=(0,F.G)();return r?null:(0,u.jsx)(q,{className:t,buttonClassName:"dark"===n?V.darkNavbarColorModeToggle:void 0,value:o,onChange:a})}var K=n(3465);function G(){return(0,u.jsx)(K.A,{className:"navbar__brand",imageClassName:"navbar__logo",titleClassName:"navbar__title text--truncate"})}function Q(){const e=(0,j.M)();return(0,u.jsx)("button",{type:"button","aria-label":(0,s.T)({id:"theme.docs.sidebar.closeSidebarButtonAriaLabel",message:"Close navigation bar",description:"The ARIA label for close button of mobile sidebar"}),className:"clean-btn navbar-sidebar__close",onClick:()=>e.toggle(),children:(0,u.jsx)(k,{color:"var(--ifm-color-emphasis-600)"})})}function Y(){return(0,u.jsxs)("div",{className:"navbar-sidebar__brand",children:[(0,u.jsx)(G,{}),(0,u.jsx)(W,{className:"margin-right--md"}),(0,u.jsx)(Q,{})]})}var Z=n(8774),X=n(6025),J=n(6654),ee=n(1252),te=n(3186);function ne(e){let{activeBasePath:t,activeBaseRegex:n,to:r,href:o,label:a,html:i,isDropdownLink:l,prependBaseUrlToHref:s,...c}=e;const d=(0,X.Ay)(r),p=(0,X.Ay)(t),f=(0,X.Ay)(o,{forcePrependBaseUrl:!0}),m=a&&o&&!(0,J.A)(o),h=i?{dangerouslySetInnerHTML:{__html:i}}:{children:(0,u.jsxs)(u.Fragment,{children:[a,m&&(0,u.jsx)(te.A,{...l&&{width:12,height:12}})]})};return o?(0,u.jsx)(Z.A,{href:s?f:o,...c,...h}):(0,u.jsx)(Z.A,{to:d,isNavLink:!0,...(t||n)&&{isActive:(e,t)=>n?(0,ee.G)(n,t.pathname):t.pathname.startsWith(p)},...c,...h})}function re(e){let{className:t,isDropdownItem:n=!1,...r}=e;const a=(0,u.jsx)(ne,{className:(0,o.A)(n?"dropdown__link":"navbar__item navbar__link",t),isDropdownLink:n,...r});return n?(0,u.jsx)("li",{children:a}):a}function oe(e){let{className:t,isDropdownItem:n,...r}=e;return(0,u.jsx)("li",{className:"menu__list-item",children:(0,u.jsx)(ne,{className:(0,o.A)("menu__link",t),...r})})}function ae(e){let{mobile:t=!1,position:n,...r}=e;const o=t?oe:re;return(0,u.jsx)(o,{...r,activeClassName:r.activeClassName??(t?"menu__link--active":"navbar__link--active")})}var ie=n(1422),le=n(9169),se=n(4586);const ce={dropdownNavbarItemMobile:"dropdownNavbarItemMobile_S0Fm"};function ue(e,t){return e.some((e=>function(e,t){return!!(0,le.ys)(e.to,t)||!!(0,ee.G)(e.activeBaseRegex,t)||!(!e.activeBasePath||!t.startsWith(e.activeBasePath))}(e,t)))}function de(e){let{items:t,position:n,className:a,onClick:i,...l}=e;const s=(0,r.useRef)(null),[c,d]=(0,r.useState)(!1);return(0,r.useEffect)((()=>{const e=e=>{s.current&&!s.current.contains(e.target)&&d(!1)};return document.addEventListener("mousedown",e),document.addEventListener("touchstart",e),document.addEventListener("focusin",e),()=>{document.removeEventListener("mousedown",e),document.removeEventListener("touchstart",e),document.removeEventListener("focusin",e)}}),[s]),(0,u.jsxs)("div",{ref:s,className:(0,o.A)("navbar__item","dropdown","dropdown--hoverable",{"dropdown--right":"right"===n,"dropdown--show":c}),children:[(0,u.jsx)(ne,{"aria-haspopup":"true","aria-expanded":c,role:"button",href:l.to?void 0:"#",className:(0,o.A)("navbar__link",a),...l,onClick:l.to?void 0:e=>e.preventDefault(),onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),d(!c))},children:l.children??l.label}),(0,u.jsx)("ul",{className:"dropdown__menu",children:t.map(((e,t)=>(0,r.createElement)(Fe,{isDropdownItem:!0,activeClassName:"dropdown__link--active",...e,key:t})))})]})}function pe(e){let{items:t,className:n,position:a,onClick:i,...s}=e;const c=function(){const{siteConfig:{baseUrl:e}}=(0,se.A)(),{pathname:t}=(0,l.zy)();return t.replace(e,"/")}(),d=ue(t,c),{collapsed:p,toggleCollapsed:f,setCollapsed:m}=(0,ie.u)({initialState:()=>!d});return(0,r.useEffect)((()=>{d&&m(!d)}),[c,d,m]),(0,u.jsxs)("li",{className:(0,o.A)("menu__list-item",{"menu__list-item--collapsed":p}),children:[(0,u.jsx)(ne,{role:"button",className:(0,o.A)(ce.dropdownNavbarItemMobile,"menu__link menu__link--sublist menu__link--sublist-caret",n),...s,onClick:e=>{e.preventDefault(),f()},children:s.children??s.label}),(0,u.jsx)(ie.N,{lazy:!0,as:"ul",className:"menu__list",collapsed:p,children:t.map(((e,t)=>(0,r.createElement)(Fe,{mobile:!0,isDropdownItem:!0,onClick:i,activeClassName:"menu__link--active",...e,key:t})))})]})}function fe(e){let{mobile:t=!1,...n}=e;const r=t?pe:de;return(0,u.jsx)(r,{...n})}var me=n(2131);function he(e){let{width:t=20,height:n=20,...r}=e;return(0,u.jsx)("svg",{viewBox:"0 0 24 24",width:t,height:n,"aria-hidden":!0,...r,children:(0,u.jsx)("path",{fill:"currentColor",d:"M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"})})}const ge="iconLanguage_nlXk";var be=n(961),ve=n(3219),ye=n(5260),we=n(4255),Se=n(1062),ke=n(2967),xe=n(2565);function Ee(){return[`language:${(0,se.A)().i18n.currentLocale}`,function(){const e=(0,xe.v)();return[ke.C,...e]}().map((e=>`docusaurus_tag:${e}`))]}const _e={button:{buttonText:(0,s.T)({id:"theme.SearchBar.label",message:"Search",description:"The ARIA label and placeholder for search button"}),buttonAriaLabel:(0,s.T)({id:"theme.SearchBar.label",message:"Search",description:"The ARIA label and placeholder for search button"})},modal:{searchBox:{resetButtonTitle:(0,s.T)({id:"theme.SearchModal.searchBox.resetButtonTitle",message:"Clear the query",description:"The label and ARIA label for search box reset button"}),resetButtonAriaLabel:(0,s.T)({id:"theme.SearchModal.searchBox.resetButtonTitle",message:"Clear the query",description:"The label and ARIA label for search box reset button"}),cancelButtonText:(0,s.T)({id:"theme.SearchModal.searchBox.cancelButtonText",message:"Cancel",description:"The label and ARIA label for search box cancel button"}),cancelButtonAriaLabel:(0,s.T)({id:"theme.SearchModal.searchBox.cancelButtonText",message:"Cancel",description:"The label and ARIA label for search box cancel button"})},startScreen:{recentSearchesTitle:(0,s.T)({id:"theme.SearchModal.startScreen.recentSearchesTitle",message:"Recent",description:"The title for recent searches"}),noRecentSearchesText:(0,s.T)({id:"theme.SearchModal.startScreen.noRecentSearchesText",message:"No recent searches",description:"The text when no recent searches"}),saveRecentSearchButtonTitle:(0,s.T)({id:"theme.SearchModal.startScreen.saveRecentSearchButtonTitle",message:"Save this search",description:"The label for save recent search button"}),removeRecentSearchButtonTitle:(0,s.T)({id:"theme.SearchModal.startScreen.removeRecentSearchButtonTitle",message:"Remove this search from history",description:"The label for remove recent search button"}),favoriteSearchesTitle:(0,s.T)({id:"theme.SearchModal.startScreen.favoriteSearchesTitle",message:"Favorite",description:"The title for favorite searches"}),removeFavoriteSearchButtonTitle:(0,s.T)({id:"theme.SearchModal.startScreen.removeFavoriteSearchButtonTitle",message:"Remove this search from favorites",description:"The label for remove favorite search button"})},errorScreen:{titleText:(0,s.T)({id:"theme.SearchModal.errorScreen.titleText",message:"Unable to fetch results",description:"The title for error screen of search modal"}),helpText:(0,s.T)({id:"theme.SearchModal.errorScreen.helpText",message:"You might want to check your network connection.",description:"The help text for error screen of search modal"})},footer:{selectText:(0,s.T)({id:"theme.SearchModal.footer.selectText",message:"to select",description:"The explanatory text of the action for the enter key"}),selectKeyAriaLabel:(0,s.T)({id:"theme.SearchModal.footer.selectKeyAriaLabel",message:"Enter key",description:"The ARIA label for the Enter key button that makes the selection"}),navigateText:(0,s.T)({id:"theme.SearchModal.footer.navigateText",message:"to navigate",description:"The explanatory text of the action for the Arrow up and Arrow down key"}),navigateUpKeyAriaLabel:(0,s.T)({id:"theme.SearchModal.footer.navigateUpKeyAriaLabel",message:"Arrow up",description:"The ARIA label for the Arrow up key button that makes the navigation"}),navigateDownKeyAriaLabel:(0,s.T)({id:"theme.SearchModal.footer.navigateDownKeyAriaLabel",message:"Arrow down",description:"The ARIA label for the Arrow down key button that makes the navigation"}),closeText:(0,s.T)({id:"theme.SearchModal.footer.closeText",message:"to close",description:"The explanatory text of the action for Escape key"}),closeKeyAriaLabel:(0,s.T)({id:"theme.SearchModal.footer.closeKeyAriaLabel",message:"Escape key",description:"The ARIA label for the Escape key button that close the modal"}),searchByText:(0,s.T)({id:"theme.SearchModal.footer.searchByText",message:"Search by",description:"The text explain that the search is making by Algolia"})},noResultsScreen:{noResultsText:(0,s.T)({id:"theme.SearchModal.noResultsScreen.noResultsText",message:"No results for",description:"The text explains that there are no results for the following search"}),suggestedQueryText:(0,s.T)({id:"theme.SearchModal.noResultsScreen.suggestedQueryText",message:"Try searching for",description:"The text for the suggested query when no results are found for the following search"}),reportMissingResultsText:(0,s.T)({id:"theme.SearchModal.noResultsScreen.reportMissingResultsText",message:"Believe this query should return results?",description:"The text for the question where the user thinks there are missing results"}),reportMissingResultsLinkText:(0,s.T)({id:"theme.SearchModal.noResultsScreen.reportMissingResultsLinkText",message:"Let us know.",description:"The text for the link to report missing results"})}},placeholder:(0,s.T)({id:"theme.SearchModal.placeholder",message:"Search docs",description:"The placeholder of the input of the DocSearch pop-up modal"})};let Oe=null;function Ce(e){let{hit:t,children:n}=e;return(0,u.jsx)(Z.A,{to:t.url,children:n})}function Ae(e){let{state:t,onClose:n}=e;const r=(0,we.w)();return(0,u.jsx)(Z.A,{to:r(t.query),onClick:n,children:(0,u.jsx)(s.A,{id:"theme.SearchBar.seeAll",values:{count:t.context.nbHits},children:"See all {count} results"})})}function je(e){let{contextualSearch:t,externalUrlRegex:o,...a}=e;const{siteMetadata:i}=(0,se.A)(),s=(0,Se.C)(),c=Ee(),d=a.searchParameters?.facetFilters??[],p=t?function(e,t){const n=e=>"string"==typeof e?[e]:e;return[...n(e),...n(t)]}(c,d):d,f={...a.searchParameters,facetFilters:p},m=(0,l.W6)(),h=(0,r.useRef)(null),g=(0,r.useRef)(null),[b,v]=(0,r.useState)(!1),[y,w]=(0,r.useState)(void 0),S=(0,r.useCallback)((()=>Oe?Promise.resolve():Promise.all([n.e(8158).then(n.bind(n,8158)),Promise.all([n.e(1869),n.e(8913)]).then(n.bind(n,8913)),Promise.all([n.e(1869),n.e(416)]).then(n.bind(n,416))]).then((e=>{let[{DocSearchModal:t}]=e;Oe=t}))),[]),k=(0,r.useCallback)((()=>{if(!h.current){const e=document.createElement("div");h.current=e,document.body.insertBefore(e,document.body.firstChild)}}),[]),x=(0,r.useCallback)((()=>{k(),S().then((()=>v(!0)))}),[S,k]),E=(0,r.useCallback)((()=>{v(!1),g.current?.focus()}),[]),_=(0,r.useCallback)((e=>{"f"===e.key&&(e.metaKey||e.ctrlKey)||(e.preventDefault(),w(e.key),x())}),[x]),O=(0,r.useRef)({navigate(e){let{itemUrl:t}=e;(0,ee.G)(o,t)?window.location.href=t:m.push(t)}}).current,C=(0,r.useRef)((e=>a.transformItems?a.transformItems(e):e.map((e=>({...e,url:s(e.url)}))))).current,A=(0,r.useMemo)((()=>e=>(0,u.jsx)(Ae,{...e,onClose:E})),[E]),j=(0,r.useCallback)((e=>(e.addAlgoliaAgent("docusaurus",i.docusaurusVersion),e)),[i.docusaurusVersion]);return(0,ve.E8)({isOpen:b,onOpen:x,onClose:E,onInput:_,searchButtonRef:g}),(0,u.jsxs)(u.Fragment,{children:[(0,u.jsx)(ye.A,{children:(0,u.jsx)("link",{rel:"preconnect",href:`https://${a.appId}-dsn.algolia.net`,crossOrigin:"anonymous"})}),(0,u.jsx)(ve.Bc,{onTouchStart:S,onFocus:S,onMouseOver:S,onClick:x,ref:g,translations:_e.button}),b&&Oe&&h.current&&(0,be.createPortal)((0,u.jsx)(Oe,{onClose:E,initialScrollY:window.scrollY,initialQuery:y,navigator:O,transformItems:C,hitComponent:Ce,transformSearchClient:j,...a.searchPagePath&&{resultsFooterComponent:A},...a,searchParameters:f,placeholder:_e.placeholder,translations:_e.modal}),h.current)]})}function Te(){const{siteConfig:e}=(0,se.A)();return(0,u.jsx)(je,{...e.themeConfig.algolia})}const Pe={navbarSearchContainer:"navbarSearchContainer_Bca1"};function Ie(e){let{children:t,className:n}=e;return(0,u.jsx)("div",{className:(0,o.A)(n,Pe.navbarSearchContainer),children:t})}var Ne=n(4070),Le=n(6972);var Re=n(3886);function De(e,t){return t.alternateDocVersions[e.name]??function(e){return e.docs.find((t=>t.id===e.mainDocId))}(e)}const Me={default:ae,localeDropdown:function(e){let{mobile:t,dropdownItemsBefore:n,dropdownItemsAfter:r,queryString:o="",...a}=e;const{i18n:{currentLocale:i,locales:c,localeConfigs:d}}=(0,se.A)(),p=(0,me.o)(),{search:f,hash:m}=(0,l.zy)(),h=[...n,...c.map((e=>{const n=`${`pathname://${p.createUrl({locale:e,fullyQualified:!1})}`}${f}${m}${o}`;return{label:d[e].label,lang:d[e].htmlLang,to:n,target:"_self",autoAddBaseUrl:!1,className:e===i?t?"menu__link--active":"dropdown__link--active":""}})),...r],g=t?(0,s.T)({message:"Languages",id:"theme.navbar.mobileLanguageDropdown.label",description:"The label for the mobile language switcher dropdown"}):d[i].label;return(0,u.jsx)(fe,{...a,mobile:t,label:(0,u.jsxs)(u.Fragment,{children:[(0,u.jsx)(he,{className:ge}),g]}),items:h})},search:function(e){let{mobile:t,className:n}=e;return t?null:(0,u.jsx)(Ie,{className:n,children:(0,u.jsx)(Te,{})})},dropdown:fe,html:function(e){let{value:t,className:n,mobile:r=!1,isDropdownItem:a=!1}=e;const i=a?"li":"div";return(0,u.jsx)(i,{className:(0,o.A)({navbar__item:!r&&!a,"menu__list-item":r},n),dangerouslySetInnerHTML:{__html:t}})},doc:function(e){let{docId:t,label:n,docsPluginId:r,...o}=e;const{activeDoc:a}=(0,Ne.zK)(r),i=(0,Le.QB)(t,r),l=a?.path===i?.path;return null===i||i.unlisted&&!l?null:(0,u.jsx)(ae,{exact:!0,...o,isActive:()=>l||!!a?.sidebar&&a.sidebar===i.sidebar,label:n??i.id,to:i.path})},docSidebar:function(e){let{sidebarId:t,label:n,docsPluginId:r,...o}=e;const{activeDoc:a}=(0,Ne.zK)(r),i=(0,Le.fW)(t,r).link;if(!i)throw new Error(`DocSidebarNavbarItem: Sidebar with ID "${t}" doesn't have anything to be linked to.`);return(0,u.jsx)(ae,{exact:!0,...o,isActive:()=>a?.sidebar===t,label:n??i.label,to:i.path})},docsVersion:function(e){let{label:t,to:n,docsPluginId:r,...o}=e;const a=(0,Le.Vd)(r)[0],i=t??a.label,l=n??(e=>e.docs.find((t=>t.id===e.mainDocId)))(a).path;return(0,u.jsx)(ae,{...o,label:i,to:l})},docsVersionDropdown:function(e){let{mobile:t,docsPluginId:n,dropdownActiveClassDisabled:r,dropdownItemsBefore:o,dropdownItemsAfter:a,...i}=e;const{search:c,hash:d}=(0,l.zy)(),p=(0,Ne.zK)(n),f=(0,Ne.jh)(n),{savePreferredVersionName:m}=(0,Re.g1)(n),h=[...o,...f.map((function(e){const t=De(e,p);return{label:e.label,to:`${t.path}${c}${d}`,isActive:()=>e===p.activeVersion,onClick:()=>m(e.name)}})),...a],g=(0,Le.Vd)(n)[0],b=t&&h.length>1?(0,s.T)({id:"theme.navbar.mobileVersionsDropdown.label",message:"Versions",description:"The label for the navbar versions dropdown on mobile view"}):g.label,v=t&&h.length>1?void 0:De(g,p).path;return h.length<=1?(0,u.jsx)(ae,{...i,mobile:t,label:b,to:v,isActive:r?()=>!1:void 0}):(0,u.jsx)(fe,{...i,mobile:t,label:b,to:v,items:h,isActive:r?()=>!1:void 0})}};function Fe(e){let{type:t,...n}=e;const r=function(e,t){return e&&"default"!==e?e:"items"in t?"dropdown":"default"}(t,n),o=Me[r];if(!o)throw new Error(`No NavbarItem component found for type "${t}".`);return(0,u.jsx)(o,{...n})}function Be(){const e=(0,j.M)(),t=(0,w.p)().navbar.items;return(0,u.jsx)("ul",{className:"menu__list",children:t.map(((t,n)=>(0,r.createElement)(Fe,{mobile:!0,...t,onClick:()=>e.toggle(),key:n})))})}function ze(e){return(0,u.jsx)("button",{...e,type:"button",className:"clean-btn navbar-sidebar__back",children:(0,u.jsx)(s.A,{id:"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel",description:"The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)",children:"\u2190 Back to main menu"})})}function $e(){const e=0===(0,w.p)().navbar.items.length,t=D();return(0,u.jsxs)(u.Fragment,{children:[!e&&(0,u.jsx)(ze,{onClick:()=>t.hide()}),t.content]})}function Ue(){const e=(0,j.M)();var t;return void 0===(t=e.shown)&&(t=!0),(0,r.useEffect)((()=>(document.body.style.overflow=t?"hidden":"visible",()=>{document.body.style.overflow="visible"})),[t]),e.shouldRender?(0,u.jsx)(M,{header:(0,u.jsx)(Y,{}),primaryMenu:(0,u.jsx)(Be,{}),secondaryMenu:(0,u.jsx)($e,{})}):null}const He={navbarHideable:"navbarHideable_m1mJ",navbarHidden:"navbarHidden_jGov"};function qe(e){return(0,u.jsx)("div",{role:"presentation",...e,className:(0,o.A)("navbar-sidebar__backdrop",e.className)})}function Ve(e){let{children:t}=e;const{navbar:{hideOnScroll:n,style:a}}=(0,w.p)(),i=(0,j.M)(),{navbarRef:l,isNavbarVisible:d}=function(e){const[t,n]=(0,r.useState)(e),o=(0,r.useRef)(!1),a=(0,r.useRef)(0),i=(0,r.useCallback)((e=>{null!==e&&(a.current=e.getBoundingClientRect().height)}),[]);return(0,T.Mq)(((t,r)=>{let{scrollY:i}=t;if(!e)return;if(i=l?n(!1):i+c{if(!e)return;const r=t.location.hash;if(r?document.getElementById(r.substring(1)):void 0)return o.current=!0,void n(!1);n(!0)})),{navbarRef:i,isNavbarVisible:t}}(n);return(0,u.jsxs)("nav",{ref:l,"aria-label":(0,s.T)({id:"theme.NavBar.navAriaLabel",message:"Main",description:"The ARIA label for the main navigation"}),className:(0,o.A)("navbar","navbar--fixed-top",n&&[He.navbarHideable,!d&&He.navbarHidden],{"navbar--dark":"dark"===a,"navbar--primary":"primary"===a,"navbar-sidebar--show":i.shown}),children:[t,(0,u.jsx)(qe,{onClick:i.toggle}),(0,u.jsx)(Ue,{})]})}var We=n(440);const Ke={errorBoundaryError:"errorBoundaryError_a6uf",errorBoundaryFallback:"errorBoundaryFallback_VBag"};function Ge(e){return(0,u.jsx)("button",{type:"button",...e,children:(0,u.jsx)(s.A,{id:"theme.ErrorPageContent.tryAgain",description:"The label of the button to try again rendering when the React error boundary captures an error",children:"Try again"})})}function Qe(e){let{error:t}=e;const n=(0,We.rA)(t).map((e=>e.message)).join("\n\nCause:\n");return(0,u.jsx)("p",{className:Ke.errorBoundaryError,children:n})}class Ye extends r.Component{componentDidCatch(e,t){throw this.props.onError(e,t)}render(){return this.props.children}}const Ze="right";function Xe(e){let{width:t=30,height:n=30,className:r,...o}=e;return(0,u.jsx)("svg",{className:r,width:t,height:n,viewBox:"0 0 30 30","aria-hidden":"true",...o,children:(0,u.jsx)("path",{stroke:"currentColor",strokeLinecap:"round",strokeMiterlimit:"10",strokeWidth:"2",d:"M4 7h22M4 15h22M4 23h22"})})}function Je(){const{toggle:e,shown:t}=(0,j.M)();return(0,u.jsx)("button",{onClick:e,"aria-label":(0,s.T)({id:"theme.docs.sidebar.toggleSidebarButtonAriaLabel",message:"Toggle navigation bar",description:"The ARIA label for hamburger menu button of mobile navigation"}),"aria-expanded":t,className:"navbar__toggle clean-btn",type:"button",children:(0,u.jsx)(Xe,{})})}const et={colorModeToggle:"colorModeToggle_DEke"};function tt(e){let{items:t}=e;return(0,u.jsx)(u.Fragment,{children:t.map(((e,t)=>(0,u.jsx)(Ye,{onError:t=>new Error(`A theme navbar item failed to render.\nPlease double-check the following navbar item (themeConfig.navbar.items) of your Docusaurus config:\n${JSON.stringify(e,null,2)}`,{cause:t}),children:(0,u.jsx)(Fe,{...e})},t)))})}function nt(e){let{left:t,right:n}=e;return(0,u.jsxs)("div",{className:"navbar__inner",children:[(0,u.jsx)("div",{className:"navbar__items",children:t}),(0,u.jsx)("div",{className:"navbar__items navbar__items--right",children:n})]})}function rt(){const e=(0,j.M)(),t=(0,w.p)().navbar.items,[n,r]=function(e){function t(e){return"left"===(e.position??Ze)}return[e.filter(t),e.filter((e=>!t(e)))]}(t),o=t.find((e=>"search"===e.type));return(0,u.jsx)(nt,{left:(0,u.jsxs)(u.Fragment,{children:[!e.disabled&&(0,u.jsx)(Je,{}),(0,u.jsx)(G,{}),(0,u.jsx)(tt,{items:n})]}),right:(0,u.jsxs)(u.Fragment,{children:[(0,u.jsx)(tt,{items:r}),(0,u.jsx)(W,{className:et.colorModeToggle}),!o&&(0,u.jsx)(Ie,{children:(0,u.jsx)(Te,{})})]})})}function ot(){return(0,u.jsx)(Ve,{children:(0,u.jsx)(rt,{})})}function at(e){let{item:t}=e;const{to:n,href:r,label:o,prependBaseUrlToHref:a,...i}=t,l=(0,X.Ay)(n),s=(0,X.Ay)(r,{forcePrependBaseUrl:!0});return(0,u.jsxs)(Z.A,{className:"footer__link-item",...r?{href:a?s:r}:{to:l},...i,children:[o,r&&!(0,J.A)(r)&&(0,u.jsx)(te.A,{})]})}function it(e){let{item:t}=e;return t.html?(0,u.jsx)("li",{className:"footer__item",dangerouslySetInnerHTML:{__html:t.html}}):(0,u.jsx)("li",{className:"footer__item",children:(0,u.jsx)(at,{item:t})},t.href??t.to)}function lt(e){let{column:t}=e;return(0,u.jsxs)("div",{className:"col footer__col",children:[(0,u.jsx)("div",{className:"footer__title",children:t.title}),(0,u.jsx)("ul",{className:"footer__items clean-list",children:t.items.map(((e,t)=>(0,u.jsx)(it,{item:e},t)))})]})}function st(e){let{columns:t}=e;return(0,u.jsx)("div",{className:"row footer__links",children:t.map(((e,t)=>(0,u.jsx)(lt,{column:e},t)))})}function ct(){return(0,u.jsx)("span",{className:"footer__link-separator",children:"\xb7"})}function ut(e){let{item:t}=e;return t.html?(0,u.jsx)("span",{className:"footer__link-item",dangerouslySetInnerHTML:{__html:t.html}}):(0,u.jsx)(at,{item:t})}function dt(e){let{links:t}=e;return(0,u.jsx)("div",{className:"footer__links text--center",children:(0,u.jsx)("div",{className:"footer__links",children:t.map(((e,n)=>(0,u.jsxs)(r.Fragment,{children:[(0,u.jsx)(ut,{item:e}),t.length!==n+1&&(0,u.jsx)(ct,{})]},n)))})})}function pt(e){let{links:t}=e;return function(e){return"title"in e[0]}(t)?(0,u.jsx)(st,{columns:t}):(0,u.jsx)(dt,{links:t})}var ft=n(1122);const mt={footerLogoLink:"footerLogoLink_BH7S"};function ht(e){let{logo:t}=e;const{withBaseUrl:n}=(0,X.hH)(),r={light:n(t.src),dark:n(t.srcDark??t.src)};return(0,u.jsx)(ft.A,{className:(0,o.A)("footer__logo",t.className),alt:t.alt,sources:r,width:t.width,height:t.height,style:t.style})}function gt(e){let{logo:t}=e;return t.href?(0,u.jsx)(Z.A,{href:t.href,className:mt.footerLogoLink,target:t.target,children:(0,u.jsx)(ht,{logo:t})}):(0,u.jsx)(ht,{logo:t})}function bt(e){let{copyright:t}=e;return(0,u.jsx)("div",{className:"footer__copyright",dangerouslySetInnerHTML:{__html:t}})}function vt(e){let{style:t,links:n,logo:r,copyright:a}=e;return(0,u.jsx)("footer",{className:(0,o.A)("footer",{"footer--dark":"dark"===t}),children:(0,u.jsxs)("div",{className:"container container-fluid",children:[n,(r||a)&&(0,u.jsxs)("div",{className:"footer__bottom text--center",children:[r&&(0,u.jsx)("div",{className:"margin-bottom--sm",children:r}),a]})]})})}function yt(){const{footer:e}=(0,w.p)();if(!e)return null;const{copyright:t,links:n,logo:r,style:o}=e;return(0,u.jsx)(vt,{style:o,links:n&&n.length>0&&(0,u.jsx)(pt,{links:n}),logo:r&&(0,u.jsx)(gt,{logo:r}),copyright:t&&(0,u.jsx)(bt,{copyright:t})})}const wt=r.memo(yt),St=(0,P.fM)([F.a,S.o,T.Tv,Re.VQ,i.Jx,function(e){let{children:t}=e;return(0,u.jsx)(I.y_,{children:(0,u.jsx)(j.e,{children:(0,u.jsx)(L,{children:t})})})}]);function kt(e){let{children:t}=e;return(0,u.jsx)(St,{children:t})}var xt=n(1107);function Et(e){let{error:t,tryAgain:n}=e;return(0,u.jsx)("main",{className:"container margin-vert--xl",children:(0,u.jsx)("div",{className:"row",children:(0,u.jsxs)("div",{className:"col col--6 col--offset-3",children:[(0,u.jsx)(xt.A,{as:"h1",className:"hero__title",children:(0,u.jsx)(s.A,{id:"theme.ErrorPageContent.title",description:"The title of the fallback page when the page crashed",children:"This page crashed."})}),(0,u.jsx)("div",{className:"margin-vert--lg",children:(0,u.jsx)(Ge,{onClick:n,className:"button button--primary shadow--lw"})}),(0,u.jsx)("hr",{}),(0,u.jsx)("div",{className:"margin-vert--md",children:(0,u.jsx)(Qe,{error:t})})]})})})}const _t={mainWrapper:"mainWrapper_z2l0"};function Ot(e){const{children:t,noFooter:n,wrapperClassName:r,title:l,description:s}=e;return(0,b.J)(),(0,u.jsxs)(kt,{children:[(0,u.jsx)(i.be,{title:l,description:s}),(0,u.jsx)(y,{}),(0,u.jsx)(A,{}),(0,u.jsx)(ot,{}),(0,u.jsx)("div",{id:d,className:(0,o.A)(g.G.wrapper.main,_t.mainWrapper,r),children:(0,u.jsx)(a.A,{fallback:e=>(0,u.jsx)(Et,{...e}),children:t})}),!n&&(0,u.jsx)(wt,{})]})}},3465:(e,t,n)=>{"use strict";n.d(t,{A:()=>u});n(6540);var r=n(8774),o=n(6025),a=n(4586),i=n(6342),l=n(1122),s=n(4848);function c(e){let{logo:t,alt:n,imageClassName:r}=e;const a={light:(0,o.Ay)(t.src),dark:(0,o.Ay)(t.srcDark||t.src)},i=(0,s.jsx)(l.A,{className:t.className,sources:a,height:t.height,width:t.width,alt:n,style:t.style});return r?(0,s.jsx)("div",{className:r,children:i}):i}function u(e){const{siteConfig:{title:t}}=(0,a.A)(),{navbar:{title:n,logo:l}}=(0,i.p)(),{imageClassName:u,titleClassName:d,...p}=e,f=(0,o.Ay)(l?.href||"/"),m=n?"":t,h=l?.alt??m;return(0,s.jsxs)(r.A,{to:f,...p,...l?.target&&{target:l.target},children:[l&&(0,s.jsx)(c,{logo:l,alt:h,imageClassName:u}),null!=n&&(0,s.jsx)("b",{className:d,children:n})]})}},1463:(e,t,n)=>{"use strict";n.d(t,{A:()=>a});n(6540);var r=n(5260),o=n(4848);function a(e){let{locale:t,version:n,tag:a}=e;const i=t;return(0,o.jsxs)(r.A,{children:[t&&(0,o.jsx)("meta",{name:"docusaurus_locale",content:t}),n&&(0,o.jsx)("meta",{name:"docusaurus_version",content:n}),a&&(0,o.jsx)("meta",{name:"docusaurus_tag",content:a}),i&&(0,o.jsx)("meta",{name:"docsearch:language",content:i}),n&&(0,o.jsx)("meta",{name:"docsearch:version",content:n}),a&&(0,o.jsx)("meta",{name:"docsearch:docusaurus_tag",content:a})]})}},1122:(e,t,n)=>{"use strict";n.d(t,{A:()=>u});var r=n(6540),o=n(5066),a=n(2303),i=n(5293);const l={themedComponent:"themedComponent_mlkZ","themedComponent--light":"themedComponent--light_NVdE","themedComponent--dark":"themedComponent--dark_xIcU"};var s=n(4848);function c(e){let{className:t,children:n}=e;const c=(0,a.A)(),{colorMode:u}=(0,i.G)();return(0,s.jsx)(s.Fragment,{children:(c?"dark"===u?["dark"]:["light"]:["light","dark"]).map((e=>{const a=n({theme:e,className:(0,o.A)(t,l.themedComponent,l[`themedComponent--${e}`])});return(0,s.jsx)(r.Fragment,{children:a},e)}))})}function u(e){const{sources:t,className:n,alt:r,...o}=e;return(0,s.jsx)(c,{className:n,children:e=>{let{theme:n,className:a}=e;return(0,s.jsx)("img",{src:t[n],alt:r,className:a,...o})}})}},1422:(e,t,n)=>{"use strict";n.d(t,{N:()=>b,u:()=>c});var r=n(6540),o=n(8193),a=n(205),i=n(3109),l=n(4848);const s="ease-in-out";function c(e){let{initialState:t}=e;const[n,o]=(0,r.useState)(t??!1),a=(0,r.useCallback)((()=>{o((e=>!e))}),[]);return{collapsed:n,setCollapsed:o,toggleCollapsed:a}}const u={display:"none",overflow:"hidden",height:"0px"},d={display:"block",overflow:"visible",height:"auto"};function p(e,t){const n=t?u:d;e.style.display=n.display,e.style.overflow=n.overflow,e.style.height=n.height}function f(e){let{collapsibleRef:t,collapsed:n,animation:o}=e;const a=(0,r.useRef)(!1);(0,r.useEffect)((()=>{const e=t.current;function r(){const t=e.scrollHeight,n=o?.duration??function(e){if((0,i.O)())return 1;const t=e/36;return Math.round(10*(4+15*t**.25+t/5))}(t);return{transition:`height ${n}ms ${o?.easing??s}`,height:`${t}px`}}function l(){const t=r();e.style.transition=t.transition,e.style.height=t.height}if(!a.current)return p(e,n),void(a.current=!0);return e.style.willChange="height",function(){const t=requestAnimationFrame((()=>{n?(l(),requestAnimationFrame((()=>{e.style.height=u.height,e.style.overflow=u.overflow}))):(e.style.display="block",requestAnimationFrame((()=>{l()})))}));return()=>cancelAnimationFrame(t)}()}),[t,n,o])}function m(e){if(!o.A.canUseDOM)return e?u:d}function h(e){let{as:t="div",collapsed:n,children:o,animation:a,onCollapseTransitionEnd:i,className:s,disableSSRStyle:c}=e;const u=(0,r.useRef)(null);return f({collapsibleRef:u,collapsed:n,animation:a}),(0,l.jsx)(t,{ref:u,style:c?void 0:m(n),onTransitionEnd:e=>{"height"===e.propertyName&&(p(u.current,n),i?.(n))},className:s,children:o})}function g(e){let{collapsed:t,...n}=e;const[o,i]=(0,r.useState)(!t),[s,c]=(0,r.useState)(t);return(0,a.A)((()=>{t||i(!0)}),[t]),(0,a.A)((()=>{o&&c(t)}),[o,t]),o?(0,l.jsx)(h,{...n,collapsed:s}):null}function b(e){let{lazy:t,...n}=e;const r=t?g:h;return(0,l.jsx)(r,{...n})}},5041:(e,t,n)=>{"use strict";n.d(t,{M:()=>h,o:()=>m});var r=n(6540),o=n(2303),a=n(679),i=n(9532),l=n(6342),s=n(4848);const c=(0,a.Wf)("docusaurus.announcement.dismiss"),u=(0,a.Wf)("docusaurus.announcement.id"),d=()=>"true"===c.get(),p=e=>c.set(String(e)),f=r.createContext(null);function m(e){let{children:t}=e;const n=function(){const{announcementBar:e}=(0,l.p)(),t=(0,o.A)(),[n,a]=(0,r.useState)((()=>!!t&&d()));(0,r.useEffect)((()=>{a(d())}),[]);const i=(0,r.useCallback)((()=>{p(!0),a(!0)}),[]);return(0,r.useEffect)((()=>{if(!e)return;const{id:t}=e;let n=u.get();"annoucement-bar"===n&&(n="announcement-bar");const r=t!==n;u.set(t),r&&p(!1),!r&&d()||a(!1)}),[e]),(0,r.useMemo)((()=>({isActive:!!e&&!n,close:i})),[e,n,i])}();return(0,s.jsx)(f.Provider,{value:n,children:t})}function h(){const e=(0,r.useContext)(f);if(!e)throw new i.dV("AnnouncementBarProvider");return e}},5293:(e,t,n)=>{"use strict";n.d(t,{G:()=>b,a:()=>g});var r=n(6540),o=n(8193),a=n(9532),i=n(679),l=n(6342),s=n(4848);const c=r.createContext(void 0),u="theme",d=(0,i.Wf)(u),p={light:"light",dark:"dark"},f=e=>e===p.dark?p.dark:p.light,m=e=>o.A.canUseDOM?f(document.documentElement.getAttribute("data-theme")):f(e),h=e=>{d.set(f(e))};function g(e){let{children:t}=e;const n=function(){const{colorMode:{defaultMode:e,disableSwitch:t,respectPrefersColorScheme:n}}=(0,l.p)(),[o,a]=(0,r.useState)(m(e));(0,r.useEffect)((()=>{t&&d.del()}),[t]);const i=(0,r.useCallback)((function(t,r){void 0===r&&(r={});const{persist:o=!0}=r;t?(a(t),o&&h(t)):(a(n?window.matchMedia("(prefers-color-scheme: dark)").matches?p.dark:p.light:e),d.del())}),[n,e]);(0,r.useEffect)((()=>{document.documentElement.setAttribute("data-theme",f(o))}),[o]),(0,r.useEffect)((()=>{if(t)return;const e=e=>{if(e.key!==u)return;const t=d.get();null!==t&&i(f(t))};return window.addEventListener("storage",e),()=>window.removeEventListener("storage",e)}),[t,i]);const s=(0,r.useRef)(!1);return(0,r.useEffect)((()=>{if(t&&!n)return;const e=window.matchMedia("(prefers-color-scheme: dark)"),r=()=>{window.matchMedia("print").matches||s.current?s.current=window.matchMedia("print").matches:i(null)};return e.addListener(r),()=>e.removeListener(r)}),[i,t,n]),(0,r.useMemo)((()=>({colorMode:o,setColorMode:i,get isDarkTheme(){return o===p.dark},setLightTheme(){i(p.light)},setDarkTheme(){i(p.dark)}})),[o,i])}();return(0,s.jsx)(c.Provider,{value:n,children:t})}function b(){const e=(0,r.useContext)(c);if(null==e)throw new a.dV("ColorModeProvider","Please see https://docusaurus.io/docs/api/themes/configuration#use-color-mode.");return e}},2069:(e,t,n)=>{"use strict";n.d(t,{M:()=>f,e:()=>p});var r=n(6540),o=n(5600),a=n(4581),i=n(7485),l=n(6342),s=n(9532),c=n(4848);const u=r.createContext(void 0);function d(){const e=function(){const e=(0,o.YL)(),{items:t}=(0,l.p)().navbar;return 0===t.length&&!e.component}(),t=(0,a.l)(),n=!e&&"mobile"===t,[s,c]=(0,r.useState)(!1);(0,i.$Z)((()=>{if(s)return c(!1),!1}));const u=(0,r.useCallback)((()=>{c((e=>!e))}),[]);return(0,r.useEffect)((()=>{"desktop"===t&&c(!1)}),[t]),(0,r.useMemo)((()=>({disabled:e,shouldRender:n,toggle:u,shown:s})),[e,n,u,s])}function p(e){let{children:t}=e;const n=d();return(0,c.jsx)(u.Provider,{value:n,children:t})}function f(){const e=r.useContext(u);if(void 0===e)throw new s.dV("NavbarMobileSidebarProvider");return e}},5600:(e,t,n)=>{"use strict";n.d(t,{GX:()=>c,YL:()=>s,y_:()=>l});var r=n(6540),o=n(9532),a=n(4848);const i=r.createContext(null);function l(e){let{children:t}=e;const n=(0,r.useState)({component:null,props:null});return(0,a.jsx)(i.Provider,{value:n,children:t})}function s(){const e=(0,r.useContext)(i);if(!e)throw new o.dV("NavbarSecondaryMenuContentProvider");return e[0]}function c(e){let{component:t,props:n}=e;const a=(0,r.useContext)(i);if(!a)throw new o.dV("NavbarSecondaryMenuContentProvider");const[,l]=a,s=(0,o.Be)(n);return(0,r.useEffect)((()=>{l({component:t,props:s})}),[l,t,s]),(0,r.useEffect)((()=>()=>l({component:null,props:null})),[l]),null}},4090:(e,t,n)=>{"use strict";n.d(t,{w:()=>o,J:()=>a});var r=n(6540);const o="navigation-with-keyboard";function a(){(0,r.useEffect)((()=>{function e(e){"keydown"===e.type&&"Tab"===e.key&&document.body.classList.add(o),"mousedown"===e.type&&document.body.classList.remove(o)}return document.addEventListener("keydown",e),document.addEventListener("mousedown",e),()=>{document.body.classList.remove(o),document.removeEventListener("keydown",e),document.removeEventListener("mousedown",e)}}),[])}},4255:(e,t,n)=>{"use strict";n.d(t,{b:()=>l,w:()=>s});var r=n(6540),o=n(4586),a=n(7485);const i="q";function l(){return(0,a.l)(i)}function s(){const{siteConfig:{baseUrl:e,themeConfig:t}}=(0,o.A)(),{algolia:{searchPagePath:n}}=t;return(0,r.useCallback)((t=>`${e}${n}?${i}=${encodeURIComponent(t)}`),[e,n])}},4581:(e,t,n)=>{"use strict";n.d(t,{l:()=>l});var r=n(6540),o=n(8193);const a={desktop:"desktop",mobile:"mobile",ssr:"ssr"},i=996;function l(e){let{desktopBreakpoint:t=i}=void 0===e?{}:e;const[n,l]=(0,r.useState)((()=>"ssr"));return(0,r.useEffect)((()=>{function e(){l(function(e){if(!o.A.canUseDOM)throw new Error("getWindowSize() should only be called after React hydration");return window.innerWidth>e?a.desktop:a.mobile}(t))}return e(),window.addEventListener("resize",e),()=>{window.removeEventListener("resize",e)}}),[t]),n}},7559:(e,t,n)=>{"use strict";n.d(t,{G:()=>r});const r={page:{blogListPage:"blog-list-page",blogPostPage:"blog-post-page",blogTagsListPage:"blog-tags-list-page",blogTagPostListPage:"blog-tags-post-list-page",blogAuthorsListPage:"blog-authors-list-page",blogAuthorsPostsPage:"blog-authors-posts-page",docsDocPage:"docs-doc-page",docsTagsListPage:"docs-tags-list-page",docsTagDocListPage:"docs-tags-doc-list-page",mdxPage:"mdx-page"},wrapper:{main:"main-wrapper",blogPages:"blog-wrapper",docsPages:"docs-wrapper",mdxPages:"mdx-wrapper"},common:{editThisPage:"theme-edit-this-page",lastUpdated:"theme-last-updated",backToTopButton:"theme-back-to-top-button",codeBlock:"theme-code-block",admonition:"theme-admonition",unlistedBanner:"theme-unlisted-banner",draftBanner:"theme-draft-banner",admonitionType:e=>`theme-admonition-${e}`},layout:{},docs:{docVersionBanner:"theme-doc-version-banner",docVersionBadge:"theme-doc-version-badge",docBreadcrumbs:"theme-doc-breadcrumbs",docMarkdown:"theme-doc-markdown",docTocMobile:"theme-doc-toc-mobile",docTocDesktop:"theme-doc-toc-desktop",docFooter:"theme-doc-footer",docFooterTagsRow:"theme-doc-footer-tags-row",docFooterEditMetaRow:"theme-doc-footer-edit-meta-row",docSidebarContainer:"theme-doc-sidebar-container",docSidebarMenu:"theme-doc-sidebar-menu",docSidebarItemCategory:"theme-doc-sidebar-item-category",docSidebarItemLink:"theme-doc-sidebar-item-link",docSidebarItemCategoryLevel:e=>`theme-doc-sidebar-item-category-level-${e}`,docSidebarItemLinkLevel:e=>`theme-doc-sidebar-item-link-level-${e}`},blog:{blogFooterTagsRow:"theme-blog-footer-tags-row",blogFooterEditMetaRow:"theme-blog-footer-edit-meta-row"},pages:{pageFooterEditMetaRow:"theme-pages-footer-edit-meta-row"}}},3109:(e,t,n)=>{"use strict";function r(){return window.matchMedia("(prefers-reduced-motion: reduce)").matches}n.d(t,{O:()=>r})},481:(e,t,n)=>{"use strict";n.d(t,{s:()=>o});var r=n(4586);function o(e){const{siteConfig:t}=(0,r.A)(),{title:n,titleDelimiter:o}=t;return e?.trim().length?`${e.trim()} ${o} ${n}`:n}},7485:(e,t,n)=>{"use strict";n.d(t,{$Z:()=>i,aZ:()=>s,l:()=>c});var r=n(6540),o=n(6347),a=n(9532);function i(e){!function(e){const t=(0,o.W6)(),n=(0,a._q)(e);(0,r.useEffect)((()=>t.block(((e,t)=>n(e,t)))),[t,n])}(((t,n)=>{if("POP"===n)return e(t,n)}))}function l(e){const t=(0,o.W6)();return(0,r.useSyncExternalStore)(t.listen,(()=>e(t)),(()=>e(t)))}function s(e){return l((t=>null===e?null:new URLSearchParams(t.location.search).get(e)))}function c(e){const t=s(e)??"",n=function(e){const t=(0,o.W6)();return(0,r.useCallback)(((n,r)=>{const o=new URLSearchParams(t.location.search);n?o.set(e,n):o.delete(e),(r?.push?t.push:t.replace)({search:o.toString()})}),[e,t])}(e);return[t,n]}},1682:(e,t,n)=>{"use strict";function r(e,t){return void 0===t&&(t=(e,t)=>e===t),e.filter(((n,r)=>e.findIndex((e=>t(e,n)))!==r))}function o(e){return Array.from(new Set(e))}function a(e,t){const n={};let r=0;for(const o of e){const e=t(o,r);n[e]??=[],n[e].push(o),r+=1}return n}n.d(t,{$z:()=>a,XI:()=>r,sb:()=>o})},5500:(e,t,n)=>{"use strict";n.d(t,{Jx:()=>f,be:()=>u,e3:()=>p});var r=n(6540),o=n(5066),a=n(5260),i=n(6803),l=n(6025),s=n(481),c=n(4848);function u(e){let{title:t,description:n,keywords:r,image:o,children:i}=e;const u=(0,s.s)(t),{withBaseUrl:d}=(0,l.hH)(),p=o?d(o,{absolute:!0}):void 0;return(0,c.jsxs)(a.A,{children:[t&&(0,c.jsx)("title",{children:u}),t&&(0,c.jsx)("meta",{property:"og:title",content:u}),n&&(0,c.jsx)("meta",{name:"description",content:n}),n&&(0,c.jsx)("meta",{property:"og:description",content:n}),r&&(0,c.jsx)("meta",{name:"keywords",content:Array.isArray(r)?r.join(","):r}),p&&(0,c.jsx)("meta",{property:"og:image",content:p}),p&&(0,c.jsx)("meta",{name:"twitter:image",content:p}),i]})}const d=r.createContext(void 0);function p(e){let{className:t,children:n}=e;const i=r.useContext(d),l=(0,o.A)(i,t);return(0,c.jsxs)(d.Provider,{value:l,children:[(0,c.jsx)(a.A,{children:(0,c.jsx)("html",{className:l})}),n]})}function f(e){let{children:t}=e;const n=(0,i.A)(),r=`plugin-${n.plugin.name.replace(/docusaurus-(?:plugin|theme)-(?:content-)?/gi,"")}`;const a=`plugin-id-${n.plugin.id}`;return(0,c.jsx)(p,{className:(0,o.A)(r,a),children:t})}},9532:(e,t,n)=>{"use strict";n.d(t,{Be:()=>c,ZC:()=>l,_q:()=>i,dV:()=>s,fM:()=>u});var r=n(6540),o=n(205),a=n(4848);function i(e){const t=(0,r.useRef)(e);return(0,o.A)((()=>{t.current=e}),[e]),(0,r.useCallback)((function(){return t.current(...arguments)}),[])}function l(e){const t=(0,r.useRef)();return(0,o.A)((()=>{t.current=e})),t.current}class s extends Error{constructor(e,t){super(),this.name="ReactContextError",this.message=`Hook ${this.stack?.split("\n")[1]?.match(/at (?:\w+\.)?(?\w+)/)?.groups.name??""} is called outside the <${e}>. ${t??""}`}}function c(e){const t=Object.entries(e);return t.sort(((e,t)=>e[0].localeCompare(t[0]))),(0,r.useMemo)((()=>e),t.flat())}function u(e){return t=>{let{children:n}=t;return(0,a.jsx)(a.Fragment,{children:e.reduceRight(((e,t)=>(0,a.jsx)(t,{children:e})),n)})}}},1252:(e,t,n)=>{"use strict";function r(e,t){return void 0!==e&&void 0!==t&&new RegExp(e,"gi").test(t)}n.d(t,{G:()=>r})},9169:(e,t,n)=>{"use strict";n.d(t,{Dt:()=>l,ys:()=>i});var r=n(6540),o=n(8328),a=n(4586);function i(e,t){const n=e=>(!e||e.endsWith("/")?e:`${e}/`)?.toLowerCase();return n(e)===n(t)}function l(){const{baseUrl:e}=(0,a.A)().siteConfig;return(0,r.useMemo)((()=>function(e){let{baseUrl:t,routes:n}=e;function r(e){return e.path===t&&!0===e.exact}function o(e){return e.path===t&&!e.exact}return function e(t){if(0===t.length)return;return t.find(r)||e(t.filter(o).flatMap((e=>e.routes??[])))}(n)}({routes:o.A,baseUrl:e})),[e])}},3104:(e,t,n)=>{"use strict";n.d(t,{Mq:()=>f,Tv:()=>u,a_:()=>m,gk:()=>h});var r=n(6540),o=n(8193),a=n(2303),i=n(205),l=n(9532),s=n(4848);const c=r.createContext(void 0);function u(e){let{children:t}=e;const n=function(){const e=(0,r.useRef)(!0);return(0,r.useMemo)((()=>({scrollEventsEnabledRef:e,enableScrollEvents:()=>{e.current=!0},disableScrollEvents:()=>{e.current=!1}})),[])}();return(0,s.jsx)(c.Provider,{value:n,children:t})}function d(){const e=(0,r.useContext)(c);if(null==e)throw new l.dV("ScrollControllerProvider");return e}const p=()=>o.A.canUseDOM?{scrollX:window.pageXOffset,scrollY:window.pageYOffset}:null;function f(e,t){void 0===t&&(t=[]);const{scrollEventsEnabledRef:n}=d(),o=(0,r.useRef)(p()),a=(0,l._q)(e);(0,r.useEffect)((()=>{const e=()=>{if(!n.current)return;const e=p();a(e,o.current),o.current=e},t={passive:!0};return e(),window.addEventListener("scroll",e,t),()=>window.removeEventListener("scroll",e,t)}),[a,n,...t])}function m(){const e=d(),t=function(){const e=(0,r.useRef)({elem:null,top:0}),t=(0,r.useCallback)((t=>{e.current={elem:t,top:t.getBoundingClientRect().top}}),[]),n=(0,r.useCallback)((()=>{const{current:{elem:t,top:n}}=e;if(!t)return{restored:!1};const r=t.getBoundingClientRect().top-n;return r&&window.scrollBy({left:0,top:r}),e.current={elem:null,top:0},{restored:0!==r}}),[]);return(0,r.useMemo)((()=>({save:t,restore:n})),[n,t])}(),n=(0,r.useRef)(void 0),o=(0,r.useCallback)((r=>{t.save(r),e.disableScrollEvents(),n.current=()=>{const{restored:r}=t.restore();if(n.current=void 0,r){const t=()=>{e.enableScrollEvents(),window.removeEventListener("scroll",t)};window.addEventListener("scroll",t)}else e.enableScrollEvents()}}),[e,t]);return(0,i.A)((()=>{queueMicrotask((()=>n.current?.()))})),{blockElementScrollPositionUntilNextRender:o}}function h(){const e=(0,r.useRef)(null),t=(0,a.A)()&&"smooth"===getComputedStyle(document.documentElement).scrollBehavior;return{startScroll:n=>{e.current=t?function(e){return window.scrollTo({top:e,behavior:"smooth"}),()=>{}}(n):function(e){let t=null;const n=document.documentElement.scrollTop>e;return function r(){const o=document.documentElement.scrollTop;(n&&o>e||!n&&ot&&cancelAnimationFrame(t)}(n)},cancelScroll:()=>e.current?.()}}},2967:(e,t,n)=>{"use strict";n.d(t,{C:()=>r});const r="default"},679:(e,t,n)=>{"use strict";n.d(t,{Wf:()=>u,Dv:()=>d});var r=n(6540);const o=JSON.parse('{"N":"localStorage","M":""}'),a=o.N;function i(e){let{key:t,oldValue:n,newValue:r,storage:o}=e;if(n===r)return;const a=document.createEvent("StorageEvent");a.initStorageEvent("storage",!1,!1,t,n,r,window.location.href,o),window.dispatchEvent(a)}function l(e){if(void 0===e&&(e=a),"undefined"==typeof window)throw new Error("Browser storage is not available on Node.js/Docusaurus SSR process.");if("none"===e)return null;try{return window[e]}catch(n){return t=n,s||(console.warn("Docusaurus browser storage is not available.\nPossible reasons: running Docusaurus in an iframe, in an incognito browser session, or using too strict browser privacy settings.",t),s=!0),null}var t}let s=!1;const c={get:()=>null,set:()=>{},del:()=>{},listen:()=>()=>{}};function u(e,t){const n=`${e}${o.M}`;if("undefined"==typeof window)return function(e){function t(){throw new Error(`Illegal storage API usage for storage key "${e}".\nDocusaurus storage APIs are not supposed to be called on the server-rendering process.\nPlease only call storage APIs in effects and event handlers.`)}return{get:t,set:t,del:t,listen:t}}(n);const r=l(t?.persistence);return null===r?c:{get:()=>{try{return r.getItem(n)}catch(e){return console.error(`Docusaurus storage error, can't get key=${n}`,e),null}},set:e=>{try{const t=r.getItem(n);r.setItem(n,e),i({key:n,oldValue:t,newValue:e,storage:r})}catch(t){console.error(`Docusaurus storage error, can't set ${n}=${e}`,t)}},del:()=>{try{const e=r.getItem(n);r.removeItem(n),i({key:n,oldValue:e,newValue:null,storage:r})}catch(e){console.error(`Docusaurus storage error, can't delete key=${n}`,e)}},listen:e=>{try{const t=t=>{t.storageArea===r&&t.key===n&&e(t)};return window.addEventListener("storage",t),()=>window.removeEventListener("storage",t)}catch(t){return console.error(`Docusaurus storage error, can't listen for changes of key=${n}`,t),()=>{}}}}}function d(e,t){const n=(0,r.useRef)((()=>null===e?c:u(e,t))).current(),o=(0,r.useCallback)((e=>"undefined"==typeof window?()=>{}:n.listen(e)),[n]);return[(0,r.useSyncExternalStore)(o,(()=>"undefined"==typeof window?null:n.get()),(()=>null)),n]}},2131:(e,t,n)=>{"use strict";n.d(t,{o:()=>i});var r=n(4586),o=n(6347),a=n(440);function i(){const{siteConfig:{baseUrl:e,url:t,trailingSlash:n},i18n:{defaultLocale:i,currentLocale:l}}=(0,r.A)(),{pathname:s}=(0,o.zy)(),c=(0,a.Ks)(s,{trailingSlash:n,baseUrl:e}),u=l===i?e:e.replace(`/${l}/`,"/"),d=c.replace(e,"");return{createUrl:function(e){let{locale:n,fullyQualified:r}=e;return`${r?t:""}${function(e){return e===i?`${u}`:`${u}${e}/`}(n)}${d}`}}}},5062:(e,t,n)=>{"use strict";n.d(t,{$:()=>i});var r=n(6540),o=n(6347),a=n(9532);function i(e){const t=(0,o.zy)(),n=(0,a.ZC)(t),i=(0,a._q)(e);(0,r.useEffect)((()=>{n&&t!==n&&i({location:t,previousLocation:n})}),[i,t,n])}},6342:(e,t,n)=>{"use strict";n.d(t,{p:()=>o});var r=n(4586);function o(){return(0,r.A)().siteConfig.themeConfig}},8126:(e,t,n)=>{"use strict";n.d(t,{c:()=>o});var r=n(4586);function o(){const{siteConfig:{themeConfig:e}}=(0,r.A)();return e}},1062:(e,t,n)=>{"use strict";n.d(t,{C:()=>l});var r=n(6540),o=n(1252),a=n(6025),i=n(8126);function l(){const{withBaseUrl:e}=(0,a.hH)(),{algolia:{externalUrlRegex:t,replaceSearchResultPathname:n}}=(0,i.c)();return(0,r.useCallback)((r=>{const a=new URL(r);if((0,o.G)(t,a.href))return r;const i=`${a.pathname+a.hash}`;return e(function(e,t){return t?e.replaceAll(new RegExp(t.from,"g"),t.to):e}(i,n))}),[e,t,n])}},2983:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.addTrailingSlash=o,t.default=function(e,t){const{trailingSlash:n,baseUrl:r}=t;if(e.startsWith("#"))return e;if(void 0===n)return e;const[i]=e.split(/[#?]/),l="/"===i||i===r?i:(s=i,c=n,c?o(s):a(s));var s,c;return e.replace(i,l)},t.addLeadingSlash=function(e){return(0,r.addPrefix)(e,"/")},t.removeTrailingSlash=a;const r=n(2566);function o(e){return e.endsWith("/")?e:`${e}/`}function a(e){return(0,r.removeSuffix)(e,"/")}},253:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getErrorCausalChain=function e(t){if(t.cause)return[t,...e(t.cause)];return[t]}},440:(e,t,n)=>{"use strict";t.rA=t.Ks=t.LU=void 0;const r=n(1635);t.LU="__blog-post-container";var o=n(2983);Object.defineProperty(t,"Ks",{enumerable:!0,get:function(){return r.__importDefault(o).default}});var a=n(2566);var i=n(253);Object.defineProperty(t,"rA",{enumerable:!0,get:function(){return i.getErrorCausalChain}})},2566:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.addPrefix=function(e,t){return e.startsWith(t)?e:`${t}${e}`},t.removeSuffix=function(e,t){if(""===t)return e;return e.endsWith(t)?e.slice(0,-t.length):e},t.addSuffix=function(e,t){return e.endsWith(t)?e:`${e}${t}`},t.removePrefix=function(e,t){return e.startsWith(t)?e.slice(t.length):e}},1513:(e,t,n)=>{"use strict";n.d(t,{zR:()=>w,TM:()=>O,yJ:()=>f,sC:()=>A,AO:()=>p});var r=n(8168);function o(e){return"/"===e.charAt(0)}function a(e,t){for(var n=t,r=n+1,o=e.length;r=0;p--){var f=i[p];"."===f?a(i,p):".."===f?(a(i,p),d++):d&&(a(i,p),d--)}if(!c)for(;d--;d)i.unshift("..");!c||""===i[0]||i[0]&&o(i[0])||i.unshift("");var m=i.join("/");return n&&"/"!==m.substr(-1)&&(m+="/"),m};var l=n(1561);function s(e){return"/"===e.charAt(0)?e:"/"+e}function c(e){return"/"===e.charAt(0)?e.substr(1):e}function u(e,t){return function(e,t){return 0===e.toLowerCase().indexOf(t.toLowerCase())&&-1!=="/?#".indexOf(e.charAt(t.length))}(e,t)?e.substr(t.length):e}function d(e){return"/"===e.charAt(e.length-1)?e.slice(0,-1):e}function p(e){var t=e.pathname,n=e.search,r=e.hash,o=t||"/";return n&&"?"!==n&&(o+="?"===n.charAt(0)?n:"?"+n),r&&"#"!==r&&(o+="#"===r.charAt(0)?r:"#"+r),o}function f(e,t,n,o){var a;"string"==typeof e?(a=function(e){var t=e||"/",n="",r="",o=t.indexOf("#");-1!==o&&(r=t.substr(o),t=t.substr(0,o));var a=t.indexOf("?");return-1!==a&&(n=t.substr(a),t=t.substr(0,a)),{pathname:t,search:"?"===n?"":n,hash:"#"===r?"":r}}(e),a.state=t):(void 0===(a=(0,r.A)({},e)).pathname&&(a.pathname=""),a.search?"?"!==a.search.charAt(0)&&(a.search="?"+a.search):a.search="",a.hash?"#"!==a.hash.charAt(0)&&(a.hash="#"+a.hash):a.hash="",void 0!==t&&void 0===a.state&&(a.state=t));try{a.pathname=decodeURI(a.pathname)}catch(l){throw l instanceof URIError?new URIError('Pathname "'+a.pathname+'" could not be decoded. This is likely caused by an invalid percent-encoding.'):l}return n&&(a.key=n),o?a.pathname?"/"!==a.pathname.charAt(0)&&(a.pathname=i(a.pathname,o.pathname)):a.pathname=o.pathname:a.pathname||(a.pathname="/"),a}function m(){var e=null;var t=[];return{setPrompt:function(t){return e=t,function(){e===t&&(e=null)}},confirmTransitionTo:function(t,n,r,o){if(null!=e){var a="function"==typeof e?e(t,n):e;"string"==typeof a?"function"==typeof r?r(a,o):o(!0):o(!1!==a)}else o(!0)},appendListener:function(e){var n=!0;function r(){n&&e.apply(void 0,arguments)}return t.push(r),function(){n=!1,t=t.filter((function(e){return e!==r}))}},notifyListeners:function(){for(var e=arguments.length,n=new Array(e),r=0;rt?n.splice(t,n.length-t,o):n.push(o),d({action:r,location:o,index:t,entries:n})}}))},replace:function(e,t){var r="REPLACE",o=f(e,t,h(),w.location);u.confirmTransitionTo(o,r,n,(function(e){e&&(w.entries[w.index]=o,d({action:r,location:o}))}))},go:y,goBack:function(){y(-1)},goForward:function(){y(1)},canGo:function(e){var t=w.index+e;return t>=0&&t{"use strict";var r=n(4363),o={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},a={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},i={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},l={};function s(e){return r.isMemo(e)?i:l[e.$$typeof]||o}l[r.ForwardRef]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},l[r.Memo]=i;var c=Object.defineProperty,u=Object.getOwnPropertyNames,d=Object.getOwnPropertySymbols,p=Object.getOwnPropertyDescriptor,f=Object.getPrototypeOf,m=Object.prototype;e.exports=function e(t,n,r){if("string"!=typeof n){if(m){var o=f(n);o&&o!==m&&e(t,o,r)}var i=u(n);d&&(i=i.concat(d(n)));for(var l=s(t),h=s(n),g=0;g{"use strict";e.exports=function(e,t,n,r,o,a,i,l){if(!e){var s;if(void 0===t)s=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var c=[n,r,o,a,i,l],u=0;(s=new Error(t.replace(/%s/g,(function(){return c[u++]})))).name="Invariant Violation"}throw s.framesToPop=1,s}}},4634:e=>{e.exports=Array.isArray||function(e){return"[object Array]"==Object.prototype.toString.call(e)}},119:(e,t,n)=>{"use strict";n.r(t)},1043:(e,t,n)=>{"use strict";n.r(t)},5947:function(e,t,n){var r,o;r=function(){var e,t,n={version:"0.2.0"},r=n.settings={minimum:.08,easing:"ease",positionUsing:"",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,showSpinner:!0,barSelector:'[role="bar"]',spinnerSelector:'[role="spinner"]',parent:"body",template:'
'};function o(e,t,n){return en?n:e}function a(e){return 100*(-1+e)}function i(e,t,n){var o;return(o="translate3d"===r.positionUsing?{transform:"translate3d("+a(e)+"%,0,0)"}:"translate"===r.positionUsing?{transform:"translate("+a(e)+"%,0)"}:{"margin-left":a(e)+"%"}).transition="all "+t+"ms "+n,o}n.configure=function(e){var t,n;for(t in e)void 0!==(n=e[t])&&e.hasOwnProperty(t)&&(r[t]=n);return this},n.status=null,n.set=function(e){var t=n.isStarted();e=o(e,r.minimum,1),n.status=1===e?null:e;var a=n.render(!t),c=a.querySelector(r.barSelector),u=r.speed,d=r.easing;return a.offsetWidth,l((function(t){""===r.positionUsing&&(r.positionUsing=n.getPositioningCSS()),s(c,i(e,u,d)),1===e?(s(a,{transition:"none",opacity:1}),a.offsetWidth,setTimeout((function(){s(a,{transition:"all "+u+"ms linear",opacity:0}),setTimeout((function(){n.remove(),t()}),u)}),u)):setTimeout(t,u)})),this},n.isStarted=function(){return"number"==typeof n.status},n.start=function(){n.status||n.set(0);var e=function(){setTimeout((function(){n.status&&(n.trickle(),e())}),r.trickleSpeed)};return r.trickle&&e(),this},n.done=function(e){return e||n.status?n.inc(.3+.5*Math.random()).set(1):this},n.inc=function(e){var t=n.status;return t?("number"!=typeof e&&(e=(1-t)*o(Math.random()*t,.1,.95)),t=o(t+e,0,.994),n.set(t)):n.start()},n.trickle=function(){return n.inc(Math.random()*r.trickleRate)},e=0,t=0,n.promise=function(r){return r&&"resolved"!==r.state()?(0===t&&n.start(),e++,t++,r.always((function(){0==--t?(e=0,n.done()):n.set((e-t)/e)})),this):this},n.render=function(e){if(n.isRendered())return document.getElementById("nprogress");u(document.documentElement,"nprogress-busy");var t=document.createElement("div");t.id="nprogress",t.innerHTML=r.template;var o,i=t.querySelector(r.barSelector),l=e?"-100":a(n.status||0),c=document.querySelector(r.parent);return s(i,{transition:"all 0 linear",transform:"translate3d("+l+"%,0,0)"}),r.showSpinner||(o=t.querySelector(r.spinnerSelector))&&f(o),c!=document.body&&u(c,"nprogress-custom-parent"),c.appendChild(t),t},n.remove=function(){d(document.documentElement,"nprogress-busy"),d(document.querySelector(r.parent),"nprogress-custom-parent");var e=document.getElementById("nprogress");e&&f(e)},n.isRendered=function(){return!!document.getElementById("nprogress")},n.getPositioningCSS=function(){var e=document.body.style,t="WebkitTransform"in e?"Webkit":"MozTransform"in e?"Moz":"msTransform"in e?"ms":"OTransform"in e?"O":"";return t+"Perspective"in e?"translate3d":t+"Transform"in e?"translate":"margin"};var l=function(){var e=[];function t(){var n=e.shift();n&&n(t)}return function(n){e.push(n),1==e.length&&t()}}(),s=function(){var e=["Webkit","O","Moz","ms"],t={};function n(e){return e.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,(function(e,t){return t.toUpperCase()}))}function r(t){var n=document.body.style;if(t in n)return t;for(var r,o=e.length,a=t.charAt(0).toUpperCase()+t.slice(1);o--;)if((r=e[o]+a)in n)return r;return t}function o(e){return e=n(e),t[e]||(t[e]=r(e))}function a(e,t,n){t=o(t),e.style[t]=n}return function(e,t){var n,r,o=arguments;if(2==o.length)for(n in t)void 0!==(r=t[n])&&t.hasOwnProperty(n)&&a(e,n,r);else a(e,o[1],o[2])}}();function c(e,t){return("string"==typeof e?e:p(e)).indexOf(" "+t+" ")>=0}function u(e,t){var n=p(e),r=n+t;c(n,t)||(e.className=r.substring(1))}function d(e,t){var n,r=p(e);c(e,t)&&(n=r.replace(" "+t+" "," "),e.className=n.substring(1,n.length-1))}function p(e){return(" "+(e.className||"")+" ").replace(/\s+/gi," ")}function f(e){e&&e.parentNode&&e.parentNode.removeChild(e)}return n},void 0===(o="function"==typeof r?r.call(t,n,t,e):r)||(e.exports=o)},5302:(e,t,n)=>{var r=n(4634);e.exports=f,e.exports.parse=a,e.exports.compile=function(e,t){return l(a(e,t),t)},e.exports.tokensToFunction=l,e.exports.tokensToRegExp=p;var o=new RegExp(["(\\\\.)","([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))"].join("|"),"g");function a(e,t){for(var n,r=[],a=0,i=0,l="",u=t&&t.delimiter||"/";null!=(n=o.exec(e));){var d=n[0],p=n[1],f=n.index;if(l+=e.slice(i,f),i=f+d.length,p)l+=p[1];else{var m=e[i],h=n[2],g=n[3],b=n[4],v=n[5],y=n[6],w=n[7];l&&(r.push(l),l="");var S=null!=h&&null!=m&&m!==h,k="+"===y||"*"===y,x="?"===y||"*"===y,E=n[2]||u,_=b||v;r.push({name:g||a++,prefix:h||"",delimiter:E,optional:x,repeat:k,partial:S,asterisk:!!w,pattern:_?c(_):w?".*":"[^"+s(E)+"]+?"})}}return i{Prism.languages.go=Prism.languages.extend("clike",{string:{pattern:/(^|[^\\])"(?:\\.|[^"\\\r\n])*"|`[^`]*`/,lookbehind:!0,greedy:!0},keyword:/\b(?:break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go(?:to)?|if|import|interface|map|package|range|return|select|struct|switch|type|var)\b/,boolean:/\b(?:_|false|iota|nil|true)\b/,number:[/\b0(?:b[01_]+|o[0-7_]+)i?\b/i,/\b0x(?:[a-f\d_]+(?:\.[a-f\d_]*)?|\.[a-f\d_]+)(?:p[+-]?\d+(?:_\d+)*)?i?(?!\w)/i,/(?:\b\d[\d_]*(?:\.[\d_]*)?|\B\.\d[\d_]*)(?:e[+-]?[\d_]+)?i?(?!\w)/i],operator:/[*\/%^!=]=?|\+[=+]?|-[=-]?|\|[=|]?|&(?:=|&|\^=?)?|>(?:>=?|=)?|<(?:<=?|=|-)?|:=|\.\.\./,builtin:/\b(?:append|bool|byte|cap|close|complex|complex(?:64|128)|copy|delete|error|float(?:32|64)|u?int(?:8|16|32|64)?|imag|len|make|new|panic|print(?:ln)?|real|recover|rune|string|uintptr)\b/}),Prism.languages.insertBefore("go","string",{char:{pattern:/'(?:\\.|[^'\\\r\n]){0,10}'/,greedy:!0}}),delete Prism.languages.go["class-name"]},5538:()=>{!function(e){var t={pattern:/((?:^|[^\\$])(?:\\{2})*)\$(?:\w+|\{[^{}]*\})/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{?|\}$/,alias:"punctuation"},expression:{pattern:/[\s\S]+/,inside:null}}};e.languages.groovy=e.languages.extend("clike",{string:{pattern:/'''(?:[^\\]|\\[\s\S])*?'''|'(?:\\.|[^\\'\r\n])*'/,greedy:!0},keyword:/\b(?:abstract|as|assert|boolean|break|byte|case|catch|char|class|const|continue|def|default|do|double|else|enum|extends|final|finally|float|for|goto|if|implements|import|in|instanceof|int|interface|long|native|new|package|private|protected|public|return|short|static|strictfp|super|switch|synchronized|this|throw|throws|trait|transient|try|void|volatile|while)\b/,number:/\b(?:0b[01_]+|0x[\da-f_]+(?:\.[\da-f_p\-]+)?|[\d_]+(?:\.[\d_]+)?(?:e[+-]?\d+)?)[glidf]?\b/i,operator:{pattern:/(^|[^.])(?:~|==?~?|\?[.:]?|\*(?:[.=]|\*=?)?|\.[@&]|\.\.<|\.\.(?!\.)|-[-=>]?|\+[+=]?|!=?|<(?:<=?|=>?)?|>(?:>>?=?|=)?|&[&=]?|\|[|=]?|\/=?|\^=?|%=?)/,lookbehind:!0},punctuation:/\.+|[{}[\];(),:$]/}),e.languages.insertBefore("groovy","string",{shebang:{pattern:/#!.+/,alias:"comment",greedy:!0},"interpolation-string":{pattern:/"""(?:[^\\]|\\[\s\S])*?"""|(["/])(?:\\.|(?!\1)[^\\\r\n])*\1|\$\/(?:[^/$]|\$(?:[/$]|(?![/$]))|\/(?!\$))*\/\$/,greedy:!0,inside:{interpolation:t,string:/[\s\S]+/}}}),e.languages.insertBefore("groovy","punctuation",{"spock-block":/\b(?:and|cleanup|expect|given|setup|then|when|where):/}),e.languages.insertBefore("groovy","function",{annotation:{pattern:/(^|[^.])@\w+/,lookbehind:!0,alias:"punctuation"}}),t.inside.expression.inside=e.languages.groovy}(Prism)},6976:()=>{!function(e){var t=/\b(?:abstract|assert|boolean|break|byte|case|catch|char|class|const|continue|default|do|double|else|enum|exports|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|module|native|new|non-sealed|null|open|opens|package|permits|private|protected|provides|public|record(?!\s*[(){}[\]<>=%~.:,;?+\-*/&|^])|requires|return|sealed|short|static|strictfp|super|switch|synchronized|this|throw|throws|to|transient|transitive|try|uses|var|void|volatile|while|with|yield)\b/,n=/(?:[a-z]\w*\s*\.\s*)*(?:[A-Z]\w*\s*\.\s*)*/.source,r={pattern:RegExp(/(^|[^\w.])/.source+n+/[A-Z](?:[\d_A-Z]*[a-z]\w*)?\b/.source),lookbehind:!0,inside:{namespace:{pattern:/^[a-z]\w*(?:\s*\.\s*[a-z]\w*)*(?:\s*\.)?/,inside:{punctuation:/\./}},punctuation:/\./}};e.languages.java=e.languages.extend("clike",{string:{pattern:/(^|[^\\])"(?:\\.|[^"\\\r\n])*"/,lookbehind:!0,greedy:!0},"class-name":[r,{pattern:RegExp(/(^|[^\w.])/.source+n+/[A-Z]\w*(?=\s+\w+\s*[;,=()]|\s*(?:\[[\s,]*\]\s*)?::\s*new\b)/.source),lookbehind:!0,inside:r.inside},{pattern:RegExp(/(\b(?:class|enum|extends|implements|instanceof|interface|new|record|throws)\s+)/.source+n+/[A-Z]\w*\b/.source),lookbehind:!0,inside:r.inside}],keyword:t,function:[e.languages.clike.function,{pattern:/(::\s*)[a-z_]\w*/,lookbehind:!0}],number:/\b0b[01][01_]*L?\b|\b0x(?:\.[\da-f_p+-]+|[\da-f_]+(?:\.[\da-f_p+-]+)?)\b|(?:\b\d[\d_]*(?:\.[\d_]*)?|\B\.\d[\d_]*)(?:e[+-]?\d[\d_]*)?[dfl]?/i,operator:{pattern:/(^|[^.])(?:<<=?|>>>?=?|->|--|\+\+|&&|\|\||::|[?:~]|[-+*/%&|^!=<>]=?)/m,lookbehind:!0},constant:/\b[A-Z][A-Z_\d]+\b/}),e.languages.insertBefore("java","string",{"triple-quoted-string":{pattern:/"""[ \t]*[\r\n](?:(?:"|"")?(?:\\.|[^"\\]))*"""/,greedy:!0,alias:"string"},char:{pattern:/'(?:\\.|[^'\\\r\n]){1,6}'/,greedy:!0}}),e.languages.insertBefore("java","class-name",{annotation:{pattern:/(^|[^.])@\w+(?:\s*\.\s*\w+)*/,lookbehind:!0,alias:"punctuation"},generics:{pattern:/<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&))*>)*>)*>)*>/,inside:{"class-name":r,keyword:t,punctuation:/[<>(),.:]/,operator:/[?&|]/}},import:[{pattern:RegExp(/(\bimport\s+)/.source+n+/(?:[A-Z]\w*|\*)(?=\s*;)/.source),lookbehind:!0,inside:{namespace:r.inside.namespace,punctuation:/\./,operator:/\*/,"class-name":/\w+/}},{pattern:RegExp(/(\bimport\s+static\s+)/.source+n+/(?:\w+|\*)(?=\s*;)/.source),lookbehind:!0,alias:"static",inside:{namespace:r.inside.namespace,static:/\b\w+$/,punctuation:/\./,operator:/\*/,"class-name":/\w+/}}],namespace:{pattern:RegExp(/(\b(?:exports|import(?:\s+static)?|module|open|opens|package|provides|requires|to|transitive|uses|with)\s+)(?!)[a-z]\w*(?:\.[a-z]\w*)*\.?/.source.replace(//g,(function(){return t.source}))),lookbehind:!0,inside:{punctuation:/\./}}})}(Prism)},9700:()=>{!function(e){function t(e,t){return"___"+e.toUpperCase()+t+"___"}Object.defineProperties(e.languages["markup-templating"]={},{buildPlaceholders:{value:function(n,r,o,a){if(n.language===r){var i=n.tokenStack=[];n.code=n.code.replace(o,(function(e){if("function"==typeof a&&!a(e))return e;for(var o,l=i.length;-1!==n.code.indexOf(o=t(r,l));)++l;return i[l]=e,o})),n.grammar=e.languages.markup}}},tokenizePlaceholders:{value:function(n,r){if(n.language===r&&n.tokenStack){n.grammar=e.languages[r];var o=0,a=Object.keys(n.tokenStack);!function i(l){for(var s=0;s=a.length);s++){var c=l[s];if("string"==typeof c||c.content&&"string"==typeof c.content){var u=a[o],d=n.tokenStack[u],p="string"==typeof c?c:c.content,f=t(r,u),m=p.indexOf(f);if(m>-1){++o;var h=p.substring(0,m),g=new e.Token(r,e.tokenize(d,n.grammar),"language-"+r,d),b=p.substring(m+f.length),v=[];h&&v.push.apply(v,i([h])),v.push(g),b&&v.push.apply(v,i([b])),"string"==typeof c?l.splice.apply(l,[s,1].concat(v)):c.content=v}}else c.content&&i(c.content)}return l}(n.tokens)}}}})}(Prism)},9535:()=>{!function(e){var t=/\b(?:bool|bytes|double|s?fixed(?:32|64)|float|[su]?int(?:32|64)|string)\b/;e.languages.protobuf=e.languages.extend("clike",{"class-name":[{pattern:/(\b(?:enum|extend|message|service)\s+)[A-Za-z_]\w*(?=\s*\{)/,lookbehind:!0},{pattern:/(\b(?:rpc\s+\w+|returns)\s*\(\s*(?:stream\s+)?)\.?[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*(?=\s*\))/,lookbehind:!0}],keyword:/\b(?:enum|extend|extensions|import|message|oneof|option|optional|package|public|repeated|required|reserved|returns|rpc(?=\s+\w)|service|stream|syntax|to)\b(?!\s*=\s*\d)/,function:/\b[a-z_]\w*(?=\s*\()/i}),e.languages.insertBefore("protobuf","operator",{map:{pattern:/\bmap<\s*[\w.]+\s*,\s*[\w.]+\s*>(?=\s+[a-z_]\w*\s*[=;])/i,alias:"class-name",inside:{punctuation:/[<>.,]/,builtin:t}},builtin:t,"positional-class-name":{pattern:/(?:\b|\B\.)[a-z_]\w*(?:\.[a-z_]\w*)*(?=\s+[a-z_]\w*\s*[=;])/i,alias:"class-name",inside:{punctuation:/\./}},annotation:{pattern:/(\[\s*)[a-z_]\w*(?=\s*=)/i,lookbehind:!0}})}(Prism)},3524:(e,t,n)=>{var r={"./prism-go":6378,"./prism-groovy":5538,"./prism-java":6976,"./prism-protobuf":9535};function o(e){var t=a(e);return n(t)}function a(e){if(!n.o(r,e)){var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}return r[e]}o.keys=function(){return Object.keys(r)},o.resolve=a,e.exports=o,o.id=3524},2694:(e,t,n)=>{"use strict";var r=n(6925);function o(){}function a(){}a.resetWarningCache=o,e.exports=function(){function e(e,t,n,o,a,i){if(i!==r){var l=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw l.name="Invariant Violation",l}}function t(){return e}e.isRequired=e;var n={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:a,resetWarningCache:o};return n.PropTypes=n,n}},5556:(e,t,n)=>{e.exports=n(2694)()},6925:e=>{"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},2551:(e,t,n)=>{"use strict";var r=n(6540),o=n(9982);function a(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n