[{"data":1,"prerenderedAt":9449},["ShallowReactive",2],{"blog-post-content-weekly-digest-2023-cw5-cw6":3,"list-blog-posts-null-null-null":199},{"id":4,"title":5,"active":6,"body":7,"date":186,"description":187,"duration":188,"extension":189,"level":190,"meta":191,"navigation":6,"ogDescription":190,"ogImage":190,"ogImageAlt":190,"ogTitle":190,"path":192,"pinned":190,"seo":193,"stem":194,"tags":195,"titleAlt":197,"titlePage":190,"__hash__":198},"blog\u002Fblog\u002F10007.weekly-digest-2023-cw5-cw6.md","Weekly Digest · 2023 CW 5 + CW 6",true,{"type":8,"value":9,"toc":182},"minimark",[10,15,82,86,105,109],[11,12,14],"h4",{"id":13},"interesting-reads","Interesting reads",[16,17,18,51,70],"ul",{},[19,20,21,22,29,32,33,38],"li",{},"Actually already almost a year old... but a good talk nevertheless: ",[23,24,28],"a",{"href":25,"rel":26},"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=zNNtnzLkFsY",[27],"nofollow","Decodable presentation for Netflix",[30,31],"br",{},"\nPart 2 is a very informative and natural demo by ",[23,34,37],{"href":35,"rel":36},"https:\u002F\u002Ftwitter.com\u002Fsharon_rxie",[27],"Sharon Xie @sharon_rxie",[16,39,40,43],{},[19,41,42],{},"..Decodable is a real-time stream processing platform",[19,44,45,46],{},"Decodable has already moved forward massively over the course of the past year, so go and check out the latest docs & talks on their ",[23,47,50],{"href":48,"rel":49},"https:\u002F\u002Fwww.decodable.co\u002Fblog",[27],"blog",[19,52,53,54,58,59,64,66],{},"Another blog post on '",[55,56,57],"strong",{},"the rapid rise of apache flink","' by ",[23,60,63],{"href":61,"rel":62},"https:\u002F\u002Ftwitter.com\u002Frmetzger_",[27],"@rmetzger_",[30,65],{},[23,67,68],{"href":68,"rel":69},"https:\u002F\u002Fwww.datanami.com\u002F2023\u002F01\u002F30\u002Ffive-drivers-behind-the-rapid-rise-of-apache-flink\u002F",[27],[19,71,72,73,76,77],{},"Brush up your understanding on ",[55,74,75],{},"Kafka Streams: Transactions & Exactly-Once Messaging"," in this descriptive ",[23,78,81],{"href":79,"rel":80},"https:\u002F\u002Fmedium.com\u002Flydtech-consulting\u002Fkafka-streams-transactions-exactly-once-messaging-82194b50900a",[27],"blog post",[11,83,85],{"id":84},"released-this-week","Released this week",[16,87,88,95],{},[19,89,90],{},[23,91,94],{"href":92,"rel":93},"https:\u002F\u002Fblog.jetbrains.com\u002Fkotlin\u002F2023\u002F02\u002Fk2-kotlin-2-0\u002F",[27],"The K2 Compiler is going stable in Kotlin 2.0",[19,96,97,100,101],{},[55,98,99],{},"Lighthouse 10"," with changes to scoring and new audits: ",[23,102,103],{"href":103,"rel":104},"https:\u002F\u002Fdeveloper.chrome.com\u002Fblog\u002Flighthouse-10-0\u002F",[27],[11,106,108],{"id":107},"progress-on-thrivingdev","Progress on thriving.dev",[16,110,111,130,133],{},[19,112,113,114],{},"thriving.dev is now enabled for crawlers\n",[16,115,116],{},[19,117,118,119,124,125],{},"also with auto-generated sitemap.xml using ",[23,120,123],{"href":121,"rel":122},"https:\u002F\u002Fnuxt.com\u002Fmodules\u002Fsimple-sitemap",[27],"nuxt-simple-sitemap",", built by ",[23,126,129],{"href":127,"rel":128},"https:\u002F\u002Ftwitter.com\u002Fharlan_zw",[27],"@harlan_zw",[19,131,132],{},"added Open Graph & Twitter cards support",[19,134,135,136,140,141,144,145],{},"changed ",[137,138,139],"code",{},"font-display"," to ",[55,142,143],{},"swap"," and added fallback fonts to improve CLS scores\n",[16,146,147,164,173],{},[19,148,149,150,154,155,158,159],{},"fallback fonts for ",[151,152,153],"em",{},"Montserrat"," & ",[151,156,157],{},"Raleway"," are generated\u002Ftaken from ",[23,160,163],{"href":161,"rel":162},"https:\u002F\u002Fdeploy-preview-15--upbeat-shirley-608546.netlify.app\u002Fperfect-ish-font-fallback\u002F?font=Raleway",[27],"this page",[19,165,166,167,172],{},"Note: I've also tried nuxt module ",[23,168,171],{"href":169,"rel":170},"https:\u002F\u002Fnuxt.com\u002Fmodules\u002Ffontaine",[27],"fontaine"," but found it wasn't doing quite as good a job",[19,174,175,176,181],{},"FYI: there's also a tool ",[23,177,180],{"href":178,"rel":179},"https:\u002F\u002Fmeowni.ca\u002Ffont-style-matcher\u002F",[27],"font-style-matcher"," which is interesting but I found for different content the accuracy can significantly differ.",{"title":183,"searchDepth":184,"depth":184,"links":185},"",2,[],"2023-02-13","This week's share of articles I found helpful or interesting, releases, tips.","2min","md",null,{},"\u002Fblog\u002Fweekly-digest-2023-cw5-cw6",{"title":5,"description":187},"blog\u002F10007.weekly-digest-2023-cw5-cw6",[196],"activity","2023 Calendar Week #5 & #6","YCbN9ak8EbXZAkZ2zZztm24Tc_OaFKbwY2sugkMjEdk",[200,1022,1443,1855,2198,2649,2961,3537,3961,4385,5097,6404,6632,6788,6906,7563,8879,8990,9071,9149,9228,9316,9354,9399],{"id":201,"title":202,"active":6,"body":203,"date":1006,"description":1007,"duration":1008,"extension":189,"level":1009,"meta":1010,"navigation":6,"ogDescription":190,"ogImage":1011,"ogImageAlt":211,"ogTitle":202,"path":1012,"pinned":184,"seo":1013,"stem":1014,"tags":1015,"titleAlt":190,"titlePage":1020,"__hash__":1021},"blog\u002Fblog\u002F8.reduce-rebalance-downtime-for-stateless-kafka-streams-apps.md","Reduce Rebalance Downtime (by x450) for Stateless Kafka Streams Apps [Simple Steps]",{"type":8,"value":204,"toc":986},[205,215,222,237,240,249,260,275,280,285,288,298,332,341,348,359,362,366,387,399,419,425,429,432,435,438,444,449,455,470,473,485,488,540,543,553,563,571,574,597,601,611,614,618,628,634,642,646,652,660,666,676,680,683,693,700,703,713,730,736,740,744,755,761,791,806,810,813,822,826,844,850,860,863,930,934,982],[206,207,208],"p",{},[209,210],"img",{"alt":211,"height":212,"src":213,"width":214,"preload":183},"Cover image showing the Kafka log, a stopwatch, suggesting 'kafka-streams partition rebalance downtime' to be reduced from 45s to 100ms",1906,"\u002Fassets\u002Fblog\u002F8.reduce-rebalance-downtime-for-stateless-kafka-streams-apps\u002Fleave-group-on-close_header.png",3797,[206,216,217,218,221],{},"In this post, we’ll ",[55,219,220],{},"learn how Kafka Streams Consumers behave differently from regular Kafka Consumers",", the consequences for the application, as well as steps to minimise downtimes in event processing when consumer group members change.",[206,223,224,225,228,229,232,233,236],{},"With the ",[55,226,227],{},"default configuration",", a containerised ",[151,230,231],{},"stateless"," ",[55,234,235],{},"Streams app pauses processing for >45s"," when one app instance (group member) is removed or restarted.\nFor real-time data streaming workloads with a low e2e latency as an NFR (non-functional requirement), such a long ‘rebalance downtime’ often is unacceptable.",[206,238,239],{},"Fortunately, there’s a simple yet efficient solution to address this problem.",[241,242,246],"tip-box",{"icon":243,"icon-classes":244,"title":245},"ph:student","text-gray-900\u002F90 ml-0 -mt-px","Bonus",[206,247,248],{},"As a bonus, we will look under the hood of Kafka Consumer Groups, the Group Coordinator & Rebalance Protocol, and measure, analyse and evaluate a simulation of a group member (replica) re-creation running on Kubernetes.",[206,250,251,252,255,256,259],{},"...",[55,253,254],{},"TLDR","? => ",[55,257,258],{},"Spoiler",":",[261,262,266],"pre",{"className":263,"code":264,"language":265,"meta":183,"style":183},"language-java shiki shiki-themes github-light github-dark","props.put(\"internal.leave.group.on.close\", true);\n","java",[137,267,268],{"__ignoreMap":183},[269,270,273],"span",{"class":271,"line":272},"line",1,[269,274,264],{},[276,277,279],"h2",{"id":278},"theory","Theory",[281,282,284],"h3",{"id":283},"regular-consumer-behaviour","Regular Consumer Behaviour",[206,286,287],{},"Let’s briefly recap on Kafka Consumers and Consumer groups.",[289,290,291],"blockquote",{},[206,292,293,294,297],{},"An Apache Kafka® ",[55,295,296],{},"Consumer"," is a client application that subscribes to (reads and processes) events.",[289,299,300,311,318,325],{},[206,301,302,303,306,307,310],{},"A ",[55,304,305],{},"consumer group"," is a set of consumers which cooperate to consume data from some topics. The partitions of all the topics are divided among the consumers in the group. As new group members arrive and old members leave, the partitions are re-assigned so that each member receives a proportional share of the partitions. This is known as ",[151,308,309],{},"rebalancing"," the group.",[206,312,313,314,317],{},"(…) One of the brokers is designated as the group’s ",[55,315,316],{},"coordinator"," and is responsible for managing the members of the group as well as their partition assignments.",[206,319,320,321,324],{},"(…) When the consumer starts up, it finds the coordinator for its group and sends a request to join the group. The coordinator then begins a group rebalance so that the new member is assigned its fair share of the group’s partitions. Every rebalance results in a new ",[55,322,323],{},"generation"," of the group.",[206,326,327,328,331],{},"Each member in the group must send heartbeats to the coordinator in order to remain a member of the group. If no heartbeat is received before expiration of the configured ",[55,329,330],{},"session timeout",", then the coordinator will kick the member out of the group and reassign its partitions to another member.",[206,333,334,335,340],{},"(Source: ",[23,336,339],{"href":337,"rel":338},"https:\u002F\u002Fdocs.confluent.io\u002Fplatform\u002Fcurrent\u002Fclients\u002Fconsumer.html",[27],"Kafka Consumer | Confluent Documentation",")",[206,342,343,344,347],{},"When a consumer leaves a group due to a controlled shutdown or a crash, its partitions are reassigned automatically to other consumers. Similarly, when a consumer (re) joins an existing group, all partitions are rebalanced among the group members. This dynamic group cooperation is facilitated by the ",[55,345,346],{},"Kafka Rebalance Protocol",".",[206,349,350,351,354,355,358],{},"For a rebalance scenario where one instance is stopped, the Consumer sends a ",[137,352,353],{},"LeaveGroup"," request to the coordinator before stopping (as part of a graceful shutdown, ",[137,356,357],{},"Consumer#close()","), which triggers a rebalance.",[206,360,361],{},"During the entire rebalancing process, i.e. as long as the partitions are not reassigned, consumers no longer process any data. Fortunately, rebalancing is very fast, typically between anything from 50ms to seconds. It may vary depending on different factors, such as load on your Kafka cluster or the complexity of your Streams topology (no. of input topics, streams tasks := partitions, and state stores, … -> total no. of consumers).",[281,363,365],{"id":364},"streams-consumer-behaviour","!= Streams Consumer Behaviour",[206,367,368,369,374,375,378,379,382,383,386],{},"For Kafka Streams, some config properties are overridden via (",[23,370,373],{"href":371,"rel":372},"https:\u002F\u002Fgithub.com\u002Fapache\u002Fkafka\u002Fblob\u002F3.4.1\u002Fstreams\u002Fsrc\u002Fmain\u002Fjava\u002Forg\u002Fapache\u002Fkafka\u002Fstreams\u002FStreamsConfig.java#L1115",[27],"StreamsConfig.CONSUMER_DEFAULT_OVERRIDES","). One of those properties is ",[137,376,377],{},"\"internal.leave.group.on.close\"",", set to ",[137,380,381],{},"false"," (enabled by default for ",[151,384,385],{},"regular"," Consumers).",[206,388,389,390,393,394,347],{},"Please note it’s a ",[151,391,392],{},"non-public"," config, which may change without prior notice with new releases.\nReference: ",[23,395,398],{"href":396,"rel":397},"https:\u002F\u002Fgithub.com\u002Fapache\u002Fkafka\u002Fblob\u002F3.4.1\u002Fclients\u002Fsrc\u002Fmain\u002Fjava\u002Forg\u002Fapache\u002Fkafka\u002Fclients\u002Fconsumer\u002FConsumerConfig.java#L300",[27],"ConsumerConfig.LEAVE_GROUP_ON_CLOSE_CONFIG",[206,400,401,402,404,405,408,409,412,413,418],{},"This means Consumers will not send ",[137,403,353],{}," requests when stopped but will be removed by the coordinator only when the Consumer session times out (ref. ",[137,406,407],{},"session.timeout.ms",").\nThe ",[55,410,411],{},"default Consumer session timeout is 45s"," (note: was 10s before the Kafka 3.0.0 release, ref\n",[23,414,417],{"href":415,"rel":416},"https:\u002F\u002Fcwiki.apache.org\u002Fconfluence\u002Fdisplay\u002FKAFKA\u002FKIP-735%3A+Increase+default+consumer+session+timeout",[27],"KIP-735","). Consequently, no data is processed for more than 45 seconds for tasks assigned to the Consumer that had been stopped.",[206,420,421,422,424],{},"It even worsens if a new Consumer (re) joins the group while suspected dead (no more heartbeats received), where all consumers shut down, and task assignment is blocked until the timeout is exceeded. The coordinator evicts the old Consumer that had been stopped from the group. Until then, processing comes completely to a halt for all tasks, also known as ‘stop-the-world’ rebalancing. While the ‘incremental cooperative rebalancing protocol’ introduced with Kafka 2.5 avoids ‘stop-the-world’ rebalancing for ",[151,423,385],{}," Consumers, the mentioned Kafka Streams overrides nullify some aspects.",[276,426,428],{"id":427},"example-scenario-kubernetes-pod-evicted-and-replaced","Example Scenario: Kubernetes Pod Evicted … and Replaced",[206,430,431],{},"Running your apps on Kubernetes takes a long way to achieve a robust, highly-available deployment. Kubernetes monitors your containers’ health, allows you to scale, and ensures all desired replicas are up and running according to your spec.",[206,433,434],{},"But still, to be truly elastic and minimise downtime of your data stream processing, your application must be able to handle Pods (\u002Fcontainer) to be restarted, evicted, and re-created gracefully.\nThere are many potential causes, e.g. application upgrades (CI\u002FCD), k8s cluster security patching, (auto-)scaling, resource shortage, or k8s nodes running on Spot instances being interrupted.",[206,436,437],{},"Next, we look at a simple yet common example.",[206,439,440,443],{},[55,441,442],{},"Example infrastructure setup:"," Stateless Kafka Streams app, 6 streams tasks, running on Kubernetes as Deployment, with 3 replicas.",[445,446],"img-carousel",{":images":447,"subtitle":448},"[{\"src\":\"\u002Fassets\u002Fblog\u002F8.reduce-rebalance-downtime-for-stateless-kafka-streams-apps\u002Fleave-group-on-close_pod-eviction-replace_1_v2.0.png\",\"alt\":\"'Software Architecture' \u002F 'Kubernetes Deployment' diagram, showing the setup of the simulation - Step 1\",\"width\":2427,\"height\":1428,\"quality\":80,\"format\":\"webp\",\"sizes\":\"sm:792 lg:1584\",\"loading\":\"lazy\"},{\"src\":\"\u002Fassets\u002Fblog\u002F8.reduce-rebalance-downtime-for-stateless-kafka-streams-apps\u002Fleave-group-on-close_pod-eviction-replace_2_v2.0.png\",\"alt\":\"'Software Architecture' \u002F 'Kubernetes Deployment' diagram, showing the setup of the simulation - Step 2\",\"width\":2427,\"height\":1426,\"quality\":80,\"format\":\"webp\",\"sizes\":\"sm:792 lg:1584\",\"loading\":\"lazy\"},{\"src\":\"\u002Fassets\u002Fblog\u002F8.reduce-rebalance-downtime-for-stateless-kafka-streams-apps\u002Fleave-group-on-close_pod-eviction-replace_3_v2.0.png\",\"alt\":\"'Software Architecture' \u002F 'Kubernetes Deployment' diagram, showing the setup of the simulation - Step 3\",\"width\":2427,\"height\":1768,\"quality\":80,\"format\":\"webp\",\"sizes\":\"sm:792 lg:1584\",\"loading\":\"lazy\"},{\"src\":\"\u002Fassets\u002Fblog\u002F8.reduce-rebalance-downtime-for-stateless-kafka-streams-apps\u002Fleave-group-on-close_pod-eviction-replace_4_v2.0.png\",\"alt\":\"'Software Architecture' \u002F 'Kubernetes Deployment' diagram, showing the setup of the simulation - Step 4\",\"width\":2427,\"height\":1768,\"quality\":80,\"format\":\"webp\",\"sizes\":\"sm:792 lg:1584\",\"loading\":\"lazy\"}]","Image: Software Architecture \u002F Kubernetes Deployment diagram, simulation setup. Steps 1-4",[206,450,451,454],{},[55,452,453],{},"Scenario:"," One pod is terminated and successively replaced.",[456,457,458,461,464,467],"ol",{},[19,459,460],{},"Initial state, all 3 pods are running & healthy, the streams app is processing, balanced task assignment",[19,462,463],{},"Pod (P1.1) terminated (deleted) by k8s, shutting down gracefully",[19,465,466],{},"A replacement Pod (P1.2) is scheduled & placed",[19,468,469],{},"Final state, the replacement Pod is running & healthy, the streams app is processing, balanced task assignment",[206,471,472],{},"I would like to share a screenshot depicting the consumer lag metrics, rendered in Grafana, for a simulation of our scenario.",[206,474,475],{},[209,476],{"alt":477,"height":478,"src":479,"width":480,"className":481,"dataZoomSrc":483,"loading":484},"Screenshot depicting the consumer lag metrics, rendered in Grafana - Kafka Streams default config",2302,"\u002Fassets\u002Fblog\u002F8.reduce-rebalance-downtime-for-stateless-kafka-streams-apps\u002Fgrafana_consumer-group-lag_kafka-streams-default.png",3058,[482],"image-zoomable","\u002Fassets\u002Fblog\u002F8.reduce-rebalance-downtime-for-stateless-kafka-streams-apps\u002Fgrafana_consumer-group-lag_kafka-streams-default.webp","lazy",[206,486,487],{},"Let’s walk through the results and explain the behaviour:",[489,490,495],"div",{":mb-4":491,"className":492},"true",[493,494],"text-sm","-mt-2",[16,496,497,507,517,523,528,534],{},[19,498,499,502,503,506],{},[55,500,501],{},"18:34:00:"," the Pod (P1.1) is terminated and stops processing. The consumer lag of partitions ",[137,504,505],{},"[1,4]"," starts to build up",[19,508,509,512,513,516],{},[55,510,511],{},"18:34:20:"," the replacement Pod (P1.2) has come up; the streams task sends a ",[137,514,515],{},"JoinGroup","  to the group coordinator",[19,518,519,522],{},[55,520,521],{},"18:34:21:"," rebalancing triggered, assignments revoked, pauses - due to no heartbeats received from (P1.1)",[19,524,525,527],{},[55,526,521],{}," all consumers pause processing, waiting for assignment; lag starts to build up for all partitions",[19,529,530,533],{},[55,531,532],{},"18:34:45:"," rebalancing continues, new assignment, processing continues",[19,535,536,539],{},[55,537,538],{},"18:34:48:"," all consumers caught up; consumer lags are back to healthy jitter",[206,541,542],{},"To better illustrate everything that is happening over time, here’s a time bar diagram highlighting all important steps:",[206,544,545],{},[209,546],{"alt":547,"height":548,"src":549,"width":550,"className":551,"dataZoomSrc":552,"loading":484},"Diagram illustrating the rebalancing behaviour for a k8s Pod recreation - Kafka Streams default config",3486,"\u002Fassets\u002Fblog\u002F8.reduce-rebalance-downtime-for-stateless-kafka-streams-apps\u002Fkafka-streams-app_k8s-pod-recreate_rebalancing-behaviour_default-config.png",5713,[482],"\u002Fassets\u002Fblog\u002F8.reduce-rebalance-downtime-for-stateless-kafka-streams-apps\u002Fkafka-streams-app_k8s-pod-recreate_rebalancing-behaviour_default-config.webp",[206,554,555,556,559,560,347],{},"Here are the belonging application logs for the rebalancing, which occurred at ",[137,557,558],{},"16:34:44.988"," and took ",[55,561,562],{},"92ms",[261,564,569],{"className":565,"code":567,"language":568},[566],"language-text","2023-06-17 16:34:44,988 INFO State transition from RUNNING to REBALANCING\n2023-06-17 16:34:45,080 INFO State transition from REBALANCING to RUNNING\n","text",[137,570,567],{"__ignoreMap":183},[206,572,573],{},"So we can conclude the following downtimes:",[16,575,576,586],{},[19,577,578,579,582,583],{},"partitions ",[137,580,581],{},"[2,5]",": ",[55,584,585],{},"48s",[19,587,578,588,582,591,594,595,347],{},[137,589,590],{},"[0,1,2,3,4]",[55,592,593],{},"25s","\nWhile the actual rebalancing took only ",[55,596,562],{},[276,598,600],{"id":599},"wait-48s-really","😵 Wait, 48s? Really???",[206,602,603,604,607,608,347],{},"Depending on your stream processing use case, 45s+ downtime might be no big deal, but ",[55,605,606],{},"for real-time low-latency data streams",", it’s a massive ",[55,609,610],{},"breach of the NFR",[206,612,613],{},"So let’s see what options we’ve got to mitigate:",[281,615,617],{"id":616},"option-1-lower-consumer-session-timeout","Option 1: Lower consumer session timeout",[206,619,620,621,623,624,627],{},"Since the session timeout determines the downtime, one way to mitigate is to reduce ",[137,622,407],{},".\nDon’t forget to decrease the value of ",[137,625,626],{},"heartbeat.interval.ms"," to ensure three heartbeats plus a buffer can fit within the timeout period.",[261,629,632],{"className":630,"code":631,"language":568},[566],"session.timeout.ms=6000\nheartbeat.interval.ms=1500\n",[137,633,631],{"__ignoreMap":183},[206,635,636,637],{},"Read the config here: ",[23,638,641],{"href":639,"rel":640},"https:\u002F\u002Fdocs.confluent.io\u002Fplatform\u002Fcurrent\u002Finstallation\u002Fconfiguration\u002Fconsumer-configs.html",[27],"Kafka Consumer Configurations",[281,643,645],{"id":644},"option-2-enable-leavegrouponclose","Option 2: Enable ‘leaveGroupOnClose’",[206,647,648,649,651],{},"…but why work with timeouts when it’s perfectly valid to have your ",[151,650,231],{}," Streams Consumers notify the coordinator when closing down?!?",[206,653,654,655,659],{},"To enable ‘leaveGroupOnClose’ (overriding the ",[23,656,658],{"href":371,"rel":657},[27],"override"," 😜), configure your Kafka Streams app with following property:",[261,661,664],{"className":662,"code":663,"language":568},[566],"internal.leave.group.on.close=true\n",[137,665,663],{"__ignoreMap":183},[667,668,669],"warn-box",{},[206,670,389,671,393,673,347],{},[151,672,392],{},[23,674,398],{"href":396,"rel":675},[27],[276,677,679],{"id":678},"re-do-the-example-with-leavegrouponclose","Re-do the Example with ‘leaveGroupOnClose’ 🚀",[206,681,682],{},"Drum roll 🥁 … and here, without further ado, the results:",[206,684,685],{},[209,686],{"alt":687,"height":688,"src":689,"width":690,"className":691,"dataZoomSrc":692,"loading":484},"Screenshot depicting the consumer lag metrics, rendered in Grafana - Kafka Streams with 'internal.leave.group.on.close=true'",2300,"\u002Fassets\u002Fblog\u002F8.reduce-rebalance-downtime-for-stateless-kafka-streams-apps\u002Fgrafana_consumer-group-lag_kafka-streams_leave-group-on-close.png",3056,[482],"\u002Fassets\u002Fblog\u002F8.reduce-rebalance-downtime-for-stateless-kafka-streams-apps\u002Fgrafana_consumer-group-lag_kafka-streams_leave-group-on-close.webp",[206,694,695,696,699],{},"As we can (",[151,697,698],{},"not",") see - the two rebalancings complete so fast that there's not even the slightest consumer lag increase visible in the metrics.",[206,701,702],{},"Here’s the visual explanation:",[206,704,705],{},[209,706],{"alt":707,"height":708,"src":709,"width":710,"className":711,"dataZoomSrc":712,"loading":484},"Diagram illustrating the rebalancing behaviour for a k8s Pod recreation - Kafka Streams with 'internal.leave.group.on.close=true'",3525,"\u002Fassets\u002Fblog\u002F8.reduce-rebalance-downtime-for-stateless-kafka-streams-apps\u002Fkafka-streams-app_k8s-pod-recreate_rebalancing-behaviour_leave-group-on-close.png",3636,[482],"\u002Fassets\u002Fblog\u002F8.reduce-rebalance-downtime-for-stateless-kafka-streams-apps\u002Fkafka-streams-app_k8s-pod-recreate_rebalancing-behaviour_leave-group-on-close.webp",[206,714,715,716,719,720,722,723,726,727,347],{},"Finally, here are also the application logs showing the timings of the rebalancing, which happened twice. One at ",[137,717,718],{},"17:46:00.332"," that took ",[55,721,562],{},", and the other at ",[137,724,725],{},"17:46:21.361"," in ",[55,728,729],{},"98ms",[261,731,734],{"className":732,"code":733,"language":568},[566],"2023-06-17 17:46:00,332 INFO State transition from RUNNING to REBALANCING\n2023-06-17 17:46:00,424 INFO State transition from REBALANCING to RUNNING\n2023-06-17 17:46:21,361 INFO State transition from RUNNING to REBALANCING\n2023-06-17 17:46:21,458 INFO State transition from REBALANCING to RUNNING\n",[137,735,733],{"__ignoreMap":183},[276,737,739],{"id":738},"pro-tips","Pro Tips",[281,741,743],{"id":742},"stateless-stateful","Stateless \u003C> Stateful",[206,745,746,747,750,751,754],{},"This post recommends setting ",[137,748,749],{},"internal.leave.group.on.close=true"," for ",[55,752,753],{},"stateless (!)"," Kafka Streams applications.",[206,756,757,758,760],{},"Before implementing ",[137,759,749],{}," for stateful applications, it is crucial to understand all potential consequences.",[762,763,764,773,785],"info-box",{},[206,765,766,767,769,770,772],{},"Unfortunately, my evaluation using ",[137,768,749],{}," in combination with standby replicas was not very promising.",[30,771],{},"\nThe expected fluent task re-assignment to hot standby while one replica \"restarts\" - and subsequent re-distribution of tasks, does not work.",[206,774,775,776,779,780,784],{},"The Kafka Streams specific ",[137,777,778],{},"HighAvailabilityTaskAssignor"," has known issues such as uneven task assignment, frozen warmup tasks ('task movement'), and not recognising caught-up standby tasks when the consumer group changes.\nPlease note there are plans to address those issues with the next version of the Consumer Rebalance Protocol (see ",[23,781,783],{"href":782},"#footnotes","footnotes",").",[206,786,787,788,790],{},"Often the best plan to keep downtimes low during rebalance for stateful apps is to stick with RocksDB + StatefulSet + PersistentVolumes + restart within (!) the session timeout",[30,789],{},"\n=> re-join with previous assignment, re-use RocksDB state, and avoid rebalancing entirely...",[241,792,793],{},[206,794,795,796,801,802,347],{},"Alternatively, take a look at ",[23,797,800],{"href":798,"rel":799},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fkafka-streams-cassandra-state-store",[27],"kafka-streams-cassandra-state-store",", introduced in an ",[23,803,805],{"href":804},"\u002Fblog\u002Fintroducing-kafka-streams-cassandra-state-store","earlier blog post",[281,807,809],{"id":808},"k8s-deployment-specminreadyseconds","k8s Deployment .spec.minReadySeconds",[206,811,812],{},"Frequently rebalancing within a short timeframe can cause consumer delays and strain the Kafka cluster.",[206,814,815,816,821],{},"If your application\u002Fcontainer has a quick restart time, such as when running as a GraalVM native executable, it’s worth considering the use of ",[23,817,820],{"href":818,"rel":819},"https:\u002F\u002Fkubernetes.io\u002Fdocs\u002Fconcepts\u002Fworkloads\u002Fcontrollers\u002Fdeployment\u002F#min-ready-seconds",[27],".spec.minReadySeconds","  to maintain control and ensure upgrades occur in a controlled manner. This will help prevent frequent rebalancing within a short timeframe.",[276,823,825],{"id":824},"conclusion","Conclusion",[206,827,828,829,831,832,835,836,839,840,843],{},"By configuring your Kafka Streams app with ",[137,830,749],{},", a graceful ",[55,833,834],{},"shutdown immediately triggers a rebalancing process"," and tasks are re-assigned to other active members within the group.\nThe ",[55,837,838],{},"processing downtime is significantly reduced"," while ",[55,841,842],{},"also improving elasticity and resilience",". As a result, your applications enables interruption-free CI\u002FCD and can be auto-scaled.",[206,845,846,847,849],{},"Please note that this recommendation only applies to ",[151,848,231],{}," streams applications.! Tread carefully for stateful topologies, and do your homework!",[206,851,852,853,856,857,859],{},"Remember that ",[137,854,855],{},"internal.leave.group.on.close"," is a ",[151,858,392],{}," config, which may change without prior notice with new releases. Always check the source code for changes when upgrading the Kafka Streams dependency.",[276,861,862],{"id":783},"Footnotes",[16,864,865,868,881,892,903,921],{},[19,866,867],{},"When writing this blog post, the latest version of kafka-streams was 3.4.1.",[19,869,870,871,876,877,880],{},"There’s a ticket ",[23,872,875],{"href":873,"rel":874},"https:\u002F\u002Fissues.apache.org\u002Fjira\u002Fbrowse\u002FKAFKA-6995",[27],"KAFKA-6995"," from June 2018 proposing to make the config public.\nThe ticket is closed as ",[55,878,879],{},"’Won’t Fix’",". Concerns of the core developer team can be found in the discussion.",[19,882,883,884,886,887],{},"Looking into the crystal ball: A Kafka Design Proposal (KIP) is in progress to introduce a new group membership and rebalance protocol for the Kafka Consumer and, by extensions, Kafka Streams.",[30,885],{},"\n=> ",[23,888,891],{"href":889,"rel":890},"https:\u002F\u002Fcwiki.apache.org\u002Fconfluence\u002Fdisplay\u002FKAFKA\u002FKIP-848:+The+Next+Generation+of+the+Consumer+Rebalance+Protocol",[27],"KIP-848: The Next Generation of the Consumer Rebalance Protocol",[19,893,894,895,582,898],{},"It was also introduced on ",[151,896,897],{},"Current 2022",[23,899,902],{"href":900,"rel":901},"https:\u002F\u002Fwww.confluent.io\u002Fen-gb\u002Fevents\u002Fcurrent-2022\u002Fthe-next-generation-of-the-consumer-rebalance-protocol\u002F",[27],"The Next Generation of the Consumer Rebalance Protocol With David Jacot | UK",[19,904,905,906,908,232,917],{},"The application + docker-compose setup that was put together for this article can be found on the thriving-dev GitHub Organisation:",[30,907],{},[909,910],"icon",{"className":911,"name":916},[912,913,914,915],"inline","-mt-0.5","w-6","h-6","mdi-github",[23,918,919],{"href":919,"rel":920},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fkafka-streams-leave-group-on-close",[27],[19,922,923,924,929],{},"Many thanks to  ",[23,925,928],{"href":926,"rel":927},"https:\u002F\u002Ftwitter.com\u002FMatthiasJSax",[27],"@MatthiasJSax","  for proofreading the blog post! 🙇",[276,931,933],{"id":932},"references-and-further-reading","References and Further Reading",[16,935,936,943,950,957,964,971],{},[19,937,938],{},[23,939,942],{"href":940,"rel":941},"https:\u002F\u002Fmedium.com\u002Flydtech-consulting\u002Fkafka-consumer-group-rebalance-1-of-2-7a3e00aa3bb4",[27],"Kafka Consumer Group Rebalance (1 of 2)",[19,944,945],{},[23,946,949],{"href":947,"rel":948},"https:\u002F\u002Fmedium.com\u002Flydtech-consulting\u002Fkafka-consumer-group-rebalance-2-of-2-5d1d60c71e6e",[27],"Kafka Consumer Group Rebalance (2 of 2)",[19,951,952],{},[23,953,956],{"href":954,"rel":955},"https:\u002F\u002Fstackoverflow.com\u002Fquestions\u002F54398754\u002Fkafka-streams-delay-to-kick-rebalancing-on-consumer-graceful-shutdown",[27],"Kafka-streams delay to kick rebalancing on consumer graceful shutdown - Stack Overflow",[19,958,959],{},[23,960,963],{"href":961,"rel":962},"https:\u002F\u002Fwww.confluent.io\u002Fblog\u002Fcooperative-rebalancing-in-kafka-streams-consumer-ksqldb\u002F",[27],"Cooperative Rebalancing in the Kafka Consumer, Streams & ksqlDB",[19,965,966],{},[23,967,970],{"href":968,"rel":969},"https:\u002F\u002Fmedium.com\u002Fstreamthoughts\u002Fapache-kafka-rebalance-protocol-or-the-magic-behind-your-streams-applications-e94baf68e4f2",[27],"Apache Kafka Rebalance Protocol, or the magic behind your streams applications | by Florian Hussonnois | StreamThoughts | Medium",[19,972,973],{},[23,974,977,978,981],{"href":975,"rel":976},"https:\u002F\u002Fcwiki.apache.org\u002Fconfluence\u002Fdisplay\u002FKAFKA\u002FKIP-812%3A+Introduce+another+form+of+the+%60KafkaStreams.close%28%29%60+API+that+forces+the+member+to+leave+the+consumer+group",[27],"KIP-812: Introduce another form of the ",[137,979,980],{},"KafkaStreams.close()"," API that forces the member to leave the consumer group - Apache Kafka - Apache Software Foundation",[983,984,985],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":183,"searchDepth":184,"depth":184,"links":987},[988,993,994,998,999,1003,1004,1005],{"id":278,"depth":184,"text":279,"children":989},[990,992],{"id":283,"depth":991,"text":284},3,{"id":364,"depth":991,"text":365},{"id":427,"depth":184,"text":428},{"id":599,"depth":184,"text":600,"children":995},[996,997],{"id":616,"depth":991,"text":617},{"id":644,"depth":991,"text":645},{"id":678,"depth":184,"text":679},{"id":738,"depth":184,"text":739,"children":1000},[1001,1002],{"id":742,"depth":991,"text":743},{"id":808,"depth":991,"text":809},{"id":824,"depth":184,"text":825},{"id":783,"depth":184,"text":862},{"id":932,"depth":184,"text":933},"2023-06-17","With a single config change, reduce rebalance downtime for your stateless Kafka Streams Apps from 45s to \u003C100ms!","8min","intermediate",{},"\u002Fassets\u002Fblog\u002F8.reduce-rebalance-downtime-for-stateless-kafka-streams-apps\u002Fleave-group-on-close_cover_twitter.jpg","\u002Fblog\u002Freduce-rebalance-downtime-for-stateless-kafka-streams-apps",{"title":202,"description":1007},"blog\u002F8.reduce-rebalance-downtime-for-stateless-kafka-streams-apps",[1016,1017,1018,1019],"stream-processing","kafka-streams","performance","tips","Reduce Rebalance Downtime (by x450) for Stateless Kafka Streams Apps","3TGIzmn35glqBPdESsF6oGeuy_YOQoSOfe8UJfEqCtA",{"id":1023,"title":1024,"active":6,"body":1025,"date":1427,"description":1428,"duration":1429,"extension":189,"level":1009,"meta":1430,"navigation":6,"ogDescription":190,"ogImage":1434,"ogImageAlt":1053,"ogTitle":190,"path":1435,"pinned":272,"seo":1436,"stem":1437,"tags":1438,"titleAlt":190,"titlePage":190,"__hash__":1442},"blog\u002Fblog\u002F13.streamline-micronaut-gradle-updates-with-renovate-1.md","Streamline Micronaut + Gradle Updates with Renovate (1\u002F4)",{"type":8,"value":1026,"toc":1417},[1027,1049,1062,1065,1069,1072,1085,1101,1105,1114,1128,1131,1135,1142,1153,1157,1177,1194,1200,1222,1226,1229,1242,1255,1267,1280,1286,1298,1315,1336,1347,1351,1373,1377,1384,1397,1402,1415],[289,1028,1029],{},[206,1030,1031],{},[151,1032,1033,1036,1037,1042,1043,1048],{},[55,1034,1035],{},"TLDR:"," Renovate cannot automatically update Micronaut dependencies for gradle projects created via ",[23,1038,1041],{"href":1039,"rel":1040},"https:\u002F\u002Fmicronaut.io\u002Flaunch",[27],"'Launch'"," or ",[23,1044,1047],{"href":1045,"rel":1046},"https:\u002F\u002Fdocs.micronaut.io\u002Flatest\u002Fguide\u002F#cli",[27],"CLI",". This first part dives into why. Parts 2-4 present different solutions.",[206,1050,1051],{},[209,1052],{"alt":1053,"className":1054,"height":1058,"sizes":1059,"src":1060,"width":1061,"preload":183},"Blog header image showing the logos of Micronaut, Gradle and Mend Renovate",[1055,1056,1057],"mx-auto","w-full","sm:max-w-[500px]",1500,"500","\u002Fassets\u002Fblog\u002F13.streamline-micronaut-gradle-updates-with-renovate-1\u002Fstreamline-micronaut-gradle-updates-with-renovate_v1.png",1800,[206,1063,1064],{},"This first part of a 4-part series dives into why Renovate can't automatically update Micronaut dependencies in Gradle projects created through Launch or CLI. Don't despair! Parts 2-4 unveil alternative solutions to get your updates flowing smoothly. Buckle up and join the quest for effortless Micronaut dependency management!",[276,1066,1068],{"id":1067},"what","What?",[206,1070,1071],{},"First things first, let's define what do we want to achieve. Here's our user story:",[289,1073,1074],{},[206,1075,1076,347],{},[55,1077,1078,1079,1084],{},"As a Software Developer, I want to use ",[23,1080,1083],{"href":1081,"rel":1082},"https:\u002F\u002Fwww.mend.io\u002Frenovate\u002F",[27],"Renovate"," to automate keeping a Micronaut Gradle project up-to-date",[762,1086,1087,1093],{},[206,1088,1089,1092],{},[55,1090,1091],{},"Mend Renovate"," (RenovateBot) is an open-source tool that automates the process of keeping software dependencies up-to-date by scanning code repositories, identifying outdated dependencies, and generating automated pull\u002Fmerge requests to update them.",[206,1094,1095,1096,347],{},"The recommended way to add renovate to your GitHub repository is to use the ",[23,1097,1100],{"href":1098,"rel":1099},"https:\u002F\u002Fgithub.com\u002Fapps\u002Frenovate",[27],"Renovate GitHub App",[276,1102,1104],{"id":1103},"problem-statement","Problem Statement",[206,1106,1107,1108,1113],{},"To tick off the basics, Renovate can update ",[23,1109,1112],{"href":1110,"rel":1111},"https:\u002F\u002Fdocs.renovatebot.com\u002Fjava\u002F",[27],"Java, Gradle and Maven dependencies",". This includes libraries and plugins as well as the Gradle Wrapper.",[206,1115,1116,1117,347,1122,1124,1125,1127],{},"The manager for Gradle makes use of the ",[23,1118,1121],{"href":1119,"rel":1120},"https:\u002F\u002Fdocs.renovatebot.com\u002Fmodules\u002Fdatasource\u002Fmaven\u002F",[27],"maven datasource",[30,1123],{},"\nRenovate can be configured to access more repositories and access repositories authenticated, such as e.g. Artifactory.",[30,1126],{},"\nMaven Central is supported by default.",[206,1129,1130],{},"So if everything needed is in place, why are Micronaut projects not updatable then?\nThe answer lies with how Micronaut manages the transitive dependencies of Micronaut modules.",[281,1132,1134],{"id":1133},"micronaut-gradle-plugin","Micronaut Gradle Plugin",[206,1136,1137,1138,347],{},"To make the developer experience as smooth as possible, Micronaut provides its own ",[23,1139,1134],{"href":1140,"rel":1141},"https:\u002F\u002Fmicronaut-projects.github.io\u002Fmicronaut-gradle-plugin",[27],[206,1143,1144,1145,1148,1149,340],{},"There are actually multiple ones, even...! A typical Micronaut Application, with support for GraalVM and Docker,\nthat one that is pre-configured for projects created via Launch or CLI is ",[137,1146,1147],{},"io.micronaut.application",". (ref ",[23,1150,1151],{"href":1151,"rel":1152},"https:\u002F\u002Fplugins.gradle.org\u002Fplugin\u002Fio.micronaut.application",[27],[281,1154,1156],{"id":1155},"selecting-the-micronaut-platform-version","Selecting the Micronaut Platform Version",[206,1158,1159,1160,726,1163,1166,1167,1172,1173,1176],{},"The easiest is to set ",[55,1161,1162],{},"micronautVersion",[137,1164,1165],{},"gradle.properties",". If you use a ",[23,1168,1171],{"href":1169,"rel":1170},"https:\u002F\u002Fdocs.gradle.org\u002Fcurrent\u002Fuserguide\u002Fplatforms.html#sub:central-declaration-of-dependencies",[27],"version catalog",", you can also set the version of Micronaut directly in your ",[137,1174,1175],{},"libs.versions.toml"," file:",[261,1178,1182],{"className":1179,"code":1180,"language":1181,"meta":183,"style":183},"language-toml shiki shiki-themes github-light github-dark","[versions]\nmicronaut=\"4.0.0\"\n","toml",[137,1183,1184,1189],{"__ignoreMap":183},[269,1185,1186],{"class":271,"line":272},[269,1187,1188],{},"[versions]\n",[269,1190,1191],{"class":271,"line":184},[269,1192,1193],{},"micronaut=\"4.0.0\"\n",[206,1195,1196,1197,259],{},"This version will be shared by all Micronaut modules of your project. Alternatively, you can set the version in your ",[137,1198,1199],{},"build.gradle(.kts)",[261,1201,1205],{"className":1202,"code":1203,"language":1204,"meta":183,"style":183},"language-kotlin shiki shiki-themes github-light github-dark","micronaut {\n    version(\"4.2.1\")\n}\n","kotlin",[137,1206,1207,1212,1217],{"__ignoreMap":183},[269,1208,1209],{"class":271,"line":272},[269,1210,1211],{},"micronaut {\n",[269,1213,1214],{"class":271,"line":184},[269,1215,1216],{},"    version(\"4.2.1\")\n",[269,1218,1219],{"class":271,"line":991},[269,1220,1221],{},"}\n",[281,1223,1225],{"id":1224},"dazzling-now-what-about-renovate","Dazzling, now What About Renovate?",[206,1227,1228],{},"So the gradle setup is good, even three ways to choose how to set the platform. Renovate also supports the project stack.",[206,1230,1231,1232,1235,1236,1238],{},"Here's a starter project, created via ",[23,1233,1041],{"href":1039,"rel":1234},[27],", with Renovate enabled:",[30,1237],{},[23,1239,1240],{"href":1240,"rel":1241},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fmicronaut-gradle-renovate-example-0",[27],[762,1243,1244],{},[206,1245,1246,1247,1254],{},"Renovate is configured via ",[23,1248,1251],{"href":1249,"rel":1250},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fmicronaut-gradle-renovate-example-0\u002Fblob\u002Fmain\u002Frenovate.json",[27],[137,1252,1253],{},"renovate.json"," in the GitHub Repository.",[206,1256,1257,1258,1263,1264,259],{},"Let's take a look at this screenshot of the Renovate ",[23,1259,1262],{"href":1260,"rel":1261},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fmicronaut-gradle-renovate-example-0\u002Fissues\u002F1",[27],"dependency dashboard",", ",[151,1265,1266],{},"Feb 2, 2024",[206,1268,1269],{},[209,1270],{"alt":1271,"className":1272,"height":1275,"sizes":1276,"src":1277,"width":1278,"dataZoomSrc":1279},"screenshot of the dependency dashboard of the example project, does show all dependencies tracked, missing the micronaut platform",[482,1273,1274],"rounded","my-3",1782,"md:652 lg:792","\u002Fassets\u002Fblog\u002F13.streamline-micronaut-gradle-updates-with-renovate-1\u002Fmicronaut-gradle-with-renovate-example-dependency-dashboard.png",1872,"\u002Fassets\u002Fblog\u002F13.streamline-micronaut-gradle-updates-with-renovate-1\u002Fmicronaut-gradle-with-renovate-example-dependency-dashboard.webp",[206,1281,1282,1283,347],{},"We can find two micronaut dependencies, listed under ",[137,1284,1285],{},"build.gradle.kts",[16,1287,1288,1293],{},[19,1289,1290],{},[137,1291,1292],{},"io.micronaut.application 4.3.1",[19,1294,1295],{},[137,1296,1297],{},"io.micronaut.aot 4.3.1",[206,1299,1300,1301,1306,1307],{},"These are the two micronaut ",[55,1302,1303],{},[151,1304,1305],{},"plugins"," used. They are tracked and updated by Renovate just fine. ",[909,1308],{"className":1309,"name":1314},[912,1310,1311,1312,1313],"-mt-1","w-5","h-5","text-green-400","mdi:check",[206,1316,1317,1318,1323,1324,1330,1331],{},"The ",[55,1319,1320],{},[151,1321,1322],{},"platform"," on the other hand, defined in ",[23,1325,1328],{"href":1326,"rel":1327},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fmicronaut-gradle-renovate-example-0\u002Fblob\u002Fmain\u002Fgradle.properties",[27],[137,1329,1165],{}," that determines the actual version of micronaut-core and other modules, is not tracked. ",[909,1332],{"className":1333,"name":1335},[912,1310,1311,1312,1334],"text-red-400","mdi:close",[261,1337,1341],{"className":1338,"code":1339,"language":1340,"meta":183,"style":183},"language-properties shiki shiki-themes github-light github-dark","micronautVersion=4.2.3\n","properties",[137,1342,1343],{"__ignoreMap":183},[269,1344,1345],{"class":271,"line":272},[269,1346,1339],{},[281,1348,1350],{"id":1349},"cause","Cause",[206,1352,1353,1354,1358,1360,1361,1366,1367,1372],{},"The explanation to the version not tracked is relatively simple. ",[909,1355],{"className":1356,"name":1357},[912,1310,914,915],"mdi:hand-pointing-up",[30,1359],{},"\n...Renovate cannot resolve the ",[55,1362,1363],{},[151,1364,1365],{},"packageName"," and ",[55,1368,1369],{},[151,1370,1371],{},"datasource"," since there's no reference.",[276,1374,1376],{"id":1375},"how-to-resolve","How to resolve?",[206,1378,1379,1380,1383],{},"When writing up this blog post I found ",[55,1381,1382],{},"three"," different solutions to the problem introduced.",[206,1385,1386,1387,1389,1390,1396],{},"Please continue with part 2!",[30,1388],{},"\n-> '",[23,1391,1393,1394],{"href":1392},"\u002Fblog\u002Fstreamline-micronaut-gradle-updates-with-renovate-2","Solution 1: Gradle Version Catalog + Dependency Hint in ",[137,1395,1175],{},"'.",[289,1398,1399],{},[206,1400,1401],{},"Footnote: This blog post was written to work with Micronaut 4.x and Gradle 8.",[241,1403,1404],{},[206,1405,1406,1407,1411,1412,1414],{},"If you're a maintainer or contributor to Open-Source Java libraries, take a look at ",[23,1408,1410],{"href":1409},"\u002Fblog\u002Fjava-library-development-get-started-quickly-with-java-library-template","thriving-dev\u002Fjava-library-template"," introduced in September 2023. It ships with a ready to use ",[137,1413,1253],{}," and setup guide.",[983,1416,985],{},{"title":183,"searchDepth":184,"depth":184,"links":1418},[1419,1420,1426],{"id":1067,"depth":184,"text":1068},{"id":1103,"depth":184,"text":1104,"children":1421},[1422,1423,1424,1425],{"id":1133,"depth":991,"text":1134},{"id":1155,"depth":991,"text":1156},{"id":1224,"depth":991,"text":1225},{"id":1349,"depth":991,"text":1350},{"id":1375,"depth":184,"text":1376},"2024-02-06","Renovate hit a snag w\u002F Micronaut updates? Don't worry! This 4-part guide reveals the issue & offers workarounds in Parts 2-4.  Stay tuned! #Micronaut #Gradle #dependencies","4min",{"series":1431},{"id":1432,"title":1433,"part":272},"Automated Dependency Updates for Micronaut + Gradle with Renovate","Opener: What to Achieve, Problem, Cause","\u002Fassets\u002Fblog\u002F13.streamline-micronaut-gradle-updates-with-renovate-1\u002Fstreamline-micronaut-gradle-updates-with-renovate_og_v1.jpg","\u002Fblog\u002Fstreamline-micronaut-gradle-updates-with-renovate-1",{"title":1024,"description":1428},"blog\u002F13.streamline-micronaut-gradle-updates-with-renovate-1",[1439,265,1019,1440,1441],"ci-cd","gradle","micronaut","4fhaXf1bgITeGuz_DvYuSTOaMmKmaZhMBhsYFRm31yw",{"id":1444,"title":1445,"active":6,"body":1446,"date":1843,"description":1844,"duration":1845,"extension":189,"level":1009,"meta":1846,"navigation":6,"ogDescription":190,"ogImage":1434,"ogImageAlt":1053,"ogTitle":190,"path":1849,"pinned":190,"seo":1850,"stem":1851,"tags":1852,"titleAlt":190,"titlePage":190,"__hash__":1854},"blog\u002Fblog\u002F17.patch-quarkus-gradle-like-a-pro-with-renovate.md","Patch Quarkus Gradle Like a Pro With Renovate",{"type":8,"value":1447,"toc":1836},[1448,1463,1467,1486,1490,1509,1516,1520,1529,1539,1565,1575,1743,1750,1754,1767,1783,1820,1825,1833],[289,1449,1450],{},[206,1451,1452],{},[151,1453,1454,1456,1457,1462],{},[55,1455,1035],{}," Renovate cannot automatically update Quarkus gradle plugin dependencies for projects created via ",[23,1458,1461],{"href":1459,"rel":1460},"https:\u002F\u002Fquarkus.io\u002Fguides\u002Fcli-tooling",[27],"Quarkus CLI",". The explanation to the version not tracked is relatively simple ...Renovate cannot resolve the packageName and datasource.",[276,1464,1466],{"id":1465},"problem-recap-original-guide-for-micronaut","Problem Recap (Original Guide for Micronaut)",[206,1468,1469,1470,1473,1474,1366,1478,1482,1483,1485],{},"As covered in detail in the ",[23,1471,1472],{"href":1435},"first post of this series","\nRenovate cannot resolve the ",[55,1475,1476],{},[151,1477,1365],{},[55,1479,1480],{},[151,1481,1371],{}," from the ",[137,1484,1165],{}," file.",[276,1487,1489],{"id":1488},"updating-quarkus","Updating Quarkus",[206,1491,1492,1493,1498,1499,1504,1505,1508],{},"Similar to Micronaut, ",[23,1494,1497],{"href":1495,"rel":1496},"https:\u002F\u002Fquarkus.io\u002F",[27],"Quarkus"," comes with its own ",[23,1500,1503],{"href":1501,"rel":1502},"https:\u002F\u002Fplugins.gradle.org\u002Fplugin\u002Fio.quarkus",[27],"Gradle Plugin","\nand ",[23,1506,1047],{"href":1459,"rel":1507},[27]," to provide an optimal developer experience.",[206,1510,1511,1512,1515],{},"Renovate seems to be able to resolve, track, and update the Quarkus ",[151,1513,1514],{},"Platform version"," for Gradle Kotlin DSL projects created via CLI just fine.\nBut the quarkus plugin version is not detected.",[276,1517,1519],{"id":1518},"solution","Solution",[762,1521,1522],{},[206,1523,1524,1525,347],{},"To avoid duplicate content, for more details on this solution, please read the ",[23,1526,1528],{"href":1527},"\u002Fblog\u002Fstreamline-micronaut-gradle-updates-with-renovate-4","previous post of this series",[206,1530,1531,1532,347],{},"One way to extend and customise Renovate is through ",[23,1533,1536],{"href":1534,"rel":1535},"https:\u002F\u002Fdocs.renovatebot.com\u002Fmodules\u002Fmanager\u002Fregex\u002F",[27],[137,1537,1538],{},"customManagers",[206,1540,1541,1542,1263,1544,1263,1547,1263,1550,1553,1554,1556,1557,1560,1561,1564],{},"By adding our own regex manager, we can tell Renovate how to update the Quarkus plugin.\nWe define the ",[151,1543,1371],{},[151,1545,1546],{},"registryUrl",[151,1548,1549],{},"depName",[151,1551,1552],{},"depType",", and ",[151,1555,1365],{}," for all ",[137,1558,1559],{},"*\u002Fgradle.properties"," files of your project, that contains a line of text like ",[137,1562,1563],{},"quarkusPluginVersion=x.y.z",", matched by a regular expression.",[206,1566,1567,1568,1571,1572,1574],{},"This ",[151,1569,1570],{},"customManager"," is to be added to the ",[137,1573,1253],{}," in your project:",[261,1576,1580],{"className":1577,"code":1578,"language":1579,"meta":183,"style":183},"language-json shiki shiki-themes github-light github-dark","  \"customManagers\": [\n    {\n      \"customType\": \"regex\",\n      \"datasourceTemplate\": \"maven\",\n      \"registryUrlTemplate\": \"https:\u002F\u002Fplugins.gradle.org\u002Fm2\",\n      \"depNameTemplate\": \"io.quarkus\",\n      \"depTypeTemplate\": \"plugin\",\n      \"packageNameTemplate\": \"io.quarkus:io.quarkus.gradle.plugin\",\n      \"fileMatch\": [\"(^|\u002F)gradle\\\\.properties\"],\n      \"matchStrings\": [\n        \"quarkusPluginVersion=(?\u003CcurrentValue>[\\\\w+\\\\.\\\\-]*)\"\n      ]\n    }\n  ]\n","json",[137,1581,1582,1592,1597,1611,1624,1637,1650,1663,1676,1697,1705,1725,1731,1737],{"__ignoreMap":183},[269,1583,1584,1588],{"class":271,"line":272},[269,1585,1587],{"class":1586},"sZZnC","  \"customManagers\"",[269,1589,1591],{"class":1590},"sVt8B",": [\n",[269,1593,1594],{"class":271,"line":184},[269,1595,1596],{"class":1590},"    {\n",[269,1598,1599,1603,1605,1608],{"class":271,"line":991},[269,1600,1602],{"class":1601},"sj4cs","      \"customType\"",[269,1604,582],{"class":1590},[269,1606,1607],{"class":1586},"\"regex\"",[269,1609,1610],{"class":1590},",\n",[269,1612,1614,1617,1619,1622],{"class":271,"line":1613},4,[269,1615,1616],{"class":1601},"      \"datasourceTemplate\"",[269,1618,582],{"class":1590},[269,1620,1621],{"class":1586},"\"maven\"",[269,1623,1610],{"class":1590},[269,1625,1627,1630,1632,1635],{"class":271,"line":1626},5,[269,1628,1629],{"class":1601},"      \"registryUrlTemplate\"",[269,1631,582],{"class":1590},[269,1633,1634],{"class":1586},"\"https:\u002F\u002Fplugins.gradle.org\u002Fm2\"",[269,1636,1610],{"class":1590},[269,1638,1640,1643,1645,1648],{"class":271,"line":1639},6,[269,1641,1642],{"class":1601},"      \"depNameTemplate\"",[269,1644,582],{"class":1590},[269,1646,1647],{"class":1586},"\"io.quarkus\"",[269,1649,1610],{"class":1590},[269,1651,1653,1656,1658,1661],{"class":271,"line":1652},7,[269,1654,1655],{"class":1601},"      \"depTypeTemplate\"",[269,1657,582],{"class":1590},[269,1659,1660],{"class":1586},"\"plugin\"",[269,1662,1610],{"class":1590},[269,1664,1666,1669,1671,1674],{"class":271,"line":1665},8,[269,1667,1668],{"class":1601},"      \"packageNameTemplate\"",[269,1670,582],{"class":1590},[269,1672,1673],{"class":1586},"\"io.quarkus:io.quarkus.gradle.plugin\"",[269,1675,1610],{"class":1590},[269,1677,1679,1682,1685,1688,1691,1694],{"class":271,"line":1678},9,[269,1680,1681],{"class":1601},"      \"fileMatch\"",[269,1683,1684],{"class":1590},": [",[269,1686,1687],{"class":1586},"\"(^|\u002F)gradle",[269,1689,1690],{"class":1601},"\\\\",[269,1692,1693],{"class":1586},".properties\"",[269,1695,1696],{"class":1590},"],\n",[269,1698,1700,1703],{"class":271,"line":1699},10,[269,1701,1702],{"class":1601},"      \"matchStrings\"",[269,1704,1591],{"class":1590},[269,1706,1708,1711,1713,1716,1718,1720,1722],{"class":271,"line":1707},11,[269,1709,1710],{"class":1586},"        \"quarkusPluginVersion=(?\u003CcurrentValue>[",[269,1712,1690],{"class":1601},[269,1714,1715],{"class":1586},"w+",[269,1717,1690],{"class":1601},[269,1719,347],{"class":1586},[269,1721,1690],{"class":1601},[269,1723,1724],{"class":1586},"-]*)\"\n",[269,1726,1728],{"class":271,"line":1727},12,[269,1729,1730],{"class":1590},"      ]\n",[269,1732,1734],{"class":271,"line":1733},13,[269,1735,1736],{"class":1590},"    }\n",[269,1738,1740],{"class":271,"line":1739},14,[269,1741,1742],{"class":1590},"  ]\n",[206,1744,1745,1746],{},"=> Et Voilà! ",[909,1747],{"className":1748,"name":1749},[912,1310,914,915,1313],"noto:party-popper",[281,1751,1753],{"id":1752},"example","Example",[206,1755,1756,1757,1235,1761,1763],{},"Again, here's a starter project, originally created via ",[23,1758,1760],{"href":1459,"rel":1759},[27],"'CLI'",[30,1762],{},[23,1764,1765],{"href":1765,"rel":1766},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fquarkus-gradle-renovate-example",[27],[206,1768,1769,1770,1366,1773,1776,1777,1782],{},"The project is automatically kept up-to-date for ",[151,1771,1772],{},"minor",[151,1774,1775],{},"patch"," version updates, with ",[23,1778,1781],{"href":1779,"rel":1780},"https:\u002F\u002Fdocs.renovatebot.com\u002Fconfiguration-options\u002F#automerge",[27],"automerge"," enabled.",[16,1784,1785,1804],{},[19,1786,1787,1788,1791,1792,1796,1797,1800,1801],{},"You can see the gradle plugin ",[137,1789,1790],{},"io.quarkus 3.x.x"," tracked in the ",[23,1793,1262],{"href":1794,"rel":1795},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fquarkus-gradle-renovate-example\u002Fissues\u002F1",[27]," (in the new ",[137,1798,1799],{},"regex"," collection). ",[909,1802],{"className":1803,"name":1314},[912,1310,1311,1312,1313],[19,1805,1317,1806,1366,1811,1816,1817],{},[23,1807,1810],{"href":1808,"rel":1809},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fquarkus-gradle-renovate-example\u002Fpulls?q=is%3Apr+is%3Aclosed",[27],"closed Renovate bot PRs",[23,1812,1815],{"href":1813,"rel":1814},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fquarkus-gradle-renovate-example\u002Fcommits\u002Fmain\u002F",[27],"commit history"," proofs everything is working as it should. ",[909,1818],{"className":1819,"name":1314},[912,1310,1311,1312,1313],[289,1821,1822],{},[206,1823,1824],{},"Footnote: This blog post was written to work with Quarkus 3.7.x and Gradle 8.",[241,1826,1827],{},[206,1828,1406,1829,1411,1831,1414],{},[23,1830,1410],{"href":1409},[137,1832,1253],{},[983,1834,1835],{},"html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":183,"searchDepth":184,"depth":184,"links":1837},[1838,1839,1840],{"id":1465,"depth":184,"text":1466},{"id":1488,"depth":184,"text":1489},{"id":1518,"depth":184,"text":1519,"children":1841},[1842],{"id":1752,"depth":991,"text":1753},"2024-02-10","Renovate hit a snag w\u002F Quarkus updates? Don't worry! The gradle plugin can be updated using a custom regex manager! #Quarkus #Gradle #dependencies","3min",{"series":1847},{"id":1432,"title":1848,"part":1626},"Bonus: Solution applied to a Quarkus Project","\u002Fblog\u002Fpatch-quarkus-gradle-like-a-pro-with-renovate",{"title":1445,"description":1844},"blog\u002F17.patch-quarkus-gradle-like-a-pro-with-renovate",[1439,1204,1019,1440,1853],"quarkus","t4OeTE7Dus_0EMEzpRwGnNZzNHZi4dLuSRPh85kJB90",{"id":1856,"title":1857,"active":6,"body":1858,"date":2189,"description":2190,"duration":1845,"extension":189,"level":1009,"meta":2191,"navigation":6,"ogDescription":190,"ogImage":1434,"ogImageAlt":1053,"ogTitle":190,"path":1527,"pinned":190,"seo":2194,"stem":2195,"tags":2196,"titleAlt":190,"titlePage":190,"__hash__":2197},"blog\u002Fblog\u002F16.streamline-micronaut-gradle-updates-with-renovate-4.md","Streamline Micronaut + Gradle Updates with Renovate (4\u002F4)",{"type":8,"value":1859,"toc":2179},[1860,1875,1879,1896,1900,1903,1912,1921,1929,1936,1939,1954,1960,2066,2071,2073,2085,2094,2124,2128,2140,2144,2150,2158,2162,2165,2169,2177],[289,1861,1862],{},[206,1863,1864],{},[151,1865,1866,1036,1868,1042,1871,1874],{},[55,1867,1035],{},[23,1869,1041],{"href":1039,"rel":1870},[27],[23,1872,1047],{"href":1045,"rel":1873},[27],". The final and best solution uses a Renovate custom regex manager.",[276,1876,1878],{"id":1877},"problem-recap","Problem Recap",[206,1880,1469,1881,1473,1883,1366,1887,1482,1891,1485],{},[23,1882,1472],{"href":1435},[55,1884,1885],{},[151,1886,1365],{},[55,1888,1889],{},[151,1890,1371],{},[23,1892,1894],{"href":1326,"rel":1893},[27],[137,1895,1165],{},[276,1897,1899],{"id":1898},"solution-3","Solution 3 🥇",[206,1901,1902],{},"The final solution does not require any changes to the Micronaut starter project and is entirely solved through Renovate config.",[206,1904,1905,1906,1911],{},"Renovate is very flexible. It's based on a ",[23,1907,1910],{"href":1908,"rel":1909},"https:\u002F\u002Fdocs.renovatebot.com\u002Fkey-concepts\u002Fhow-renovate-works\u002F",[27],"modular"," architecture,\nand is extensible to the point that you can even contribute your own modules, if you want.",[206,1913,1914,1915,1920],{},"Further, the concept of ",[23,1916,1919],{"href":1917,"rel":1918},"https:\u002F\u002Fdocs.renovatebot.com\u002Fkey-concepts\u002Fpresets\u002F",[27],"presets"," allows you to start with good default settings, share and re-use configuration.",[206,1922,1923,1924,347],{},"Finally, an advanced way to extend Renovate is via ",[23,1925,1927],{"href":1534,"rel":1926},[27],[137,1928,1538],{},[289,1930,1931],{},[206,1932,1933],{},[55,1934,1935],{},"With the regex manager you can configure Renovate, so it finds dependencies that are not detected by its other built-in package managers.",[206,1937,1938],{},"Great, that's actually exactly what we need!",[206,1940,1941,1942,1263,1944,1366,1946,750,1948,1950,1951,1564],{},"Long story short, here's a customManager that defines the ",[151,1943,1371],{},[151,1945,1549],{},[151,1947,1365],{},[137,1949,1559],{}," files of your project, that contains a line of text ",[137,1952,1953],{},"micronautVersion=x.y.z",[206,1955,1567,1956,1571,1958,1574],{},[151,1957,1570],{},[137,1959,1253],{},[261,1961,1963],{"className":1577,"code":1962,"language":1579,"meta":183,"style":183},"  \"customManagers\": [\n    {\n      \"customType\": \"regex\",\n      \"datasourceTemplate\": \"maven\",\n      \"depNameTemplate\": \"micronaut\",\n      \"packageNameTemplate\": \"io.micronaut.platform:micronaut-platform\",\n      \"fileMatch\": [\"(^|\u002F)gradle\\\\.properties\"],\n      \"matchStrings\": [\n        \"micronautVersion=(?\u003CcurrentValue>[\\\\w+\\\\.\\\\-]*)\"\n      ]\n    }\n  ]\n",[137,1964,1965,1971,1975,1985,1995,2006,2017,2031,2037,2054,2058,2062],{"__ignoreMap":183},[269,1966,1967,1969],{"class":271,"line":272},[269,1968,1587],{"class":1586},[269,1970,1591],{"class":1590},[269,1972,1973],{"class":271,"line":184},[269,1974,1596],{"class":1590},[269,1976,1977,1979,1981,1983],{"class":271,"line":991},[269,1978,1602],{"class":1601},[269,1980,582],{"class":1590},[269,1982,1607],{"class":1586},[269,1984,1610],{"class":1590},[269,1986,1987,1989,1991,1993],{"class":271,"line":1613},[269,1988,1616],{"class":1601},[269,1990,582],{"class":1590},[269,1992,1621],{"class":1586},[269,1994,1610],{"class":1590},[269,1996,1997,1999,2001,2004],{"class":271,"line":1626},[269,1998,1642],{"class":1601},[269,2000,582],{"class":1590},[269,2002,2003],{"class":1586},"\"micronaut\"",[269,2005,1610],{"class":1590},[269,2007,2008,2010,2012,2015],{"class":271,"line":1639},[269,2009,1668],{"class":1601},[269,2011,582],{"class":1590},[269,2013,2014],{"class":1586},"\"io.micronaut.platform:micronaut-platform\"",[269,2016,1610],{"class":1590},[269,2018,2019,2021,2023,2025,2027,2029],{"class":271,"line":1652},[269,2020,1681],{"class":1601},[269,2022,1684],{"class":1590},[269,2024,1687],{"class":1586},[269,2026,1690],{"class":1601},[269,2028,1693],{"class":1586},[269,2030,1696],{"class":1590},[269,2032,2033,2035],{"class":271,"line":1665},[269,2034,1702],{"class":1601},[269,2036,1591],{"class":1590},[269,2038,2039,2042,2044,2046,2048,2050,2052],{"class":271,"line":1678},[269,2040,2041],{"class":1586},"        \"micronautVersion=(?\u003CcurrentValue>[",[269,2043,1690],{"class":1601},[269,2045,1715],{"class":1586},[269,2047,1690],{"class":1601},[269,2049,347],{"class":1586},[269,2051,1690],{"class":1601},[269,2053,1724],{"class":1586},[269,2055,2056],{"class":271,"line":1699},[269,2057,1730],{"class":1590},[269,2059,2060],{"class":271,"line":1707},[269,2061,1736],{"class":1590},[269,2063,2064],{"class":271,"line":1727},[269,2065,1742],{"class":1590},[206,2067,1745,2068],{},[909,2069],{"className":2070,"name":1749},[912,1310,914,915,1313],[281,2072,1753],{"id":1752},[206,2074,2075,2076,1235,2079,2081],{},"Again, here's an adjusted starter project, originally created via ",[23,2077,1041],{"href":1039,"rel":2078},[27],[30,2080],{},[23,2082,2083],{"href":2083,"rel":2084},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fmicronaut-gradle-renovate-example-3",[27],[206,2086,1769,2087,1366,2089,1776,2091,1782],{},[151,2088,1772],{},[151,2090,1775],{},[23,2092,1781],{"href":1779,"rel":2093},[27],[16,2095,2096,2111],{},[19,2097,2098,2099,1791,2102,1796,2106,1800,2108],{},"You can see the maven package ",[137,2100,2101],{},"io.micronaut.platform:micronaut-platform 4.x.x",[23,2103,1262],{"href":2104,"rel":2105},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fmicronaut-gradle-renovate-example-3\u002Fissues\u002F1",[27],[137,2107,1799],{},[909,2109],{"className":2110,"name":1314},[912,1310,1311,1312,1313],[19,2112,1317,2113,1366,2117,1816,2121],{},[23,2114,1810],{"href":2115,"rel":2116},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fmicronaut-gradle-renovate-example-3\u002Fpulls?q=is%3Apr+is%3Aclosed",[27],[23,2118,1815],{"href":2119,"rel":2120},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fmicronaut-gradle-renovate-example-3\u002Fcommits\u002Fmain\u002F",[27],[909,2122],{"className":2123,"name":1314},[912,1310,1311,1312,1313],[276,2125,2127],{"id":2126},"summary","Summary",[16,2129,2130],{},[19,2131,2132,2133,2137,2138],{},"Add the ",[55,2134,2135],{},[151,2136,1570],{}," introduced above to ",[137,2139,1253],{},[276,2141,2143],{"id":2142},"we-have-a-winner","We Have a Winner 🥇",[206,2145,2146,2147,2149],{},"As the author, I get to decide 😼.",[30,2148],{},"\nFor me this third solution clearly takes first place with a wide margin.",[206,2151,2152,2153,2157],{},"Disagree? Leave a ",[23,2154,2156],{"href":2155},"#comments","comment"," below!!",[281,2159,2161],{"id":2160},"next","Next?",[206,2163,2164],{},"There's actually a bonus post under way! (to be released some time soon... so stay tuned!)",[289,2166,2167],{},[206,2168,1401],{},[241,2170,2171],{},[206,2172,1406,2173,1411,2175,1414],{},[23,2174,1410],{"href":1409},[137,2176,1253],{},[983,2178,1835],{},{"title":183,"searchDepth":184,"depth":184,"links":2180},[2181,2182,2185,2186],{"id":1877,"depth":184,"text":1878},{"id":1898,"depth":184,"text":1899,"children":2183},[2184],{"id":1752,"depth":991,"text":1753},{"id":2126,"depth":184,"text":2127},{"id":2142,"depth":184,"text":2143,"children":2187},[2188],{"id":2160,"depth":991,"text":2161},"2024-02-09","Renovate hit a snag w\u002F Micronaut updates? Don't worry! Having saved the best for last -> Solution 3! #Micronaut #Gradle #dependencies",{"series":2192},{"id":1432,"title":2193,"part":1613},"Solution 3: Renovate Custom Regex Manager! 🥇",{"title":1857,"description":2190},"blog\u002F16.streamline-micronaut-gradle-updates-with-renovate-4",[1439,265,1019,1440,1441],"gQiiN4IVtgdgF92_xwpY346VH9iliycxRkRA6j_xO0k",{"id":2199,"title":2200,"active":6,"body":2201,"date":2639,"description":2640,"duration":1845,"extension":189,"level":1009,"meta":2641,"navigation":6,"ogDescription":190,"ogImage":1434,"ogImageAlt":1053,"ogTitle":190,"path":2644,"pinned":190,"seo":2645,"stem":2646,"tags":2647,"titleAlt":190,"titlePage":190,"__hash__":2648},"blog\u002Fblog\u002F15.streamline-micronaut-gradle-updates-with-renovate-3.md","Streamline Micronaut + Gradle Updates with Renovate (3\u002F4)",{"type":8,"value":2202,"toc":2631},[2203,2222,2224,2241,2245,2252,2272,2277,2378,2385,2394,2407,2430,2436,2439,2495,2513,2515,2526,2535,2562,2564,2601,2605,2609,2617,2621,2629],[289,2204,2205],{},[206,2206,2207],{},[151,2208,2209,1036,2211,1042,2214,2217,2218,2221],{},[55,2210,1035],{},[23,2212,1041],{"href":1039,"rel":2213},[27],[23,2215,1047],{"href":1045,"rel":2216},[27],". The second solution (out of 3) is another workaround, using a version reference from ",[137,2219,2220],{},"settings.gradke(.kts)"," in the micronaut gradle plugin 'MicronautExtension'.",[276,2223,1878],{"id":1877},[206,2225,1469,2226,1473,2228,1366,2232,1482,2236,1485],{},[23,2227,1472],{"href":1435},[55,2229,2230],{},[151,2231,1365],{},[55,2233,2234],{},[151,2235,1371],{},[23,2237,2239],{"href":1326,"rel":2238},[27],[137,2240,1165],{},[276,2242,2244],{"id":2243},"solution-2","Solution 2",[206,2246,2247,2248,2251],{},"The second solution is similar to ",[23,2249,2250],{"href":1392},"solution 1",".\nSince we're all busy developers, I'll keep this part short and concise to avoid repetition.",[206,2253,2254,2255,2258,2259,2262,2263,2266,2267,2269,2270,1485],{},"Same as before, I have refactored the gradle project to use a ",[23,2256,1171],{"href":1169,"rel":2257},[27]," -\nbut this time using the ",[151,2260,2261],{},"script style"," -> via the ",[137,2264,2265],{},"settings.gradle(.kts)"," instead of a ",[137,2268,1175],{}," file.\nAlong with that, same as before, remember to delete the ",[137,2271,1165],{},[206,2273,2274,2275,1176],{},"Here's the content of the ",[137,2276,2265],{},[261,2278,2280],{"className":1202,"code":2279,"language":1204,"meta":183,"style":183},"rootProject.name = \"micronaut-gradle-renovate-example-2\"\n\ndependencyResolutionManagement {\n    versionCatalogs {\n        create(\"libs\") {\n            plugin(\"johnrengelman-shadow\", \"com.github.johnrengelman.shadow\").version(\"8.1.1\")\n            plugin(\"micronaut-application\", \"io.micronaut.application\").version(\"4.3.2\")\n            plugin(\"micronaut-aot\", \"io.micronaut.aot\").version(\"4.3.2\")\n\n            library(\"micronaut-platform\", \"io.micronaut.platform:micronaut-platform:4.3.0\")\n            library(\"junit\", \"org.junit.jupiter:junit-jupiter:5.10.2\")\n            library(\"assertj\", \"org.assertj:assertj-core:3.25.3\")\n            library(\"testcontainers\", \"org.testcontainers:testcontainers:1.19.4\")\n            library(\"testcontainers-junit5\", \"org.testcontainers:junit-jupiter:1.19.4\")\n\n            bundle(\"testcontainers-junit\", listOf(\"testcontainers\", \"testcontainers-junit5\"))\n        }\n    }\n}\n",[137,2281,2282,2287,2292,2297,2302,2307,2312,2317,2322,2326,2331,2336,2341,2346,2351,2356,2362,2368,2373],{"__ignoreMap":183},[269,2283,2284],{"class":271,"line":272},[269,2285,2286],{},"rootProject.name = \"micronaut-gradle-renovate-example-2\"\n",[269,2288,2289],{"class":271,"line":184},[269,2290,2291],{"emptyLinePlaceholder":6},"\n",[269,2293,2294],{"class":271,"line":991},[269,2295,2296],{},"dependencyResolutionManagement {\n",[269,2298,2299],{"class":271,"line":1613},[269,2300,2301],{},"    versionCatalogs {\n",[269,2303,2304],{"class":271,"line":1626},[269,2305,2306],{},"        create(\"libs\") {\n",[269,2308,2309],{"class":271,"line":1639},[269,2310,2311],{},"            plugin(\"johnrengelman-shadow\", \"com.github.johnrengelman.shadow\").version(\"8.1.1\")\n",[269,2313,2314],{"class":271,"line":1652},[269,2315,2316],{},"            plugin(\"micronaut-application\", \"io.micronaut.application\").version(\"4.3.2\")\n",[269,2318,2319],{"class":271,"line":1665},[269,2320,2321],{},"            plugin(\"micronaut-aot\", \"io.micronaut.aot\").version(\"4.3.2\")\n",[269,2323,2324],{"class":271,"line":1678},[269,2325,2291],{"emptyLinePlaceholder":6},[269,2327,2328],{"class":271,"line":1699},[269,2329,2330],{},"            library(\"micronaut-platform\", \"io.micronaut.platform:micronaut-platform:4.3.0\")\n",[269,2332,2333],{"class":271,"line":1707},[269,2334,2335],{},"            library(\"junit\", \"org.junit.jupiter:junit-jupiter:5.10.2\")\n",[269,2337,2338],{"class":271,"line":1727},[269,2339,2340],{},"            library(\"assertj\", \"org.assertj:assertj-core:3.25.3\")\n",[269,2342,2343],{"class":271,"line":1733},[269,2344,2345],{},"            library(\"testcontainers\", \"org.testcontainers:testcontainers:1.19.4\")\n",[269,2347,2348],{"class":271,"line":1739},[269,2349,2350],{},"            library(\"testcontainers-junit5\", \"org.testcontainers:junit-jupiter:1.19.4\")\n",[269,2352,2354],{"class":271,"line":2353},15,[269,2355,2291],{"emptyLinePlaceholder":6},[269,2357,2359],{"class":271,"line":2358},16,[269,2360,2361],{},"            bundle(\"testcontainers-junit\", listOf(\"testcontainers\", \"testcontainers-junit5\"))\n",[269,2363,2365],{"class":271,"line":2364},17,[269,2366,2367],{},"        }\n",[269,2369,2371],{"class":271,"line":2370},18,[269,2372,1736],{},[269,2374,2376],{"class":271,"line":2375},19,[269,2377,1221],{},[206,2379,2380,2381,2384],{},"As with the first solution, I want to point out the ",[55,2382,2383],{},"additional dependency"," added (compared to the original Micronaut starter template):",[261,2386,2388],{"className":1202,"code":2387,"language":1204,"meta":183,"style":183},"library(\"micronaut-platform\", \"io.micronaut.platform:micronaut-platform:4.2.3\")\n",[137,2389,2390],{"__ignoreMap":183},[269,2391,2392],{"class":271,"line":272},[269,2393,2387],{},[206,2395,2396,2397,2400,2401,2404,2405,259],{},"In contrary to how this works with the ",[151,2398,2399],{},".toml"," file, the ",[151,2402,2403],{},"version"," now needs to be defined as part of the ",[137,2406,1199],{},[261,2408,2410],{"className":1202,"code":2409,"language":1204,"meta":183,"style":183},"micronaut {\n    version(\"4.2.3\")\n    \u002F\u002F ...\n}\n",[137,2411,2412,2416,2421,2426],{"__ignoreMap":183},[269,2413,2414],{"class":271,"line":272},[269,2415,1211],{},[269,2417,2418],{"class":271,"line":184},[269,2419,2420],{},"    version(\"4.2.3\")\n",[269,2422,2423],{"class":271,"line":991},[269,2424,2425],{},"    \u002F\u002F ...\n",[269,2427,2428],{"class":271,"line":1613},[269,2429,1221],{},[206,2431,2432,2433,347],{},"So far so good. Again, the gradle project is fine, but still not ",[151,2434,2435],{},"renovated",[206,2437,2438],{},"...and here's the new trick: Instead of setting a static version, we simply reference the version of the library mentioned above!!",[261,2440,2442],{"className":1202,"code":2441,"language":1204,"meta":183,"style":183},"micronaut {\n    \u002F*\n    reference to the version catalog instead of using a static version string -> `version(\"4.2.1\")`\n    or via gradle.properties as used by default 'Micronaut Launch'.\n    The version defined here is ignored by renovate but actually used to determine all micronaut dependencies\n    *\u002F\n    version(libs.micronaut.platform.get().version)\n    \n    \u002F\u002F ...\n    \n}\n",[137,2443,2444,2448,2453,2458,2463,2468,2473,2478,2483,2487,2491],{"__ignoreMap":183},[269,2445,2446],{"class":271,"line":272},[269,2447,1211],{},[269,2449,2450],{"class":271,"line":184},[269,2451,2452],{},"    \u002F*\n",[269,2454,2455],{"class":271,"line":991},[269,2456,2457],{},"    reference to the version catalog instead of using a static version string -> `version(\"4.2.1\")`\n",[269,2459,2460],{"class":271,"line":1613},[269,2461,2462],{},"    or via gradle.properties as used by default 'Micronaut Launch'.\n",[269,2464,2465],{"class":271,"line":1626},[269,2466,2467],{},"    The version defined here is ignored by renovate but actually used to determine all micronaut dependencies\n",[269,2469,2470],{"class":271,"line":1639},[269,2471,2472],{},"    *\u002F\n",[269,2474,2475],{"class":271,"line":1652},[269,2476,2477],{},"    version(libs.micronaut.platform.get().version)\n",[269,2479,2480],{"class":271,"line":1665},[269,2481,2482],{},"    \n",[269,2484,2485],{"class":271,"line":1678},[269,2486,2425],{},[269,2488,2489],{"class":271,"line":1699},[269,2490,2482],{},[269,2492,2493],{"class":271,"line":1707},[269,2494,1221],{},[206,2496,2497,2498,2501,2502,2505,2506,2509,2510],{},"=> Renovate tracks and updates the ",[151,2499,2500],{},"micronaut-platform"," dependency which is consecutively used in the ",[151,2503,2504],{},"MavenExtension"," of the ",[23,2507,1134],{"href":1140,"rel":2508},[27],"! ",[909,2511],{"className":2512,"name":1749},[912,1310,914,915,1313],[281,2514,1753],{"id":1752},[206,2516,2075,2517,1235,2520,2522],{},[23,2518,1041],{"href":1039,"rel":2519},[27],[30,2521],{},[23,2523,2524],{"href":2524,"rel":2525},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fmicronaut-gradle-renovate-example-2",[27],[206,2527,1769,2528,1366,2530,1776,2532,1782],{},[151,2529,1772],{},[151,2531,1775],{},[23,2533,1781],{"href":1779,"rel":2534},[27],[16,2536,2537,2549],{},[19,2538,2098,2539,1791,2541,2545,2546],{},[137,2540,2101],{},[23,2542,1262],{"href":2543,"rel":2544},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fmicronaut-gradle-renovate-example-2\u002Fissues\u002F1",[27],". ",[909,2547],{"className":2548,"name":1314},[912,1310,1311,1312,1313],[19,2550,1317,2551,1366,2555,1816,2559],{},[23,2552,1810],{"href":2553,"rel":2554},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fmicronaut-gradle-renovate-example-2\u002Fpulls?q=is%3Apr+is%3Aclosed",[27],[23,2556,1815],{"href":2557,"rel":2558},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fmicronaut-gradle-renovate-example-2\u002Fcommits\u002Fmain\u002F",[27],[909,2560],{"className":2561,"name":1314},[912,1310,1311,1312,1313],[276,2563,2127],{"id":2126},[16,2565,2566,2578,2585],{},[19,2567,2568,2569,2572,2573,2575,2576],{},"Use ",[55,2570,2571],{},"gradle version catalog"," with ",[137,2574,2265],{},", drop the ",[137,2577,1165],{},[19,2579,2580,2581,2584],{},"Include a dependency ",[137,2582,2583],{},"library(\"micronaut-platform\", \"io.micronaut.platform:micronaut-platform:4.x.x\")"," to the version catalog",[19,2586,2587,2588,2590,2591,2594,2595,2598,2599],{},"Set the ",[151,2589,2403],{}," for the ",[151,2592,2593],{},"MicronautExtension"," using a reference to the library -> ",[137,2596,2597],{},"version(libs.micronaut.platform.get().version)"," \u003C- in the ",[137,2600,1199],{},[276,2602,2604],{"id":2603},"so-is-this-the-best-solution","So, Is THIS The Best Solution?",[206,2606,1379,2607,1383],{},[55,2608,1382],{},[206,2610,2611,2612,1389,2614,1396],{},"Please continue with part 4!",[30,2613],{},[23,2615,2616],{"href":1527},"🥇 Solution 3: Renovate Custom Regex Manager!",[289,2618,2619],{},[206,2620,1401],{},[241,2622,2623],{},[206,2624,1406,2625,1411,2627,1414],{},[23,2626,1410],{"href":1409},[137,2628,1253],{},[983,2630,985],{},{"title":183,"searchDepth":184,"depth":184,"links":2632},[2633,2634,2637,2638],{"id":1877,"depth":184,"text":1878},{"id":2243,"depth":184,"text":2244,"children":2635},[2636],{"id":1752,"depth":991,"text":1753},{"id":2126,"depth":184,"text":2127},{"id":2603,"depth":184,"text":2604},"2024-02-08","Renovate hit a snag w\u002F Micronaut updates? Don't worry! The third part of our 4-part guide reveals another workaround! #Micronaut #Gradle #dependencies",{"series":2642},{"id":1432,"title":2643,"part":991},"Solution 2: Gradle Version Catalog (settings.gradle.kts) + Hint + Reference","\u002Fblog\u002Fstreamline-micronaut-gradle-updates-with-renovate-3",{"title":2200,"description":2640},"blog\u002F15.streamline-micronaut-gradle-updates-with-renovate-3",[1439,265,1019,1440,1441],"Hw8g1YJ3booYCmtnvFAAiKUyTzwq4C39JqMkpdaWm30",{"id":2650,"title":2651,"active":6,"body":2652,"date":2952,"description":2953,"duration":1845,"extension":189,"level":1009,"meta":2954,"navigation":6,"ogDescription":190,"ogImage":1434,"ogImageAlt":1053,"ogTitle":190,"path":1392,"pinned":190,"seo":2957,"stem":2958,"tags":2959,"titleAlt":190,"titlePage":190,"__hash__":2960},"blog\u002Fblog\u002F14.streamline-micronaut-gradle-updates-with-renovate-2.md","Streamline Micronaut + Gradle Updates with Renovate (2\u002F4)",{"type":8,"value":2653,"toc":2944},[2654,2671,2673,2690,2694,2704,2726,2745,2750,2757,2782,2797,2806,2815,2817,2829,2838,2864,2873,2875,2910,2914,2918,2930,2934,2942],[289,2655,2656],{},[206,2657,2658],{},[151,2659,2660,1036,2662,1042,2665,2668,2669,347],{},[55,2661,1035],{},[23,2663,1041],{"href":1039,"rel":2664},[27],[23,2666,1047],{"href":1045,"rel":2667},[27],". The first solution (out of 3) is a workaround, adding a 'hint' to ",[137,2670,1175],{},[276,2672,1878],{"id":1877},[206,2674,1469,2675,1473,2677,1366,2681,1482,2685,1485],{},[23,2676,1472],{"href":1435},[55,2678,2679],{},[151,2680,1365],{},[55,2682,2683],{},[151,2684,1371],{},[23,2686,2688],{"href":1326,"rel":2687},[27],[137,2689,1165],{},[276,2691,2693],{"id":2692},"solution-1","Solution 1",[206,2695,2696,2697,2699,2700,2703],{},"IMO the first ",[151,2698,1518],{}," I found is closer to a ",[151,2701,2702],{},"workaround",". Once understood, is actually pretty straightforward.",[206,2705,2706,2707,2572,2710,2712,2713,2715,2716,2719,2720,2725],{},"In a first effort refactored the gradle project to use a ",[23,2708,1171],{"href":1169,"rel":2709},[27],[137,2711,1175],{}," file.\nAlong with that, I deleted the ",[137,2714,1165],{}," file and defined the micronaut platform version in the ",[151,2717,2718],{},"toml file"," instead (",[23,2721,2724],{"href":2722,"rel":2723},"https:\u002F\u002Fmicronaut-projects.github.io\u002Fmicronaut-gradle-plugin\u002F4.3.2\u002Findex.html#sec:micronaut-platform-catalog-plugin",[27],"ref","):",[261,2727,2729],{"className":1338,"code":2728,"language":1340,"meta":183,"style":183},"[versions]\n# note: the version named `micronaut` is picked up by the micronaut gradle plugin\nmicronaut = \"4.3.0\"\n",[137,2730,2731,2735,2740],{"__ignoreMap":183},[269,2732,2733],{"class":271,"line":272},[269,2734,1188],{},[269,2736,2737],{"class":271,"line":184},[269,2738,2739],{},"# note: the version named `micronaut` is picked up by the micronaut gradle plugin\n",[269,2741,2742],{"class":271,"line":991},[269,2743,2744],{},"micronaut = \"4.3.0\"\n",[206,2746,2747,2748,347],{},"So far so good. The gradle project is fine, but still not ",[151,2749,2435],{},[206,2751,2752,2753,2756],{},"...now here's the trick. I added one additional line of code under the ",[137,2754,2755],{},"[libraries]"," section:",[261,2758,2760],{"className":1338,"code":2759,"language":1340,"meta":183,"style":183},"[libraries]\n# note: the defined library 'micronaut-platform' is not referenced directly in any build.gradle(.kts)\n#       but required for renovate to check and update micronaut.\nmicronaut = { module = \"io.micronaut.platform:micronaut-platform\", version.ref = \"micronaut\" }\n",[137,2761,2762,2767,2772,2777],{"__ignoreMap":183},[269,2763,2764],{"class":271,"line":272},[269,2765,2766],{},"[libraries]\n",[269,2768,2769],{"class":271,"line":184},[269,2770,2771],{},"# note: the defined library 'micronaut-platform' is not referenced directly in any build.gradle(.kts)\n",[269,2773,2774],{"class":271,"line":991},[269,2775,2776],{},"#       but required for renovate to check and update micronaut.\n",[269,2778,2779],{"class":271,"line":1613},[269,2780,2781],{},"micronaut = { module = \"io.micronaut.platform:micronaut-platform\", version.ref = \"micronaut\" }\n",[206,2783,2784,2785,1263,2787,2790,2791,2793,2794,347],{},"It's an alias for the maven artifact ",[137,2786,2014],{},[151,2788,2789],{},"linked"," to the micronaut ",[151,2792,2403],{}," via ",[137,2795,2796],{},"version.ref",[206,2798,1567,2799,2802,2803,2805],{},[151,2800,2801],{},"library"," is not actually used in any ",[137,2804,1199],{}," file as a dependency, since the micronaut gradle plugin resolves all dependencies anyway.",[206,2807,2808,2811,2812],{},[55,2809,2810],{},"But"," it allows Renovate to do its thing! ",[909,2813],{"className":2814,"name":1749},[912,1310,914,915,1313],[281,2816,1753],{"id":1752},[206,2818,2819,2820,1235,2823,2825],{},"Here's an adjusted starter project, originally created via ",[23,2821,1041],{"href":1039,"rel":2822},[27],[30,2824],{},[23,2826,2827],{"href":2827,"rel":2828},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fmicronaut-gradle-renovate-example-1",[27],[206,2830,1769,2831,1366,2833,1776,2835,1782],{},[151,2832,1772],{},[151,2834,1775],{},[23,2836,1781],{"href":1779,"rel":2837},[27],[16,2839,2840,2851],{},[19,2841,2098,2842,1791,2844,2545,2848],{},[137,2843,2101],{},[23,2845,1262],{"href":2846,"rel":2847},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fmicronaut-gradle-renovate-example-1\u002Fissues\u002F1",[27],[909,2849],{"className":2850,"name":1314},[912,1310,1311,1312,1313],[19,2852,1317,2853,1366,2857,1816,2861],{},[23,2854,1810],{"href":2855,"rel":2856},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fmicronaut-gradle-renovate-example-1\u002Fpulls?q=is%3Apr+is%3Aclosed",[27],[23,2858,1815],{"href":2859,"rel":2860},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fmicronaut-gradle-renovate-example-1\u002Fcommits\u002Fmain\u002F",[27],[909,2862],{"className":2863,"name":1314},[912,1310,1311,1312,1313],[206,2865,2866],{},[209,2867],{"alt":1053,"className":2868,"dataZoomSrc":2869,"height":2870,"sizes":1276,"src":2871,"width":2872},[482,1273],"\u002Fassets\u002Fblog\u002F14.streamline-micronaut-gradle-updates-with-renovate-2\u002Fgithub-blame-libs.versions.toml-micronaut-version-is-renovated.webp",778,"\u002Fassets\u002Fblog\u002F14.streamline-micronaut-gradle-updates-with-renovate-2\u002Fgithub-blame-libs.versions.toml-micronaut-version-is-renovated.png",2128,[276,2874,2127],{"id":2126},[16,2876,2877,2885,2893],{},[19,2878,2568,2879,2572,2881,2575,2883],{},[55,2880,2571],{},[137,2882,1175],{},[137,2884,1165],{},[19,2886,2887,2888,232,2890],{},"Define a ",[55,2889,2403],{},[137,2891,2892],{},"micronaut = \"4.x.x\"",[19,2894,2895,2896,232,2899,2902,2903,2572,2906,2909],{},"Include an ",[151,2897,2898],{},"un-referenced",[55,2900,2901],{},"libraries"," dependency on ",[137,2904,2905],{},"io.micronaut.platform:micronaut-platform",[137,2907,2908],{},"version.ref = \"micronaut\""," to link to the micronaut version",[276,2911,2913],{"id":2912},"is-this-the-best-solution","Is This THE Best Solution?",[206,2915,1379,2916,1383],{},[55,2917,1382],{},[206,2919,2920,2921,1389,2923,1396],{},"Please continue with part 3!",[30,2922],{},[23,2924,2925,2926,2929],{"href":2644},"Solution 2: Gradle Version Catalog (alternative via ",[137,2927,2928],{},"settings.gradle.kts",") + Dependency Hint + MicronautExtension 'version\u003C->libs' reference",[289,2931,2932],{},[206,2933,1401],{},[241,2935,2936],{},[206,2937,1406,2938,1411,2940,1414],{},[23,2939,1410],{"href":1409},[137,2941,1253],{},[983,2943,985],{},{"title":183,"searchDepth":184,"depth":184,"links":2945},[2946,2947,2950,2951],{"id":1877,"depth":184,"text":1878},{"id":2692,"depth":184,"text":2693,"children":2948},[2949],{"id":1752,"depth":991,"text":1753},{"id":2126,"depth":184,"text":2127},{"id":2912,"depth":184,"text":2913},"2024-02-07","Renovate hit a snag w\u002F Micronaut updates? Don't worry! The second part of our 4-part guide reveals a first workaround! #Micronaut #Gradle #dependencies",{"series":2955},{"id":1432,"title":2956,"part":184},"Solution 1: Gradle Version Catalog + Dependency Hint in `libs.versions.toml`",{"title":2651,"description":2953},"blog\u002F14.streamline-micronaut-gradle-updates-with-renovate-2",[1439,265,1019,1440,1441],"Xf3emxvIiW3NG_D_BseZT-qx4Pxi6LTo9y0VraNMGIc",{"id":2962,"title":2963,"active":6,"body":2964,"date":3525,"description":3526,"duration":3527,"extension":189,"level":3528,"meta":3529,"navigation":6,"ogDescription":3526,"ogImage":3530,"ogImageAlt":3531,"ogTitle":190,"path":1409,"pinned":190,"seo":3532,"stem":3533,"tags":3534,"titleAlt":190,"titlePage":190,"__hash__":3536},"blog\u002Fblog\u002F12.java-library-development-get-started-quickly-with-java-library-template.md","Java Library Development: Get Started Quickly with java-library-template",{"type":8,"value":2965,"toc":3509},[2966,2975,2986,2989,2992,2995,2999,3079,3083,3092,3098,3102,3115,3119,3128,3138,3141,3179,3183,3189,3215,3222,3256,3260,3263,3282,3286,3289,3306,3316,3319,3333,3342,3352,3356,3360,3375,3383,3386,3398,3408,3417,3421,3436,3439,3448,3456,3466,3470,3483,3489,3493],[206,2967,2968],{},[151,2969,2970,2971],{},"TLDR: GitHub Template Repository • Gradle Kotlin DSL • GitHub Actions CI\u002FCD Pipeline • One-click Release & Publish to Maven Central • Renovate • Trivy Vulnerability Scan • Issue & PR Templates -> ",[23,2972,1410],{"href":2973,"rel":2974},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fjava-library-template",[27],[206,2976,2977],{},[209,2978],{"alt":2979,"className":2980,"height":2982,"src":2983,"width":2984,"preload":183,"sizes":2985},"Drake Hotline Bling Meme | creating a new java library; creating a new java-library-template'",[1055,2981],"max-w-[500px]",988,"\u002Fassets\u002Fblog\u002F12.java-library-development-get-started-quickly-with-java-library-template\u002Fcreating-java-library-template-meme.png",986,"lg:500",[206,2987,2988],{},"The 'java-library-template' is a comprehensive solution for Java library developers that simplifies every aspect of library creation and maintenance.",[206,2990,2991],{},"This blog post explores the template's array of features, including one-click project setup, automated releases, security scans, and effortless Javadoc generation. Discover how to keep dependencies up to date with Renovate and ensure seamless publication to Maven Central.",[206,2993,2994],{},"Whether you're a seasoned developer or new to Java, this template empowers you to create high-quality libraries efficiently and with confidence.",[276,2996,2998],{"id":2997},"features","Features",[16,3000,3001,3008,3018,3024,3037,3051,3063,3072],{},[19,3002,3003,3004,3007],{},"🥷 One-click ",[55,3005,3006],{},"automated initial project migration workflow"," (GitHub Action)",[19,3009,3010,3013,3014,3017],{},[55,3011,3012],{},"Java 21"," (corretto) 🤝 ",[55,3015,3016],{},"Gradle Kotlin DSL",", version catalog",[19,3019,3020,3023],{},[55,3021,3022],{},"GitHub Actions CI\u002FCD pipeline",", 👷 efficient build pipeline, caching, integration tests, test report & failed test annotations",[19,3025,3026,3027,3030,3031,140,3034],{},"🚀 ",[55,3028,3029],{},"One-click release"," process + ",[55,3032,3033],{},"publish",[55,3035,3036],{},"Maven Central",[19,3038,3039,3042,3043,3050],{},[55,3040,3041],{},"Security & vulnerability scan"," 🚦 with ",[55,3044,3045],{},[23,3046,3049],{"href":3047,"rel":3048},"https:\u002F\u002Fgithub.com\u002Faquasecurity\u002Ftrivy",[27],"trivy"," & GitHub CodeQL Analysis",[19,3052,3053,2572,3056,3062],{},[55,3054,3055],{},"Automated dependency updates",[55,3057,3058],{},[23,3059,1083],{"href":3060,"rel":3061},"https:\u002F\u002Fgithub.com\u002Frenovatebot\u002Frenovate",[27]," 🤖",[19,3064,3065,3068,3069],{},[55,3066,3067],{},"Javadoc"," deployed with ",[55,3070,3071],{},"GitHub Pages",[19,3073,3074,3075,3078],{},"Open Source ",[55,3076,3077],{},"Community ready"," (Code of Conduct, Contribution guidelines, Issue & PR Templates)",[276,3080,3082],{"id":3081},"java-library-template-in-60s","'java-library-template' in 60s",[206,3084,3085,3086,3091],{},"If you prefer a video over reading this post, there's a 60s intro available on the ",[23,3087,3090],{"href":3088,"rel":3089},"https:\u002F\u002Fwww.youtube.com\u002F@thriving_dev",[27],"@thriving_dev"," YouTube Channel!",[3093,3094],"youtube-embed",{"id":3095,"subtitle":3096,"thumbnail":3097,"title":3096},"nXs7hSV6ris","Introducing 'java-library-template' in 60s","\u002Fassets\u002Fblog\u002F12.java-library-development-get-started-quickly-with-java-library-template\u002Fjava-library-template_youtube_cover_v2_cropped1.webp",[276,3099,3101],{"id":3100},"quick-start","Quick Start",[206,3103,3104,3109,3110,347],{},[23,3105,3108],{"href":3106,"rel":3107},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fjava-library-template\u002Fgenerate",[27],"Use the template"," to create your own repository and follow the instructions in the ",[23,3111,3114],{"href":3112,"rel":3113},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fjava-library-template#quick-start",[27],"README.md",[276,3116,3118],{"id":3117},"cicd-pipeline","CI\u002FCD Pipeline",[206,3120,3121,3122,3127],{},"The heart of this template is the 'Main GitHub Actions CI\u002FCD Pipeline'. See it in ",[23,3123,3126],{"href":3124,"rel":3125},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fjava-library-template\u002Factions\u002Fworkflows\u002F1.pipeline.yml",[27],"Actions"," (👻).",[206,3129,3130],{},[209,3131],{"alt":3132,"className":3133,"height":3134,"src":3135,"width":3136,"dataZoomSrc":3137,"preload":183},"image",[482,1273],1338,"\u002Fassets\u002Fblog\u002F12.java-library-development-get-started-quickly-with-java-library-template\u002Fjava-library-template_ci-cd-pipeline.png",3446,"\u002Fassets\u002Fblog\u002F12.java-library-development-get-started-quickly-with-java-library-template\u002Fjava-library-template_ci-cd-pipeline.webp",[206,3139,3140],{},"The workflow encompasses multiple jobs, modelled and linked with dependencies and conditions.\nBased on the context (trigger, ref, input arguments) it meets different use cases:",[456,3142,3143,3152,3161,3170],{},[19,3144,3145,3148,3149,3151],{},[55,3146,3147],{},"Check",": Build, test, integration test; code quality & vulnerability scans.",[30,3150],{},"\nRuns for active PRs - as well as part of all subsequent listed use cases.",[19,3153,3154,3157,3158,3160],{},[55,3155,3156],{},"Latest",": Publish SNAPSHOT version to Maven Central and Javadoc (GitHub Pages).",[30,3159],{},"\nRuns on pushes to the main branch.",[19,3162,3163,3166,3167,3169],{},[55,3164,3165],{},"Release (Process)",": Execute (major|minor|patch) release process via Gradle plugin.",[30,3168],{},"\nManually triggered workflow via GitHub UI\u002FAPI.",[19,3171,3172,3175,3176,3178],{},[55,3173,3174],{},"Release",": Publish RELEASE version to Maven Central and Javadoc (GitHub Pages).",[30,3177],{},"\nRuns for pushed tags.",[276,3180,3182],{"id":3181},"project-structure","Project Structure",[206,3184,3185,3186,259],{},"The project template consists of three top level ",[151,3187,3188],{},"folders",[16,3190,3191,3197,3209],{},[19,3192,3193,3196],{},[137,3194,3195],{},".github\u002F",": Defines the Github Actions CI tasks and templates for new pull requests, issues, etc.",[19,3198,3199,3202,3203,3208],{},[137,3200,3201],{},"gradle\u002F",": Contains Gradle Configuration files such as the Gradle ",[23,3204,3207],{"href":3205,"rel":3206},"https:\u002F\u002Fdocs.gradle.org\u002Fcurrent\u002Fuserguide\u002Fplatforms.html",[27],"Version Catalog"," and the Gradle Wrapper.",[19,3210,3211,3214],{},[137,3212,3213],{},"java-library-template\u002F",": The library source code (gradle sub-project).",[206,3216,3217,3218,3221],{},"In addition, following ",[151,3219,3220],{},"files"," are worth highlighting:",[16,3223,3224,3236,3241,3250],{},[19,3225,3226,3229,3230,3235],{},[137,3227,3228],{},"gradle\u002Flibs.versions.toml",": A ",[23,3231,3234],{"href":3232,"rel":3233},"https:\u002F\u002Fdocs.gradle.org\u002Fcurrent\u002Fuserguide\u002Fplatforms.html#sub:conventional-dependencies-toml",[27],"conventional file"," to declare a version catalog.",[19,3237,3238,3240],{},[137,3239,2928],{},": The multi-project Gradle settings file. Here are all sub-projects defined.",[19,3242,3243,3245,3246,347],{},[137,3244,1165],{},": Holds the library version, needed & maintained by the CI\u002FCD pipeline ",[23,3247,3249],{"href":3248},"#release-process","release process",[19,3251,3252,3255],{},[137,3253,3254],{},"**\u002Fbuild.gradle.kts",": Gradle build file",[276,3257,3259],{"id":3258},"publish-to-maven-central","Publish to Maven Central",[206,3261,3262],{},"The maven publish process is fully automated and does not require manual action.",[16,3264,3265,3277],{},[19,3266,1317,3267,3270,3271,3276],{},[151,3268,3269],{},"main"," branch (per process definition) always is set to the next ",[23,3272,3275],{"href":3273,"rel":3274},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fjava-library-template\u002Fblob\u002Fmain\u002Fgradle.properties",[27],"SNAPSHOT version"," and is published to the Sonatype snapshot repository with each main CI\u002FCD pipeline run. The pipeline runs e.g. when a PR is merged, but can also be triggered manually.",[19,3278,3279,3280,340],{},"Release deployment happens when a new tag is pushed to GitHub. (Part of the ",[23,3281,3249],{"href":3248},[276,3283,3285],{"id":3284},"release-process","Release Process",[206,3287,3288],{},"To release a new version via the CI\u002FCD Pipeline, please follow instructions below.",[16,3290,3291,3294,3297,3300,3303],{},[19,3292,3293],{},"Navigate to Actions (1)",[19,3295,3296],{},"> Main Pipeline (2)",[19,3298,3299],{},"Click 'Run workflow' button (3)",[19,3301,3302],{},"Select a semver release type with the 'Release Library' dropdown (4)",[19,3304,3305],{},"'Run the workflow' (5)",[206,3307,3308],{},[209,3309],{"alt":3310,"className":3311,"height":3312,"src":3313,"width":3314,"dataZoomSrc":3315,"loading":484},"Instructions (1) of the Release Process",[482,1273],1683,"\u002Fassets\u002Fblog\u002F12.java-library-development-get-started-quickly-with-java-library-template\u002Fjava-library-template_ci-cd-pipeline_release-process-1.png",2533,"\u002Fassets\u002Fblog\u002F12.java-library-development-get-started-quickly-with-java-library-template\u002Fjava-library-template_ci-cd-pipeline_release-process-1.webp",[206,3317,3318],{},"The release process includes",[16,3320,3321,3324,3327,3330],{},[19,3322,3323],{},"Pipeline run (incl. build & tests) that executes the release plugin (6)",[19,3325,3326],{},"The release plugin first sets & commits the new version (7a)",[19,3328,3329],{},"Creates & pushes a new tag (7b)",[19,3331,3332],{},"Sets the main branch to the next SNAPSHOT version (7c)",[206,3334,3335],{},[209,3336],{"alt":3132,"className":3337,"height":3338,"src":3339,"width":3340,"dataZoomSrc":3341,"loading":484},[482,1273],1583,"\u002Fassets\u002Fblog\u002F12.java-library-development-get-started-quickly-with-java-library-template\u002Fjava-library-template_ci-cd-pipeline_release-process-2.png",2529,"\u002Fassets\u002Fblog\u002F12.java-library-development-get-started-quickly-with-java-library-template\u002Fjava-library-template_ci-cd-pipeline_release-process-2.webp",[206,3343,3344,3345],{},"The new version is automatically published to Maven Central! 🚀\n",[209,3346],{"alt":3132,"className":3347,"height":3348,"src":3349,"width":3350,"dataZoomSrc":3351,"loading":484},[482,1273],1595,"\u002Fassets\u002Fblog\u002F12.java-library-development-get-started-quickly-with-java-library-template\u002Fjava-library-template_ci-cd-pipeline_release-process-3.png",2548,"\u002Fassets\u002Fblog\u002F12.java-library-development-get-started-quickly-with-java-library-template\u002Fjava-library-template_ci-cd-pipeline_release-process-3.webp",[276,3353,3355],{"id":3354},"security-codeql-analysis","Security & CodeQL Analysis",[281,3357,3359],{"id":3358},"common-vulnerabilities-and-exposures-cve","Common Vulnerabilities and Exposures (CVE)",[206,3361,3362,3363,2572,3368,3374],{},"The libraries gradle dependencies are scanned for known ",[23,3364,3367],{"href":3365,"rel":3366},"https:\u002F\u002Fwww.cve.org\u002F",[27],"CVE",[55,3369,3370],{},[23,3371,3373],{"href":3047,"rel":3372},[27],"Trivy",". The scan results can be reviewed and managed under 'Security > Vulnerability alerts > Code scanning'.",[289,3376,3377],{},[206,3378,3379,3380,3382],{},"ℹ️ ",[55,3381,3373],{}," is an open-source vulnerability scanner that quickly identifies security vulnerabilities in container images and applications, making it a valuable tool for enhancing the security of containerized environments.",[206,3384,3385],{},"Scans are triggered",[456,3387,3388,3391],{},[19,3389,3390],{},"with each main CI\u002FCD pipeline run",[19,3392,3393,3394,340],{},"Scheduled (weekly) (",[23,3395,2724],{"href":3396,"rel":3397},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fjava-library-template\u002Fblob\u002Fmain\u002F.github\u002Fworkflows\u002F2.scheduled.code-analysis.yml",[27],[206,3399,3400],{},[209,3401],{"alt":3402,"className":3403,"height":3404,"src":3405,"width":3406,"dataZoomSrc":3407,"loading":484},"Preview of a critical CVE listed in the GitHub Security 'Code scanning' overview page",[482,1273],865,"\u002Fassets\u002Fblog\u002F12.java-library-development-get-started-quickly-with-java-library-template\u002Fjava-library-template_ci-cd-pipeline_trivy-scan-cve-review.png",2233,"\u002Fassets\u002Fblog\u002F12.java-library-development-get-started-quickly-with-java-library-template\u002Fjava-library-template_ci-cd-pipeline_trivy-scan-cve-review.webp",[206,3409,3410,3411,3416],{},"Please refer to ",[23,3412,3415],{"href":3413,"rel":3414},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Fcode-security\u002Fcode-scanning\u002Fintroduction-to-code-scanning\u002Fabout-code-scanning",[27],"official GitHub documentation"," for more details.",[281,3418,3420],{"id":3419},"github-codeql-analysis","GitHub CodeQL Analysis",[206,3422,3423,3424,3429,3430,3435],{},"Further, the codebase is analysed with ",[23,3425,3428],{"href":3426,"rel":3427},"https:\u002F\u002Fcodeql.github.com\u002F",[27],"GitHub CodeQL",". Please refer to the ",[23,3431,3434],{"href":3432,"rel":3433},"https:\u002F\u002Fcodeql.github.com\u002Fdocs\u002Fcodeql-overview\u002Fabout-codeql\u002F",[27],"official docs"," to learn more about CodeQL.",[276,3437,3067],{"id":3438},"javadoc",[206,3440,3441,3442,3444,3445,347],{},"A Javadoc website of your library, generated by gradle, is 'published' to GitHub Pages by the CI\u002FCD pipeline. In addition to each released version, the current snapshot version (",[151,3443,3269],{}," branch) is published as ",[137,3446,3447],{},"current",[206,3449,3450,3451],{},"-> ",[23,3452,3455],{"href":3453,"rel":3454},"https:\u002F\u002Fthriving-dev.github.io\u002Fjava-library-template\u002Fjavadoc\u002F",[27],"Live preview",[206,3457,3458],{},[209,3459],{"alt":3460,"className":3461,"height":3462,"src":3463,"width":3464,"dataZoomSrc":3465,"loading":484},"Preview of Javadoc published to GitHub Pages by the CI\u002FCD pipeline",[482,1273],1168,"\u002Fassets\u002Fblog\u002F12.java-library-development-get-started-quickly-with-java-library-template\u002Fjava-library-template_ci-cd-pipeline_published-javadoc.png",2166,"\u002Fassets\u002Fblog\u002F12.java-library-development-get-started-quickly-with-java-library-template\u002Fjava-library-template_ci-cd-pipeline_published-javadoc.webp",[276,3467,3469],{"id":3468},"automated-dependency-updates-with-renovate","Automated Dependency Updates with Renovate",[206,3471,3472,3473,347,3476,3478,3479,347],{},"The recommended way to enable renovate is to use the ",[23,3474,1100],{"href":1098,"rel":3475},[27],[30,3477],{},"\nThis template ships with a prepared ",[23,3480,1253],{"href":3481,"rel":3482},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fjava-library-template\u002Fblob\u002Fmain\u002Frenovate.json",[27],[289,3484,3485],{},[206,3486,3379,3487,1092],{},[55,3488,1083],{},[276,3490,3492],{"id":3491},"credits","Credits",[16,3494,3495,3502],{},[19,3496,3497,3498],{},"inspired by ",[23,3499,3500],{"href":3500,"rel":3501},"https:\u002F\u002Fgithub.com\u002Fcortinico\u002Fkotlin-android-template",[27],[19,3503,3504,3505],{},"PR & issue templates copied \u002F adapted from ",[23,3506,3507],{"href":3507,"rel":3508},"https:\u002F\u002Fgithub.com\u002Fnuxt\u002Fnuxt",[27],{"title":183,"searchDepth":184,"depth":184,"links":3510},[3511,3512,3513,3514,3515,3516,3517,3518,3522,3523,3524],{"id":2997,"depth":184,"text":2998},{"id":3081,"depth":184,"text":3082},{"id":3100,"depth":184,"text":3101},{"id":3117,"depth":184,"text":3118},{"id":3181,"depth":184,"text":3182},{"id":3258,"depth":184,"text":3259},{"id":3284,"depth":184,"text":3285},{"id":3354,"depth":184,"text":3355,"children":3519},[3520,3521],{"id":3358,"depth":991,"text":3359},{"id":3419,"depth":991,"text":3420},{"id":3438,"depth":184,"text":3067},{"id":3468,"depth":184,"text":3469},{"id":3491,"depth":184,"text":3492},"2023-09-19","Lightweight, non-opinionated 'starter' for your next Open Source Java\u002FKotlin library. Ready-to-use CI\u002FCD pipeline feat. one-click release process, publish to Maven Central, Renovate, Trivy Scan, Javadoc.","7min","basic",{},"\u002Fassets\u002Fblog\u002F12.java-library-development-get-started-quickly-with-java-library-template\u002Fjava-library-template_og_v1.jpg","Titled 'java-library-template' shows a summary of features and the thriving.dev logo",{"title":2963,"description":3526},"blog\u002F12.java-library-development-get-started-quickly-with-java-library-template",[1439,3535,265,1440],"github-actions","XDW12IUd1ADAbU9jC48mx9xGh5F9CLM18Xy14Bmbhzs",{"id":3538,"title":3539,"active":6,"body":3540,"date":3950,"description":3951,"duration":1429,"extension":189,"level":1009,"meta":3952,"navigation":6,"ogDescription":190,"ogImage":3953,"ogImageAlt":3954,"ogTitle":190,"path":3955,"pinned":190,"seo":3956,"stem":3957,"tags":3958,"titleAlt":190,"titlePage":3539,"__hash__":3960},"blog\u002Fblog\u002F11.kafka-streams-cassandra-state-store-8.0.0-versioned-state-store.md","Kafka Streams Cassandra State Store 0.8.0 ships VersionedKeyValueStore\u003CK, V>",{"type":8,"value":3541,"toc":3937},[3542,3553,3563,3567,3584,3590,3597,3606,3610,3613,3625,3643,3646,3649,3652,3655,3662,3665,3669,3672,3682,3686,3697,3771,3775,3778,3781,3795,3798,3802,3818,3820,3823,3831,3835,3858,3862,3865,3935],[206,3543,3544,3552],{},[55,3545,3546,3547],{},"Kafka Streams Cassandra State Store ",[23,3548,3551],{"href":3549,"rel":3550},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fkafka-streams-cassandra-state-store\u002Freleases\u002Ftag\u002F0.8.0",[27],"0.8.0"," is out, dropping support for versioned state stores.",[206,3554,3555],{},[209,3556],{"alt":3557,"className":3558,"height":3559,"preload":183,"sizes":3560,"src":3561,"width":3562},"Meme picture calling for 'TRUE TEMPORAL STREAM-TABLE JOIN ALL THE THINGS!!!'",[1055,2981],2024,"sm:396 lg:500","\u002Fassets\u002Fblog\u002F11.kafka-streams-cassandra-state-store-8.0.0-versioned-state-store\u002Ftrue-temporal-stream-table-join-all-the-things_v2.png",2750,[281,3564,3566],{"id":3565},"recap-versioned-state-stores","Recap: Versioned State Stores",[206,3568,3569,3574,3575,3580,3581,3583],{},[23,3570,3573],{"href":3571,"rel":3572},"https:\u002F\u002Fcwiki.apache.org\u002Fconfluence\u002Fdisplay\u002FKAFKA\u002FKIP-889%3A+Versioned+State+Stores",[27],"KIP-889"," (+ ",[23,3576,3579],{"href":3577,"rel":3578},"https:\u002F\u002Fcwiki.apache.org\u002Fconfluence\u002Fdisplay\u002FKAFKA\u002FKIP-914%3A+DSL+Processor+Semantics+for+Versioned+Stores",[27],"KIP-914",") introduces versioned state stores in Kafka Streams.",[30,3582],{},"\nA RocksDB based implementation was released with Kafka 3.5.0.",[206,3585,3586,3589],{},[55,3587,3588],{},"Victoria Xia's"," sums up the new feature",[289,3591,3592],{},[206,3593,3594],{},[151,3595,3596],{},"\"KIP-889 is the first in a sequence of KIPs to introduce versioned key-value stores into Kafka Streams. Versioned key-value stores enhance stateful processing capabilities by allowing users to store multiple record versions per key, rather than only the single latest version per key as is the case for existing key-value stores today. Storing multiple record versions per key unlocks use cases such as true temporal stream-table joins: when an out-of-order record arrives on the stream-side, Kafka Streams can produce the correct join result by looking 'back in time' for the table state at the timestamp of the stream-side record. Foreign-key joins will see similar benefits, and users can also support custom use cases in their applications by running interactive queries to look up older record versions from versioned state stores, or by using them in custom processors.\"",[206,3598,3599,3600,3605],{},"in the pitch for her excellent ",[23,3601,3604],{"href":3602,"rel":3603},"https:\u002F\u002Fwww.confluent.io\u002Fevents\u002Fkafka-summit-london-2023\u002Fversioned-state-stores-in-kafka-streams\u002F",[27],"session at Kafka Summit London 2023"," that introduces versioned state stores.",[276,3607,3609],{"id":3608},"versionedkeyvaluestorek-v-implementation","VersionedKeyValueStore\u003CK, V> Implementation",[206,3611,3612],{},"The library 'kafka-streams-cassandra-state-store' 0.8.0 ships two new store types:",[16,3614,3615,3620],{},[19,3616,3617],{},[55,3618,3619],{},"partitionedVersionedKeyValueStore",[19,3621,3622],{},[55,3623,3624],{},"globalVersionedKeyValueStore",[206,3626,3627,3628,3631,3632,3635,3636,154,3639,3642],{},"Both are ",[151,3629,3630],{},"persistent"," implementations of ",[137,3633,3634],{},"VersionedKeyValueStore\u003CBytes, byte[]>"," and the two variants ",[151,3637,3638],{},"partitioned",[151,3640,3641],{},"global"," build on the same fundamentals as the non-versioned stores...",[11,3644,3619],{"id":3645},"partitionedversionedkeyvaluestore",[206,3647,3648],{},"The underlying cassandra table is partitioned by the store context task partition.",[206,3650,3651],{},"It behaves exactly like the official versioned state store. All CRUD operations against this store always query by and return results for a single stream task.",[11,3653,3624],{"id":3654},"globalversionedkeyvaluestore",[206,3656,3657,3658,3661],{},"The underlying cassandra table uses the record key + validTo as composite PRIMARY KEY (",[151,3659,3660],{},"validTo"," as the clustering key).",[206,3663,3664],{},"Therefore, all CRUD operations against this store work from any streams task and therefore always are “global”.",[281,3666,3668],{"id":3667},"interactive-queries","Interactive Queries",[206,3670,3671],{},"With Kafka 3.5 interactive queries interfaces are not yet available for versioned key value stores. Plans exist to add this in the future.",[206,3673,3674,3675,3677,3678,3681],{},"Follow-up KIPs will be opened \u002F are in-progress.",[30,3676],{},"\n(",[151,3679,3680],{},"asOfTImestamp"," 2023-08-25: KIP-960, KIP-968, KIP-969)",[276,3683,3685],{"id":3684},"usage-example","Usage Example",[206,3687,3688,3689,3692,3693,3696],{},"See following code snippet of declaring a ",[137,3690,3691],{},"KTable",", materialized with a versioned store and ",[151,3694,3695],{},"historyRetention"," of 1min:",[261,3698,3700],{"className":263,"code":3699,"language":265,"meta":183,"style":183},"StreamsBuilder builder = new StreamsBuilder();\nSerde\u003CString> stringSerde = Serdes.String();\nSerde\u003CLong> longSerde = Serdes.Long();\n\n\u002F\u002F prices table\nKTable\u003CString, Long> prices = builder\n    .stream(INPUT_TOPIC_PRICES, Consumed.with(stringSerde, longSerde))\n    .toTable(Materialized.\u003CString, Long>as(\n            CassandraStores.builder(session, STORE_NAME)\n                .partitionedVersionedKeyValueStore(Duration.ofMinutes(1)))\n        .withCachingDisabled()\n        .withLoggingDisabled()\n        .withKeySerde(stringSerde)\n        .withValueSerde(longSerde));\n",[137,3701,3702,3707,3712,3717,3721,3726,3731,3736,3741,3746,3751,3756,3761,3766],{"__ignoreMap":183},[269,3703,3704],{"class":271,"line":272},[269,3705,3706],{},"StreamsBuilder builder = new StreamsBuilder();\n",[269,3708,3709],{"class":271,"line":184},[269,3710,3711],{},"Serde\u003CString> stringSerde = Serdes.String();\n",[269,3713,3714],{"class":271,"line":991},[269,3715,3716],{},"Serde\u003CLong> longSerde = Serdes.Long();\n",[269,3718,3719],{"class":271,"line":1613},[269,3720,2291],{"emptyLinePlaceholder":6},[269,3722,3723],{"class":271,"line":1626},[269,3724,3725],{},"\u002F\u002F prices table\n",[269,3727,3728],{"class":271,"line":1639},[269,3729,3730],{},"KTable\u003CString, Long> prices = builder\n",[269,3732,3733],{"class":271,"line":1652},[269,3734,3735],{},"    .stream(INPUT_TOPIC_PRICES, Consumed.with(stringSerde, longSerde))\n",[269,3737,3738],{"class":271,"line":1665},[269,3739,3740],{},"    .toTable(Materialized.\u003CString, Long>as(\n",[269,3742,3743],{"class":271,"line":1678},[269,3744,3745],{},"            CassandraStores.builder(session, STORE_NAME)\n",[269,3747,3748],{"class":271,"line":1699},[269,3749,3750],{},"                .partitionedVersionedKeyValueStore(Duration.ofMinutes(1)))\n",[269,3752,3753],{"class":271,"line":1707},[269,3754,3755],{},"        .withCachingDisabled()\n",[269,3757,3758],{"class":271,"line":1727},[269,3759,3760],{},"        .withLoggingDisabled()\n",[269,3762,3763],{"class":271,"line":1733},[269,3764,3765],{},"        .withKeySerde(stringSerde)\n",[269,3767,3768],{"class":271,"line":1739},[269,3769,3770],{},"        .withValueSerde(longSerde));\n",[276,3772,3774],{"id":3773},"operational-considerations-for-users-whod-like-to-use-them","Operational Considerations for Users Who'd Like to Use Them",[206,3776,3777],{},"While versioned stores enable true temporal stream-table joins, aggregations and other operations by correctly handling out-of-order records,\nthey also come with extra costs. Storing multiple record versions per key rather than only the single latest version requires additional interactions with the store.",[206,3779,3780],{},"Maintaining the audit per key comes with",[16,3782,3783,3786,3789],{},[19,3784,3785],{},"extra lookups and patching of versioned point-in-time values, many of them in sequential order",[19,3787,3788],{},"increase in overall data volumes of the store",[19,3790,3791,3792],{},"'cleanup' operations for ",[151,3793,3794],{},"history retention",[206,3796,3797],{},"With the state maintained external to the streams application (Cassandra) these additional store interactions are blocking IO and therefore will directly affect the throughput.",[276,3799,3801],{"id":3800},"next-steps","Next Steps",[16,3803,3804,3807,3810],{},[19,3805,3806],{},"Add interactive queries support once follow-up KIPs are delivered",[19,3808,3809],{},"Benchmark",[19,3811,3812,3813,340],{},"Consider (in-memory) caching options to improve performance (ref ",[23,3814,3817],{"href":3815,"rel":3816},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fkafka-streams-cassandra-state-store\u002Fissues\u002F18",[27],"#18",[276,3819,862],{"id":783},[206,3821,3822],{},"At the time of writing this blog post, the latest versions of relevant libs were",[16,3824,3825,3828],{},[19,3826,3827],{},"Kafka \u002F Streams API: 3.5.0",[19,3829,3830],{},"kafka-streams-cassandra-state-store: 0.8.0",[276,3832,3834],{"id":3833},"references","References",[16,3836,3837,3843,3848,3853],{},[19,3838,3839],{},[23,3840,3841],{"href":3841,"rel":3842},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fkafka-streams-cassandra-state-store\u002Fpull\u002F21",[27],[19,3844,3845],{},[23,3846,3549],{"href":3549,"rel":3847},[27],[19,3849,3850],{},[23,3851,3571],{"href":3571,"rel":3852},[27],[19,3854,3855],{},[23,3856,3602],{"href":3602,"rel":3857},[27],[281,3859,3861],{"id":3860},"live-coding-series","Live Coding Series",[206,3863,3864],{},"This feature was live coded and recorded over a course of 6 parts, available on YouTube:",[16,3866,3867,3877,3887,3897,3911,3921],{},[19,3868,3869,3872,3873],{},[55,3870,3871],{},"Part 1: Requirement Engineering, Analysis, Design, POC",". During this session, I’m defining the new table schema, defining the different queries to lookup records, as well as the business process for use cases such as ‘get current’, ‘get asOfTimestamp’, ‘put new current record’, ‘put point-in-time record’, ‘delete point-in-time record’, ‘cleanup as per historic retention’. ",[23,3874,3875],{"href":3875,"rel":3876},"https:\u002F\u002Fyoutu.be\u002FzuMvGdmRqfs",[27],[19,3878,3879,3882,3883],{},[55,3880,3881],{},"Part 2: First integration test with testcontainers",". In this part, the first new integration test is implemented using JUnit5 and the testcontainers framework. The test setup had to be changed ad-hoc because no access to the store is provided with Kafka Streams 3.5.0, which took me by surprise. ",[23,3884,3885],{"href":3885,"rel":3886},"https:\u002F\u002Fyoutu.be\u002FaXyAUc-lJR4",[27],[19,3888,3889,3892,3893],{},[55,3890,3891],{},"Part 3: Integration test improvements and adding more test cases."," This follow-up part still focuses on integration tests (before the actual implementation), improving the test setup to allow accessing the state store and adding additional test cases to cover all versioned store interface methods and data & access edge cases. ",[23,3894,3895],{"href":3895,"rel":3896},"https:\u002F\u002Fyoutu.be\u002Fpb7SoPGdGz4",[27],[19,3898,3899,3902,3903,3906,3907],{},[55,3900,3901],{},"Part 4:"," 🔥 ",[55,3904,3905],{},"Implementation of the new feature."," Next comes the highlight of this series: the actual implementation of the new feature, putting together all the gained knowledge. Designs, the prepared CQL statements and business processes. The result is a complete implementation of ‘CassandraVersionedKeyValueStore’ with a ‘partitioned’ type underlying data schema (‘global’ type to be added in a subsequent part). ",[23,3908,3909],{"href":3909,"rel":3910},"https:\u002F\u002Fyoutu.be\u002FeaNximD1LKY",[27],[19,3912,3913,3916,3917],{},[55,3914,3915],{},"Part 5: Debugging, subsequent works, and making the tests pass!"," Having completed an initial implementation of the versioned store interface, we now run the integration tests written in part2 & part3. There was a considerable amount of debugging and logic left to be done to work out and fix all the nitty gritty details. Due to tiredness, this took quite some time to complete but ended with passing tests and a code complete state for the CassandraVersionedKeyValueStore & PartitionedCassandraVersionedKeyValueStoreRepository. 💪 ",[23,3918,3919],{"href":3919,"rel":3920},"https:\u002F\u002Fyoutu.be\u002FXydzLX4sa00",[27],[19,3922,3923,3930,3931],{},[55,3924,3925,3926,3929],{},"Part 6: ‘Global’ ",[151,3927,3928],{},"type"," db schema based store."," In this final part, we implement the ‘global’ type store and do some extra refactoring. With this, the feature is completed!! 🥳",[23,3932,3933],{"href":3933,"rel":3934},"https:\u002F\u002Fyoutu.be\u002FSRdLZhH2roE",[27],[983,3936,985],{},{"title":183,"searchDepth":184,"depth":184,"links":3938},[3939,3940,3943,3944,3945,3946,3947],{"id":3565,"depth":991,"text":3566},{"id":3608,"depth":184,"text":3609,"children":3941},[3942],{"id":3667,"depth":991,"text":3668},{"id":3684,"depth":184,"text":3685},{"id":3773,"depth":184,"text":3774},{"id":3800,"depth":184,"text":3801},{"id":783,"depth":184,"text":862},{"id":3833,"depth":184,"text":3834,"children":3948},[3949],{"id":3860,"depth":991,"text":3861},"2023-08-27","The library 'kafka-streams-cassandra-state-store' 0.8.0 is out, bringing versioned state store support with two new store types (partitionedVersionedKeyValueStore, globalVersionedKeyValueStore).",{},"\u002Fassets\u002Fblog\u002F11.kafka-streams-cassandra-state-store-8.0.0-versioned-state-store\u002Ftrue-temporal-stream-table-join-all-the-things_og_v2.jpg","Meme picture calling for 'TRUE TEMPORAL STREAM-TABLE JOIN ALL THE THINGS!!!","\u002Fblog\u002Fkafka-streams-cassandra-state-store-8.0.0-versioned-state-store",{"title":3539,"description":3951},"blog\u002F11.kafka-streams-cassandra-state-store-8.0.0-versioned-state-store",[1016,1017,3959,265],"cassandra","4mpGr1onJswZ-cNm3jh-X-tgKQeDfNXB_0hR_Wt_lMA",{"id":3962,"title":3963,"active":6,"body":3964,"date":4371,"description":4372,"duration":1845,"extension":189,"level":4373,"meta":4374,"navigation":6,"ogDescription":190,"ogImage":4378,"ogImageAlt":4049,"ogTitle":190,"path":4379,"pinned":190,"seo":4380,"stem":4381,"tags":4382,"titleAlt":190,"titlePage":4383,"__hash__":4384},"blog\u002Fblog\u002F10.interactive-queries-with-kafka-streams-cassandra-state-store-part-2.md","Interactive Queries with 'kafka-streams-cassandra-state-store' (Part 2)",{"type":8,"value":3965,"toc":4361},[3966,3976,3985,4019,4035,4038,4042,4045,4055,4062,4074,4078,4091,4103,4106,4200,4207,4219,4222,4226,4232,4238,4242,4286,4303,4305,4307,4313,4322,4324,4326,4333,4335,4359],[206,3967,3968,3969,3975],{},"Today's article covers ",[55,3970,3971,3972,3974],{},"accessing ",[151,3973,3638],{}," Kafka Streams Cassandra KeyValueStores via 'Interactive Queries'",", including a demo showing how to expose the\nstate of your applications via a REST API.",[206,3977,3978,3979,3984],{},"With the latest release ",[23,3980,3983],{"href":3981,"rel":3982},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fkafka-streams-cassandra-state-store\u002Freleases\u002Ftag\u002F0.7.0",[27],"0.7.0"," a new feature + example have been added:",[456,3986,3987,3998],{},[19,3988,3989,3990,3993,3994,3997],{},"Advanced, optimised, efficient, custom implementation of ",[137,3991,3992],{},"ReadOnlyKeyValueStore"," for 'Interactive Queries' with for 'partitioned' type Cassandra KeyValueStore,\nprovided via ",[137,3995,3996],{},"CassandraStateStore"," static methods.",[19,3999,4000,4001,4006,4007,4010,4011,4014,4015,4018],{},"New example '",[23,4002,4005],{"href":4003,"rel":4004},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fkafka-streams-cassandra-state-store\u002Ftree\u002F0.7.2\u002Fexamples\u002Fpartitioned-store-restapi",[27],"partitioned-store-restapi","' that is using the new ",[137,4008,4009],{},"CassandraPartitionedReadOnlyKeyValueStore",".\nIt features a REST API with endpoints for ",[151,4012,4013],{},"all"," 'Interactive Query' ",[151,4016,4017],{},"methods"," to play with hands-on.",[762,4020,4021,4032],{},[206,4022,4023,4024,4027,4028,347],{},"This is a ",[55,4025,4026],{},"follow-up post"," of ",[23,4029,4031],{"href":4030},"\u002Fblog\u002Finteractive-queries-with-kafka-streams-cassandra-state-store","Interactive Queries with 'kafka-streams-cassandra-state-store' (Part 1)",[206,4033,4034],{},"Part 1 covers the basics of 'Interactive Queries' and how using Cassandra State Stores renders having an 'RPC layer' unnecessary.\nPlease note this post does not repeat any of the fundamentals already covered in part 1.",[206,4036,4037],{},"☝️The idea for this new feature shipped with 0.7.0, actually came up while writing the previous post...",[276,4039,4041],{"id":4040},"querying-cassandra-key-value-store","Querying Cassandra Key Value Store",[206,4043,4044],{},"All Interactive Queries can be fulfilled directly from the local instance with one or multiple queries to Cassandra.",[206,4046,4047],{},[209,4048],{"alt":4049,"className":4050,"dataZoomSrc":4051,"height":4052,"loading":484,"src":4053,"width":4054},"Integration Diagram for a Kafka Streams Cassandra State Store exposed via REST-API using Interactive Queries v1",[482],"\u002Fassets\u002Fblog\u002F9.interactive-queries-with-kafka-streams-cassandra-state-store\u002FKafka-Streams_Cassandra-State-Store_REST-API_v1.webp",1700,"\u002Fassets\u002Fblog\u002F9.interactive-queries-with-kafka-streams-cassandra-state-store\u002FKafka-Streams_Cassandra-State-Store_REST-API_v1.png",3646,[206,4056,4057,4058,4061],{},"At the time of writing the first post, we learned how this is done for 'globalKeyValueStore'.\nOn the other hand accessing 'partitionedKeyValueStore' still required following the 'RPC layer' approach, because data\naccess using the ",[137,4059,4060],{},"CassandraKeyValueStore"," is always bound and restricted to the belonging streams task (:= partition).",[206,4063,4064,4065,4067,4068,232,4071,347],{},"This constraint is now lifted through a new class ",[137,4066,4009],{}," that ",[151,4069,4070],{},"implements",[137,4072,4073],{},"ReadOnlyKeyValueStore\u003CK, V>",[281,4075,4077],{"id":4076},"update-cassandra-store-type-partitionedkeyvaluestore","UPDATE: Cassandra Store Type 'partitionedKeyValueStore'",[206,4079,4080,4081,4086,4087,4090],{},"The Cassandra ",[55,4082,4083],{},[151,4084,4085],{},"partitionedKeyValueStore"," is partitioned (CQL table primary key > partition key) by the streams ",[151,4088,4089],{},"taskId","\nand supports all methods of the KeyValueStore 'Interactive Queries' interface.",[206,4092,4093,4094,232,4096,4100,4101,347],{},"To get an instance of this brand-new shiny ",[137,4095,4009],{},[909,4097],{"className":4098,"name":4099},[912,913,914,915],"twemoji:sparkles","\n(which is package private) you need to use the static methods of the interface ",[137,4102,3996],{},[206,4104,4105],{},"Here's an example:",[261,4107,4109],{"className":263,"code":4108,"language":265,"meta":183,"style":183},"\u002F\u002F get a read-only store to exec interactive queries ('partitioned' type cassandra KeyValueStore)\nReadOnlyKeyValueStore\u003CString, Long> store = CassandraStateStore.readOnlyPartitionedKeyValueStore(\n        streams,                                                \u002F\u002F streams\n        \"word-count\",                                           \u002F\u002F storeName\n        session,                                                \u002F\u002F session\n        \"kstreams_wordcount\",                                   \u002F\u002F keyspace\n        true,                                                   \u002F\u002F isCountAllEnabled\n        \"dml\",                                                  \u002F\u002F dmlExecutionProfile\n        stringSerde,                                            \u002F\u002F keySerde\n        longSerde,                                              \u002F\u002F valueSerde\n        CassandraStateStore.DEFAULT_TABLE_NAME_FN,              \u002F\u002F tableNameFn\n        new DefaultStreamPartitioner\u003C>(keySerde.serializer())   \u002F\u002F partitioner\n);\n        \n\u002F\u002F Get the value from the store\nLong value = store.get(key);\n",[137,4110,4111,4116,4121,4126,4134,4139,4147,4152,4160,4165,4170,4175,4180,4185,4190,4195],{"__ignoreMap":183},[269,4112,4113],{"class":271,"line":272},[269,4114,4115],{},"\u002F\u002F get a read-only store to exec interactive queries ('partitioned' type cassandra KeyValueStore)\n",[269,4117,4118],{"class":271,"line":184},[269,4119,4120],{},"ReadOnlyKeyValueStore\u003CString, Long> store = CassandraStateStore.readOnlyPartitionedKeyValueStore(\n",[269,4122,4123],{"class":271,"line":991},[269,4124,4125],{},"        streams,                                                \u002F\u002F streams\n",[269,4127,4128,4131],{"class":271,"line":1613},[269,4129,4130],{},"        \"word-count\",",[269,4132,4133],{},"                                           \u002F\u002F storeName\n",[269,4135,4136],{"class":271,"line":1626},[269,4137,4138],{},"        session,                                                \u002F\u002F session\n",[269,4140,4141,4144],{"class":271,"line":1639},[269,4142,4143],{},"        \"kstreams_wordcount\",",[269,4145,4146],{},"                                   \u002F\u002F keyspace\n",[269,4148,4149],{"class":271,"line":1652},[269,4150,4151],{},"        true,                                                   \u002F\u002F isCountAllEnabled\n",[269,4153,4154,4157],{"class":271,"line":1665},[269,4155,4156],{},"        \"dml\",",[269,4158,4159],{},"                                                  \u002F\u002F dmlExecutionProfile\n",[269,4161,4162],{"class":271,"line":1678},[269,4163,4164],{},"        stringSerde,                                            \u002F\u002F keySerde\n",[269,4166,4167],{"class":271,"line":1699},[269,4168,4169],{},"        longSerde,                                              \u002F\u002F valueSerde\n",[269,4171,4172],{"class":271,"line":1707},[269,4173,4174],{},"        CassandraStateStore.DEFAULT_TABLE_NAME_FN,              \u002F\u002F tableNameFn\n",[269,4176,4177],{"class":271,"line":1727},[269,4178,4179],{},"        new DefaultStreamPartitioner\u003C>(keySerde.serializer())   \u002F\u002F partitioner\n",[269,4181,4182],{"class":271,"line":1733},[269,4183,4184],{},");\n",[269,4186,4187],{"class":271,"line":1739},[269,4188,4189],{},"        \n",[269,4191,4192],{"class":271,"line":2353},[269,4193,4194],{},"\u002F\u002F Get the value from the store\n",[269,4196,4197],{"class":271,"line":2358},[269,4198,4199],{},"Long value = store.get(key);\n",[206,4201,4202,4203,4206],{},"The instance can only be created when the streams app is in RUNNING state and can (+should) then be ",[151,4204,4205],{},"re-used","!",[762,4208,4209],{},[206,4210,4211,4212,4214,4215,4218],{},"In addition, the implementation of ",[137,4213,4009],{}," requires ",[137,4216,4217],{},"application.server"," config to be set (to be able to access metadata).",[206,4220,4221],{},"The number of method parameters is not low - but each argument to be passed in is needed to construct the repo and underlying class.\nUnfortunately, there's some overhead\u002Fredundancy with how the store is registered with the topology. I couldn't think of a better way of doing this yet.",[276,4223,4225],{"id":4224},"demo","Demo",[206,4227,4228,4229],{},"Source code for this demo: ",[23,4230,4005],{"href":4003,"rel":4231},[27],[4233,4234],"lite-you-tube-embed",{":webp":491,"id":4235,"poster":4236,"title":4237},"qUcG7M6lnBo","maxresdefault","Demo of Interactive Queries with 'kafka-streams-cassandra-state-store' type 'partitionedKeyValueStore'",[276,4239,4241],{"id":4240},"next-steps-considerations","Next Steps & Considerations",[16,4243,4244,4247,4250],{},[19,4245,4246],{},"The behaviour for large state stores has yet to be tested.",[19,4248,4249],{},"Cassandra fetches data in chunks, so reading \u002F iterating over a large number of rows should still be possible when timeouts are not breached.",[19,4251,4252,4253,4256,4257],{},"Current implementation, for the non-single result queries (all, range, prefixScan, query), is executing the cql query for all partitions in parallel, then ",[151,4254,4255],{},"iterating the (result) iterators"," one by one in sequential order.\n",[16,4258,4259,4270,4276,4283],{},[19,4260,4261,4262,4265,4266,4269],{},"This should be correct from a consistency point of view, but for large states, would the cassandra ",[137,4263,4264],{},"ResultSet"," iterators ",[151,4267,4268],{},"on-hold"," time out before being consumed?",[19,4271,4272,4273,347],{},"While results are fetched in chunks, with the 'parallel query' pattern, first chunks for all partition would be fetched at once and demand ",[151,4274,4275],{},"RAM",[19,4277,4278,4279,4282],{},"Maybe it would be better to switch to a sequential query+processing pattern iterating over the partitions (see ",[137,4280,4281],{},"org.apache.kafka.streams.state.internals.CompositeKeyValueIterator","). Or provide both and allow the user to choose which one to use.",[19,4284,4285],{},"It would be interesting to test and compare both options applied for different use cases and streams architectures.",[762,4287,4289],{"icon":4288,"title":251},"twemoji:thinking-face",[206,4290,4291,4292,4297,4298,4206],{},"This might be worth to cover in a 'Part 3' post. ",[23,4293,4296],{"href":4294,"rel":4295},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fkafka-streams-cassandra-state-store\u002Fissues\u002F26",[27],"Issue #26"," was created to keep track and potentially re-visit in the future.\nContributions are welcome, please PM me on ",[23,4299,4302],{"href":4300,"rel":4301},"https:\u002F\u002Ftwitter.com\u002FTheThrivingDev",[27],"twitter",[289,4304],{},[276,4306,825],{"id":824},[206,4308,4309,4310,4312],{},"The new ",[137,4311,4009],{}," is a good addition to the tool belt of 'kafka-streams-cassandra-state-store'\nand may proof valuable when your business logic involves 'Interactive Queries' that requires accessing the state of your\nentire streams app.",[206,4314,4315,4316,784,4319,4321],{},"The library still is in an experimental phase of life (version ",[137,4317,4318],{},"0.x",[30,4320],{},"\nFeedback, ideas, and contributions are welcome. Please reach out any time!",[276,4323,862],{"id":783},[206,4325,3822],{},[16,4327,4328,4330],{},[19,4329,3827],{},[19,4331,4332],{},"kafka-streams-cassandra-state-store: 0.7.2",[276,4334,3834],{"id":3833},[16,4336,4337,4343,4348],{},[19,4338,4339],{},[23,4340,4341],{"href":4341,"rel":4342},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fkafka-streams-cassandra-state-store\u002Fpull\u002F25",[27],[19,4344,4345],{},[23,4346,3981],{"href":3981,"rel":4347},[27],[19,4349,4350,4354,4355],{},[909,4351],{"className":4352,"name":4353},[912,913,914,915],"logos:youtube-icon"," Live-Coding session of feature #23: ",[23,4356,4357],{"href":4357,"rel":4358},"https:\u002F\u002Fyoutu.be\u002F2IrYHx1FomY",[27],[983,4360,985],{},{"title":183,"searchDepth":184,"depth":184,"links":4362},[4363,4366,4367,4368,4369,4370],{"id":4040,"depth":184,"text":4041,"children":4364},[4365],{"id":4076,"depth":991,"text":4077},{"id":4224,"depth":184,"text":4225},{"id":4240,"depth":184,"text":4241},{"id":824,"depth":184,"text":825},{"id":783,"depth":184,"text":862},{"id":3833,"depth":184,"text":3834},"2023-07-24","With 'partitioned' Cassandra State Stores, accessing the entire state from any instance is now supported. No RPC layer is necessary that distributes requests to all instances of the streams application.","advanced",{"series":4375},{"id":4376,"title":4377,"part":184},"Interactive Queries with 'kafka-streams-cassandra-state-store'","Interactive Queries for `partitionedKeyValueStore`","\u002Fassets\u002Fblog\u002F10.interactive-queries-with-kafka-streams-cassandra-state-store-part-2\u002FKafka-Streams_Cassandra-State-Store_Interactive-Queries_v1_og.jpeg","\u002Fblog\u002Finteractive-queries-with-kafka-streams-cassandra-state-store-part-2",{"title":3963,"description":4372},"blog\u002F10.interactive-queries-with-kafka-streams-cassandra-state-store-part-2",[1016,1017,3959,265],"Interactive Queries with 'kafka-streams-cassandra-state-store' (Part 2) `partitionedKeyValueStore`","JReFArLhWtSyNJj_aebXm1ZWru_RPNRFTZer8lZUJZE",{"id":4386,"title":4031,"active":6,"body":4387,"date":5085,"description":5086,"duration":5087,"extension":189,"level":4373,"meta":5088,"navigation":6,"ogDescription":190,"ogImage":5091,"ogImageAlt":4049,"ogTitle":190,"path":4030,"pinned":190,"seo":5092,"stem":5093,"tags":5094,"titleAlt":190,"titlePage":5095,"__hash__":5096},"blog\u002Fblog\u002F9.interactive-queries-with-kafka-streams-cassandra-state-store.md",{"type":8,"value":4388,"toc":5070},[4389,4395,4412,4420,4443,4450,4454,4462,4481,4485,4491,4499,4533,4538,4597,4612,4616,4619,4640,4649,4655,4723,4742,4754,4756,4760,4775,4801,4804,4809,4812,4830,4839,4842,4915,4926,4953,4973,4977,4993,4999,5005,5019,5023,5026,5029,5032,5034,5036,5043,5047,5053,5055,5068],[206,4390,3968,4391,4394],{},[55,4392,4393],{},"accessing Kafka Streams Cassandra State Stores via 'Interactive Queries'"," and how to expose the\nstate of your applications via a REST API.",[206,4396,4397,4398,4401,4402,4407,4408,4411],{},"While interactive queries had been supported since the initial release ",[151,4399,4400],{},"0.1.0",", the recent version ",[23,4403,4406],{"href":4404,"rel":4405},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fkafka-streams-cassandra-state-store\u002Freleases",[27],"0.5.0"," ships with\n",[55,4409,4410],{},"convenient helper methods"," to get hold of a correctly set up read-only store facade to query your state.",[206,4413,4414],{},[209,4415],{"alt":4416,"height":4417,"src":4418,"width":4419,"preload":183},"Integration Diagram for a Kafka Streams State Store exposed via REST-API using Interactive Queries v1",963,"\u002Fassets\u002Fblog\u002F9.interactive-queries-with-kafka-streams-cassandra-state-store\u002FKafka-Streams_Cassandra-State-Store_Interactive-Queries_v1.png",1783,[206,4421,4422,4423,4426,4427,4430,4431,4434,4435,4442],{},"While for ",[55,4424,4425],{},"the regular Kafka Streams Stores"," (RocksDB & InMemory), state always is ",[55,4428,4429],{},"partitioned and local"," to the individual running\napplication instance, ",[55,4432,4433],{},"for Cassandra Stores"," all ",[55,4436,4437,4438,4441],{},"state resides in a ",[151,4439,4440],{},"common"," external database"," (cluster) that all\ninstances can access.",[206,4444,4445,4446,4449],{},"This means that ",[55,4447,4448],{},"for 'global' Cassandra State Stores there's no need for an RPC layer"," proxying and fanning out requests\nto all instances of your streams application. Stay tuned for more details...",[276,4451,4453],{"id":4452},"basics-recap","Basics Recap",[289,4455,4456],{},[206,4457,4458,4461],{},[55,4459,4460],{},"Kafka Streams 'Interactive Queries'"," is a feature in Apache Kafka Streams that allows applications to query the state of\na stream processor. It enables real-time interactive access to the internal state stores of a Kafka Streams application,\nallowing applications to perform dynamic lookups.",[289,4463,4464],{},[206,4465,4466,4469,4470,4473,4474,4477,4478,347],{},[55,4467,4468],{},"'kafka-streams-cassandra-state-store'"," is a Kafka Streams State Store implementation that persists data to Apache Cassandra.\nIt's a 'drop-in' replacement for the official Kafka Streams state store solutions, notably ",[151,4471,4472],{},"RocksDB"," (default) and ",[151,4475,4476],{},"InMemory",".\n@see ",[23,4479,4480],{"href":804},"the blog post introducing the library",[276,4482,4484],{"id":4483},"querying-local-state-stores-for-an-app-instance","Querying Local State Stores for an App Instance",[281,4486,4488,4489],{"id":4487},"interface-readonlykeyvaluestore","Interface ",[137,4490,3992],{},[206,4492,4493,4494,347],{},"The process of accessing local stores from the streams instance is a well known and well documented pattern.\nBelow the most basic example is provided, for mor information, please refer to the ",[23,4495,4498],{"href":4496,"rel":4497},"https:\u002F\u002Fdocs.confluent.io\u002Fplatform\u002Fcurrent\u002Fstreams\u002Fdeveloper-guide\u002Finteractive-queries.html#querying-local-key-value-stores",[27],"Confluent docs",[261,4500,4502],{"className":263,"code":4501,"language":265,"meta":183,"style":183},"\u002F\u002F get the key-value store named \"word-counts\"\nReadOnlyKeyValueStore\u003CString, Long> store = streams.store(\n        StoreQueryParameters.fromNameAndType(\"word-counts\", QueryableStoreTypes.keyValueStore()));\n\n\u002F\u002F get value by key\nSystem.out.println(\"count for 'hello': \" + store.get(\"hello\"));\n",[137,4503,4504,4509,4514,4519,4523,4528],{"__ignoreMap":183},[269,4505,4506],{"class":271,"line":272},[269,4507,4508],{},"\u002F\u002F get the key-value store named \"word-counts\"\n",[269,4510,4511],{"class":271,"line":184},[269,4512,4513],{},"ReadOnlyKeyValueStore\u003CString, Long> store = streams.store(\n",[269,4515,4516],{"class":271,"line":991},[269,4517,4518],{},"        StoreQueryParameters.fromNameAndType(\"word-counts\", QueryableStoreTypes.keyValueStore()));\n",[269,4520,4521],{"class":271,"line":1613},[269,4522,2291],{"emptyLinePlaceholder":6},[269,4524,4525],{"class":271,"line":1626},[269,4526,4527],{},"\u002F\u002F get value by key\n",[269,4529,4530],{"class":271,"line":1639},[269,4531,4532],{},"System.out.println(\"count for 'hello': \" + store.get(\"hello\"));\n",[206,4534,1317,4535,4537],{},[137,4536,3992],{}," interface provides following methods:",[261,4539,4541],{"className":263,"code":4540,"language":265,"meta":183,"style":183},"public interface ReadOnlyKeyValueStore\u003CK, V> {\n    V get(K key);\n    KeyValueIterator\u003CK, V> range(K from, K to);\n    KeyValueIterator\u003CK, V> reverseRange(K from, K to);\n    KeyValueIterator\u003CK, V> all();\n    KeyValueIterator\u003CK, V> reverseAll();\n    \u003CPS extends Serializer\u003CP>, P> KeyValueIterator\u003CK, V> prefixScan(\n            P prefix, PS prefixKeySerializer\n    );\n    long approximateNumEntries();\n}\n",[137,4542,4543,4548,4553,4558,4563,4568,4573,4578,4583,4588,4593],{"__ignoreMap":183},[269,4544,4545],{"class":271,"line":272},[269,4546,4547],{},"public interface ReadOnlyKeyValueStore\u003CK, V> {\n",[269,4549,4550],{"class":271,"line":184},[269,4551,4552],{},"    V get(K key);\n",[269,4554,4555],{"class":271,"line":991},[269,4556,4557],{},"    KeyValueIterator\u003CK, V> range(K from, K to);\n",[269,4559,4560],{"class":271,"line":1613},[269,4561,4562],{},"    KeyValueIterator\u003CK, V> reverseRange(K from, K to);\n",[269,4564,4565],{"class":271,"line":1626},[269,4566,4567],{},"    KeyValueIterator\u003CK, V> all();\n",[269,4569,4570],{"class":271,"line":1639},[269,4571,4572],{},"    KeyValueIterator\u003CK, V> reverseAll();\n",[269,4574,4575],{"class":271,"line":1652},[269,4576,4577],{},"    \u003CPS extends Serializer\u003CP>, P> KeyValueIterator\u003CK, V> prefixScan(\n",[269,4579,4580],{"class":271,"line":1665},[269,4581,4582],{},"            P prefix, PS prefixKeySerializer\n",[269,4584,4585],{"class":271,"line":1678},[269,4586,4587],{},"    );\n",[269,4589,4590],{"class":271,"line":1699},[269,4591,4592],{},"    long approximateNumEntries();\n",[269,4594,4595],{"class":271,"line":1707},[269,4596,1221],{},[206,4598,4599],{},[269,4600,4604],{"className":4601},[493,4602,4603],"block","-mt-3",[151,4605,4606,4607],{},"Full source code incl. Javadoc: ",[23,4608,4611],{"href":4609,"rel":4610},"https:\u002F\u002Fgithub.com\u002Fapache\u002Fkafka\u002Fblob\u002F3.5\u002Fstreams\u002Fsrc\u002Fmain\u002Fjava\u002Forg\u002Fapache\u002Fkafka\u002Fstreams\u002Fstate\u002FReadOnlyKeyValueStore.java",[27],"https:\u002F\u002Fgithub.com\u002Fapache\u002Fkafka\u002F...\u002FReadOnlyKeyValueStore.java",[276,4613,4615],{"id":4614},"querying-remote-state-stores-for-the-entire-app","Querying Remote State Stores for the Entire App",[206,4617,4618],{},"In order to make the application's complete state available, it is necessary to access the app's remote states, including those operating on different instances.",[206,4620,4621,4622,4627,4628,4630,4631,4634,4635,347],{},"Confluent named this pattern ",[23,4623,4626],{"href":4624,"rel":4625},"https:\u002F\u002Fdocs.confluent.io\u002Fplatform\u002Fcurrent\u002Fstreams\u002Fdeveloper-guide\u002Finteractive-queries.html#streams-developer-guide-interactive-queries-rpc-layer",[27],"'Adding an RPC layer'",".\nRPC endpoints are configured in clients via ",[137,4629,4217],{}," property (=> unique ",[151,4632,4633],{},"host:port"," pair), synchronised, and made\navailable to all instances via ",[23,4636,4639],{"href":4637,"rel":4638},"https:\u002F\u002Fdocs.confluent.io\u002Fplatform\u002F7.4\u002Fstreams\u002Fjavadocs\u002Fjavadoc\u002Forg\u002Fapache\u002Fkafka\u002Fstreams\u002Fstate\u002FStreamsMetadata.html",[27],"StreamsMetadata",[206,4641,4642],{},[209,4643],{"alt":4416,"height":4644,"src":4645,"width":4646,"className":4647,"dataZoomSrc":4648,"loading":484},1686,"\u002Fassets\u002Fblog\u002F9.interactive-queries-with-kafka-streams-cassandra-state-store\u002FKafka-Streams_State-Store_REST-API_v1.png",3647,[482],"\u002Fassets\u002Fblog\u002F9.interactive-queries-with-kafka-streams-cassandra-state-store\u002FKafka-Streams_State-Store_REST-API_v1.webp",[206,4650,4651,4652,4654],{},"To get the value by key, the host can be read from streams metadata and looked up from local state or RPC.",[30,4653],{},"\nSimplified logic as follows:",[261,4656,4658],{"className":263,"code":4657,"language":265,"meta":183,"style":183},"\u002F\u002F We first find the one app instance that has the state for given `key`\nStreamsMetadata metadata = streams.metadataForKey(\"word-count\", key, Serdes.String().serializer());\n\n\u002F\u002F check if key resides on the own instance \nif (metadata.hostInfo().equals(this.hostInfo)) {\n    \u002F\u002F access local store\n    return store.get(key);\n} else {\n    \u002F\u002F from the metadata we can read the host + port and construct\n    String baseUrl = \"https:\u002F\u002F\" + metadata.host() + \":\" + metadata.port() + \"\u002Fapi\u002Fword-count\u002F\" + key;\n    \u002F\u002F make RPC...\n    {...}\n}\n",[137,4659,4660,4665,4670,4674,4679,4684,4689,4694,4699,4704,4709,4714,4719],{"__ignoreMap":183},[269,4661,4662],{"class":271,"line":272},[269,4663,4664],{},"\u002F\u002F We first find the one app instance that has the state for given `key`\n",[269,4666,4667],{"class":271,"line":184},[269,4668,4669],{},"StreamsMetadata metadata = streams.metadataForKey(\"word-count\", key, Serdes.String().serializer());\n",[269,4671,4672],{"class":271,"line":991},[269,4673,2291],{"emptyLinePlaceholder":6},[269,4675,4676],{"class":271,"line":1613},[269,4677,4678],{},"\u002F\u002F check if key resides on the own instance \n",[269,4680,4681],{"class":271,"line":1626},[269,4682,4683],{},"if (metadata.hostInfo().equals(this.hostInfo)) {\n",[269,4685,4686],{"class":271,"line":1639},[269,4687,4688],{},"    \u002F\u002F access local store\n",[269,4690,4691],{"class":271,"line":1652},[269,4692,4693],{},"    return store.get(key);\n",[269,4695,4696],{"class":271,"line":1665},[269,4697,4698],{},"} else {\n",[269,4700,4701],{"class":271,"line":1678},[269,4702,4703],{},"    \u002F\u002F from the metadata we can read the host + port and construct\n",[269,4705,4706],{"class":271,"line":1699},[269,4707,4708],{},"    String baseUrl = \"https:\u002F\u002F\" + metadata.host() + \":\" + metadata.port() + \"\u002Fapi\u002Fword-count\u002F\" + key;\n",[269,4710,4711],{"class":271,"line":1707},[269,4712,4713],{},"    \u002F\u002F make RPC...\n",[269,4715,4716],{"class":271,"line":1727},[269,4717,4718],{},"    {...}\n",[269,4720,4721],{"class":271,"line":1733},[269,4722,1221],{},[762,4724,4725],{},[206,4726,1317,4727,4730,4731,4733,4734,4737,4738,4741],{},[55,4728,4729],{},"number of remote calls depends on the operation"," (query).",[30,4732],{},"\nWhile for a ",[137,4735,4736],{},"get(K key)"," it's sufficient to make a single call, for a ",[137,4739,4740],{},"range(K from, K to)"," query all instances have to be involved and the results combined.",[241,4743,4744,4751],{},[206,4745,4746,4747,4750],{},"To improve availability and the chance to have data required in the local state, ",[55,4748,4749],{},"standby tasks"," may be added.",[206,4752,4753],{},"⚠ It's important to keep in mind that standby tasks are processed with lower priority and consistently lag behind the original task\u002Finstance.",[276,4755,4041],{"id":4040},[281,4757,4759],{"id":4758},"cassandra-store-type-globalkeyvaluestore","Cassandra Store Type 'globalKeyValueStore'",[206,4761,4762,4763,4766,4767,4770,4771,4774],{},"For the ",[55,4764,4765],{},"'global' type store"," of 'kafka-streams-cassandra-state-store' there's ",[55,4768,4769],{},"no need for"," any ",[55,4772,4773],{},"RPC"," between instances.\n(☝️though the query to Cassandra of course is network IO)",[762,4776,4777],{},[206,4778,4779,4780,4783,4784,4789,4790,4795,4796,3416],{},"The 'global' type store is ",[151,4781,4782],{},"globally"," accessible from all instances (no matter the store's ",[23,4785,4788],{"href":4786,"rel":4787},"https:\u002F\u002Fdocs.confluent.io\u002Fplatform\u002F7.4\u002Fstreams\u002Fjavadocs\u002Fjavadoc\u002Forg\u002Fapache\u002Fkafka\u002Fstreams\u002Fprocessor\u002FStateStoreContext.html",[27],"context","),\nbut a regular KV Store. It's ",[55,4791,4792],{},[151,4793,4794],{},"NOT"," a Kafka Streams Global Store (\u002FGlobalKTable).\nPlease refer to the ",[23,4797,4800],{"href":4798,"rel":4799},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fkafka-streams-cassandra-state-store#globalkeyvaluestore",[27],"docs",[206,4802,4803],{},"All Interactive Queries can be fulfilled directly from the local instance with a single query to Cassandra.",[206,4805,4806],{},[209,4807],{"alt":4049,"height":4052,"src":4053,"width":4054,"className":4808,"dataZoomSrc":4051,"loading":484},[482],[206,4810,4811],{},"To safely use Interactive Queries with the 'globalKeyValueStore' there are a few significant details to pay attention to.",[206,4813,4814,4815,4818,4819,4822,4823,4826,4827,4829],{},"🧐 For querying the 'global store', the ",[137,4816,4817],{},"WrappingStoreProvider"," must be restricted to a single (assigned) partition.\nThe ",[137,4820,4821],{},"KafkaStreams"," instance returns a ",[137,4824,4825],{},"CompositeReadOnlyKeyValueStore"," that holds the ",[137,4828,4817],{},", wrapping all assigned tasks' stores.",[667,4831,4832],{},[206,4833,4834,4835,4838],{},"Without the correct ",[137,4836,4837],{},"StoreQueryParameters"," the same query is executed multiple times (for all local assigned tasks) and\ncombines the same results multiple times 🐛.",[206,4840,4841],{},"After taking everything into account, the outcome is the code snippet presented below:",[261,4843,4845],{"className":263,"code":4844,"language":265,"meta":183,"style":183},"\u002F\u002F get the first active task partition for the first streams thread\nfinal int firstActiveTaskPartition = streams.metadataForLocalThreads()\n        .stream().findFirst()\n        .orElseThrow(() -> new RuntimeException(\"no streams threads found\"))\n        .activeTasks()\n        .stream().findFirst()\n        .orElseThrow(() -> new RuntimeException(\"no active task found\"))\n        .taskId().partition();\n\n\u002F\u002F get a WrappingStoreProvider 'withPartition' -> query only a single store (the first active task)!\n\u002F\u002F (WrappingStoreProvider otherwise iterates over all storeProviders for all assigned tasks and repeatedly query Cassandra)\nReadOnlyKeyValueStore\u003CX, Y> store = streams.store(fromNameAndType(storeName, QueryableStoreTypes.\u003CX, Y>keyValueStore())\n        .enableStaleStores() \u002F\u002F should be unnecessary -> CassandraStateStore should always be used with logging disabled and without standby tasks...\n        .withPartition(firstActiveTaskPartition));\n",[137,4846,4847,4852,4857,4862,4867,4872,4876,4881,4886,4890,4895,4900,4905,4910],{"__ignoreMap":183},[269,4848,4849],{"class":271,"line":272},[269,4850,4851],{},"\u002F\u002F get the first active task partition for the first streams thread\n",[269,4853,4854],{"class":271,"line":184},[269,4855,4856],{},"final int firstActiveTaskPartition = streams.metadataForLocalThreads()\n",[269,4858,4859],{"class":271,"line":991},[269,4860,4861],{},"        .stream().findFirst()\n",[269,4863,4864],{"class":271,"line":1613},[269,4865,4866],{},"        .orElseThrow(() -> new RuntimeException(\"no streams threads found\"))\n",[269,4868,4869],{"class":271,"line":1626},[269,4870,4871],{},"        .activeTasks()\n",[269,4873,4874],{"class":271,"line":1639},[269,4875,4861],{},[269,4877,4878],{"class":271,"line":1652},[269,4879,4880],{},"        .orElseThrow(() -> new RuntimeException(\"no active task found\"))\n",[269,4882,4883],{"class":271,"line":1665},[269,4884,4885],{},"        .taskId().partition();\n",[269,4887,4888],{"class":271,"line":1678},[269,4889,2291],{"emptyLinePlaceholder":6},[269,4891,4892],{"class":271,"line":1699},[269,4893,4894],{},"\u002F\u002F get a WrappingStoreProvider 'withPartition' -> query only a single store (the first active task)!\n",[269,4896,4897],{"class":271,"line":1707},[269,4898,4899],{},"\u002F\u002F (WrappingStoreProvider otherwise iterates over all storeProviders for all assigned tasks and repeatedly query Cassandra)\n",[269,4901,4902],{"class":271,"line":1727},[269,4903,4904],{},"ReadOnlyKeyValueStore\u003CX, Y> store = streams.store(fromNameAndType(storeName, QueryableStoreTypes.\u003CX, Y>keyValueStore())\n",[269,4906,4907],{"class":271,"line":1733},[269,4908,4909],{},"        .enableStaleStores() \u002F\u002F should be unnecessary -> CassandraStateStore should always be used with logging disabled and without standby tasks...\n",[269,4911,4912],{"class":271,"line":1739},[269,4913,4914],{},"        .withPartition(firstActiveTaskPartition));\n",[206,4916,4917,4918,4920,4921,4925],{},"The interface ",[137,4919,3996],{}," that was released with ",[23,4922,4406],{"href":4923,"rel":4924},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fkafka-streams-cassandra-state-store\u002Freleases\u002Ftag\u002F0.5.0",[27],"\nprovides convenient static helper methods to get properly configured stores for interactive queries:",[261,4927,4929],{"className":263,"code":4928,"language":265,"meta":183,"style":183},"\u002F\u002F get a store to exec interactive queries\nReadOnlyKeyValueStore\u003CString, Long> store = CassandraStateStore.readOnlyGlobalKeyValueStore(streams, STORE_NAME);\n        \n\u002F\u002F Get the value from the store\nLong value = store.get(key);\n",[137,4930,4931,4936,4941,4945,4949],{"__ignoreMap":183},[269,4932,4933],{"class":271,"line":272},[269,4934,4935],{},"\u002F\u002F get a store to exec interactive queries\n",[269,4937,4938],{"class":271,"line":184},[269,4939,4940],{},"ReadOnlyKeyValueStore\u003CString, Long> store = CassandraStateStore.readOnlyGlobalKeyValueStore(streams, STORE_NAME);\n",[269,4942,4943],{"class":271,"line":991},[269,4944,4189],{},[269,4946,4947],{"class":271,"line":1613},[269,4948,4194],{},[269,4950,4951],{"class":271,"line":1626},[269,4952,4199],{},[762,4954,4955],{},[206,4956,4957,4958,4961,4962,347,4965,4967,4968,347],{},"Please take note that ",[55,4959,4960],{},"for 'globalKeyValueStore'"," store type, ",[55,4963,4964],{},"not all operations are supported",[30,4966],{},"\nRead the ",[23,4969,4972],{"href":4970,"rel":4971},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fkafka-streams-cassandra-state-store#supported-operations-by-store-type",[27],"the documentation",[281,4974,4976],{"id":4975},"cassandra-store-type-partitionedkeyvaluestore","Cassandra Store Type 'partitionedKeyValueStore'",[206,4978,4080,4979,4086,4983,4985,4986,1366,4989,4992],{},[55,4980,4981],{},[151,4982,4085],{},[151,4984,4089],{},"\nto support ",[137,4987,4988],{},"range",[137,4990,4991],{},"prefixScan"," queries.",[206,4994,4995,4996,4998],{},"In theory, no RPC would be required since each instance still can access all table rows with the taskId as\nCQL query condition - but results for interactive queries other than ",[137,4997,4736],{}," still need to be queried separately\nfor all tasks and the results combined\u002Fmerged.",[206,5000,5001,5002,5004],{},"Currently, no custom ",[137,5003,3992],{}," implementation provided to do that. Just like for RocksDB\u002FInMemory\nstate stores the 'RPC layer' pattern has to be utilised for this store type.",[762,5006,5007],{},[206,5008,5009,5010,5015,5016,4206],{},"Actually, this idea came up while writing this very post... ",[23,5011,5014],{"href":5012,"rel":5013},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fkafka-streams-cassandra-state-store\u002Fissues\u002F23",[27],"issue #23"," was created to implement this feature.\nContributions are welcome, please PM me on ",[23,5017,4302],{"href":4300,"rel":5018},[27],[276,5020,5022],{"id":5021},"assessment-conclusion","Assessment & Conclusion",[206,5024,5025],{},"Kafka Streams Interactive Queries are very powerful and allow for many use cases, such as advanced queries to your store\nfrom the Processor API, or exposing the state via REST API for access from outside the application.",[206,5027,5028],{},"An 'RPC layer' allows access to the complete state, which is distributed across all instances of your application.\nDepending on the operation, remote calls to a single or all instances is needed.",[206,5030,5031],{},"With 'global' Cassandra State Stores the RPC layer can be avoided by reading all data directly from Cassandra.\nThe larger the state and scale (no. of instances) of your application, the more benefits Cassandra may be able to provide.",[276,5033,862],{"id":783},[206,5035,3822],{},[16,5037,5038,5040],{},[19,5039,3827],{},[19,5041,5042],{},"kafka-streams-cassandra-state-store: 0.6.0",[11,5044,5046],{"id":5045},"update-24072023","UPDATE 24\u002F07\u002F2023",[206,5048,5049,5050],{},"Follow-up Post was Published -> ",[23,5051,5052],{"href":4379},"Interactive Queries with 'kafka-streams-cassandra-state-store' (Part 2) for 'partitionedKeyValueStore'",[276,5054,3834],{"id":3833},[16,5056,5057,5063],{},[19,5058,5059],{},[23,5060,5061],{"href":5061,"rel":5062},"https:\u002F\u002Fdocs.confluent.io\u002Fplatform\u002Fcurrent\u002Fstreams\u002Fdeveloper-guide\u002Finteractive-queries.html",[27],[19,5064,5065],{},[23,5066,5012],{"href":5012,"rel":5067},[27],[983,5069,985],{},{"title":183,"searchDepth":184,"depth":184,"links":5071},[5072,5073,5077,5078,5082,5083,5084],{"id":4452,"depth":184,"text":4453},{"id":4483,"depth":184,"text":4484,"children":5074},[5075],{"id":4487,"depth":991,"text":5076},"Interface ReadOnlyKeyValueStore",{"id":4614,"depth":184,"text":4615},{"id":4040,"depth":184,"text":4041,"children":5079},[5080,5081],{"id":4758,"depth":991,"text":4759},{"id":4975,"depth":991,"text":4976},{"id":5021,"depth":184,"text":5022},{"id":783,"depth":184,"text":862},{"id":3833,"depth":184,"text":3834},"2023-07-14","To access 'global' Kafka Streams Cassandra State Stores via 'Interactive Queries' there's no need for an RPC layer proxying and fanning out requests to all instances of your streams application.","5min",{"series":5089},{"id":4376,"title":5090,"part":272},"Interactive Queries for `globalKeyValueStore`","\u002Fassets\u002Fblog\u002F9.interactive-queries-with-kafka-streams-cassandra-state-store\u002FKafka-Streams_Cassandra-State-Store_Interactive-Queries_v1_og.jpeg",{"title":4031,"description":5086},"blog\u002F9.interactive-queries-with-kafka-streams-cassandra-state-store",[1016,1017,3959,265],"Interactive Queries with 'kafka-streams-cassandra-state-store' (Part 1) `globalKeyValueStore`","EjL9zG2mju6HFl1B_RHhDCKkIUTf6zsRGyK6xzTTWzw",{"id":5098,"title":5099,"active":6,"body":5100,"date":6395,"description":6396,"duration":6397,"extension":189,"level":4373,"meta":6398,"navigation":6,"ogDescription":190,"ogImage":6399,"ogImageAlt":190,"ogTitle":5099,"path":804,"pinned":190,"seo":6400,"stem":6401,"tags":6402,"titleAlt":190,"titlePage":190,"__hash__":6403},"blog\u002Fblog\u002F7.introducing-kafka-streams-cassandra-state-store.md","Introducing 'kafka-streams-cassandra-state-store'",{"type":8,"value":5101,"toc":6358},[5102,5110,5117,5126,5146,5153,5159,5175,5191,5193,5201,5204,5212,5217,5225,5229,5236,5241,5245,5252,5269,5272,5276,5279,5291,5294,5297,5309,5313,5322,5325,5328,5337,5344,5369,5375,5404,5412,5415,5418,5438,5469,5476,5479,5507,5516,5518,5522,5529,5533,5565,5569,5580,5586,5588,5592,5606,5612,5671,5675,5689,5696,5753,5755,5774,5781,5785,5789,5792,5800,5804,5819,5822,5831,5842,5845,5848,5864,5868,5872,5883,5887,5904,5908,5919,5923,5926,5929,5971,5974,5977,6008,6012,6039,6048,6083,6087,6090,6093,6098,6120,6145,6149,6152,6156,6166,6172,6175,6196,6199,6218,6225,6244,6246,6249,6302,6309,6311,6314,6317,6320,6323,6325,6327,6338,6340,6356],[206,5103,5104,5105,5109],{},"The Java library to be introduced - ",[23,5106,5108],{"href":798,"rel":5107},[27],"thriving-dev\u002Fkafka-streams-cassandra-state-store","  - is a Kafka Streams State Store implementation that persists data to Apache Cassandra.",[206,5111,5112,5113,4473,5115,347],{},"It's a 'drop-in' replacement for the official Kafka Streams state store solutions, notably ",[151,5114,4472],{},[151,5116,4476],{},[206,5118,5119],{},[209,5120],{"alt":5099,"className":5121,"dataZoomSrc":5122,"height":5123,"preload":183,"src":5124,"width":5125},[482,1273],"\u002Fassets\u002Fblog\u002F7.introducing-kafka-streams-cassandra-state-store\u002FIntroducing_kafka-streams-cassandra-state-store.webp",1473,"\u002Fassets\u002Fblog\u002F7.introducing-kafka-streams-cassandra-state-store\u002FIntroducing_kafka-streams-cassandra-state-store.png",3457,[206,5127,5128,5129,5132,5133,5136,5137,5142,5143,347],{},"By moving the state to an ",[151,5130,5131],{},"external"," datastore the ",[55,5134,5135],{},"stateful streams app"," (from a deployment point of view) ",[55,5138,5139,5140],{},"effectively becomes ",[151,5141,231],{}," - which greatly ",[55,5144,5145],{},"improves elasticity, reduces rebalancing downtimes & failure recovery",[206,5147,5148,5149,5152],{},"Cassandra\u002FScyllaDB is horizontally scalable and allows for ",[55,5150,5151],{},"huge amounts of data"," which provides a boost to your existing Kafka Streams application with very little change to your existing source code.",[206,5154,5155,5156,5158],{},"In addition to the ",[137,5157,4060],{}," this post will also cover all out-of-the-box state store solutions, explain individual characteristics, benefits, drawbacks, and limitations in detail.",[206,5160,5161,5164,5165,5167,5168,5170,5171,347],{},[909,5162],{"className":5163,"name":4353},[912,913,914,915]," Following the introduction and getting started guide, there's also a ",[55,5166,4224],{}," available.",[30,5169],{},"\nIf you don't want to wait, feel free to head over to the ",[23,5172,5174],{"href":3088,"rel":5173},[27],"Thriving.dev YouTube Channel",[289,5176,5177],{},[206,5178,5179,5180,5182,5183,5186,5187,4206],{},"The first public release was on 9 January 2023.",[30,5181],{},"\nWhen writing this blog post the latest version was: ",[137,5184,5185],{},"0.4.0"," - available on ",[23,5188,3036],{"href":5189,"rel":5190},"https:\u002F\u002Fcentral.sonatype.com\u002Fartifact\u002Fdev.thriving.oss\u002Fkafka-streams-cassandra-state-store",[27],[276,5192,4453],{"id":4452},[206,5194,5195,5196,5200],{},"(Feel free to skip straight to the ",[23,5197,5199],{"href":5198},"#purpose","next section"," if you're already familiar with Kafka Streams and Apache Cassandra…)",[281,5202,5203],{"id":1017},"Kafka Streams",[206,5205,5206,5207,259],{},"Quoting ",[23,5208,5211],{"href":5209,"rel":5210},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FApache_Kafka#Streams_API",[27],"Apache Kafka - Wikipedia",[289,5213,5214],{},[206,5215,5216],{},"“Kafka Streams (or Streams API) is a stream-processing library written in Java. It was added in the Kafka 0.10.0.0 release. The library allows for the development of stateful stream-processing applications that are scalable, elastic, and fully fault-tolerant. The main API is a stream-processing  domain-specific language  (DSL) that offers high-level operators like filter,  map, grouping, windowing, aggregation, joins, and the notion of tables. Additionally, the Processor API can be used to implement custom operators for a more low-level development approach. The DSL and Processor API can be mixed, too. For stateful stream processing, Kafka Streams uses  RocksDB  to maintain local operator state. Because RocksDB can write to disk, the maintained state can be larger than available main memory. For fault-tolerance, all updates to local state stores are also written into a topic in the Kafka cluster. This allows recreating state by reading those topics and feed all data into RocksDB.”",[206,5218,5219,5220],{},"In case you are entirely new to Kafka Streams, I recommend to get started with reading some official materials provided by Confluent, e.g. ",[23,5221,5224],{"href":5222,"rel":5223},"https:\u002F\u002Fdocs.confluent.io\u002Fplatform\u002Fcurrent\u002Fstreams\u002Fintroduction.html",[27],"Introduction Kafka Streams API",[281,5226,5228],{"id":5227},"apache-cassandra","Apache Cassandra",[206,5230,5206,5231,259],{},[23,5232,5235],{"href":5233,"rel":5234},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FApache_Cassandra",[27],"Apache Cassandra - Wikipedia",[289,5237,5238],{},[206,5239,5240],{},"“Apache Cassandra is a free and open-source, distributed, wide-column store, NoSQL database management system designed to handle large amounts of data across many commodity servers, providing high availability with no single point of failure. Cassandra offers support for clusters spanning multiple datacenters, with asynchronous masterless replication allowing low latency operations for all clients.”",[276,5242,5244],{"id":5243},"purpose","Purpose",[206,5246,5247,5248,5251],{},"While Wikipedia’s summary (see above) only mentions RocksDB, Kafka Streams ships with following ",[137,5249,5250],{},"KeyValueStore"," implementations:",[16,5253,5254,5259,5264],{},[19,5255,5256],{},[137,5257,5258],{},"org.apache.kafka.streams.state.internals.RocksDBStore",[19,5260,5261],{},[137,5262,5263],{},"org.apache.kafka.streams.state.internals.InMemoryKeyValueStore",[19,5265,5266],{},[137,5267,5268],{},"org.apache.kafka.streams.state.internals.MemoryLRUCache",[206,5270,5271],{},"Let’s look at the traits of each store implementation in more detail…",[281,5273,5275],{"id":5274},"rocksdbstore","RocksDBStore",[206,5277,5278],{},"RocksDB is the default state store for Kafka Streams.",[206,5280,1317,5281,856,5283,5285,5286,5290],{},[137,5282,5275],{},[151,5284,3630],{}," key-value store based on ",[23,5287,4472],{"href":5288,"rel":5289},"https:\u002F\u002Frocksdb.org\u002F",[27]," (surprise!). State is flushed to disk, allowing the state to exceed the size of available memory.",[206,5292,5293],{},"Since the state is persisted to disk, it can be re-used and does not need to be restored (changelog topic replay) when the application instance comes up after a restart (e.g. following an upgrade, instance migration, or failure).",[206,5295,5296],{},"The RocksDB state store provides good performance and is well configured out of the box, but might need to be tuned for certain use cases (which is no small feat and requires an understanding of RocksDB configuration). Writing to and reading from disk comes with I\u002FO, for performance reasons buffering and caching patterns are in place. The record cache (on heap) is particularly useful for optimising writes by reducing the number of updates to local state and changelog topics. The RocksDB block cache (off heap) optimises reads.",[762,5298,5299],{},[206,5300,5301,5302,5305,5306,347],{},"In a typical modern setup stateful Kafka Streams applications run on Kubernetes as a ",[151,5303,5304],{},"StatefulSet"," with persistent state stores (RocksDB) on ",[151,5307,5308],{},"PersistentVolumes",[281,5310,5312],{"id":5311},"inmemorykeyvaluestore","InMemoryKeyValueStore",[206,5314,1317,5315,5317,5318,5321],{},[137,5316,5312],{},", as the name suggests, maintains state ",[151,5319,5320],{},"in-memory"," (RAM).",[206,5323,5324],{},"One obvious benefit is that the pure in-memory stores come with good performance (operates in RAM…). Further, hosting and operating are simpler compared to RocksDB, since there is no requirement to provide and manage disks.",[206,5326,5327],{},"Drawbacks to having the store in-memory are limitations in store size and increased infrastructure costs (RAM is more expensive than disk storage). Further, state always is lost on application restart and therefore first needs to be restored from changelog topics (recovery takes longer).",[241,5329,5330],{},[206,5331,5332,5333,5336],{},"When low rebalance downtimes \u002F quick recovery is concerned, using standby replicas (",[137,5334,5335],{},"num.standby.replicas",") help to reduce recovery time.",[281,5338,5340,5341,340],{"id":5339},"memorylrucache-storeslrumap","MemoryLRUCache (",[137,5342,5343],{},"Stores.lruMap",[206,5345,1317,5346,5349,5350,5352,5353,5356,5357,5360,5361,5364,5365,5368],{},[137,5347,5348],{},"MemoryLRUCache"," is an ",[151,5351,5320],{}," store based on ",[151,5354,5355],{},"HashMap",". The term ",[151,5358,5359],{},"cache"," comes from the ",[151,5362,5363],{},"LRU"," (least recently used) behaviour combined with the ",[151,5366,5367],{},"maxCacheSize"," cap (per streams task!).",[206,5370,5371,5372,5374],{},"It’s a rather uncommon choice but can be a valid fit for certain use cases. Same as the ",[151,5373,5312],{}," state always is lost on application restart and is restored from changelog topics.",[762,5376,5378,5393],{"title":5377},"Note",[206,5379,5380,5382,5383,5386,5387,5390,5391,347],{},[137,5381,5367],{}," applies client-side only (in-memory HashMap, per streams task state store -> the least recently used entry is dropped when the underlying HashMap’s capacity is breached) but does not ‘cleanup’ the changelog topic (send ",[151,5384,5385],{},"tombstones","). The (",[151,5388,5389],{},"compacted",") changelog topic keeps growing in size while the state available to processing is constrained by ",[151,5392,5367],{},[206,5394,5395,5396,5399,5400,5403],{},"Therefore, it is recommended to use in combination with custom changelog topic config ",[137,5397,5398],{},"cleanup.policy=[compact,delete]"," (also ",[137,5401,5402],{},"retention.ms",") to have a time-based retention in place that satisfies your functional data requirements (if possible).",[667,5405,5407],{"title":5406},"Reminder",[206,5408,1317,5409,5411],{},[151,5410,5367],{}," is applied per streams task (~input topic partitions), so take into consideration when calculating total capacity, memory requirements per app instance, …",[281,5413,4060],{"id":5414},"cassandrakeyvaluestore",[206,5416,5417],{},"Now finally we get to the subject of this blog post, the custom implementation of a state store that persists data to Apache Cassandra.",[206,5419,5420,5421,5423,5424,5427,5428,5433,5434,5437],{},"With ",[137,5422,4060],{}," data is ",[55,5425,5426],{},"persistently stored"," in an external database -> Apache Cassandra \u003C- *or compatible solutions (e.g. ",[23,5429,5432],{"href":5430,"rel":5431},"https:\u002F\u002Fwww.scylladb.com\u002F",[27],"ScyllaDB","). Apache Cassandra is a distributed, clustered data store that allows to scale horizontally to enable up to Petabytes of data, thus ",[55,5435,5436],{},"very large Kafka Streams state"," can be accommodated.",[206,5439,5440,5441,5443,5444,5448,5449,5454,5455,5458,5459,5462,5463,1366,5466,347],{},"Moving the state into an ",[151,5442,5131],{}," data store - outside the application so to say - allows you to effectively run the app in a ",[55,5445,5446],{},[151,5447,231],{}," fashion. Further, with ",[23,5450,5453],{"href":5451,"rel":5452},"https:\u002F\u002Fkafka.apache.org\u002F34\u002Fjavadoc\u002Forg\u002Fapache\u002Fkafka\u002Fstreams\u002Fstate\u002FStoreBuilder.html#withLoggingDisabled()",[27],"logging disabled",", there's ",[55,5456,5457],{},"no changelog topic"," -> ",[55,5460,5461],{},"no state restore"," required which enables fluent rebalancing and helps ",[55,5464,5465],{},"reduce rebalance downtimes",[55,5467,5468],{},"recovery time",[206,5470,5471,5472,5475],{},"This greatly ",[55,5473,5474],{},"improves the elasticity and scalability"," of your application, which opens up more possibilities such as e.g. efficient & fluent autoscaling...",[206,5477,5478],{},"It can also help ease\u002Favoid known problems with the 'Kafka Streams'-specific task assignment such as 'uneven load distribution' and 'idle consumers' (I'm thinking about writing a separate blog post on these issues...).",[241,5480,5481,5490,5498,5504],{},[206,5482,5483,5484,5486,5487,5489],{},"Kafka Streams property ",[137,5485,749],{}," allows to achieve low rebalance downtimes by telling the consumers to send a ",[137,5488,353],{}," request to the group leader on graceful shutdown.",[206,5491,5492,5493,347],{},"For more information on such kafka internals I can recommend to watch+read following Confluent developer guide: ",[23,5494,5497],{"href":5495,"rel":5496},"https:\u002F\u002Fdeveloper.confluent.io\u002Flearn-kafka\u002Farchitecture\u002Fconsumer-group-protocol\u002F",[27],"Consumer Group Protocol",[206,5499,5500,5501,347],{},"Note that this property is also used & explained in the ",[23,5502,4224],{"href":5503},"#demo",[206,5505,5506],{},"⚠ Please be aware this is an in-official property (not part of the public API), thus can be deprecated or dropped any time.",[667,5508,5509],{},[206,5510,5511,5512,5515],{},"Adding an external, 3rd party Software to the heart (or rather stomach?) to your stream processing application, adds a new, additional ",[55,5513,5514],{},"single point of failure"," to your architecture.",[276,5517,3685],{"id":3684},[281,5519,5521],{"id":5520},"get-it","Get it!",[206,5523,5524,5525,259],{},"The artifact is available on ",[23,5526,3036],{"href":5527,"rel":5528},"https:\u002F\u002Fcentral.sonatype.com\u002Fartifact\u002Fdev.thriving.oss\u002Fkafka-streams-cassandra-state-store\u002F",[27],[11,5530,5532],{"id":5531},"maven","Maven",[261,5534,5538],{"className":5535,"code":5536,"language":5537,"meta":183,"style":183},"language-xml shiki shiki-themes github-light github-dark","\u003Cdependency>\n    \u003CgroupId>dev.thriving.oss\u003C\u002FgroupId>\n    \u003CartifactId>kafka-streams-cassandra-state-store\u003C\u002FartifactId>\n    \u003Cversion>${version}\u003C\u002Fversion>\n\u003C\u002Fdependency>\n","xml",[137,5539,5540,5545,5550,5555,5560],{"__ignoreMap":183},[269,5541,5542],{"class":271,"line":272},[269,5543,5544],{},"\u003Cdependency>\n",[269,5546,5547],{"class":271,"line":184},[269,5548,5549],{},"    \u003CgroupId>dev.thriving.oss\u003C\u002FgroupId>\n",[269,5551,5552],{"class":271,"line":991},[269,5553,5554],{},"    \u003CartifactId>kafka-streams-cassandra-state-store\u003C\u002FartifactId>\n",[269,5556,5557],{"class":271,"line":1613},[269,5558,5559],{},"    \u003Cversion>${version}\u003C\u002Fversion>\n",[269,5561,5562],{"class":271,"line":1626},[269,5563,5564],{},"\u003C\u002Fdependency>\n",[11,5566,5568],{"id":5567},"gradle-groovy-dsl","Gradle (Groovy DSL)",[261,5570,5574],{"className":5571,"code":5572,"language":5573,"meta":183,"style":183},"language-groovy shiki shiki-themes github-light github-dark","implementation 'dev.thriving.oss:kafka-streams-cassandra-state-store:${version}’\n","groovy",[137,5575,5576],{"__ignoreMap":183},[269,5577,5578],{"class":271,"line":272},[269,5579,5572],{},[206,5581,5582,5583,347],{},"Classes of this library are in the package ",[137,5584,5585],{},"dev.thriving.oss.kafka.streams.cassandra.state.store",[281,5587,3101],{"id":3100},[11,5589,5591],{"id":5590},"high-level-dsl-storesupplier","High-level DSL \u003C> StoreSupplier",[206,5593,5594,5595,5598,5599,5602,5603,347],{},"When using the high-level DSL, i.e., ",[137,5596,5597],{},"StreamsBuilder",", users create ",[137,5600,5601],{},"StoreSupplier","s that can be further customized via ",[137,5604,5605],{},"Materialized",[206,5607,5608,5609,5611],{},"For example, a topic read as ",[137,5610,3691],{}," can be materialized into a Cassandra k\u002Fv store with custom key\u002Fvalue Serdes, with logging and caching disabled:",[261,5613,5615],{"className":263,"code":5614,"language":265,"meta":183,"style":183},"StreamsBuilder builder = new StreamsBuilder();\nKTable\u003CLong,String> table = builder.table(\n  \"topicName\",\n  Materialized.\u003CLong,String>as(\n                 CassandraStores.builder(session, \"store-name\")\n                         .keyValueStore()\n              )\n              .withKeySerde(Serdes.Long())\n              .withValueSerde(Serdes.String())\n              .withLoggingDisabled()\n              .withCachingDisabled());\n",[137,5616,5617,5621,5626,5631,5636,5641,5646,5651,5656,5661,5666],{"__ignoreMap":183},[269,5618,5619],{"class":271,"line":272},[269,5620,3706],{},[269,5622,5623],{"class":271,"line":184},[269,5624,5625],{},"KTable\u003CLong,String> table = builder.table(\n",[269,5627,5628],{"class":271,"line":991},[269,5629,5630],{},"  \"topicName\",\n",[269,5632,5633],{"class":271,"line":1613},[269,5634,5635],{},"  Materialized.\u003CLong,String>as(\n",[269,5637,5638],{"class":271,"line":1626},[269,5639,5640],{},"                 CassandraStores.builder(session, \"store-name\")\n",[269,5642,5643],{"class":271,"line":1639},[269,5644,5645],{},"                         .keyValueStore()\n",[269,5647,5648],{"class":271,"line":1652},[269,5649,5650],{},"              )\n",[269,5652,5653],{"class":271,"line":1665},[269,5654,5655],{},"              .withKeySerde(Serdes.Long())\n",[269,5657,5658],{"class":271,"line":1678},[269,5659,5660],{},"              .withValueSerde(Serdes.String())\n",[269,5662,5663],{"class":271,"line":1699},[269,5664,5665],{},"              .withLoggingDisabled()\n",[269,5667,5668],{"class":271,"line":1707},[269,5669,5670],{},"              .withCachingDisabled());\n",[11,5672,5674],{"id":5673},"processor-api-storebuilder","Processor API \u003C> StoreBuilder",[206,5676,5677,5678,5598,5681,5684,5685,5688],{},"When using the Processor API, i.e., ",[137,5679,5680],{},"Topology",[137,5682,5683],{},"StoreBuilder","s that can be attached to ",[137,5686,5687],{},"Processor","s.",[206,5690,5691,5692,5695],{},"For example, you can create a Cassandra ",[137,5693,5694],{},"KeyValueStore\u003CString, Long>"," with custom Serdes, logging and caching disabled like:",[261,5697,5699],{"className":263,"code":5698,"language":265,"meta":183,"style":183},"Topology topology = new Topology();\n\nStoreBuilder\u003CKeyValueStore\u003CString, Long>> storeBuilder = Stores.keyValueStoreBuilder(\n                CassandraStores.builder(session, \"store-name\")\n                        .keyValueStore(),\n                Serdes.String(),\n                Serdes.Long())\n        .withLoggingDisabled()\n        .withCachingDisabled();\n\ntopology.addStateStore(storeBuilder);\n",[137,5700,5701,5706,5710,5715,5720,5725,5730,5735,5739,5744,5748],{"__ignoreMap":183},[269,5702,5703],{"class":271,"line":272},[269,5704,5705],{},"Topology topology = new Topology();\n",[269,5707,5708],{"class":271,"line":184},[269,5709,2291],{"emptyLinePlaceholder":6},[269,5711,5712],{"class":271,"line":991},[269,5713,5714],{},"StoreBuilder\u003CKeyValueStore\u003CString, Long>> storeBuilder = Stores.keyValueStoreBuilder(\n",[269,5716,5717],{"class":271,"line":1613},[269,5718,5719],{},"                CassandraStores.builder(session, \"store-name\")\n",[269,5721,5722],{"class":271,"line":1626},[269,5723,5724],{},"                        .keyValueStore(),\n",[269,5726,5727],{"class":271,"line":1639},[269,5728,5729],{},"                Serdes.String(),\n",[269,5731,5732],{"class":271,"line":1652},[269,5733,5734],{},"                Serdes.Long())\n",[269,5736,5737],{"class":271,"line":1665},[269,5738,3760],{},[269,5740,5741],{"class":271,"line":1678},[269,5742,5743],{},"        .withCachingDisabled();\n",[269,5745,5746],{"class":271,"line":1699},[269,5747,2291],{"emptyLinePlaceholder":6},[269,5749,5750],{"class":271,"line":1707},[269,5751,5752],{},"topology.addStateStore(storeBuilder);\n",[281,5754,4225],{"id":4224},[206,5756,5757,5758,5761,5762,5766,5767,5770,5771,347],{},"Features the notorious ",[55,5759,5760],{},"'word-count example'"," (",[23,5763,2724],{"href":5764,"rel":5765},"https:\u002F\u002Fdocs.confluent.io\u002Fplatform\u002F6.2\u002Fstreams\u002Fquickstart.html",[27],"), written as a ",[55,5768,5769],{},"quarkus application",", running in a fully ",[55,5772,5773],{},"clustered docker-compose localstack",[206,5775,4228,5776],{},[23,5777,5780],{"href":5778,"rel":5779},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fkafka-streams-cassandra-state-store\u002Ftree\u002F0.4.0\u002Fexamples\u002Fword-count-quarkus",[27],"kafka-streams-cassandra-state-store\u002Fexamples\u002Fword-count-quarkus (at 0.4.0)",[4233,5782],{":webp":491,"id":5783,"poster":4236,"title":5784},"2Co9-8E-uJE","Introduction & Demo of 'kafka-streams-cassandra-state-store'",[281,5786,5788],{"id":5787},"store-types","Store Types",[206,5790,5791],{},"kafka-streams-cassandra-state-store comes with 2 different store types:",[16,5793,5794,5797],{},[19,5795,5796],{},"keyValueStore",[19,5798,5799],{},"globalKeyValueStore",[11,5801,5803],{"id":5802},"keyvaluestore-recommended-default","keyValueStore (recommended default)",[206,5805,5806,5807,5810,5811,5814,5815,5818],{},"A persistent ",[137,5808,5809],{},"KeyValueStore\u003CBytes, byte[]>",".\nThe underlying cassandra table is ",[55,5812,5813],{},"partitioned by"," the store context ",[55,5816,5817],{},"task partition",".\nTherefore, all CRUD operations against this store always query by and return results for a single stream task.",[11,5820,5799],{"id":5821},"globalkeyvaluestore",[206,5823,5806,5824,5826,5827,5830],{},[137,5825,5809],{},".\nThe underlying cassandra table uses the ",[55,5828,5829],{},"record key as sole \u002FPRIMARY KEY\u002F",".\nTherefore, all CRUD operations against this store work from any streams task and therefore always are “global”.\nDue to the nature of cassandra tables having a single PK (no clustering key), this store supports only a limited number of operations.",[667,5832,5834],{"title":5833},"Attention",[206,5835,5836,5837,5841],{},"If you're planning to use this store type, please make sure to get a full understanding of the specifics by reading the ",[23,5838,5840],{"href":4798,"rel":5839},[27],"relevant docs"," to understand its behaviour.",[281,5843,5844],{"id":4373},"Advanced",[206,5846,5847],{},"For more detailed documentation, please visit the GitHub project…",[16,5849,5850,5857],{},[19,5851,5852],{},[23,5853,5856],{"href":5854,"rel":5855},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fkafka-streams-cassandra-state-store#store-types",[27],"Store types, supported operations",[19,5858,5859],{},[23,5860,5863],{"href":5861,"rel":5862},"https:\u002F\u002Fgithub.com\u002Fthriving-dev\u002Fkafka-streams-cassandra-state-store#builder",[27],"Builder usage + config options",[276,5865,5867],{"id":5866},"under-the-hood","Under the hood",[281,5869,5871],{"id":5870},"implementedcompiled-with","Implemented\u002Fcompiled with",[16,5873,5874,5877,5880],{},[19,5875,5876],{},"Java 17",[19,5878,5879],{},"kafka-streams 3.4",[19,5881,5882],{},"datastax java-driver-core 4.15.0",[281,5884,5886],{"id":5885},"supported-client-libs","Supported client-libs",[16,5888,5889,5892,5898],{},[19,5890,5891],{},"Kafka Streams 2.7.0+ (maybe even earlier versions, but wasn’t tested further back)",[19,5893,5894,5895],{},"Datastax java client (v4) ",[137,5896,5897],{},"'com.datastax.oss:java-driver-core:4.15.0'",[19,5899,5900,5901],{},"ScyllaDB shard-aware datastax java client (v4) fork ",[137,5902,5903],{},"'com.scylladb:java-driver-core:4.14.1.0'",[281,5905,5907],{"id":5906},"supported-databases","Supported databases",[16,5909,5910,5913,5916],{},[19,5911,5912],{},"Apache Cassandra 3.11",[19,5914,5915],{},"Apache Cassandra 4.0, 4.1",[19,5917,5918],{},"ScyllaDB (should work from 4.3+)",[281,5920,5922],{"id":5921},"underlying-cql-schema","Underlying CQL Schema",[11,5924,5796],{"id":5925},"keyvaluestore",[206,5927,5928],{},"Using defaults, for a state store named \"word-count\" the following CQL Schema applies:",[261,5930,5934],{"className":5931,"code":5932,"language":5933,"meta":183,"style":183},"language-sql shiki shiki-themes github-light github-dark","CREATE TABLE IF NOT EXISTS word_count_kstreams_store (\n    partition int,\n    key blob,\n    time timestamp,\n    value blob,\n    PRIMARY KEY ((partition), key)\n) WITH compaction = { 'class' : 'LeveledCompactionStrategy' }\n","sql",[137,5935,5936,5941,5946,5951,5956,5961,5966],{"__ignoreMap":183},[269,5937,5938],{"class":271,"line":272},[269,5939,5940],{},"CREATE TABLE IF NOT EXISTS word_count_kstreams_store (\n",[269,5942,5943],{"class":271,"line":184},[269,5944,5945],{},"    partition int,\n",[269,5947,5948],{"class":271,"line":991},[269,5949,5950],{},"    key blob,\n",[269,5952,5953],{"class":271,"line":1613},[269,5954,5955],{},"    time timestamp,\n",[269,5957,5958],{"class":271,"line":1626},[269,5959,5960],{},"    value blob,\n",[269,5962,5963],{"class":271,"line":1639},[269,5964,5965],{},"    PRIMARY KEY ((partition), key)\n",[269,5967,5968],{"class":271,"line":1652},[269,5969,5970],{},") WITH compaction = { 'class' : 'LeveledCompactionStrategy' }\n",[11,5972,5799],{"id":5973},"globalkeyvaluestore-1",[206,5975,5976],{},"Using defaults, for a state store named \"clicks-global\" the following CQL Schema applies:",[261,5978,5980],{"className":5931,"code":5979,"language":5933,"meta":183,"style":183},"CREATE TABLE IF NOT EXISTS clicks_global_kstreams_store (\n    key blob,\n    time timestamp,\n    value blob,\n    PRIMARY KEY (key)\n) WITH compaction = { 'class' : 'LeveledCompactionStrategy' }\n",[137,5981,5982,5987,5991,5995,5999,6004],{"__ignoreMap":183},[269,5983,5984],{"class":271,"line":272},[269,5985,5986],{},"CREATE TABLE IF NOT EXISTS clicks_global_kstreams_store (\n",[269,5988,5989],{"class":271,"line":184},[269,5990,5950],{},[269,5992,5993],{"class":271,"line":991},[269,5994,5955],{},[269,5996,5997],{"class":271,"line":1613},[269,5998,5960],{},[269,6000,6001],{"class":271,"line":1626},[269,6002,6003],{},"    PRIMARY KEY (key)\n",[269,6005,6006],{"class":271,"line":1639},[269,6007,5970],{},[281,6009,6011],{"id":6010},"feat-cassandra-table-with-default-ttl","Feat: Cassandra table with default TTL",[241,6013,6015,6022],{"title":6014},"Pro Tip",[206,6016,6017,6018,6021],{},"Cassandra has a table option ",[137,6019,6020],{},"default_time_to_live"," (default expiration time (“TTL”) in seconds for a table) which can be useful for certain use cases where data (state) expires after a known period.",[206,6023,6024,6027,6028,6031,6032,6035,6036,784],{},[55,6025,6026],{},"Please note"," writes to Cassandra are made with system time. The table ",[55,6029,6030],{},"TTL is applied"," based on ",[55,6033,6034],{},"'time of write'"," -> the time of the current record being processed (",[151,6037,6038],{},"!= stream time",[206,6040,1317,6041,6043,6044,6047],{},[137,6042,6020],{}," can be defined via the builder ",[137,6045,6046],{},"withTableOptions"," method, e.g.:",[261,6049,6051],{"className":263,"code":6050,"language":265,"meta":183,"style":183},"CassandraStores.builder(session, \"word-grouped-count\")\n        .withTableOptions(\"\"\"\n                compaction = { 'class' : 'LeveledCompactionStrategy' }\n                AND default_time_to_live = 86400\n                \"\"\")\n        .keyValueStore()\n",[137,6052,6053,6058,6063,6068,6073,6078],{"__ignoreMap":183},[269,6054,6055],{"class":271,"line":272},[269,6056,6057],{},"CassandraStores.builder(session, \"word-grouped-count\")\n",[269,6059,6060],{"class":271,"line":184},[269,6061,6062],{},"        .withTableOptions(\"\"\"\n",[269,6064,6065],{"class":271,"line":991},[269,6066,6067],{},"                compaction = { 'class' : 'LeveledCompactionStrategy' }\n",[269,6069,6070],{"class":271,"line":1613},[269,6071,6072],{},"                AND default_time_to_live = 86400\n",[269,6074,6075],{"class":271,"line":1626},[269,6076,6077],{},"                \"\"\")\n",[269,6079,6080],{"class":271,"line":1639},[269,6081,6082],{},"        .keyValueStore()\n",[281,6084,6086],{"id":6085},"cassandra-table-partitioning-avoiding-large-partitions","Cassandra table partitioning (avoiding large partitions)",[206,6088,6089],{},"Kafka is persisting data in segments and is built for sequential r\u002Fw. As long as there’s sufficient disk storage space available to brokers, a high number of messages for a single topic partition is not a problem.",[206,6091,6092],{},"Apache Cassandra on the other hand can get inefficient (up to severe failures such as load shedding, dropped messages, and crashed and downed nodes) when the partition size grows too large.\nThe reason is that searching becomes too slow as the search within a partition is slow. Also, it puts a lot of pressure on the (JVM) heap.",[667,6094,6095],{},[206,6096,6097],{},"The community has offered a standard recommendation for Cassandra users to keep Partitions under 400MB, and preferably under 100MB.",[206,6099,6100,6101,6104,6105,6109,6110,6112,6113,6115,6116,6119],{},"For the current implementation, the Cassandra table created for the ‘default’ key-value store is partitioned by the Kafka ",[151,6102,6103],{},"partition key"," (“wide partition pattern”).\nPlease keep these issues in mind when working with relevant data volumes.\nIn case you don’t need to query your store \u002F only lookup by key (‘range’, ‘prefixScan’; ref ",[23,6106,6108],{"href":6107},"#supported-operations-by-store-type","Supported operations by store type",") it’s recommended to use ",[137,6111,5799],{}," rather than ",[137,6114,5796],{}," since it is partitioned by the ",[151,6117,6118],{},"event key"," (:= primary key).",[762,6121,6122],{"title":3834},[16,6123,6124,6138],{},[19,6125,6126,6127,6132,6134,6137],{},"blog post on ",[23,6128,6131],{"href":6129,"rel":6130},"https:\u002F\u002Fthelastpickle.com\u002Fblog\u002F2019\u002F01\u002F11\u002Fwide-partitions-cassandra-3-11.html",[27],"Wide Partitions in Apache Cassandra 3.11",[30,6133],{},[55,6135,6136],{},"Note:"," in case anyone has funded knowledge if\u002Fhow this has changed with Cassandra 4, please share in the comments below!!",[19,6139,6140],{},[23,6141,6144],{"href":6142,"rel":6143},"https:\u002F\u002Fstackoverflow.com\u002Fquestions\u002F68237371\u002Fwide-partition-pattern-in-cassandra",[27],"stackoverflow question",[276,6146,6148],{"id":6147},"known-limitations","Known Limitations",[206,6150,6151],{},"Adding additional infrastructure for data persistence external to Kafka comes with certain risks and constraints.",[281,6153,6155],{"id":6154},"consistency","Consistency",[206,6157,6158,6159,1366,6162,6165],{},"Kafka Streams supports ",[151,6160,6161],{},"at-least-once",[151,6163,6164],{},"exactly-once"," processing guarantees. At-least-once semantics is enabled by default.",[206,6167,6168,6169,6171],{},"Kafka Streams ",[151,6170,6164],{}," processing guarantees is using Kafka transactions. These transactions wrap the entirety of processing a message throughout your streams topology, including messages published to outbound topic(s), changelog topic(s), and consumer offsets topic(s).",[206,6173,6174],{},"This is possible through transactional interaction with a single distributed system (Apache Kafka). Bringing an external system (Cassandra) into play breaks this pattern. Once data is written to the database it can’t be rolled back in the event of a subsequent error \u002F failure to complete the current message processing.",[667,6176,6177,6190],{},[206,6178,6179,6180,6182,6183,6186,6187,6189],{},"=> If you need strong consistency, have ",[151,6181,6164],{}," processing enabled (streams config: ",[137,6184,6185],{},"processing.guarantee=\"exactly_once_v2\"","), and\u002For your processing logic is not fully idempotent then using ",[55,6188,800],{}," is discouraged!",[206,6191,6192,6193,6195],{},"ℹ️ Please note this is also the case when using kafka-streams with the native state stores (RocksDB\u002FInMemory) with ",[151,6194,6161],{}," processing.guarantee (default).",[206,6197,6198],{},"For more information on Kafka Streams processing guarantees, check the sources referenced below.",[16,6200,6201,6206,6212],{},[19,6202,6203],{},[23,6204,79],{"href":79,"rel":6205},[27],[19,6207,6208],{},[23,6209,6210],{"href":6210,"rel":6211},"https:\u002F\u002Fdocs.confluent.io\u002Fplatform\u002Fcurrent\u002Fstreams\u002Fdeveloper-guide\u002Fconfig-streams.html#processing-guarantee",[27],[19,6213,6214],{},[23,6215,6216],{"href":6216,"rel":6217},"https:\u002F\u002Fdocs.confluent.io\u002Fplatform\u002Fcurrent\u002Fstreams\u002Fconcepts.html#processing-guarantees",[27],[281,6219,6221,6222],{"id":6220},"incomplete-implementation-of-interfaces-statestore","Incomplete Implementation of Interfaces ",[137,6223,6224],{},"StateStore",[206,6226,6227,6228,6230,6231,6234,6235,6238,6239,6243],{},"For now, only ",[137,6229,5250],{}," is supported (vs. e.g. ",[137,6232,6233],{},"WindowStore","\u002F",[137,6236,6237],{},"SessionStore",").\nAlso, not all methods have been implemented. Please check ",[23,6240,6242],{"href":6241},"\u002F#store-types\u002F","store types method support table"," above for more details.",[276,6245,3801],{"id":3800},[206,6247,6248],{},"Here are some of the tasks (high level) in the current backlog:",[16,6250,6251,6281,6291],{},[19,6252,6253,6254],{},"Features\n",[16,6255,6256,6264,6273],{},[19,6257,6258,6259,6263],{},"Implement ",[23,6260,6262],{"href":3571,"rel":6261},[27],"KIP-889: Versioned State Stores"," (coming soon with Kafka 3.5.0 release)",[19,6265,6266,6267,6272],{},"Add a simple (optional) InMemory read cache -> ",[23,6268,6271],{"href":6269,"rel":6270},"https:\u002F\u002Fgithub.com\u002Fben-manes\u002Fcaffeine",[27],"Caffeine","?",[19,6274,6275,6276,6278,6279],{},"Support ",[137,6277,6233],{}," \u002F ",[137,6280,6237],{},[19,6282,6283,6284],{},"Non-functional\n",[16,6285,6286,6288],{},[19,6287,3809],{},[19,6289,6290],{},"Add metrics",[19,6292,6293,6294],{},"Ops\n",[16,6295,6296,6299],{},[19,6297,6298],{},"GitHub actions to release + publish to maven central (snapshot \u002F releases)",[19,6300,6301],{},"Add Renovate",[206,6303,6304,6305,4206],{},"Interested to contribute? Please ",[23,6306,6308],{"href":6307},"\u002Fabout","reach out",[276,6310,825],{"id":824},[206,6312,6313],{},"It's been a fun journey so far, starting from an initial POC to a working library published to maven central - though still to be considered 'experimental', since it's not been production-tested yet.",[206,6315,6316],{},"The out-of-the-box state stores satisfy most requirements, no need to switch without necessity.\nStill, it's a usable piece of software that may fill a gap for specific requirements.",[206,6318,6319],{},"I'm looking forward to working on next steps such as benchmarking \u002F load testing.",[206,6321,6322],{},"Feedback is very welcome, also, if you are planning to, or have decided to use the library in a project, please leave a comment below.",[276,6324,862],{"id":783},[206,6326,3822],{},[16,6328,6329,6332,6335],{},[19,6330,6331],{},"Kafka \u002F Streams API: 3.4.0",[19,6333,6334],{},"Cassandra java-driver-core: 4.15.0",[19,6336,6337],{},"kafka-streams-cassandra-state-store: 0.4.0",[276,6339,3834],{"id":3833},[16,6341,6342,6349],{},[19,6343,6344],{},[23,6345,6348],{"href":6346,"rel":6347},"https:\u002F\u002Fwww.oreilly.com\u002Flibrary\u002Fview\u002Fmastering-kafka-streams\u002F9781492062486\u002Fch04.html",[27],"4. Stateful Processing - Mastering Kafka Streams and ksqlDB Book",[19,6350,6351],{},[23,6352,6355],{"href":6353,"rel":6354},"https:\u002F\u002Fdocs.confluent.io\u002Fplatform\u002Fcurrent\u002Fstreams\u002Fdeveloper-guide\u002Fmemory-mgmt.html",[27],"Kafka Streams Memory Management | Confluent Documentation",[983,6357,985],{},{"title":183,"searchDepth":184,"depth":184,"links":6359},[6360,6364,6371,6378,6386,6391,6392,6393,6394],{"id":4452,"depth":184,"text":4453,"children":6361},[6362,6363],{"id":1017,"depth":991,"text":5203},{"id":5227,"depth":991,"text":5228},{"id":5243,"depth":184,"text":5244,"children":6365},[6366,6367,6368,6370],{"id":5274,"depth":991,"text":5275},{"id":5311,"depth":991,"text":5312},{"id":5339,"depth":991,"text":6369},"MemoryLRUCache (Stores.lruMap)",{"id":5414,"depth":991,"text":4060},{"id":3684,"depth":184,"text":3685,"children":6372},[6373,6374,6375,6376,6377],{"id":5520,"depth":991,"text":5521},{"id":3100,"depth":991,"text":3101},{"id":4224,"depth":991,"text":4225},{"id":5787,"depth":991,"text":5788},{"id":4373,"depth":991,"text":5844},{"id":5866,"depth":184,"text":5867,"children":6379},[6380,6381,6382,6383,6384,6385],{"id":5870,"depth":991,"text":5871},{"id":5885,"depth":991,"text":5886},{"id":5906,"depth":991,"text":5907},{"id":5921,"depth":991,"text":5922},{"id":6010,"depth":991,"text":6011},{"id":6085,"depth":991,"text":6086},{"id":6147,"depth":184,"text":6148,"children":6387},[6388,6389],{"id":6154,"depth":991,"text":6155},{"id":6220,"depth":991,"text":6390},"Incomplete Implementation of Interfaces StateStore",{"id":3800,"depth":184,"text":3801},{"id":824,"depth":184,"text":825},{"id":783,"depth":184,"text":862},{"id":3833,"depth":184,"text":3834},"2023-05-30","'Drop-in' Kafka Streams State Store implementation that persists data to Apache Cassandra \u002F ScyllaDB","15min",{},"\u002Fassets\u002Fblog\u002F7.introducing-kafka-streams-cassandra-state-store\u002FIntroducing_kafka-streams-cassandra-state-store_og.jpg",{"title":5099,"description":6396},"blog\u002F7.introducing-kafka-streams-cassandra-state-store",[1016,1017,3959,265],"5FDvmi3o_Enw-z1oOpUkcKNatHpmHW7ftHJa2xGrG3E",{"id":6405,"title":6406,"active":6,"body":6407,"date":6624,"description":187,"duration":5087,"extension":189,"level":190,"meta":6625,"navigation":6,"ogDescription":190,"ogImage":190,"ogImageAlt":190,"ogTitle":190,"path":6626,"pinned":190,"seo":6627,"stem":6628,"tags":6629,"titleAlt":6630,"titlePage":190,"__hash__":6631},"blog\u002Fblog\u002F10010.activity-report-2023-cw21.md","Activity Report · 2023 CW 21",{"type":8,"value":6408,"toc":6622},[6409,6413,6531,6535,6576,6580,6608,6612],[11,6410,6412],{"id":6411},"what-ive-been-looking-into","What I’ve been looking into",[16,6414,6415,6466,6505],{},[19,6416,6417,6420],{},[55,6418,6419],{},"DevOps \u002F gitops",[16,6421,6422,6454],{},[19,6423,6424,6429,6431,6432,6435,6436,6439,6440,6445,6446,6448,6449],{},[23,6425,6428],{"href":6426,"rel":6427},"https:\u002F\u002Fargoproj.github.io\u002Fargo-workflows\u002F",[27],"Argo Workflows - The workflow engine for Kubernetes",[30,6430],{},"\nFor a client’s requirements on advanced ",[55,6433,6434],{},"blue\u002Fgreen deployments"," I’ve been looking for a workflow solution to work ",[55,6437,6438],{},"in a k8s+gitops context",". A colleage recommended ",[55,6441,6442],{},[151,6443,6444],{},"Argo Workflows"," which looks very promising and fitting my needs at a first glance.",[30,6447],{},"\n=> Get a quick overview on ",[23,6450,6453],{"href":6451,"rel":6452},"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=TZgLkCFQ2tk",[27],"YouTube - Argo Workflows in 5 minutes",[19,6455,6456,6461,6462,6465],{},[23,6457,6460],{"href":6458,"rel":6459},"https:\u002F\u002Factuated.dev\u002F",[27],"Actuated",": 'Fast and secure ",[55,6463,6464],{},"GitHub Actions\non your own infrastructure","'",[19,6467,6468,6471],{},[55,6469,6470],{},"Projects + Tooling around Apache Cassandra",[16,6472,6473,6481,6489,6497],{},[19,6474,6475,6480],{},[23,6476,6479],{"href":6477,"rel":6478},"https:\u002F\u002Fstargate.io\u002F",[27],"Stargate"," is an open source data API gateway",[19,6482,6483,6488],{},[23,6484,6487],{"href":6485,"rel":6486},"https:\u002F\u002Fwww.datastax.com\u002Fproducts\u002Fdatastax-astra",[27],"Datastax Astra DB"," is a fully managed, open, multi-cloud serverless DBaaS built on Apache Cassandra",[19,6490,6491,6496],{},[23,6492,6495],{"href":6493,"rel":6494},"https:\u002F\u002Fk8ssandra.io\u002F",[27],"K8ssandra"," brings together a complete operational data platform for Kubernetes",[19,6498,6499,6504],{},[23,6500,6503],{"href":6501,"rel":6502},"https:\u002F\u002Fgithub.com\u002Fthelastpickle\u002Fcassandra-medusa",[27],"thelastpickle\u002Fcassandra-medusa"," is a cassandra backup & restore tool",[19,6506,6507,6510],{},[55,6508,6509],{},"Libraries",[16,6511,6512],{},[19,6513,6514,6519,6520,5761,6523,6526,6527,6530],{},[23,6515,6518],{"href":6516,"rel":6517},"https:\u002F\u002Flucia-auth.com\u002F",[27],"Lucia Auth"," is a simple and flexible ",[55,6521,6522],{},"user and session management",[151,6524,6525],{},"server side",") ",[55,6528,6529],{},"JavaScript library"," with a wide framework support",[11,6532,6534],{"id":6533},"interesting-postsvideos","Interesting Posts\u002FVideos",[16,6536,6537,6563],{},[19,6538,6539,6540,6545,6546,6549,6550,6553,6554,6557,6558],{},"Informative ",[23,6541,6544],{"href":6542,"rel":6543},"https:\u002F\u002Ftwitter.com\u002FMichaelThiessen\u002Fstatus\u002F1603359219444580354?s=20&t=SihyvlM-zrAPYHj2FVVeBw",[27],"twitter thread"," on different ",[55,6547,6548],{},"states"," to consider when ",[55,6551,6552],{},"building UIs"," (routing, fetching data, data ",[151,6555,6556],{},"edge cases",", ...) by ",[23,6559,6562],{"href":6560,"rel":6561},"https:\u002F\u002Ftwitter.com\u002FMichaelThiessen",[27],"@MichaelThiessen",[19,6564,6565,6570,6571],{},[23,6566,6569],{"href":6567,"rel":6568},"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=FLe5dvqV6xs",[27],"Your Overengineering May Be A Problem"," - 'Continuous Delivery' episode by ",[23,6572,6575],{"href":6573,"rel":6574},"https:\u002F\u002Ftwitter.com\u002Fdavefarley77",[27],"Dave Farley",[11,6577,6579],{"id":6578},"featured-releases","Featured Releases",[16,6581,6582,6594,6601],{},[19,6583,6584,6585,6590,6591],{},"Read the announcement of ",[23,6586,6589],{"href":6587,"rel":6588},"https:\u002F\u002Fflink.apache.org\u002F2023\u002F03\u002F23\u002Fannouncing-the-release-of-apache-flink-1.17\u002F",[27],"Apache Flink 1.17"," release ",[909,6592],{"className":6593,"name":4099},[912,913,914,915],[19,6595,6596],{},[23,6597,6600],{"href":6598,"rel":6599},"https:\u002F\u002Fblog.vuejs.org\u002Fposts\u002Fvue-3-3",[27],"Vue 3.3",[19,6602,6603],{},[23,6604,6607],{"href":6605,"rel":6606},"https:\u002F\u002Fgithub.com\u002Fnuxt\u002Fnuxt\u002Freleases\u002Ftag\u002Fv3.5.0",[27],"Nuxt 3.5.0",[11,6609,6611],{"id":6610},"featured-tool","Featured Tool",[16,6613,6614],{},[19,6615,6616,6621],{},[23,6617,6620],{"href":6618,"rel":6619},"https:\u002F\u002Fjust.maciejwalkowiak.com\u002F",[27],"just"," is a 'Command Line toolkit for developing Spring Boot applications'",{"title":183,"searchDepth":184,"depth":184,"links":6623},[],"2023-05-28",{},"\u002Fblog\u002Factivity-report-2023-cw21",{"title":6406,"description":187},"blog\u002F10010.activity-report-2023-cw21",[196],"2023 Calendar Week #21","6DALpwQdLS0n3peIpOFAwV4rFymrHRfRGuac6N3uRck",{"id":6633,"title":6634,"active":6,"body":6635,"date":6780,"description":187,"duration":1845,"extension":189,"level":190,"meta":6781,"navigation":6,"ogDescription":190,"ogImage":190,"ogImageAlt":190,"ogTitle":190,"path":6782,"pinned":190,"seo":6783,"stem":6784,"tags":6785,"titleAlt":6786,"titlePage":190,"__hash__":6787},"blog\u002Fblog\u002F10009.weekly-digest-2023-cw8-cw9.md","Weekly Digest · 2023 CW 8 + CW 9",{"type":8,"value":6636,"toc":6778},[6637,6640,6644,6683,6687,6721,6724,6764,6766],[206,6638,6639],{},"This bi-weekly is rather short, I've personally been busy with planning and organising my move.",[11,6641,6643],{"id":6642},"future-prospects","Future Prospects",[16,6645,6646,6664],{},[19,6647,6648,6649,6656,6657,6659,6660,6663],{},"Check out this year's ",[23,6650,6653],{"href":6651,"rel":6652},"https:\u002F\u002Fwww.scylladb.com\u002Fpresentations\u002Fto-serverless-and-beyond\u002F",[27],[55,6654,6655],{},"ScyllaDB Summit Keynote"," by Dor Laor.",[30,6658],{},"\nI'm excited on all the new features planned for ScyllaDB, in particular what's lined up for serverless, tablets, S3 storage option (much cheaper for high volume, ",[151,6661,6662],{},"low"," throughput), point-in-time recovery, free disaster recovery backup.",[19,6665,6666,6673,6674,726,6677,4206,6680,6682],{},[23,6667,6670],{"href":6668,"rel":6669},"https:\u002F\u002Fopenjdk.org\u002Fjeps\u002F8303683",[27],[55,6671,6672],{},"This JEP"," proposes to ",[55,6675,6676],{},"finalize Virtual Threads",[55,6678,6679],{},"JDK 21",[30,6681],{},"\n(after 2 rounds of previews & feedback in JDK 19\u002F20)",[11,6684,6686],{"id":6685},"interesting-posts","Interesting Posts",[16,6688,6689],{},[19,6690,6691,6696,6697,6702,6703,6710,6712,232,6716],{},[23,6692,6695],{"href":6693,"rel":6694},"https:\u002F\u002Felk.zone\u002Fm.webtoo.ls\u002F@patak\u002F110005221389752040",[27],"Interesting >> thread \u003C\u003C"," on the official release of ",[23,6698,6701],{"href":6699,"rel":6700},"https:\u002F\u002Fwww.rspack.dev\u002Fblog\u002Fannouncement.html",[27],"rspack.dev"," by ",[55,6704,6705,6706],{},"@",[23,6707,6709],{"href":6708},"mailto:patak@webtoo.ls","patak@webtoo.ls",[30,6711],{},[909,6713],{"className":6714,"name":6715},[912,913,914,915],"carbon:information-square",[269,6717,6720],{"className":6718},[493,6719],"italic","\"Rspack is a Rust-based JavaScript bundler developed by the ByteDance Web Infra team that has features including high-performance, webpack interoperability, flexible configuration etc.\"",[11,6722,6723],{"id":84},"Released this Week",[16,6725,6726,6740,6750],{},[19,6727,6728,6735,6736,6739],{},[23,6729,6732],{"href":6730,"rel":6731},"https:\u002F\u002Fdeno.com\u002Fblog\u002Fv1.31",[27],[55,6733,6734],{},"Deno 1.31"," which drops ",[151,6737,6738],{},"package.json"," support!! (...among other improvements)",[19,6741,6742,6749],{},[23,6743,6746],{"href":6744,"rel":6745},"https:\u002F\u002Fastro.build\u002Fblog\u002Fastro-210\u002F",[27],[55,6747,6748],{},"Astro 2.1"," featuring build-in image support, markdown integration, and inferred types for dynamic routes",[19,6751,6752,6759,6760,6763],{},[23,6753,6756],{"href":6754,"rel":6755},"https:\u002F\u002Fgithub.com\u002Felk-zone\u002Felk\u002Freleases\u002Ftag\u002Fv0.7.5",[27],[55,6757,6758],{},"Elk v0.7.5"," which includes my first contribution (!) delivering the functionality of basic keyboard shortcuts. I'm thinking about writing a blog post on the ",[151,6761,6762],{},"how"," and key challenges once more shortcuts have been completed.",[11,6765,6611],{"id":6610},[16,6767,6768],{},[19,6769,6770,6777],{},[23,6771,6774],{"href":6772,"rel":6773},"https:\u002F\u002Fexcalidraw.com",[27],[55,6775,6776],{},"Excalidraw"," is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them. If you still haven't head of it, give it a try!!! It's super easy to pick up, also you can sketch very fast using the keyboard shortcuts provided.",{"title":183,"searchDepth":184,"depth":184,"links":6779},[],"2023-03-13",{},"\u002Fblog\u002Fweekly-digest-2023-cw8-cw9",{"title":6634,"description":187},"blog\u002F10009.weekly-digest-2023-cw8-cw9",[196],"2023 Calendar Week #8 & #9","9ZIfQjUvVCS6lYCU2srQ4oLs1FaHXDp1i363U6p32o8",{"id":6789,"title":6790,"active":6,"body":6791,"date":6898,"description":187,"duration":188,"extension":189,"level":190,"meta":6899,"navigation":6,"ogDescription":190,"ogImage":190,"ogImageAlt":190,"ogTitle":190,"path":6900,"pinned":190,"seo":6901,"stem":6902,"tags":6903,"titleAlt":6904,"titlePage":190,"__hash__":6905},"blog\u002Fblog\u002F10008.weekly-digest-2023-cw6-cw7.md","Weekly Digest · 2023 CW 6 + CW 7",{"type":8,"value":6792,"toc":6896},[6793,6795,6843,6845,6865,6867],[11,6794,14],{"id":13},[16,6796,6797,6815,6827],{},[19,6798,6799,6800,6803,6804],{},"Did you know that starting with Apache Flink 1.13, ",[55,6801,6802],{},"Flame Graphs are natively supported in Flink","? This is super useful for analysis and identifying performance issues.\n",[16,6805,6806],{},[19,6807,6808,6809,6814],{},"Read the ",[23,6810,6813],{"href":6811,"rel":6812},"https:\u002F\u002Fnightlies.apache.org\u002Fflink\u002Fflink-docs-release-1.16\u002Fdocs\u002Fops\u002Fdebugging\u002Fflame_graphs\u002F",[27],"documentation"," for more details",[19,6816,6817,6818,6821,6822,6826],{},"Freshen up your knowledge on the ",[55,6819,6820],{},"Apache Kafka Rebalance Protocol"," in this ",[23,6823,6825],{"href":968,"rel":6824},[27],"Medium blog post"," by Florian Hussonnois",[19,6828,6829,6834,6835,6838,6839,6842],{},[23,6830,6833],{"href":6831,"rel":6832},"https:\u002F\u002Ftwitter.com\u002Ffniephaus\u002Fstatus\u002F1626146456657616897?s=20",[27],"Fabio Niephaus on twitter",": \"",[55,6836,6837],{},"Native Image"," will ship as part of the ",[55,6840,6841],{},"@GraalVM JDK"," in the next release and thus no longer needs to be installed via 'gu install native-image'. 🚀\"",[11,6844,85],{"id":84},[16,6846,6847],{},[19,6848,6849,6854],{},[23,6850,6853],{"href":6851,"rel":6852},"https:\u002F\u002Fblogs.apache.org\u002Fkafka\u002Fentry\u002Fwhat-s-new-in-apache9",[27],"Apache Kafka ® 3.4.0",[16,6855,6856],{},[19,6857,6858,6859,6864],{},"Preferring video over reading? Find all KIPs briefly explained in ",[23,6860,6863],{"href":6861,"rel":6862},"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=E9pqZ4QUeVI",[27],"this short video"," (by Confluent)",[11,6866,108],{"id":107},[16,6868,6869,6876],{},[19,6870,6871,6872],{},"Published ",[23,6873,6875],{"href":6874},"\u002Fblog\u002Fnuxt3-plugin-medium-zoom","Nuxt3 Plugin for 'medium-zoom' library",[19,6877,6878,6879,6882,6883],{},"Added ",[55,6880,6881],{},"giscus"," to the blog\n",[16,6884,6885,6888],{},[19,6886,6887],{},"giscus is an open source comments system powered by GitHub Discussions. Let visitors leave comments and reactions on your website via GitHub!",[19,6889,6890,6891,6895],{},"Here's also a short blog post ",[23,6892,6894],{"href":6893},"\u002Fblog\u002Fcomments-with-giscus","Comments with 'giscus'"," on how that's done",{"title":183,"searchDepth":184,"depth":184,"links":6897},[],"2023-02-27",{},"\u002Fblog\u002Fweekly-digest-2023-cw6-cw7",{"title":6790,"description":187},"blog\u002F10008.weekly-digest-2023-cw6-cw7",[196],"2023 Calendar Week #6 & #7","fN1LLtW1NEjy1xqG5MTjMKfGTrskR0pWTSKrXNWHHv0",{"id":6907,"title":6894,"active":6,"body":6908,"date":7552,"description":7553,"duration":1429,"extension":189,"level":190,"meta":7554,"navigation":6,"ogDescription":190,"ogImage":190,"ogImageAlt":190,"ogTitle":7555,"path":6893,"pinned":190,"seo":7556,"stem":7557,"tags":7558,"titleAlt":190,"titlePage":190,"__hash__":7562},"blog\u002Fblog\u002F6.comments-with-giscus.md",{"type":8,"value":6909,"toc":7547},[6910,6927,6943,6946,6950,6979,6988,6995,7157,7166,7182,7194,7215,7224,7231,7245,7482,7485,7487,7497,7500,7509,7512,7518,7520,7544],[206,6911,6912,6913,6917,6918,2572,6922,347],{},"Today's ",[23,6914,6916],{"href":6915},"\u002Fblog\u002Ftags\u002Ftip","#tip"," is an integration of ",[23,6919,6881],{"href":6920,"rel":6921},"https:\u002F\u002Fgiscus.vercel.app\u002F",[27],[23,6923,6926],{"href":6924,"rel":6925},"https:\u002F\u002Fnuxt.com",[27],"nuxt3",[289,6928,6929],{},[206,6930,6931,232,6934,6936,6937,6942],{},[909,6932],{"className":6933,"name":6715},[912,913,914,915],[55,6935,6881],{}," is an open source comments system powered by ",[23,6938,6941],{"href":6939,"rel":6940},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Fdiscussions",[27],"GitHub Discussions",". Let visitors leave comments and reactions on your website via GitHub!",[206,6944,6945],{},"The integration was actually that simple it's hardly worth a blog post. I decided to do a brief write up anyway.",[281,6947,6949],{"id":6948},"integration","Integration",[456,6951,6952,6964],{},[19,6953,6954,6955,6958,6959],{},"To see giscus in action and experience hands-on, scroll to ",[23,6956,6957],{"href":2155},"the bottom"," of this page & say Hi! ",[909,6960],{"className":6961,"name":6963},[912,913,914,915,6962],"opacity-80","iconoir:peace-hand",[19,6965,6966,6967,6970,6971,6975,6976,6978],{},"Head over to the ",[55,6968,6969],{},"Configuration"," section of ",[23,6972,6974],{"href":6920,"rel":6973},[27],"giscus.vercel.app"," and follow the instructions.",[30,6977],{},"\nThe page itself will be guide you through setting up a GitHub repository (public, giscus app is installed, Discussions feature enabled).",[206,6980,6981],{},[209,6982],{"alt":6983,"className":6984,"dataZoomSrc":6985,"height":6986,"preload":183,"src":6985,"width":6987},"Screenshot showing the giscus 'Configuration' section of https:\u002F\u002Fgiscus.vercel.app\u002F",[482,1273],"\u002Fassets\u002Fblog\u002F6.comments-with-giscus\u002Fgiscus-configuration_2023-02-22.webp",1434,1830,[206,6989,6990,6991,6994],{},"Having completed the entire form the page emits a ",[137,6992,6993],{},"\u003Cscript>"," tag ready to be used in your page.",[261,6996,7000],{"className":6997,"code":6998,"language":6999,"meta":183,"style":183},"language-html shiki shiki-themes github-light github-dark","\u003Cscript src=\"https:\u002F\u002Fgiscus.app\u002Fclient.js\"\n        data-repo=\"thriving-dev\u002Fthriving.dev-giscus\"\n        data-repo-id=\"R_kgDOI-x55Q\"\n        data-category=\"Blog comments\"\n        data-category-id=\"DIC_kwDOI-x55c4CUQnx\"\n        data-mapping=\"pathname\"\n        data-strict=\"0\"\n        data-reactions-enabled=\"1\"\n        data-emit-metadata=\"0\"\n        data-input-position=\"bottom\"\n        data-theme=\"light\"\n        data-lang=\"en\"\n        crossorigin=\"anonymous\"\n        async>\n\u003C\u002Fscript>\n","html",[137,7001,7002,7021,7031,7041,7051,7061,7071,7081,7091,7100,7110,7120,7130,7140,7148],{"__ignoreMap":183},[269,7003,7004,7007,7011,7015,7018],{"class":271,"line":272},[269,7005,7006],{"class":1590},"\u003C",[269,7008,7010],{"class":7009},"s9eBZ","script",[269,7012,7014],{"class":7013},"sScJk"," src",[269,7016,7017],{"class":1590},"=",[269,7019,7020],{"class":1586},"\"https:\u002F\u002Fgiscus.app\u002Fclient.js\"\n",[269,7022,7023,7026,7028],{"class":271,"line":184},[269,7024,7025],{"class":7013},"        data-repo",[269,7027,7017],{"class":1590},[269,7029,7030],{"class":1586},"\"thriving-dev\u002Fthriving.dev-giscus\"\n",[269,7032,7033,7036,7038],{"class":271,"line":991},[269,7034,7035],{"class":7013},"        data-repo-id",[269,7037,7017],{"class":1590},[269,7039,7040],{"class":1586},"\"R_kgDOI-x55Q\"\n",[269,7042,7043,7046,7048],{"class":271,"line":1613},[269,7044,7045],{"class":7013},"        data-category",[269,7047,7017],{"class":1590},[269,7049,7050],{"class":1586},"\"Blog comments\"\n",[269,7052,7053,7056,7058],{"class":271,"line":1626},[269,7054,7055],{"class":7013},"        data-category-id",[269,7057,7017],{"class":1590},[269,7059,7060],{"class":1586},"\"DIC_kwDOI-x55c4CUQnx\"\n",[269,7062,7063,7066,7068],{"class":271,"line":1639},[269,7064,7065],{"class":7013},"        data-mapping",[269,7067,7017],{"class":1590},[269,7069,7070],{"class":1586},"\"pathname\"\n",[269,7072,7073,7076,7078],{"class":271,"line":1652},[269,7074,7075],{"class":7013},"        data-strict",[269,7077,7017],{"class":1590},[269,7079,7080],{"class":1586},"\"0\"\n",[269,7082,7083,7086,7088],{"class":271,"line":1665},[269,7084,7085],{"class":7013},"        data-reactions-enabled",[269,7087,7017],{"class":1590},[269,7089,7090],{"class":1586},"\"1\"\n",[269,7092,7093,7096,7098],{"class":271,"line":1678},[269,7094,7095],{"class":7013},"        data-emit-metadata",[269,7097,7017],{"class":1590},[269,7099,7080],{"class":1586},[269,7101,7102,7105,7107],{"class":271,"line":1699},[269,7103,7104],{"class":7013},"        data-input-position",[269,7106,7017],{"class":1590},[269,7108,7109],{"class":1586},"\"bottom\"\n",[269,7111,7112,7115,7117],{"class":271,"line":1707},[269,7113,7114],{"class":7013},"        data-theme",[269,7116,7017],{"class":1590},[269,7118,7119],{"class":1586},"\"light\"\n",[269,7121,7122,7125,7127],{"class":271,"line":1727},[269,7123,7124],{"class":7013},"        data-lang",[269,7126,7017],{"class":1590},[269,7128,7129],{"class":1586},"\"en\"\n",[269,7131,7132,7135,7137],{"class":271,"line":1733},[269,7133,7134],{"class":7013},"        crossorigin",[269,7136,7017],{"class":1590},[269,7138,7139],{"class":1586},"\"anonymous\"\n",[269,7141,7142,7145],{"class":271,"line":1739},[269,7143,7144],{"class":7013},"        async",[269,7146,7147],{"class":1590},">\n",[269,7149,7150,7153,7155],{"class":271,"line":2353},[269,7151,7152],{"class":1590},"\u003C\u002F",[269,7154,7010],{"class":7009},[269,7156,7147],{"class":1590},[206,7158,7159,7160,7165],{},"Since my blog is based on Nuxt3 and Vue, we're going to use the official ",[23,7161,7164],{"href":7162,"rel":7163},"https:\u002F\u002Fgithub.com\u002Fgiscus\u002Fgiscus-component",[27],"giscus-component"," instead.",[206,7167,7168,7169,1263,7172,1263,7175,7178,7179,347],{},"At the time this article was written (23\u002F02\u002F2023) giscus provided components for ",[55,7170,7171],{},"React",[55,7173,7174],{},"Vue",[55,7176,7177],{},"Svelte",", or ",[55,7180,7181],{},"Solid",[456,7183,7184],{"start":991},[19,7185,7186,7187,7190,7191],{},"Install ",[137,7188,7189],{},"@giscus\u002Fvue"," as a ",[151,7192,7193],{},"dev dependency",[261,7195,7199],{"className":7196,"code":7197,"language":7198,"meta":183,"style":183},"language-bash shiki shiki-themes github-light github-dark","yarn add --dev @giscus\u002Fvue\n","bash",[137,7200,7201],{"__ignoreMap":183},[269,7202,7203,7206,7209,7212],{"class":271,"line":272},[269,7204,7205],{"class":7013},"yarn",[269,7207,7208],{"class":1586}," add",[269,7210,7211],{"class":1601}," --dev",[269,7213,7214],{"class":1586}," @giscus\u002Fvue\n",[456,7216,7217],{"start":1613},[19,7218,7219,7220,7223],{},"Import the ",[137,7221,7222],{},"Giscus"," component and add to the template",[206,7225,7226,7227,7230],{},"E.g. for this blog there's a page ",[137,7228,7229],{},"pages\u002Fblog\u002F[...slug].vue"," the posts are rendered with.",[206,7232,7233,7234,7237,7238,7240,7241,7244],{},"At the bottom of this vue file the ",[55,7235,7236],{},"Giscus component"," was used with attributes taken from the generated ",[137,7239,6993],{}," tag (..without the ",[137,7242,7243],{},"data-"," prefix).",[261,7246,7250],{"className":7247,"code":7248,"language":7249,"meta":183,"style":183},"language-vue shiki shiki-themes github-light github-dark","\u003Cscript setup lang=\"ts\">\nimport Giscus from '@giscus\u002Fvue'\n\u002F\u002F ...\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003C!-- blog title -->\n    \u003C!-- ... -->\n    \u003CGiscus\n        id=\"comments\"\n        repo=\"thriving-dev\u002Fthriving.dev-giscus\"\n        repo-id=\"R_kgDOI-x55Q\"\n        category=\"Blog comments\"\n        category-id=\"DIC_kwDOI-x55c4CUQnx\"\n        mapping=\"pathname\"\n        strict=\"0\"\n        reactions-enabled=\"1\"\n        emit-metadata=\"0\"\n        input-position=\"bottom\"\n        theme=\"light\"\n        lang=\"en\"\n    \u002F>\n    \u003C!-- ... -->\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n","vue",[137,7251,7252,7271,7286,7292,7300,7304,7313,7322,7327,7332,7340,7350,7359,7368,7377,7386,7395,7404,7413,7422,7432,7442,7452,7458,7463,7473],{"__ignoreMap":183},[269,7253,7254,7256,7258,7261,7264,7266,7269],{"class":271,"line":272},[269,7255,7006],{"class":1590},[269,7257,7010],{"class":7009},[269,7259,7260],{"class":7013}," setup",[269,7262,7263],{"class":7013}," lang",[269,7265,7017],{"class":1590},[269,7267,7268],{"class":1586},"\"ts\"",[269,7270,7147],{"class":1590},[269,7272,7273,7277,7280,7283],{"class":271,"line":184},[269,7274,7276],{"class":7275},"szBVR","import",[269,7278,7279],{"class":1590}," Giscus ",[269,7281,7282],{"class":7275},"from",[269,7284,7285],{"class":1586}," '@giscus\u002Fvue'\n",[269,7287,7288],{"class":271,"line":991},[269,7289,7291],{"class":7290},"sJ8bj","\u002F\u002F ...\n",[269,7293,7294,7296,7298],{"class":271,"line":1613},[269,7295,7152],{"class":1590},[269,7297,7010],{"class":7009},[269,7299,7147],{"class":1590},[269,7301,7302],{"class":271,"line":1626},[269,7303,2291],{"emptyLinePlaceholder":6},[269,7305,7306,7308,7311],{"class":271,"line":1639},[269,7307,7006],{"class":1590},[269,7309,7310],{"class":7009},"template",[269,7312,7147],{"class":1590},[269,7314,7315,7318,7320],{"class":271,"line":1652},[269,7316,7317],{"class":1590},"  \u003C",[269,7319,489],{"class":7009},[269,7321,7147],{"class":1590},[269,7323,7324],{"class":271,"line":1665},[269,7325,7326],{"class":7290},"    \u003C!-- blog title -->\n",[269,7328,7329],{"class":271,"line":1678},[269,7330,7331],{"class":7290},"    \u003C!-- ... -->\n",[269,7333,7334,7337],{"class":271,"line":1699},[269,7335,7336],{"class":1590},"    \u003C",[269,7338,7339],{"class":7009},"Giscus\n",[269,7341,7342,7345,7347],{"class":271,"line":1707},[269,7343,7344],{"class":7013},"        id",[269,7346,7017],{"class":1590},[269,7348,7349],{"class":1586},"\"comments\"\n",[269,7351,7352,7355,7357],{"class":271,"line":1727},[269,7353,7354],{"class":7013},"        repo",[269,7356,7017],{"class":1590},[269,7358,7030],{"class":1586},[269,7360,7361,7364,7366],{"class":271,"line":1733},[269,7362,7363],{"class":7013},"        repo-id",[269,7365,7017],{"class":1590},[269,7367,7040],{"class":1586},[269,7369,7370,7373,7375],{"class":271,"line":1739},[269,7371,7372],{"class":7013},"        category",[269,7374,7017],{"class":1590},[269,7376,7050],{"class":1586},[269,7378,7379,7382,7384],{"class":271,"line":2353},[269,7380,7381],{"class":7013},"        category-id",[269,7383,7017],{"class":1590},[269,7385,7060],{"class":1586},[269,7387,7388,7391,7393],{"class":271,"line":2358},[269,7389,7390],{"class":7013},"        mapping",[269,7392,7017],{"class":1590},[269,7394,7070],{"class":1586},[269,7396,7397,7400,7402],{"class":271,"line":2364},[269,7398,7399],{"class":7013},"        strict",[269,7401,7017],{"class":1590},[269,7403,7080],{"class":1586},[269,7405,7406,7409,7411],{"class":271,"line":2370},[269,7407,7408],{"class":7013},"        reactions-enabled",[269,7410,7017],{"class":1590},[269,7412,7090],{"class":1586},[269,7414,7415,7418,7420],{"class":271,"line":2375},[269,7416,7417],{"class":7013},"        emit-metadata",[269,7419,7017],{"class":1590},[269,7421,7080],{"class":1586},[269,7423,7425,7428,7430],{"class":271,"line":7424},20,[269,7426,7427],{"class":7013},"        input-position",[269,7429,7017],{"class":1590},[269,7431,7109],{"class":1586},[269,7433,7435,7438,7440],{"class":271,"line":7434},21,[269,7436,7437],{"class":7013},"        theme",[269,7439,7017],{"class":1590},[269,7441,7119],{"class":1586},[269,7443,7445,7448,7450],{"class":271,"line":7444},22,[269,7446,7447],{"class":7013},"        lang",[269,7449,7017],{"class":1590},[269,7451,7129],{"class":1586},[269,7453,7455],{"class":271,"line":7454},23,[269,7456,7457],{"class":1590},"    \u002F>\n",[269,7459,7461],{"class":271,"line":7460},24,[269,7462,7331],{"class":7290},[269,7464,7466,7469,7471],{"class":271,"line":7465},25,[269,7467,7468],{"class":1590},"  \u003C\u002F",[269,7470,489],{"class":7009},[269,7472,7147],{"class":1590},[269,7474,7476,7478,7480],{"class":271,"line":7475},26,[269,7477,7152],{"class":1590},[269,7479,7310],{"class":7009},[269,7481,7147],{"class":1590},[206,7483,7484],{},"And that's it really! Hats off to the author(s) for providing such a smooth guide.",[281,7486,825],{"id":824},[206,7488,7489,7492,7493,7496],{},[23,7490,7222],{"href":6920,"rel":7491},[27]," is a comment system powered by GitHub Discussions. It's a good choice being in the ",[151,7494,7495],{},"software engineering field"," where you can expect your readers to have a GitHub account.",[206,7498,7499],{},"The solution does not require any CMS, database, user account management, security - so it's a good fit for pre-rendered \u002F static generated websites. With no additional infrastructure required it comes without extra costs.",[206,7501,7502,7503,7508],{},"GitHub Discussion can be ",[23,7504,7507],{"href":7505,"rel":7506},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Fdiscussions\u002Fmanaging-discussions-for-your-community\u002Fmoderating-discussions",[27],"moderated"," with the appropriate repo role.",[206,7510,7511],{},"The integration is super simple via native html\u002Fjavascript or with one of the components provided for today's most popular front-end frameworks.",[206,7513,7514,7517],{},[909,7515],{"className":7516,"name":4099},[912,913,914,915]," Comment, share, thrive, enjoy!!!",[281,7519,3834],{"id":3833},[16,7521,7522,7527,7533,7539],{},[19,7523,7524],{},[23,7525,6920],{"href":6920,"rel":7526},[27],[19,7528,7529],{},[23,7530,7531],{"href":7531,"rel":7532},"https:\u002F\u002Fgithub.com\u002Fgiscus\u002Fgiscus",[27],[19,7534,7535],{},[23,7536,7537],{"href":7537,"rel":7538},"https:\u002F\u002Fgithub.com\u002Fgiscus\u002Fgiscus-component\u002Ftree\u002Fmain\u002Fdemo\u002Fvue",[27],[19,7540,7541],{},[23,7542,7505],{"href":7505,"rel":7543},[27],[983,7545,7546],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":183,"searchDepth":184,"depth":184,"links":7548},[7549,7550,7551],{"id":6948,"depth":991,"text":6949},{"id":824,"depth":991,"text":825},{"id":3833,"depth":991,"text":3834},"2023-02-23","Today thriving.dev was extended to allow for reactions and comments to blog posts with 'giscus'. Learn how quick and simple the integration was.",{},"Comments with 'giscus', powered by GitHub Discussions",{"title":6894,"description":7553},"blog\u002F6.comments-with-giscus",[7559,7560,7561,7249],"tip","frontend","featured-library","G0VaaF9IkJJ-OfMqgN-izfbw4iHxowDJnwsIPJBwqDs",{"id":7564,"title":7565,"active":6,"body":7566,"date":8870,"description":8871,"duration":8872,"extension":189,"level":190,"meta":8873,"navigation":6,"ogDescription":190,"ogImage":190,"ogImageAlt":190,"ogTitle":7565,"path":6874,"pinned":190,"seo":8874,"stem":8875,"tags":8876,"titleAlt":190,"titlePage":190,"__hash__":8878},"blog\u002Fblog\u002F5.nuxt3-plugin-medium-zoom.md","Nuxt3 Plugin for 'medium-zoom' Library",{"type":8,"value":7567,"toc":8851},[7568,7580,7590,7598,7607,7609,7617,7621,7624,7629,7645,7649,7663,7843,7855,7863,7882,7894,7898,7902,7909,7946,7950,7957,8037,8047,8113,8117,8125,8150,8153,8171,8188,8192,8195,8246,8249,8267,8274,8283,8296,8300,8389,8391,8395,8398,8406,8413,8434,8792,8794,8799,8802,8807,8809,8848],[206,7569,6912,7570,6917,7572,2572,7577,347],{},[23,7571,6916],{"href":6915},[23,7573,7576],{"href":7574,"rel":7575},"https:\u002F\u002Fgithub.com\u002Ffrancoischalifour\u002Fmedium-zoom",[27],"medium-zoom",[23,7578,6926],{"href":6924,"rel":7579},[27],[289,7581,7582],{},[206,7583,7584,232,7587,7589],{},[909,7585],{"className":7586,"name":6715},[912,913,914,915],[55,7588,7576],{}," is a JavaScript library for zooming images like Medium.",[206,7591,7592,7593,340],{},"First things first, to see medium-zoom in action, just click\u002Ftap on the 'Tree Head' (by ",[23,7594,7597],{"href":7595,"rel":7596},"https:\u002F\u002Fwww.art-ann-kathrin.com\u002F",[27],"@art_ann_kathrin",[206,7599,7600],{},[209,7601],{"alt":7602,"className":7603,"dataZoomSrc":7604,"height":7605,"preload":183,"src":7604,"width":7606},"Artwork 'Tree Head' by Ann-Kathrin Föll",[482,1273],"\u002Fassets\u002Fblog\u002F5.nuxt3-plugin-medium-zoom\u002FAnn-Kathrin_Foell_TH82_crop.webp",1055,2200,[276,7608,6949],{"id":6948},[206,7610,7611,7612,750,7615],{},"There's no ",[151,7613,7614],{},"vue component",[55,7616,7576],{},[281,7618,7620],{"id":7619},"_1-install","1. Install",[206,7622,7623],{},"The integration is implemented as a nuxt plugin.",[206,7625,7186,7626,7628],{},[137,7627,7576],{}," as a dev dependency",[261,7630,7632],{"className":7196,"code":7631,"language":7198,"meta":183,"style":183},"yarn add --dev medium-zoom\n",[137,7633,7634],{"__ignoreMap":183},[269,7635,7636,7638,7640,7642],{"class":271,"line":272},[269,7637,7205],{"class":7013},[269,7639,7208],{"class":1586},[269,7641,7211],{"class":1601},[269,7643,7644],{"class":1586}," medium-zoom\n",[281,7646,7648],{"id":7647},"_2-nuxt3-plugin","2. Nuxt3 Plugin",[206,7650,7651,7652,7655,7656,7659,7660],{},"Add following ",[151,7653,7654],{},"client-side"," (file name suffix ",[137,7657,7658],{},".client.(ts|js)",") plugin: ",[137,7661,7662],{},".\u002Fplugins\u002Fmedium-zoom.client.ts",[261,7664,7668],{"className":7665,"code":7666,"language":7667,"meta":183,"style":183},"language-ts shiki shiki-themes github-light github-dark","import { defineNuxtPlugin } from '#app'\nimport mediumZoom, { Zoom } from 'medium-zoom'\n\nexport default defineNuxtPlugin((nuxtApp) => {\n  const selector = '.image-zoomable'\n  const zoom: Zoom = mediumZoom(selector, {})\n\n  \u002F\u002F (re-)init for newly rendered page, also to work in SPA mode (client-side routing)\n  nuxtApp.hook('page:finish', () => {\n    zoom.detach(selector)\n      .attach(selector)\n  })\n\n  \u002F\u002F make available as helper to NuxtApp \n  nuxtApp.provide('mediumZoom', zoom)\n})\n","ts",[137,7669,7670,7682,7694,7698,7724,7738,7758,7762,7767,7788,7799,7809,7814,7818,7823,7838],{"__ignoreMap":183},[269,7671,7672,7674,7677,7679],{"class":271,"line":272},[269,7673,7276],{"class":7275},[269,7675,7676],{"class":1590}," { defineNuxtPlugin } ",[269,7678,7282],{"class":7275},[269,7680,7681],{"class":1586}," '#app'\n",[269,7683,7684,7686,7689,7691],{"class":271,"line":184},[269,7685,7276],{"class":7275},[269,7687,7688],{"class":1590}," mediumZoom, { Zoom } ",[269,7690,7282],{"class":7275},[269,7692,7693],{"class":1586}," 'medium-zoom'\n",[269,7695,7696],{"class":271,"line":991},[269,7697,2291],{"emptyLinePlaceholder":6},[269,7699,7700,7703,7706,7709,7712,7716,7718,7721],{"class":271,"line":1613},[269,7701,7702],{"class":7275},"export",[269,7704,7705],{"class":7275}," default",[269,7707,7708],{"class":7013}," defineNuxtPlugin",[269,7710,7711],{"class":1590},"((",[269,7713,7715],{"class":7714},"s4XuR","nuxtApp",[269,7717,6526],{"class":1590},[269,7719,7720],{"class":7275},"=>",[269,7722,7723],{"class":1590}," {\n",[269,7725,7726,7729,7732,7735],{"class":271,"line":1626},[269,7727,7728],{"class":7275},"  const",[269,7730,7731],{"class":1601}," selector",[269,7733,7734],{"class":7275}," =",[269,7736,7737],{"class":1586}," '.image-zoomable'\n",[269,7739,7740,7742,7745,7747,7750,7752,7755],{"class":271,"line":1639},[269,7741,7728],{"class":7275},[269,7743,7744],{"class":1601}," zoom",[269,7746,259],{"class":7275},[269,7748,7749],{"class":7013}," Zoom",[269,7751,7734],{"class":7275},[269,7753,7754],{"class":7013}," mediumZoom",[269,7756,7757],{"class":1590},"(selector, {})\n",[269,7759,7760],{"class":271,"line":1652},[269,7761,2291],{"emptyLinePlaceholder":6},[269,7763,7764],{"class":271,"line":1665},[269,7765,7766],{"class":7290},"  \u002F\u002F (re-)init for newly rendered page, also to work in SPA mode (client-side routing)\n",[269,7768,7769,7772,7775,7778,7781,7784,7786],{"class":271,"line":1678},[269,7770,7771],{"class":1590},"  nuxtApp.",[269,7773,7774],{"class":7013},"hook",[269,7776,7777],{"class":1590},"(",[269,7779,7780],{"class":1586},"'page:finish'",[269,7782,7783],{"class":1590},", () ",[269,7785,7720],{"class":7275},[269,7787,7723],{"class":1590},[269,7789,7790,7793,7796],{"class":271,"line":1699},[269,7791,7792],{"class":1590},"    zoom.",[269,7794,7795],{"class":7013},"detach",[269,7797,7798],{"class":1590},"(selector)\n",[269,7800,7801,7804,7807],{"class":271,"line":1707},[269,7802,7803],{"class":1590},"      .",[269,7805,7806],{"class":7013},"attach",[269,7808,7798],{"class":1590},[269,7810,7811],{"class":271,"line":1727},[269,7812,7813],{"class":1590},"  })\n",[269,7815,7816],{"class":271,"line":1733},[269,7817,2291],{"emptyLinePlaceholder":6},[269,7819,7820],{"class":271,"line":1739},[269,7821,7822],{"class":7290},"  \u002F\u002F make available as helper to NuxtApp \n",[269,7824,7825,7827,7830,7832,7835],{"class":271,"line":2353},[269,7826,7771],{"class":1590},[269,7828,7829],{"class":7013},"provide",[269,7831,7777],{"class":1590},[269,7833,7834],{"class":1586},"'mediumZoom'",[269,7836,7837],{"class":1590},", zoom)\n",[269,7839,7840],{"class":271,"line":2358},[269,7841,7842],{"class":1590},"})\n",[206,7844,7845,7846,7849,7850,7852,7853,347],{},"Now for each page rendered \u002F client-side navigated to, medium-zoom is applied accordingly for all images in the DOM matching the chosen ",[151,7847,7848],{},"selector",". In our plugin we chose a CSS selector to match all ",[137,7851,209],{}," elements with the class ",[137,7854,482],{},[206,7856,7857,7858,347],{},"You can find all supported selector types in the ",[23,7859,7862],{"href":7860,"rel":7861},"https:\u002F\u002Fgithub.com\u002Ffrancoischalifour\u002Fmedium-zoom#selectors",[27],"module's docs",[206,7864,7865,7866,7868,7869,7872,7873,232,7876,7881],{},"Running on nuxt - client-side app navigation is done via vue-router. For ",[55,7867,7576],{}," to do it's magic, it has to be ",[151,7870,7871],{},"'re-attached'"," following changes to image on the page. We use the ",[137,7874,7875],{},"page:finish",[23,7877,7880],{"href":7878,"rel":7879},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fapi\u002Fadvanced\u002Fhooks",[27],"nuxt3 lifecycle hook"," as a trigger.",[206,7883,7884,7885,7889,7890,7893],{},"Finally, we also ",[23,7886,7829],{"href":7887,"rel":7888},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fguide\u002Fdirectory-structure\u002Fplugins#automatically-providing-helpers",[27]," the ",[137,7891,7892],{},"mediumZoom"," instance as a helper.",[276,7895,7897],{"id":7896},"usage","Usage",[281,7899,7901],{"id":7900},"regular-html-image","Regular html Image",[206,7903,7904,7905,7908],{},"The most basic example is to use with plain html ",[137,7906,7907],{},"\u003Cimg>"," tag:",[261,7910,7912],{"className":6997,"code":7911,"language":6999,"meta":183,"style":183},"\u003Cimg src=\"\u002Fimages\u002Ffluffy-cat.jpg\" alt=\"A fluffy cat\" class=\"image-zoomable\" \u002F>\n",[137,7913,7914],{"__ignoreMap":183},[269,7915,7916,7918,7920,7922,7924,7927,7930,7932,7935,7938,7940,7943],{"class":271,"line":272},[269,7917,7006],{"class":1590},[269,7919,209],{"class":7009},[269,7921,7014],{"class":7013},[269,7923,7017],{"class":1590},[269,7925,7926],{"class":1586},"\"\u002Fimages\u002Ffluffy-cat.jpg\"",[269,7928,7929],{"class":7013}," alt",[269,7931,7017],{"class":1590},[269,7933,7934],{"class":1586},"\"A fluffy cat\"",[269,7936,7937],{"class":7013}," class",[269,7939,7017],{"class":1590},[269,7941,7942],{"class":1586},"\"image-zoomable\"",[269,7944,7945],{"class":1590}," \u002F>\n",[281,7947,7949],{"id":7948},"nuxt-image-module","Nuxt Image Module",[206,7951,7952,7953,7956],{},"Using with ",[137,7954,7955],{},"\u003Cnuxt-image>"," is done in exactly the same way...",[261,7958,7960],{"className":6997,"code":7959,"language":6999,"meta":183,"style":183},"\u003Cnuxt-img \n    src=\"\u002Fimages\u002Ffluffy-cat.jpg\"\n    alt=\"A fluffy cat\"\n    width=\"700\"\n    height=\"800\"\n    sizes=\"sm:224 lg:448\"\n    class=\"image-zoomable\"\n\u002F>\n",[137,7961,7962,7972,7982,7992,8002,8012,8022,8032],{"__ignoreMap":183},[269,7963,7964,7966,7969],{"class":271,"line":272},[269,7965,7006],{"class":1590},[269,7967,7968],{"class":7009},"nuxt-img",[269,7970,7971],{"class":1590}," \n",[269,7973,7974,7977,7979],{"class":271,"line":184},[269,7975,7976],{"class":7013},"    src",[269,7978,7017],{"class":1590},[269,7980,7981],{"class":1586},"\"\u002Fimages\u002Ffluffy-cat.jpg\"\n",[269,7983,7984,7987,7989],{"class":271,"line":991},[269,7985,7986],{"class":7013},"    alt",[269,7988,7017],{"class":1590},[269,7990,7991],{"class":1586},"\"A fluffy cat\"\n",[269,7993,7994,7997,7999],{"class":271,"line":1613},[269,7995,7996],{"class":7013},"    width",[269,7998,7017],{"class":1590},[269,8000,8001],{"class":1586},"\"700\"\n",[269,8003,8004,8007,8009],{"class":271,"line":1626},[269,8005,8006],{"class":7013},"    height",[269,8008,7017],{"class":1590},[269,8010,8011],{"class":1586},"\"800\"\n",[269,8013,8014,8017,8019],{"class":271,"line":1639},[269,8015,8016],{"class":7013},"    sizes",[269,8018,7017],{"class":1590},[269,8020,8021],{"class":1586},"\"sm:224 lg:448\"\n",[269,8023,8024,8027,8029],{"class":271,"line":1652},[269,8025,8026],{"class":7013},"    class",[269,8028,7017],{"class":1590},[269,8030,8031],{"class":1586},"\"image-zoomable\"\n",[269,8033,8034],{"class":271,"line":1665},[269,8035,8036],{"class":1590},"\u002F>\n",[206,8038,8039,8040,8043,8044,259],{},"Note, for ",[137,8041,8042],{},"\u003Cnuxt-picture>"," the class has to be passed via object to ",[137,8045,8046],{},":imgAttrs",[261,8048,8050],{"className":6997,"code":8049,"language":6999,"meta":183,"style":183},"\u003Cnuxt-picture\n    src=\"\u002Fimages\u002Ffluffy-cat.jpg\"\n    alt=\"A fluffy cat\"\n    width=\"700\"\n    height=\"800\"\n    sizes=\"sm:224 lg:448\"\n    :imgAttrs=\"{ class: 'rounded !my-0 !w-32 image-zoomable' }\"\n\u002F>\n",[137,8051,8052,8059,8067,8075,8083,8091,8099,8109],{"__ignoreMap":183},[269,8053,8054,8056],{"class":271,"line":272},[269,8055,7006],{"class":1590},[269,8057,8058],{"class":7009},"nuxt-picture\n",[269,8060,8061,8063,8065],{"class":271,"line":184},[269,8062,7976],{"class":7013},[269,8064,7017],{"class":1590},[269,8066,7981],{"class":1586},[269,8068,8069,8071,8073],{"class":271,"line":991},[269,8070,7986],{"class":7013},[269,8072,7017],{"class":1590},[269,8074,7991],{"class":1586},[269,8076,8077,8079,8081],{"class":271,"line":1613},[269,8078,7996],{"class":7013},[269,8080,7017],{"class":1590},[269,8082,8001],{"class":1586},[269,8084,8085,8087,8089],{"class":271,"line":1626},[269,8086,8006],{"class":7013},[269,8088,7017],{"class":1590},[269,8090,8011],{"class":1586},[269,8092,8093,8095,8097],{"class":271,"line":1639},[269,8094,8016],{"class":7013},[269,8096,7017],{"class":1590},[269,8098,8021],{"class":1586},[269,8100,8101,8104,8106],{"class":271,"line":1652},[269,8102,8103],{"class":7013},"    :imgAttrs",[269,8105,7017],{"class":1590},[269,8107,8108],{"class":1586},"\"{ class: 'rounded !my-0 !w-32 image-zoomable' }\"\n",[269,8110,8111],{"class":271,"line":1665},[269,8112,8036],{"class":1590},[281,8114,8116],{"id":8115},"nuxt-content-module-markdown","Nuxt Content Module - Markdown",[206,8118,8119,8120,259],{},"With nuxt, it's also fairly simple to apply to ",[23,8121,8124],{"href":8122,"rel":8123},"https:\u002F\u002Fcontent.nuxtjs.org\u002Fguide\u002Fwriting\u002Fmarkdown#images",[27],"nuxt-content markdown images",[261,8126,8129],{"className":8127,"code":8128,"language":189,"meta":183,"style":183},"language-md shiki shiki-themes github-light github-dark","![alt](\u002Fimages\u002Ffluffy.jpg){class=image-zoomable}\n",[137,8130,8131],{"__ignoreMap":183},[269,8132,8133,8136,8140,8143,8147],{"class":271,"line":272},[269,8134,8135],{"class":1590},"![",[269,8137,8139],{"class":8138},"svl0z","alt",[269,8141,8142],{"class":1590},"](",[269,8144,8146],{"class":8145},"s2frl","\u002Fimages\u002Ffluffy.jpg",[269,8148,8149],{"class":1590},"){class=image-zoomable}\n",[206,8151,8152],{},"or alternatively",[261,8154,8156],{"className":8127,"code":8155,"language":189,"meta":183,"style":183},"![alt](\u002Fimages\u002Ffluffy.jpg){.image-zoomable}\n",[137,8157,8158],{"__ignoreMap":183},[269,8159,8160,8162,8164,8166,8168],{"class":271,"line":272},[269,8161,8135],{"class":1590},[269,8163,8139],{"class":8138},[269,8165,8142],{"class":1590},[269,8167,8146],{"class":8145},[269,8169,8170],{"class":1590},"){.image-zoomable}\n",[206,8172,8173,8177,8178,8183,8184,8187],{},[909,8174],{"className":8175,"name":8176},[912,913,914,915],"ph:info-duotone"," The class ist passed as an ",[23,8179,8182],{"href":8180,"rel":8181},"https:\u002F\u002Fcontent.nuxtjs.org\u002Fguide\u002Fwriting\u002Fmdc#inline-method",[27],"inline property"," to the component via ",[137,8185,8186],{},"{}"," identifier.",[281,8189,8191],{"id":8190},"everything-everywhere-all-at-once","Everything Everywhere All at Once",[206,8193,8194],{},"Often you'd want to use",[16,8196,8197,8200,8206,8209,8239],{},[19,8198,8199],{},"a nuxt content page",[19,8201,8202,8203,340],{},"written in markdown (image syntax ",[137,8204,8205],{},"![alt](\u002Fsrc.ong){class=}",[19,8207,8208],{},"responsive layout",[19,8210,8211,8212,8215,8216],{},"optimised image (",[151,8213,8214],{},"nuxt-image",")\n",[16,8217,8218,8230],{},[19,8219,8220,8221,1263,8224,1263,8227,340],{},"responsive using (",[137,8222,8223],{},"width",[137,8225,8226],{},"height",[137,8228,8229],{},"sizes",[19,8231,8232,8233,1263,8236,340],{},"with additional attributes (",[137,8234,8235],{},"quality",[137,8237,8238],{},"preload",[19,8240,8241,8242,8245],{},"high-definition image to open on zoom (via ",[137,8243,8244],{},"data-zoom-src="," attrib)",[206,8247,8248],{},"The source code of the demo image at the top of this article is:",[261,8250,8252],{"className":8127,"code":8251,"language":189,"meta":183,"style":183},"![Artwork 'Tree Head' by Ann-Kathrin Föll](\u002Fassets\u002Fblog\u002F5.nuxt3-plugin-medium-zoom\u002FAnn-Kathrin_Foell_TH82_crop.webp){.image-zoomable.rounded data-zoom-src=\"\u002Fassets\u002Fblog\u002F5.nuxt3-plugin-medium-zoom\u002FAnn-Kathrin_Foell_TH82_crop.webp\" width=\"2200\" height=\"1055\" preload}\n",[137,8253,8254],{"__ignoreMap":183},[269,8255,8256,8258,8260,8262,8264],{"class":271,"line":272},[269,8257,8135],{"class":1590},[269,8259,7602],{"class":8138},[269,8261,8142],{"class":1590},[269,8263,7604],{"class":8145},[269,8265,8266],{"class":1590},"){.image-zoomable.rounded data-zoom-src=\"\u002Fassets\u002Fblog\u002F5.nuxt3-plugin-medium-zoom\u002FAnn-Kathrin_Foell_TH82_crop.webp\" width=\"2200\" height=\"1055\" preload}\n",[281,8268,8270,8273],{"id":8269},"nuxtapp-plugin-helper",[137,8271,8272],{},"NuxtApp"," Plugin Helper",[206,8275,1317,8276,8278,8279,8282],{},[137,8277,7892],{}," helper we provided from the plugin can be decomposed from ",[137,8280,8281],{},"useNuxtApp()"," and be used in any component.",[667,8284,8286],{"title":8285},"Info",[206,8287,8288,8289,8291,8292,8295],{},"Please note our plugin is defined as ",[151,8290,7654],{}," only, you must therefore access in a client-safe fashion (using e.g. ",[137,8293,8294],{},"ssrContext",")!",[11,8297,8299],{"id":8298},"composition-api","Composition API",[261,8301,8303],{"className":7247,"code":8302,"language":7249,"meta":183,"style":183},"\u003Cscript setup lang=\"ts\">\n\u002F\u002F always access in a client-safe fashion\nconst { $mediumZoom, ssrContext } = useNuxtApp()\nif (!ssrContext) console.log('$mediumZoom margin:', $mediumZoom?.getOptions().margin)\n\u003C\u002Fscript>\n",[137,8304,8305,8321,8326,8352,8381],{"__ignoreMap":183},[269,8306,8307,8309,8311,8313,8315,8317,8319],{"class":271,"line":272},[269,8308,7006],{"class":1590},[269,8310,7010],{"class":7009},[269,8312,7260],{"class":7013},[269,8314,7263],{"class":7013},[269,8316,7017],{"class":1590},[269,8318,7268],{"class":1586},[269,8320,7147],{"class":1590},[269,8322,8323],{"class":271,"line":184},[269,8324,8325],{"class":7290},"\u002F\u002F always access in a client-safe fashion\n",[269,8327,8328,8331,8334,8337,8339,8341,8344,8346,8349],{"class":271,"line":991},[269,8329,8330],{"class":7275},"const",[269,8332,8333],{"class":1590}," { ",[269,8335,8336],{"class":1601},"$mediumZoom",[269,8338,1263],{"class":1590},[269,8340,8294],{"class":1601},[269,8342,8343],{"class":1590}," } ",[269,8345,7017],{"class":7275},[269,8347,8348],{"class":7013}," useNuxtApp",[269,8350,8351],{"class":1590},"()\n",[269,8353,8354,8357,8359,8361,8364,8367,8369,8372,8375,8378],{"class":271,"line":1613},[269,8355,8356],{"class":7275},"if",[269,8358,5761],{"class":1590},[269,8360,4206],{"class":7275},[269,8362,8363],{"class":1590},"ssrContext) console.",[269,8365,8366],{"class":7013},"log",[269,8368,7777],{"class":1590},[269,8370,8371],{"class":1586},"'$mediumZoom margin:'",[269,8373,8374],{"class":1590},", $mediumZoom?.",[269,8376,8377],{"class":7013},"getOptions",[269,8379,8380],{"class":1590},"().margin)\n",[269,8382,8383,8385,8387],{"class":271,"line":1626},[269,8384,7152],{"class":1590},[269,8386,7010],{"class":7009},[269,8388,7147],{"class":1590},[276,8390,5844],{"id":4373},[281,8392,8394],{"id":8393},"responsive-margins","Responsive Margins",[206,8396,8397],{},"To add some icing, for this page I wanted to define different margins at different breakpoints.",[206,8399,1317,8400,8402,8403,8405],{},[55,8401,7576],{}," lib uses styles and calculates dimensions of the image's zoomed view dynamically, based on window size (among other factors).\nThe margin is an option of the library and used in these calculations, rather than applied via css.\nThe zoomed ",[137,8404,7907],{}," element is added directly to the body.",[206,8407,8408,8409,8412],{},"Since the margin is defined once at init time of the mediumZoom module ",[151,8410,8411],{},"instance",", it's not possible to make it responsive.",[206,8414,8415,8416,8419,8420,8423,8424,8427,8428,8433],{},"As a workaround \u002F solution the instance is updated via ",[137,8417,8418],{},"update(..)"," method, based on ",[137,8421,8422],{},"window.innerWidth",", triggered as an eventListener on ",[151,8425,8426],{},"window resize",".\nFinally, to optimise performance ",[23,8429,8432],{"href":8430,"rel":8431},"https:\u002F\u002Fvueuse.org\u002Fshared\u002FuseDebounceFn\u002F",[27],"vueuse useDebounceFn"," is used.",[261,8435,8437],{"className":7665,"code":8436,"language":7667,"meta":183,"style":183},"import { defineNuxtPlugin } from '#app'\nimport mediumZoom, { Zoom } from 'medium-zoom'\nimport { useDebounceFn } from '@vueuse\u002Fcore'\n\nexport default defineNuxtPlugin((nuxtApp) => {\n  const selector = '.image-zoomable'\n  const innerWidth = window.innerWidth\n  const zoom: Zoom = mediumZoom(selector, {\n    margin: innerWidth \u003C 640 ? 12 : innerWidth \u003C 1024 ? 24 : innerWidth \u003C 1536 ? 96 : 192,\n    background: ''\n  })\n\n  \u002F\u002F responsive varying margin, calculated based on windowSize, upon @resize, debounced\n  const debouncedFn = useDebounceFn(() => {\n    const innerWidth = window.innerWidth\n    zoom?.update({\n      margin: innerWidth \u003C 640 ? 12 : innerWidth \u003C 1024 ? 24 : innerWidth \u003C 1536 ? 96 : 192\n    })\n  }, 200)\n\n  window.addEventListener('resize', debouncedFn)\n\n  \u002F\u002F (re-)init for newly rendered page, also to work in SPA mode (client-side routing)\n  nuxtApp.hook('page:finish', () => {\n    zoom.detach(selector)\n      .attach(selector)\n  })\n\n  nuxtApp.provide('mediumZoom', zoom)\n})\n",[137,8438,8439,8449,8459,8471,8475,8493,8503,8515,8532,8585,8593,8597,8601,8606,8625,8636,8647,8689,8694,8704,8708,8724,8728,8732,8748,8756,8764,8769,8774,8787],{"__ignoreMap":183},[269,8440,8441,8443,8445,8447],{"class":271,"line":272},[269,8442,7276],{"class":7275},[269,8444,7676],{"class":1590},[269,8446,7282],{"class":7275},[269,8448,7681],{"class":1586},[269,8450,8451,8453,8455,8457],{"class":271,"line":184},[269,8452,7276],{"class":7275},[269,8454,7688],{"class":1590},[269,8456,7282],{"class":7275},[269,8458,7693],{"class":1586},[269,8460,8461,8463,8466,8468],{"class":271,"line":991},[269,8462,7276],{"class":7275},[269,8464,8465],{"class":1590}," { useDebounceFn } ",[269,8467,7282],{"class":7275},[269,8469,8470],{"class":1586}," '@vueuse\u002Fcore'\n",[269,8472,8473],{"class":271,"line":1613},[269,8474,2291],{"emptyLinePlaceholder":6},[269,8476,8477,8479,8481,8483,8485,8487,8489,8491],{"class":271,"line":1626},[269,8478,7702],{"class":7275},[269,8480,7705],{"class":7275},[269,8482,7708],{"class":7013},[269,8484,7711],{"class":1590},[269,8486,7715],{"class":7714},[269,8488,6526],{"class":1590},[269,8490,7720],{"class":7275},[269,8492,7723],{"class":1590},[269,8494,8495,8497,8499,8501],{"class":271,"line":1639},[269,8496,7728],{"class":7275},[269,8498,7731],{"class":1601},[269,8500,7734],{"class":7275},[269,8502,7737],{"class":1586},[269,8504,8505,8507,8510,8512],{"class":271,"line":1652},[269,8506,7728],{"class":7275},[269,8508,8509],{"class":1601}," innerWidth",[269,8511,7734],{"class":7275},[269,8513,8514],{"class":1590}," window.innerWidth\n",[269,8516,8517,8519,8521,8523,8525,8527,8529],{"class":271,"line":1665},[269,8518,7728],{"class":7275},[269,8520,7744],{"class":1601},[269,8522,259],{"class":7275},[269,8524,7749],{"class":7013},[269,8526,7734],{"class":7275},[269,8528,7754],{"class":7013},[269,8530,8531],{"class":1590},"(selector, {\n",[269,8533,8534,8537,8539,8542,8545,8548,8551,8554,8556,8559,8561,8564,8566,8568,8570,8573,8575,8578,8580,8583],{"class":271,"line":1678},[269,8535,8536],{"class":1590},"    margin: innerWidth ",[269,8538,7006],{"class":7275},[269,8540,8541],{"class":1601}," 640",[269,8543,8544],{"class":7275}," ?",[269,8546,8547],{"class":1601}," 12",[269,8549,8550],{"class":7275}," :",[269,8552,8553],{"class":1590}," innerWidth ",[269,8555,7006],{"class":7275},[269,8557,8558],{"class":1601}," 1024",[269,8560,8544],{"class":7275},[269,8562,8563],{"class":1601}," 24",[269,8565,8550],{"class":7275},[269,8567,8553],{"class":1590},[269,8569,7006],{"class":7275},[269,8571,8572],{"class":1601}," 1536",[269,8574,8544],{"class":7275},[269,8576,8577],{"class":1601}," 96",[269,8579,8550],{"class":7275},[269,8581,8582],{"class":1601}," 192",[269,8584,1610],{"class":1590},[269,8586,8587,8590],{"class":271,"line":1699},[269,8588,8589],{"class":1590},"    background: ",[269,8591,8592],{"class":1586},"''\n",[269,8594,8595],{"class":271,"line":1707},[269,8596,7813],{"class":1590},[269,8598,8599],{"class":271,"line":1727},[269,8600,2291],{"emptyLinePlaceholder":6},[269,8602,8603],{"class":271,"line":1733},[269,8604,8605],{"class":7290},"  \u002F\u002F responsive varying margin, calculated based on windowSize, upon @resize, debounced\n",[269,8607,8608,8610,8613,8615,8618,8621,8623],{"class":271,"line":1739},[269,8609,7728],{"class":7275},[269,8611,8612],{"class":1601}," debouncedFn",[269,8614,7734],{"class":7275},[269,8616,8617],{"class":7013}," useDebounceFn",[269,8619,8620],{"class":1590},"(() ",[269,8622,7720],{"class":7275},[269,8624,7723],{"class":1590},[269,8626,8627,8630,8632,8634],{"class":271,"line":2353},[269,8628,8629],{"class":7275},"    const",[269,8631,8509],{"class":1601},[269,8633,7734],{"class":7275},[269,8635,8514],{"class":1590},[269,8637,8638,8641,8644],{"class":271,"line":2358},[269,8639,8640],{"class":1590},"    zoom?.",[269,8642,8643],{"class":7013},"update",[269,8645,8646],{"class":1590},"({\n",[269,8648,8649,8652,8654,8656,8658,8660,8662,8664,8666,8668,8670,8672,8674,8676,8678,8680,8682,8684,8686],{"class":271,"line":2364},[269,8650,8651],{"class":1590},"      margin: innerWidth ",[269,8653,7006],{"class":7275},[269,8655,8541],{"class":1601},[269,8657,8544],{"class":7275},[269,8659,8547],{"class":1601},[269,8661,8550],{"class":7275},[269,8663,8553],{"class":1590},[269,8665,7006],{"class":7275},[269,8667,8558],{"class":1601},[269,8669,8544],{"class":7275},[269,8671,8563],{"class":1601},[269,8673,8550],{"class":7275},[269,8675,8553],{"class":1590},[269,8677,7006],{"class":7275},[269,8679,8572],{"class":1601},[269,8681,8544],{"class":7275},[269,8683,8577],{"class":1601},[269,8685,8550],{"class":7275},[269,8687,8688],{"class":1601}," 192\n",[269,8690,8691],{"class":271,"line":2370},[269,8692,8693],{"class":1590},"    })\n",[269,8695,8696,8699,8702],{"class":271,"line":2375},[269,8697,8698],{"class":1590},"  }, ",[269,8700,8701],{"class":1601},"200",[269,8703,8215],{"class":1590},[269,8705,8706],{"class":271,"line":7424},[269,8707,2291],{"emptyLinePlaceholder":6},[269,8709,8710,8713,8716,8718,8721],{"class":271,"line":7434},[269,8711,8712],{"class":1590},"  window.",[269,8714,8715],{"class":7013},"addEventListener",[269,8717,7777],{"class":1590},[269,8719,8720],{"class":1586},"'resize'",[269,8722,8723],{"class":1590},", debouncedFn)\n",[269,8725,8726],{"class":271,"line":7444},[269,8727,2291],{"emptyLinePlaceholder":6},[269,8729,8730],{"class":271,"line":7454},[269,8731,7766],{"class":7290},[269,8733,8734,8736,8738,8740,8742,8744,8746],{"class":271,"line":7460},[269,8735,7771],{"class":1590},[269,8737,7774],{"class":7013},[269,8739,7777],{"class":1590},[269,8741,7780],{"class":1586},[269,8743,7783],{"class":1590},[269,8745,7720],{"class":7275},[269,8747,7723],{"class":1590},[269,8749,8750,8752,8754],{"class":271,"line":7465},[269,8751,7792],{"class":1590},[269,8753,7795],{"class":7013},[269,8755,7798],{"class":1590},[269,8757,8758,8760,8762],{"class":271,"line":7475},[269,8759,7803],{"class":1590},[269,8761,7806],{"class":7013},[269,8763,7798],{"class":1590},[269,8765,8767],{"class":271,"line":8766},27,[269,8768,7813],{"class":1590},[269,8770,8772],{"class":271,"line":8771},28,[269,8773,2291],{"emptyLinePlaceholder":6},[269,8775,8777,8779,8781,8783,8785],{"class":271,"line":8776},29,[269,8778,7771],{"class":1590},[269,8780,7829],{"class":7013},[269,8782,7777],{"class":1590},[269,8784,7834],{"class":1586},[269,8786,7837],{"class":1590},[269,8788,8790],{"class":271,"line":8789},30,[269,8791,7842],{"class":1590},[276,8793,825],{"id":824},[206,8795,1317,8796,8798],{},[55,8797,7576],{}," library is a great fit for rich content pages with text and images such as blogs. Integration as a nuxt plugin is very convenient and easy.",[206,8800,8801],{},"It's extremely smooth to use in markdown files with nuxt content, and also plays well with nuxt image.",[206,8803,8804,7517],{},[909,8805],{"className":8806,"name":4099},[912,913,914,915],[276,8808,3834],{"id":3833},[16,8810,8811,8817,8823,8828,8833,8838,8843],{},[19,8812,8813],{},[23,8814,8815],{"href":8815,"rel":8816},"https:\u002F\u002Fmedium-zoom.francoischalifour.com\u002F",[27],[19,8818,8819],{},[23,8820,8821],{"href":8821,"rel":8822},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fguide\u002Fdirectory-structure\u002Fplugins#creating-plugins",[27],[19,8824,8825],{},[23,8826,7878],{"href":7878,"rel":8827},[27],[19,8829,8830],{},[23,8831,7887],{"href":7887,"rel":8832},[27],[19,8834,8835],{},[23,8836,8122],{"href":8122,"rel":8837},[27],[19,8839,8840],{},[23,8841,8180],{"href":8180,"rel":8842},[27],[19,8844,8845],{},[23,8846,8430],{"href":8430,"rel":8847},[27],[983,8849,8850],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .svl0z, html code.shiki .svl0z{--shiki-default:#032F62;--shiki-default-text-decoration:underline;--shiki-dark:#DBEDFF;--shiki-dark-text-decoration:underline}html pre.shiki code .s2frl, html code.shiki .s2frl{--shiki-default:#24292E;--shiki-default-text-decoration:underline;--shiki-dark:#E1E4E8;--shiki-dark-text-decoration:underline}",{"title":183,"searchDepth":184,"depth":184,"links":8852},[8853,8857,8865,8868,8869],{"id":6948,"depth":184,"text":6949,"children":8854},[8855,8856],{"id":7619,"depth":991,"text":7620},{"id":7647,"depth":991,"text":7648},{"id":7896,"depth":184,"text":7897,"children":8858},[8859,8860,8861,8862,8863],{"id":7900,"depth":991,"text":7901},{"id":7948,"depth":991,"text":7949},{"id":8115,"depth":991,"text":8116},{"id":8190,"depth":991,"text":8191},{"id":8269,"depth":991,"text":8864},"NuxtApp Plugin Helper",{"id":4373,"depth":184,"text":5844,"children":8866},[8867],{"id":8393,"depth":991,"text":8394},{"id":824,"depth":184,"text":825},{"id":3833,"depth":184,"text":3834},"2023-02-20","Learn how easy and convenient the amazing 'medium-zoom' library can be integrated with Nuxt3 as a client-side plugin.","6min",{},{"title":7565,"description":8871},"blog\u002F5.nuxt3-plugin-medium-zoom",[7559,7560,7561,8877,7249],"nuxt","wt6AyQ3vn9qnB7liSXFXbRn2RAyODRV2ism-mcFVA-I",{"id":4,"title":5,"active":6,"body":8880,"date":186,"description":187,"duration":188,"extension":189,"level":190,"meta":8987,"navigation":6,"ogDescription":190,"ogImage":190,"ogImageAlt":190,"ogTitle":190,"path":192,"pinned":190,"seo":8988,"stem":194,"tags":8989,"titleAlt":197,"titlePage":190,"__hash__":198},{"type":8,"value":8881,"toc":8985},[8882,8884,8924,8926,8940,8942],[11,8883,14],{"id":13},[16,8885,8886,8905,8917],{},[19,8887,21,8888,8891,32,8893,8896],{},[23,8889,28],{"href":25,"rel":8890},[27],[30,8892],{},[23,8894,37],{"href":35,"rel":8895},[27],[16,8897,8898,8900],{},[19,8899,42],{},[19,8901,45,8902],{},[23,8903,50],{"href":48,"rel":8904},[27],[19,8906,53,8907,58,8909,8912,8914],{},[55,8908,57],{},[23,8910,63],{"href":61,"rel":8911},[27],[30,8913],{},[23,8915,68],{"href":68,"rel":8916},[27],[19,8918,72,8919,76,8921],{},[55,8920,75],{},[23,8922,81],{"href":79,"rel":8923},[27],[11,8925,85],{"id":84},[16,8927,8928,8933],{},[19,8929,8930],{},[23,8931,94],{"href":92,"rel":8932},[27],[19,8934,8935,100,8937],{},[55,8936,99],{},[23,8938,103],{"href":103,"rel":8939},[27],[11,8941,108],{"id":107},[16,8943,8944,8956,8958],{},[19,8945,113,8946],{},[16,8947,8948],{},[19,8949,118,8950,124,8953],{},[23,8951,123],{"href":121,"rel":8952},[27],[23,8954,129],{"href":127,"rel":8955},[27],[19,8957,132],{},[19,8959,135,8960,140,8962,144,8964],{},[137,8961,139],{},[55,8963,143],{},[16,8965,8966,8975,8980],{},[19,8967,149,8968,154,8970,158,8972],{},[151,8969,153],{},[151,8971,157],{},[23,8973,163],{"href":161,"rel":8974},[27],[19,8976,166,8977,172],{},[23,8978,171],{"href":169,"rel":8979},[27],[19,8981,175,8982,181],{},[23,8983,180],{"href":178,"rel":8984},[27],{"title":183,"searchDepth":184,"depth":184,"links":8986},[],{},{"title":5,"description":187},[196],{"id":8991,"title":8992,"active":6,"body":8993,"date":9063,"description":187,"duration":188,"extension":189,"level":190,"meta":9064,"navigation":6,"ogDescription":190,"ogImage":190,"ogImageAlt":190,"ogTitle":190,"path":9065,"pinned":190,"seo":9066,"stem":9067,"tags":9068,"titleAlt":9069,"titlePage":190,"__hash__":9070},"blog\u002Fblog\u002F10006.weekly-digest-2023-cw3-cw4.md","Weekly Digest · 2023 CW 3 + CW 4",{"type":8,"value":8994,"toc":9061},[8995,8997,9027,9029,9051,9053,9056],[11,8996,14],{"id":13},[16,8998,8999,9013],{},[19,9000,9001,9002,9007,9009],{},"Blog post on data fetching with Nuxt3 by ",[23,9003,9006],{"href":9004,"rel":9005},"https:\u002F\u002Ftwitter.com\u002Fdanpastori",[27],"@danpastori",[30,9008],{},[23,9010,9011],{"href":9011,"rel":9012},"https:\u002F\u002Fserversideup.net\u002Fadvanced-data-fetching-with-nuxt-3\u002F",[27],[19,9014,9015,9016,9021,9023],{},"A new beautiful diagram on 'Nuxt3 Directory Structure' by ",[23,9017,9020],{"href":9018,"rel":9019},"https:\u002F\u002Ftwitter.com\u002FKrutiePatel",[27],"@KrutiePatel",[30,9022],{},[23,9024,9025],{"href":9025,"rel":9026},"https:\u002F\u002Ftwitter.com\u002FKrutiePatel\u002Fstatus\u002F1617431336402440194?s=20&t=MJTa9Fvbt6Plsnvry9b_JA",[27],[11,9028,85],{"id":84},[16,9030,9031,9041],{},[19,9032,9033,9036,9037],{},[55,9034,9035],{},"Nuxt v3.1.0"," minor release: ",[23,9038,9039],{"href":9039,"rel":9040},"https:\u002F\u002Fgithub.com\u002Fnuxt\u002Fnuxt\u002Freleases\u002Fv3.1.0",[27],[19,9042,9043,9046,9047],{},[55,9044,9045],{},"Astro 2.0"," has been released, congrats to the team! ",[23,9048,9049],{"href":9049,"rel":9050},"https:\u002F\u002Fastro.build\u002Fblog\u002Fastro-2\u002F",[27],[11,9052,108],{"id":107},[206,9054,9055],{},"I've been on holiday in the past week, so there's little progress on thriving.dev:",[16,9057,9058],{},[19,9059,9060],{},"thriving.dev now with some basic SEO meta tags using the brand new (nuxt v3.1.0) Composables useSeoMeta and useServerSeoMeta.",{"title":183,"searchDepth":184,"depth":184,"links":9062},[],"2023-01-30",{},"\u002Fblog\u002Fweekly-digest-2023-cw3-cw4",{"title":8992,"description":187},"blog\u002F10006.weekly-digest-2023-cw3-cw4",[196],"2023 Calendar Week #3 & #4","4Kair94jTErEbE-CXy_KYUnYOylLlaNRNxb38bEzjPw",{"id":9072,"title":9073,"active":6,"body":9074,"date":9141,"description":187,"duration":188,"extension":189,"level":190,"meta":9142,"navigation":6,"ogDescription":190,"ogImage":190,"ogImageAlt":190,"ogTitle":190,"path":9143,"pinned":190,"seo":9144,"stem":9145,"tags":9146,"titleAlt":9147,"titlePage":190,"__hash__":9148},"blog\u002Fblog\u002F10005.weekly-digest-2023-cw1-cw2.md","Weekly Digest · 2023 CW 1 + CW 2",{"type":8,"value":9075,"toc":9139},[9076,9080,9091,9093,9104,9106],[11,9077,9079],{"id":9078},"credits-references","Credits & References",[16,9081,9082],{},[19,9083,9084,9085,9087],{},"I've been analysing and profiling a Java app in need of POJO serialization for data persistence and found following comparison by @jianzhang.yjz extremely valuable:",[30,9086],{},[23,9088,9089],{"href":9089,"rel":9090},"https:\u002F\u002Fwww.alibabacloud.com\u002Fblog\u002Fan-introduction-and-comparison-of-several-common-java-serialization-frameworks_597900",[27],[11,9092,14],{"id":13},[16,9094,9095],{},[19,9096,9097,9098,9100],{},"Post by Neil Buesing on getting started with Apache Kafka, Apache Flink, Confluent’s Schema Registry:",[30,9099],{},[23,9101,9102],{"href":9102,"rel":9103},"https:\u002F\u002Fwww.kineticedge.io\u002Fblog\u002Ffink-and-kafka\u002F",[27],[11,9105,85],{"id":84},[16,9107,9108,9121],{},[19,9109,9110,154,9113,9116,9117],{},[55,9111,9112],{},"Arrow 1.1.4",[55,9114,9115],{},"1.1.5"," is now available ",[23,9118,9119],{"href":9119,"rel":9120},"https:\u002F\u002Fwww.47deg.com\u002Fblog\u002Farrow-v1-01-4-release\u002F",[27],[19,9122,9123,9124],{},"A new Thriving.dev project has been made public + tagged as an initial stable release v0.1.0:\n",[16,9125,9126,9131],{},[19,9127,9128,9130],{},[55,9129,800],{}," is a Kafka Streams State Store implementation that persists data to Apache Cassandra. For now, only KeyValueStore type is supported.",[19,9132,9133,9134,9136],{},"Source code, quick start, and examples can be found on Github",[30,9135],{},[23,9137,798],{"href":798,"rel":9138},[27],{"title":183,"searchDepth":184,"depth":184,"links":9140},[],"2023-01-16",{},"\u002Fblog\u002Fweekly-digest-2023-cw1-cw2",{"title":9073,"description":187},"blog\u002F10005.weekly-digest-2023-cw1-cw2",[196],"2023 Calendar Week #1 & #2","IWR2j7Nt6VqifxBrbFbdym1lEZjHx1fVH0xB1KAZZk0",{"id":9150,"title":9151,"active":6,"body":9152,"date":9216,"description":9217,"duration":9218,"extension":189,"level":190,"meta":9219,"navigation":6,"ogDescription":190,"ogImage":9220,"ogImageAlt":190,"ogTitle":9151,"path":9221,"pinned":190,"seo":9222,"stem":9223,"tags":9224,"titleAlt":190,"titlePage":190,"__hash__":9227},"blog\u002Fblog\u002F1.hello-world.md","Hello World!!!",{"type":8,"value":9153,"toc":9212},[9154,9163,9171,9174,9178,9185,9188,9206,9209],[206,9155,9156,9157,9162],{},"Today, I'm happy to officially announce ",[23,9158,9161],{"href":9159,"rel":9160},"https:\u002F\u002Fthriving.dev",[27],"thriving.dev"," to the world!!!",[206,9164,9165],{},[209,9166],{"alt":9167,"height":9168,"preload":183,"src":9169,"width":9170},"A picture containing the text 'Hello World' in the center, surrounded by translated variants in different languages.",853,"\u002Fassets\u002Fblog\u002F1.hello-world\u002Fthriving-dev-hello-world_t.png",2483,[206,9172,9173],{},"Thriving.dev is my new playground. A platform to provide learning materials on software architecture, coding, webapps, working with data.",[281,9175,9177],{"id":9176},"public-roadmap","Public Roadmap",[206,9179,9180,9181,347],{},"Having many things planned, I'm striving to be transparent with a public ",[23,9182,9184],{"href":9183},"\u002Froadmap","roadmap & changelog",[206,9186,9187],{},"The first 2 online courses are already in the making:",[489,9189,9192],{"className":9190},[9191],"[&>*>li]:my-0",[456,9193,9194,9200],{},[19,9195,9196],{},[23,9197,9199],{"href":9198},"\u002Fcourses\u002Fsoftware-architecture-concepts-and-methodologies","Software Architecture Concepts & Methodologies",[19,9201,9202],{},[23,9203,9205],{"href":9204},"\u002Fcourses\u002Fapplied-modern-data-driven-software-architecture","Applied Modern Data-Driven Software Architecture",[281,9207,9208],{"id":50},"Blog",[206,9210,9211],{},"While I'm somewhat struggling to get started in writing blog posts, there's already a backlog of topics I'd like to share. So be patient, bear with me and wait for good news.",{"title":183,"searchDepth":184,"depth":184,"links":9213},[9214,9215],{"id":9176,"depth":991,"text":9177},{"id":50,"depth":991,"text":9208},"2023-01-12","Say hello to Thriving.dev! A platform to provide learning materials on software architecture, coding, webapps, working with data.","1min",{},"\u002Fassets\u002Fblog\u002F1.hello-world\u002Fthriving-dev-hello-world_twitter.png","\u002Fblog\u002Fhello-world",{"title":9151,"description":9217},"blog\u002F1.hello-world",[9225,9226],"headline","announcement","fooQXZMyZgSnfepyGZOVlTgLUwJqfs_SaokZ75jUoKU",{"id":9229,"title":9230,"active":6,"body":9231,"date":9308,"description":187,"duration":188,"extension":189,"level":190,"meta":9309,"navigation":6,"ogDescription":190,"ogImage":190,"ogImageAlt":190,"ogTitle":190,"path":9310,"pinned":190,"seo":9311,"stem":9312,"tags":9313,"titleAlt":9314,"titlePage":190,"__hash__":9315},"blog\u002Fblog\u002F10004.weekly-digest-2022-cw52.md","Weekly Digest · 2022 CW 52",{"type":8,"value":9232,"toc":9306},[9233,9235,9259,9261,9276,9280,9283,9295],[11,9234,9079],{"id":9078},[16,9236,9237,9246],{},[19,9238,9239,9240,9242],{},"I've had to write a util cli, I decided to go with nodejs...",[30,9241],{},[23,9243,9244],{"href":9244,"rel":9245},"https:\u002F\u002Fwww.twilio.com\u002Fblog\u002Fhow-to-build-a-cli-with-node-js",[27],[19,9247,9248,9249,9252,9253,9255],{},"For a new maven lib in the making, I enrolled with Sonar ",[137,9250,9251],{},"dev.thriving.oss"," and setup my gradle project for publishing to maven central.",[30,9254],{},[23,9256,9257],{"href":9257,"rel":9258},"https:\u002F\u002Fh4pehl.medium.com\u002Fpublish-your-gradle-artifacts-to-maven-central-f74a0af085b1",[27],[11,9260,14],{"id":13},[16,9262,9263],{},[19,9264,9265,9266,9269,9270,9272],{},"GitHub Actions Marketplace: ",[55,9267,9268],{},"setup-rok"," installs the rok CLI of Immerok Cloud in your workflow.",[30,9271],{},[23,9273,9274],{"href":9274,"rel":9275},"https:\u002F\u002Fgithub.com\u002Fmarketplace\u002Factions\u002Fcli-for-immerok-cloud",[27],[11,9277,9279],{"id":9278},"non-technical","Non-technical",[206,9281,9282],{},"Here's a quote I came across that occupied my mind and got me thinking:",[289,9284,9285],{},[206,9286,9287,9288,3677,9290,340],{},"“The real damage is done by those millions who want to 'survive.' The honest men who just want to be left in peace. Those who don’t want their little lives disturbed by anything bigger than themselves. Those with no sides and no causes. Those who won’t take measure of their own strength, for fear of antagonizing their own weakness. Those who don’t like to make waves—or enemies. Those for whom freedom, honour, truth, and principles are only literature. Those who live small, mate small, die small. It’s the reductionist approach to life: if you keep it small, you’ll keep it under control. If you don’t make any noise, the bogeyman won’t find you. But it’s all an illusion, because they die too, those people who roll up their spirits into tiny little balls so as to be safe. Safe?! From what? Life is always on the edge of death; narrow streets lead to the same place as wide avenues, and a little candle burns itself out just like a flaming torch does. I choose my own way to burn.”",[30,9289],{},[23,9291,9294],{"href":9292,"rel":9293},"https:\u002F\u002Fwww.goodreads.com\u002Fquotes\u002F217576-the-real-damage-is-done-by-those-millions-who-want",[27],"Source",[206,9296,9297],{},[151,9298,9299,9301,9302,9305],{},[55,9300,6136],{}," The originator is put as ",[55,9303,9304],{},"Sophie Scholl"," - though it seems that's unaccounted for.",{"title":183,"searchDepth":184,"depth":184,"links":9307},[],"2023-01-02",{},"\u002Fblog\u002Fweekly-digest-2022-cw52",{"title":9230,"description":187},"blog\u002F10004.weekly-digest-2022-cw52",[196],"2022 Calendar Week #52","FDud86myeY45xrmnVcT2vRYxQECY4v9zvvCyBBBaKrk",{"id":9317,"title":9318,"active":6,"body":9319,"date":9346,"description":187,"duration":9218,"extension":189,"level":190,"meta":9347,"navigation":6,"ogDescription":190,"ogImage":190,"ogImageAlt":190,"ogTitle":190,"path":9348,"pinned":190,"seo":9349,"stem":9350,"tags":9351,"titleAlt":9352,"titlePage":190,"__hash__":9353},"blog\u002Fblog\u002F10003.weekly-digest-2022-cw46.md","Weekly Digest · 2022 CW 46",{"type":8,"value":9320,"toc":9344},[9321,9325,9334,9336],[11,9322,9324],{"id":9323},"what-peaked-my-interest","What peaked my interest",[16,9326,9327],{},[19,9328,9329,9333],{},[23,9330,9331],{"href":9331,"rel":9332},"https:\u002F\u002Finfinispan.org\u002F",[27]," - use for app embedded synchronized in-memory cache, async API. Planning to do a POC.",[11,9335,14],{"id":13},[16,9337,9338],{},[19,9339,9340],{},[23,9341,9342],{"href":9342,"rel":9343},"https:\u002F\u002Fkonghq.com\u002Fblog\u002Fkubernetes-ingress-grpc-example",[27],{"title":183,"searchDepth":184,"depth":184,"links":9345},[],"2022-11-20",{},"\u002Fblog\u002Fweekly-digest-2022-cw46",{"title":9318,"description":187},"blog\u002F10003.weekly-digest-2022-cw46",[196],"2022 Calendar Week #46","TBSufCR0dBHaBWiu6ZJkNXt7MXlpVwY6N_3ZuNDr0Zc",{"id":9355,"title":9356,"active":6,"body":9357,"date":9391,"description":187,"duration":9218,"extension":189,"level":190,"meta":9392,"navigation":6,"ogDescription":190,"ogImage":190,"ogImageAlt":190,"ogTitle":190,"path":9393,"pinned":190,"seo":9394,"stem":9395,"tags":9396,"titleAlt":9397,"titlePage":190,"__hash__":9398},"blog\u002Fblog\u002F10002.weekly-digest-2022-cw45.md","Weekly Digest · 2022 CW 45",{"type":8,"value":9358,"toc":9389},[9359,9361,9366,9368],[11,9360,9324],{"id":9323},[16,9362,9363],{},[19,9364,9365],{},"Vercel Analytics - no cookies, better fit than GA or similar?",[11,9367,14],{"id":13},[16,9369,9370,9376,9382],{},[19,9371,9372],{},[23,9373,9374],{"href":9374,"rel":9375},"https:\u002F\u002Fpiotrminkowski.com\u002F2022\u002F06\u002F28\u002Fmanage-kubernetes-cluster-with-terraform-and-argo-cd\u002F",[27],[19,9377,9378],{},[23,9379,9380],{"href":9380,"rel":9381},"https:\u002F\u002Ftailwind-elements.com\u002F",[27],[19,9383,9384,9385],{},"Fallback Font - ",[23,9386,9387],{"href":9387,"rel":9388},"https:\u002F\u002Fdeploy-preview-15--upbeat-shirley-608546.netlify.app\u002Fperfect-ish-font-fallback\u002F?font=Montserrat",[27],{"title":183,"searchDepth":184,"depth":184,"links":9390},[],"2022-11-13",{},"\u002Fblog\u002Fweekly-digest-2022-cw45",{"title":9356,"description":187},"blog\u002F10002.weekly-digest-2022-cw45",[196],"2022 Calendar Week #45","0bSGRbMce_YUFdLtWQD_OLrqrWFKZB2AKhwyO0oVIug",{"id":9400,"title":9401,"active":6,"body":9402,"date":9441,"description":187,"duration":9218,"extension":189,"level":190,"meta":9442,"navigation":6,"ogDescription":190,"ogImage":190,"ogImageAlt":190,"ogTitle":190,"path":9443,"pinned":190,"seo":9444,"stem":9445,"tags":9446,"titleAlt":9447,"titlePage":190,"__hash__":9448},"blog\u002Fblog\u002F10001.weekly-digest-2022-cw43.md","Weekly Digest · 2022 CW 43",{"type":8,"value":9403,"toc":9439},[9404,9406],[11,9405,9324],{"id":9323},[16,9407,9408,9414],{},[19,9409,9410],{},[23,9411,9412],{"href":9412,"rel":9413},"https:\u002F\u002Fflink.apache.org\u002Fnews\u002F2022\u002F10\u002F28\u002F1.16-announcement.html",[27],[19,9415,9416,9420],{},[23,9417,9418],{"href":9418,"rel":9419},"https:\u002F\u002Fvercel.com\u002Fblog\u002Fturbopack",[27],[16,9421,9422],{},[19,9423,9424,9425],{},"Exciting but also a little bit controversial, also check Evan You's analysis and point of view, here are 2 threads\n",[16,9426,9427,9433],{},[19,9428,9429],{},[23,9430,9431],{"href":9431,"rel":9432},"https:\u002F\u002Ftwitter.com\u002Fyouyuxi\u002Fstatus\u002F1585040266964406273",[27],[19,9434,9435],{},[23,9436,9437],{"href":9437,"rel":9438},"https:\u002F\u002Ftwitter.com\u002Fyouyuxi\u002Fstatus\u002F1586042491739860993",[27],{"title":183,"searchDepth":184,"depth":184,"links":9440},[],"2022-10-31",{},"\u002Fblog\u002Fweekly-digest-2022-cw43",{"title":9401,"description":187},"blog\u002F10001.weekly-digest-2022-cw43",[196],"2022 Calendar Week #43","EGEjlYs4rPi05jJehAuFl4jsroqP2GADBOaWE62J54c",1778283119779]