Skip to content

CAMEL-24626: camel-master - leadership gets its own lock, and cancelled start tasks leave the task registry - #26112

Open
henrik242 wants to merge 3 commits into
apache:mainfrom
henrik242:camel-24626-master-leadership-followups
Open

CAMEL-24626: camel-master - leadership gets its own lock, and cancelled start tasks leave the task registry#26112
henrik242 wants to merge 3 commits into
apache:mainfrom
henrik242:camel-24626-master-leadership-followups

Conversation

@henrik242

Copy link
Copy Markdown
Contributor

Fixes CAMEL-24626.

Three follow-ups to #26028, listed as known pre-existing issues in that PR's description. None of
them was introduced or fixed there.

1. A cancelled BackgroundTask stays in the TaskManagerRegistry (camel-support)

A task scheduled through BackgroundTask.schedule() adds itself to the TaskManagerRegistry from
its first run, and only a run of the task removes it again. A caller that cancels the Future
returned by schedule() therefore leaves the entry behind for the life of the CamelContext: the
task keeps being listed as an internal task, and it keeps its container reachable.

BackgroundTask.cancel(boolean) now unschedules the task, releases the latch so no queued run does
any work, marks the task Inactive and removes it from the registry. A run that starts while a
cancel is landing undoes its own registration, so the two cannot race a stale entry back in.

schedule() is used in exactly two places, camel-sjms and camel-master, and both now cancel
through the task. The other thirteen Tasks.backgroundTask() users go through the blocking run(),
which already deregisters on every exit path.

2. Lock inversion between the consumer and the cluster view (camel-master)

MasterConsumer guarded its leadership state with the BaseService lock:

  • doStop holds that lock and then needs the write lock of the view, through
    CamelClusterView.removeEventListener.
  • AbstractCamelClusterView dispatches events while holding its own read lock, and the listener of
    the consumer then needs the BaseService lock.

The unlocked isRunAllowed() fast path added in #26028 covers the common case, but a leadership
event that passes that check just before a stop acquires the lock still closes the two orders into a
deadlock.

The leadership state and the pending start task now have a lock of their own, which doStop
releases before it touches the view. Nothing that holds it ever waits for the view, so the cycle is
gone rather than narrowed. It also decouples leadership handling from the service lifecycle: a
leadership event and a start attempt no longer wait for whatever lifecycle operation is in progress.

One correction to the description of #26028, which claimed doStop waits for the leader pool to
terminate while an in-flight attempt waits for the lock. It does not:
ExecutorServiceManager.shutdown() passes an await timeout of 0, so that path was unnecessary
coupling rather than a deadlock. The code comment says what actually happens.

3. Exhausted start attempts are not documented (camel-master)

backOffMaxAttempts defaults to 10 attempts, backOffDelay apart (5000 millis). A node that uses
up its attempts keeps the leadership and consumes nothing until the leadership changes again. That
is the documented intent of the option, and backOffMaxAttempts=0 already retries for as long as
the node is the leader, because the budget builder ignores non-positive values and keeps its
unlimited default. Neither the consequence nor the escape hatch was written down, so both are now in
the component documentation.

Testing

Two tests in MasterConsumerLeadershipTest, both checked against unpatched main and failing
there:

  • testCancellingAPendingStartRemovesTheTaskFromTheRegistry keeps a start task retrying, loses the
    leadership, and asserts the task leaves the registry. Before the change it stays.
  • testEventDispatchIsNotBlockedByALifecycleOperation holds the service lock through a suspend that
    blocks inside the delegate, then asserts a leadership event is still dispatched. Before the change
    the dispatch waits for the lock, which is the wait that deadlocks against the view.

Two tests in BackgroundTaskTest cover cancelling a task that is running and cancelling one before
its first run: unscheduled, deregistered, Inactive, and no further attempt.

Green locally: camel-support (119), camel-master (30), the camel-core task tests (34), and
-Psourcecheck on both changed main modules. The camel-sjms main sources compile, but its tests
need an Artemis test-infra artifact that is not installed locally, so CI has to cover that call site.

Notes for the reviewer

  • The description of the backOffMaxAttempts option itself is deliberately unchanged. It feeds
    three generated mirrors (the component json, the catalog json and MasterComponentBuilderFactory),
    and the catalog and componentdsl ones cannot be regenerated without a full build, so a hand-edited
    version would fail the uncommitted-changes check. Worth a small follow-up from someone with a full
    build, since that description is what tooling and IDE completion show.
  • Both bugs are present on camel-4.22.x as well: the registry leak through camel-sjms predates
    this work, and the lock inversion came along with the backport of CAMEL-24583. Happy to open a
    backport PR if you want it there.

@henrik242 henrik242 changed the title Camel 24626 master leadership followups CAMEL-24626: camel-master - leadership gets its own lock, and cancelled start tasks leave the task registry Sep 4, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

🌟 Thank you for your contribution to the Apache Camel project! 🌟
🤖 CI automation will test this PR automatically.

🐫 Apache Camel Committers, please review the following items:

  • First-time contributors require MANUAL approval for the GitHub Actions to run
  • You can use the command /component-test (camel-)component-name1 (camel-)component-name2.. to request a test from the test bot although they are normally detected and executed by CI.
  • You can label PRs using skip-tests and test-dependents to fine-tune the checks executed by this PR.
  • Build and test logs are available in the summary page. Only Apache Camel committers have access to the summary.

⚠️ Be careful when sharing logs. Review their contents before sharing them publicly.

…registers a scheduled task

A task adds itself to the TaskManagerRegistry from its first run, and only a run of the task
removes it again. Cancelling the future returned by schedule() therefore left the entry behind
for the life of the context. camel-sjms now cancels through the task instead of the future.
…ot the service lock

doStop holds the service lock and needs the write lock of the cluster view to remove the
listener, while the view dispatches events holding its read lock and then needs the consumer
lock. A leadership event that passes the isRunAllowed fast path just before a stop acquires
the lock closes the two orders into a deadlock. The leadership now has a lock of its own,
which doStop releases before it touches the view.

The pending start task is also cancelled through the task rather than through its future, so
it leaves the TaskManagerRegistry with it.
…ted consumer

A leader that uses up backOffMaxAttempts consumes nothing until the leadership changes again,
and setting the option to 0 retries for as long as the node is the leader.
@davsclaus
davsclaus force-pushed the camel-24626-master-leadership-followups branch from fc50d20 to f1257f1 Compare September 5, 2026 11:38
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • catalog/camel-catalog
  • components/camel-master
  • components/camel-sjms
  • core/camel-core
  • core/camel-support

ℹ️ Dependent modules were not tested because the total number of affected modules exceeded the threshold (50). Use the test-dependents label to force testing all dependents.


🔬 Scalpel shadow comparison — Scalpel: 563 tested, 26 compile-only — current: 562 all tested

Maveniverse Scalpel detected 589 affected modules (current approach: 562).

⚠️ Modules only in Scalpel (27)
  • apache-camel
  • camel-allcomponents
  • camel-catalog
  • camel-catalog-console
  • camel-catalog-maven
  • camel-catalog-suggest
  • camel-componentdsl
  • camel-endpointdsl
  • camel-endpointdsl-support
  • camel-itest
  • camel-jbang-core
  • camel-jbang-it
  • camel-jbang-main
  • camel-jbang-plugin-edit
  • camel-jbang-plugin-generate
  • camel-jbang-plugin-kubernetes
  • camel-jbang-plugin-test
  • camel-kamelet-main
  • camel-launcher
  • camel-report-maven-plugin
  • camel-route-parser
  • camel-yaml-dsl
  • camel-yaml-dsl-deserializers
  • camel-yaml-dsl-maven-plugin
  • coverage
  • docs
  • dummy-component

Skip-tests mode would test 563 modules (5 direct + 558 downstream), skip tests for 26 (generated code, meta-modules)

Modules Scalpel would test (563)
  • archetypes
  • camel-a2a
  • camel-activemq
  • camel-activemq6
  • camel-ai-observability
  • camel-ai-observability-api
  • camel-ai-parent
  • camel-ai-resource
  • camel-ai-tool
  • camel-alibaba-common
  • camel-alibaba-eventbridge
  • camel-alibaba-fc
  • camel-alibaba-kms
  • camel-alibaba-mns
  • camel-alibaba-oss
  • camel-alibaba-ots
  • camel-alibaba-parent
  • camel-alibaba-sls
  • camel-alibaba-sms
  • camel-amqp
  • camel-api-component-maven-plugin
  • camel-arangodb
  • camel-archetype-api-component
  • camel-archetype-component
  • camel-archetype-dataformat
  • camel-archetype-java
  • camel-archetype-main
  • camel-as2
  • camel-as2-api
  • camel-as2-parent
  • camel-asn1
  • camel-asterisk
  • camel-atmosphere-websocket
  • camel-atom
  • camel-attachments
  • camel-avro
  • camel-avro-rpc
  • camel-avro-rpc-jetty
  • camel-avro-rpc-parent
  • camel-avro-rpc-spi
  • camel-aws-bedrock
  • camel-aws-cloudtrail
  • camel-aws-common
  • camel-aws-config
  • camel-aws-parameter-store
  • camel-aws-parent
  • camel-aws-secrets-manager
  • camel-aws-security-hub
  • camel-aws2-athena
  • camel-aws2-comprehend
  • camel-aws2-cw
  • camel-aws2-ddb
  • camel-aws2-ec2
  • camel-aws2-ecs
  • camel-aws2-eks
  • camel-aws2-eventbridge
  • camel-aws2-iam
  • camel-aws2-kinesis
  • camel-aws2-kms
  • camel-aws2-lambda
  • camel-aws2-mq
  • camel-aws2-msk
  • camel-aws2-polly
  • camel-aws2-redshift
  • camel-aws2-rekognition
  • camel-aws2-s3
  • camel-aws2-s3-vectors
  • camel-aws2-ses
  • camel-aws2-sns
  • camel-aws2-sqs
  • camel-aws2-step-functions
  • camel-aws2-sts
  • camel-aws2-textract
  • camel-aws2-timestream
  • camel-aws2-transcribe
  • camel-aws2-translate
  • camel-azure-common
  • camel-azure-cosmosdb
  • camel-azure-eventgrid
  • camel-azure-eventhubs
  • camel-azure-files
  • camel-azure-functions
  • camel-azure-key-vault
  • camel-azure-parent
  • camel-azure-schema-registry
  • camel-azure-servicebus
  • camel-azure-storage-blob
  • camel-azure-storage-datalake
  • camel-azure-storage-queue
  • camel-barcode
  • camel-base
  • camel-base-engine
  • camel-base64
  • camel-bean
  • camel-bean-validator
  • camel-beanio
  • camel-bindy
  • camel-bonita
  • camel-box
  • camel-box-api
  • camel-box-parent
  • camel-braintree
  • camel-browse
  • camel-caffeine
  • camel-camunda
  • camel-cassandraql
  • camel-catalog
  • camel-catalog-common
  • camel-cbor
  • camel-chatscript
  • camel-chunk
  • camel-cli-connector
  • camel-cli-debug
  • camel-clickhouse
  • camel-clickup
  • camel-cloudevents
  • camel-cluster
  • camel-cm-sms
  • camel-coap
  • camel-cometd
  • camel-console
  • camel-consul
  • camel-controlbus
  • camel-core
  • camel-core-all
  • camel-core-engine
  • camel-core-languages
  • camel-core-model
  • camel-core-processor
  • camel-core-reifier
  • camel-core-xml
  • camel-couchbase
  • camel-couchdb
  • camel-cron
  • camel-crypto
  • camel-crypto-pgp
  • camel-csv
  • camel-cxf-common
  • camel-cxf-parent
  • camel-cxf-rest
  • camel-cxf-soap
  • camel-cxf-spring-common
  • camel-cxf-spring-rest
  • camel-cxf-spring-soap
  • camel-cxf-spring-transport
  • camel-cxf-transport
  • camel-cyberark-vault
  • camel-dapr
  • camel-dataformat
  • camel-dataset
  • camel-datasonnet
  • camel-dataweave
  • camel-debezium-common
  • camel-debezium-common-parent
  • camel-debezium-db2
  • camel-debezium-maven-plugin
  • camel-debezium-mongodb
  • camel-debezium-mysql
  • camel-debezium-oracle
  • camel-debezium-parent
  • camel-debezium-postgres
  • camel-debezium-sqlserver
  • camel-debug
  • camel-dfdl
  • camel-dhis2
  • camel-dhis2-api
  • camel-dhis2-parent
  • camel-diagram
  • camel-direct
  • camel-disruptor
  • camel-djl
  • camel-dns
  • camel-docker
  • camel-docling
  • camel-drill
  • camel-dropbox
  • camel-dsl-modeline
  • camel-dsl-support
  • camel-duckdb
  • camel-dynamic-router
  • camel-ehcache
  • camel-elasticsearch
  • camel-elasticsearch-rest-client
  • camel-event
  • camel-exec
  • camel-fastjson
  • camel-fhir
  • camel-fhir-api
  • camel-fhir-parent
  • camel-file
  • camel-file-watch
  • camel-flatpack
  • camel-flink
  • camel-flowable
  • camel-fop
  • camel-fory
  • camel-freemarker
  • camel-ftp
  • camel-ftp-common
  • camel-geocoder
  • camel-git
  • camel-github2
  • camel-google-bigquery
  • camel-google-calendar
  • camel-google-common
  • camel-google-drive
  • camel-google-firestore
  • camel-google-functions
  • camel-google-mail
  • camel-google-parent
  • camel-google-pubsub
  • camel-google-secret-manager
  • camel-google-sheets
  • camel-google-speech-to-text
  • camel-google-storage
  • camel-google-text-to-speech
  • camel-google-vertexai
  • camel-google-vision
  • camel-graphql
  • camel-grok
  • camel-groovy
  • camel-grpc
  • camel-gson
  • camel-hashicorp-vault
  • camel-hazelcast
  • camel-health
  • camel-hivemq
  • camel-hl7
  • camel-http
  • camel-http-base
  • camel-http-common
  • camel-huawei-parent
  • camel-huaweicloud-common
  • camel-huaweicloud-dms
  • camel-huaweicloud-frs
  • camel-huaweicloud-functiongraph
  • camel-huaweicloud-iam
  • camel-huaweicloud-imagerecognition
  • camel-huaweicloud-obs
  • camel-huaweicloud-smn
  • camel-huggingface
  • camel-ibm-cos
  • camel-ibm-parent
  • camel-ibm-secrets-manager
  • camel-ibm-watson-discovery
  • camel-ibm-watson-language
  • camel-ibm-watson-speech-to-text
  • camel-ibm-watson-text-to-speech
  • camel-ibm-watsonx-ai
  • camel-ibm-watsonx-data
  • camel-ical
  • camel-iggy
  • camel-ignite
  • camel-infinispan
  • camel-infinispan-common
  • camel-infinispan-embedded
  • camel-infinispan-parent
  • camel-influxdb
  • camel-influxdb2
  • camel-iso8583
  • camel-jackson
  • camel-jackson-avro
  • camel-jackson-protobuf
  • camel-jackson3
  • camel-jackson3-avro
  • camel-jackson3-protobuf
  • camel-jackson3xml
  • camel-jacksonxml
  • camel-jactl
  • camel-jandex
  • camel-jasypt
  • camel-java-io
  • camel-java-joor-dsl
  • camel-javascript
  • camel-jaxb
  • camel-jbang-console
  • camel-jbang-mcp
  • camel-jbang-plugin-mcp
  • camel-jbang-plugin-route-parser
  • camel-jbang-plugin-tui
  • camel-jbang-plugin-validate
  • camel-jcache
  • camel-jcr
  • camel-jdbc
  • camel-jetty
  • camel-jetty-common
  • camel-jfr
  • camel-jgroups
  • camel-jgroups-raft
  • camel-jira
  • camel-jms
  • camel-jmx
  • camel-jolt
  • camel-jooq
  • camel-joor
  • camel-jpa
  • camel-jq
  • camel-jsch
  • camel-jslt
  • camel-json-validator
  • camel-jsonapi
  • camel-jsonata
  • camel-jsonb
  • camel-jsonpath
  • camel-jsoup
  • camel-jt400
  • camel-jta
  • camel-jte
  • camel-kafka
  • camel-kamelet
  • camel-kamelet-main-support
  • camel-keycloak
  • camel-knative
  • camel-knative-api
  • camel-knative-http
  • camel-knative-parent
  • camel-kserve
  • camel-kubernetes
  • camel-kudu
  • camel-langchain4j-agent
  • camel-langchain4j-agent-api
  • camel-langchain4j-chat
  • camel-langchain4j-core
  • camel-langchain4j-embeddings
  • camel-langchain4j-embeddingstore
  • camel-langchain4j-embeddingstore-api
  • camel-langchain4j-tokenizer
  • camel-langchain4j-web-search
  • camel-language
  • camel-launcher-container
  • camel-ldap
  • camel-ldif
  • camel-log
  • camel-lra
  • camel-lucene
  • camel-lumberjack
  • camel-lzf
  • camel-mail
  • camel-mail-microsoft-oauth
  • camel-main
  • camel-management
  • camel-mapstruct
  • camel-master
  • camel-maven-plugin
  • camel-mcp-server
  • camel-mcp-server-api
  • camel-mdc
  • camel-metrics
  • camel-micrometer
  • camel-micrometer-observability
  • camel-micrometer-prometheus
  • camel-microprofile-config
  • camel-microprofile-fault-tolerance
  • camel-microprofile-health
  • camel-microprofile-parent
  • camel-milo
  • camel-milvus
  • camel-mina
  • camel-mina-sftp
  • camel-minio
  • camel-mllp
  • camel-mock
  • camel-mongodb
  • camel-mongodb-gridfs
  • camel-mustache
  • camel-mvel
  • camel-mybatis
  • camel-nats
  • camel-neo4j
  • camel-netty
  • camel-netty-http
  • camel-oaipmh
  • camel-oauth
  • camel-observability-services
  • camel-observation
  • camel-ocsf
  • camel-ognl
  • camel-olingo2
  • camel-olingo2-api
  • camel-olingo2-parent
  • camel-olingo4
  • camel-olingo4-api
  • camel-olingo4-parent
  • camel-once
  • camel-openai
  • camel-openapi-java
  • camel-openapi-rest-dsl-generator
  • camel-openapi-validator
  • camel-opensearch
  • camel-openstack
  • camel-opentelemetry
  • camel-opentelemetry-metrics
  • camel-opentelemetry2
  • camel-optaplanner
  • camel-paho
  • camel-paho-mqtt5
  • camel-parquet-avro
  • camel-pdf
  • camel-pg-replication-slot
  • camel-pgevent
  • camel-pgvector
  • camel-pinecone
  • camel-platform-http
  • camel-platform-http-jolokia
  • camel-platform-http-main
  • camel-platform-http-vertx
  • camel-plc4x
  • camel-pqc
  • camel-printer
  • camel-protobuf
  • camel-pubnub
  • camel-pulsar
  • camel-python
  • camel-python3
  • camel-qdrant
  • camel-quartz
  • camel-quickfix
  • camel-quickjs
  • camel-reactive-streams
  • camel-reactor
  • camel-redis
  • camel-ref
  • camel-resilience4j
  • camel-resilience4j-micrometer
  • camel-resourceresolver-github
  • camel-rest
  • camel-rest-openapi
  • camel-rest-postman
  • camel-restdsl-openapi-plugin
  • camel-robotframework
  • camel-rocketmq
  • camel-rss
  • camel-rxjava
  • camel-saga
  • camel-salesforce
  • camel-salesforce-codegen
  • camel-salesforce-maven-plugin
  • camel-salesforce-parent
  • camel-sap-netweaver
  • camel-saxon
  • camel-scheduler
  • camel-schematron
  • camel-seda
  • camel-servicenow
  • camel-servicenow-maven-plugin
  • camel-servicenow-parent
  • camel-servlet
  • camel-shell
  • camel-shiro
  • camel-sjms
  • camel-sjms2
  • camel-slack
  • camel-smb
  • camel-smooks
  • camel-smpp
  • camel-snakeyaml
  • camel-snmp
  • camel-soap
  • camel-solr
  • camel-spiffe
  • camel-splunk-hec
  • camel-spring
  • camel-spring-ai-chat
  • camel-spring-ai-embeddings
  • camel-spring-ai-image
  • camel-spring-ai-parent
  • camel-spring-ai-vector-store
  • camel-spring-batch
  • camel-spring-cloud-config
  • camel-spring-jdbc
  • camel-spring-ldap
  • camel-spring-main
  • camel-spring-parent
  • camel-spring-rabbitmq
  • camel-spring-redis
  • camel-spring-security
  • camel-spring-ws
  • camel-spring-xml
  • camel-sql
  • camel-ssh
  • camel-state-store
  • camel-state-store-parent
  • camel-stax
  • camel-stitch
  • camel-stream
  • camel-streamcaching-test
  • camel-stringtemplate
  • camel-stripe
  • camel-stub
  • camel-support
  • camel-swift
  • camel-syslog
  • camel-tahu
  • camel-tarfile
  • camel-telegram
  • camel-telemetry
  • camel-telemetry-dev
  • camel-tensorflow-serving
  • camel-test-infra-all
  • camel-test-infra-artemis
  • camel-test-infra-cli
  • camel-test-infra-core
  • camel-test-infra-smb
  • camel-test-junit5
  • camel-test-junit6
  • camel-test-main-junit5
  • camel-test-main-junit6
  • camel-test-parent
  • camel-test-spring-junit5
  • camel-test-spring-junit6
  • camel-thrift
  • camel-thymeleaf
  • camel-tika
  • camel-timer
  • camel-tooling-maven
  • camel-toon
  • camel-tracing
  • camel-twilio
  • camel-twitter
  • camel-ubl
  • camel-undertow
  • camel-undertow-spring-security
  • camel-univocity-parsers
  • camel-validator
  • camel-velocity
  • camel-vertx
  • camel-vertx-common
  • camel-vertx-http
  • camel-vertx-parent
  • camel-vertx-websocket
  • camel-wal
  • camel-wasm
  • camel-weather
  • camel-weaviate
  • camel-web3j
  • camel-webhook
  • camel-whatsapp
  • camel-wordpress
  • camel-workday
  • camel-xchange
  • camel-xj
  • camel-xml-io
  • camel-xml-io-dsl
  • camel-xml-jaxb
  • camel-xml-jaxb-dsl
  • camel-xml-jaxb-dsl-test-definition
  • camel-xml-jaxb-dsl-test-spring
  • camel-xml-jaxp
  • camel-xmlsecurity
  • camel-xmpp
  • camel-xpath
  • camel-xslt
  • camel-xslt-saxon
  • camel-yaml-dsl-common
  • camel-yaml-dsl-validator
  • camel-yaml-dsl-validator-maven-plugin
  • camel-yaml-io
  • camel-zendesk
  • camel-zip-deflater
  • camel-zipfile
  • camel-zookeeper
  • camel-zookeeper-master
  • components
Modules with tests skipped (26)
  • apache-camel
  • camel-allcomponents
  • camel-catalog-console
  • camel-catalog-maven
  • camel-catalog-suggest
  • camel-componentdsl
  • camel-endpointdsl
  • camel-endpointdsl-support
  • camel-itest
  • camel-jbang-core
  • camel-jbang-it
  • camel-jbang-main
  • camel-jbang-plugin-edit
  • camel-jbang-plugin-generate
  • camel-jbang-plugin-kubernetes
  • camel-jbang-plugin-test
  • camel-kamelet-main
  • camel-launcher
  • camel-report-maven-plugin
  • camel-route-parser
  • camel-yaml-dsl
  • camel-yaml-dsl-deserializers
  • camel-yaml-dsl-maven-plugin
  • coverage
  • docs
  • dummy-component

ℹ️ Shadow mode — Scalpel observes but does not affect test execution. Learn more

⚠️ Some tests are disabled on GitHub Actions (@DisabledIfSystemProperty(named = "ci.env.name")) and require manual verification:

  • core/camel-core: 2 test(s) disabled on GitHub Actions
Build reactor — dependencies compiled but only changed modules were tested (5 modules)
  • Camel :: Catalog :: Camel Catalog
  • Camel :: Core
  • Camel :: Master
  • Camel :: Simple JMS
  • Camel :: Support

⚙️ View full build and test results

@gnodet gnodet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thorough work on all three issues. The lock inversion analysis is precise, and the dedicated leadershipLock is the right fix — narrowing the critical section to just the leadership state rather than the whole service lifecycle cleanly breaks the cycle. The BackgroundTask.cancel() addition fills a real gap in the API, and the test coverage is solid.

A few observations on the concurrency details — nothing blocking, but worth a look.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

*/
public void cancel(boolean mayInterruptIfRunning) {
// any run that has not started yet becomes a no-op
latch.countDown();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Observation (low): cancel() sets running.set(false) at the end, but a concurrent runTaskWrapper() that is already past the latch check and executing the supplier will still be running. After cancel() returns, isRunning() returns false while an attempt may still be in progress (with mayInterruptIfRunning=false).

This is probably fine in practice — callers that need to wait for the in-flight attempt to finish would use mayInterruptIfRunning=true or await the future — but it means isRunning() can briefly lie after cancel(false). Worth a comment on the Javadoc noting that cancel(false) does not wait for an in-flight supplier call to complete.

running.set(true);
Future<?> future = service.scheduleWithFixedDelay(() -> runTaskWrapper(camelContext, supplier),
budget.initialDelay(), budget.interval(), TimeUnit.MILLISECONDS);
scheduledContext.set(camelContext);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Observation (low): scheduledContext is set after scheduledFuture (line 174 vs 173), but cancel() reads scheduledContext in deregister() and scheduledFuture in unschedule() independently. There's a theoretical window where cancel() is called between the two set() calls — it would unschedule the future but deregister() would see a null context and skip the registry removal.

The race guard in runTaskWrapper (lines 108-113) catches this: a run that started before the cancel saw the latch will deregister on its next check. And the existing if (latch.getCount() == 0) { unschedule(false); } at line 176 catches the reverse. So the window is covered by defense-in-depth, but it might be slightly cleaner to set both atomically (or at least set scheduledContext first, before scheduledFuture, since cancel() checks the future first).

Looking again — scheduledContext IS set before scheduledFuture at lines 173-174. So if cancel() fires between them, it sees the context (deregisters) but doesn't see the future yet (skips unschedule). Then line 174 publishes the future, and the final latch.getCount() == 0 check at line 176 catches it. 👍 Order is correct.

// may wait for the view, and the listener bails out before locking once this consumer is stopping
// note: removeEventListener below needs the write lock of the cluster view, while an event dispatch
// takes the read lock of the view and then leadershipLock. This thread must not hold leadershipLock
// here, or the two orders deadlock

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Observation (low): doStop() acquires leadershipLock, clears state, releases the lock, then calls view.removeEventListener(). Between the lock release and the removeEventListener, a leadership event could arrive, pass the isRunAllowed() fast check (which returns false since super.doStop() ran first), and bail out — correct. But the comment on line 128-131 explaining this ordering is valuable. It might be worth adding that super.doStop() has already run at this point, so isRunAllowed() is the first gate the listener hits.

Comment on lines +335 to +337
// is cancelled no run is coming
leaderTask.cancel(mayInterruptIfRunning);
leaderTask = null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Observation (medium): The cancelLeaderTask method nulls leaderTask and leaderTaskFuture but these fields are not volatile. They're always accessed under leadershipLock, so this is safe — but it's worth noting this invariant somewhere since the fields are declared next to the volatile delegatedConsumer and view at lines 64-65, and a future reader might wonder why some fields are volatile and others aren't.

The leaderTask field could arguably be volatile for consistency with the surrounding declarations, but since all accesses are lock-guarded it's unnecessary overhead.

Comment on lines +260 to +281

view.setLeader(true);
await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> assertEquals(1, probe.started.get()));

CountDownLatch suspendEntered = new CountDownLatch(1);
CountDownLatch suspendGate = new CountDownLatch(1);
probe.suspendEntered.set(suspendEntered);
probe.suspendGate.set(suspendGate);

// suspending holds the service lock of the master consumer for as long as the delegate takes
MasterConsumer consumer = (MasterConsumer) context.getRoute("master-route").getConsumer();
Thread suspender = new Thread(consumer::suspend, "suspend");
suspender.start();
assertTrue(suspendEntered.await(20, TimeUnit.SECONDS), "The suspend of the delegate should have started");

// the cluster view dispatches its events while holding its own lock, and needs that same lock again
// to remove the listener when the consumer stops. An event that waits here for the service lock of
// the consumer is what closes that into a deadlock, so the leadership must not be guarded by it
Thread dispatcher = new Thread(() -> view.setLeader(true), "leadership-taken");
dispatcher.start();
try {
dispatcher.join(TimeUnit.SECONDS.toMillis(20));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Nice test. testEventDispatchIsNotBlockedByALifecycleOperation is a well-designed deadlock detection test — holding the service lock via a blocking suspend, then verifying the dispatch thread completes without waiting. The join(20s) timeout with the alive check is the right pattern for this.

@davsclaus

Copy link
Copy Markdown
Contributor

@henrik242 take a look at the comments from @gnodet

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants