|
46 | 46 | import com.linkedin.venice.compression.CompressionStrategy; |
47 | 47 | import com.linkedin.venice.exceptions.VeniceException; |
48 | 48 | import com.linkedin.venice.exceptions.VeniceMessageException; |
| 49 | +import com.linkedin.venice.exceptions.VeniceNoStoreException; |
49 | 50 | import com.linkedin.venice.exceptions.VeniceTimeoutException; |
50 | 51 | import com.linkedin.venice.exceptions.validation.DuplicateDataException; |
51 | 52 | import com.linkedin.venice.exceptions.validation.FatalDataValidationException; |
@@ -620,6 +621,7 @@ protected void checkLongRunningTaskState() throws InterruptedException { |
620 | 621 | boolean pushTimeout = false; |
621 | 622 | Set<Integer> timeoutPartitions = null; |
622 | 623 | long checkStartTimeInNS = System.nanoTime(); |
| 624 | + maybeTransitionPauseState(); |
623 | 625 | for (PartitionConsumptionState partitionConsumptionState: getPartitionConsumptionStateMap().values()) { |
624 | 626 | final int partition = partitionConsumptionState.getPartition(); |
625 | 627 |
|
@@ -675,7 +677,7 @@ protected void checkLongRunningTaskState() throws InterruptedException { |
675 | 677 | * online replica continue serving and do not close ingestion task. |
676 | 678 | */ |
677 | 679 | if (!partitionConsumptionState.isComplete() && !partitionConsumptionState.isErrorReported() |
678 | | - && LatencyUtils.getElapsedTimeFromMsToMs( |
| 680 | + && !partitionConsumptionState.isStoreLevelPaused() && LatencyUtils.getElapsedTimeFromMsToMs( |
679 | 681 | partitionConsumptionState.getConsumptionStartTimeInMs()) > getBootstrapTimeoutInMs()) { |
680 | 682 | if (!pushTimeout) { |
681 | 683 | pushTimeout = true; |
@@ -871,6 +873,143 @@ protected void checkLongRunningTaskState() throws InterruptedException { |
871 | 873 | getVersionTopic().getName()); |
872 | 874 | } |
873 | 875 |
|
| 876 | + /** |
| 877 | + * Reconciles each PCS's store-level pause state against the current store metadata. |
| 878 | + * <p> |
| 879 | + * On a transition into paused: removes the partition's current leader topic (or VT for |
| 880 | + * followers) from every per-broker consumer service via {@link #consumerUnSubscribeAllTopics}. |
| 881 | + * Note: cross-region RT subscriptions for an A/A leader are managed by the leader's normal A/A |
| 882 | + * code path, not by this hook — only the topic in the OffsetRecord is dropped here. |
| 883 | + * On a transition out of paused: resubscribes via {@link #resubscribe}, which rewinds from the |
| 884 | + * persisted offset to the leader topic recorded in the OffsetRecord (followers: local VT; |
| 885 | + * leaders: the recorded leader topic). |
| 886 | + * <p> |
| 887 | + * The PCS pause flag is set to its target value <em>before</em> the long-running unsubscribe / |
| 888 | + * resubscribe so the disk-quota no-op guard covers the entire transition window. Each PCS is |
| 889 | + * processed inside a try/catch so a failure on one partition does not abandon the others. |
| 890 | + */ |
| 891 | + void maybeTransitionPauseState() throws InterruptedException { |
| 892 | + Store store; |
| 893 | + try { |
| 894 | + store = storeRepository.getStoreOrThrow(storeName); |
| 895 | + } catch (VeniceNoStoreException e) { |
| 896 | + // Store metadata is genuinely unavailable (deleted, not yet propagated, etc.). Leave pause |
| 897 | + // state as-is and retry next iteration. Throttle WARN via the inherited filter so a |
| 898 | + // continuously-failing lookup logs at most once per window instead of once per SIT |
| 899 | + // iteration. Other RuntimeExceptions propagate so real bugs surface. |
| 900 | + if (!REDUNDANT_LOGGING_FILTER.isRedundantException(storeName + "-maybeTransitionPauseState-store-not-found")) { |
| 901 | + LOGGER.warn("Store {} not found while reconciling pause state; skipping transition.", storeName); |
| 902 | + } |
| 903 | + return; |
| 904 | + } |
| 905 | + boolean shouldPause = shouldPauseForStore(store); |
| 906 | + boolean transitioned = false; |
| 907 | + // Lazily probe the consumer subscription state once per loop (SIT-wide), only when a |
| 908 | + // RECONCILE_FORCE_UNSUBSCRIBE could fire (shouldPause is set). consumerHasAnySubscription is |
| 909 | + // a single fast call against aggKafkaConsumerService; consumerUnSubscribeAllTopics itself is |
| 910 | + // self-gating per topic, so triggering reconcile when only some partitions are subscribed is |
| 911 | + // benign (no-ops for the others). |
| 912 | + boolean anySubscriptionForSit = shouldPause && consumerHasAnySubscription(); |
| 913 | + for (PartitionConsumptionState pcs: getPartitionConsumptionStateMap().values()) { |
| 914 | + PauseStateTransition transition = decidePauseTransition(pcs, shouldPause, anySubscriptionForSit); |
| 915 | + if (transition == PauseStateTransition.NO_CHANGE) { |
| 916 | + continue; |
| 917 | + } |
| 918 | + try { |
| 919 | + if (transition == PauseStateTransition.ENTER_PAUSE) { |
| 920 | + // Flip the flag BEFORE the long-running unsubscribe so concurrent disk-quota callbacks |
| 921 | + // see the new state and no-op for the entire window. |
| 922 | + pcs.setStoreLevelPaused(true); |
| 923 | + consumerUnSubscribeAllTopics(pcs); |
| 924 | + LOGGER.info( |
| 925 | + "Store-level pause activated for replica: {} — unsubscribed from Kafka", |
| 926 | + Utils.getReplicaId(getKafkaVersionTopic(), pcs.getPartition())); |
| 927 | + } else if (transition == PauseStateTransition.RECONCILE_FORCE_UNSUBSCRIBE) { |
| 928 | + consumerUnSubscribeAllTopics(pcs); |
| 929 | + LOGGER.info( |
| 930 | + "Store-level pause re-applied for replica: {} — subscription was reattached, unsubscribed again", |
| 931 | + Utils.getReplicaId(getKafkaVersionTopic(), pcs.getPartition())); |
| 932 | + } else { // EXIT_PAUSE |
| 933 | + // Resubscribe BEFORE clearing the flag so quota callbacks stay no-op until the consumer |
| 934 | + // is back online; if resubscribe throws we leave the flag set so the next iteration |
| 935 | + // retries instead of leaving the partition dark. |
| 936 | + resubscribe(pcs); |
| 937 | + pcs.setStoreLevelPaused(false); |
| 938 | + pcs.resetConsumptionStartTimeInMs(); |
| 939 | + LOGGER.info( |
| 940 | + "Store-level pause deactivated for replica: {} — resubscribed from persisted offset", |
| 941 | + Utils.getReplicaId(getKafkaVersionTopic(), pcs.getPartition())); |
| 942 | + } |
| 943 | + transitioned = true; |
| 944 | + } catch (InterruptedException e) { |
| 945 | + Thread.currentThread().interrupt(); |
| 946 | + throw e; |
| 947 | + } catch (Exception e) { |
| 948 | + LOGGER.error( |
| 949 | + "Failed to apply pause transition {} for replica: {}; will retry next iteration.", |
| 950 | + transition, |
| 951 | + Utils.getReplicaId(getKafkaVersionTopic(), pcs.getPartition()), |
| 952 | + e); |
| 953 | + } |
| 954 | + } |
| 955 | + if (transitioned) { |
| 956 | + // Reflect actual post-loop state, not intent — partial-failure cases shouldn't flip the |
| 957 | + // gauge to 0 while some PCSes remain paused. |
| 958 | + boolean anyPcsPaused = false; |
| 959 | + for (PartitionConsumptionState pcs: getPartitionConsumptionStateMap().values()) { |
| 960 | + if (pcs != null && pcs.isStoreLevelPaused()) { |
| 961 | + anyPcsPaused = true; |
| 962 | + break; |
| 963 | + } |
| 964 | + } |
| 965 | + versionedIngestionStats.setStoreLevelPausedGauge(storeName, versionNumber, anyPcsPaused); |
| 966 | + } |
| 967 | + } |
| 968 | + |
| 969 | + /** |
| 970 | + * Result of evaluating a single PCS against the desired pause state. |
| 971 | + * {@code RECONCILE_FORCE_UNSUBSCRIBE} fires when the PCS is already flagged paused but a |
| 972 | + * subscription has crept back (e.g., a leader/follower transition or topic switch reattached |
| 973 | + * the consumer); the reconcile loop force-unsubscribes again so ingestion can't resume while |
| 974 | + * shouldPause is still true. |
| 975 | + */ |
| 976 | + enum PauseStateTransition { |
| 977 | + ENTER_PAUSE, EXIT_PAUSE, RECONCILE_FORCE_UNSUBSCRIBE, NO_CHANGE |
| 978 | + } |
| 979 | + |
| 980 | + /** |
| 981 | + * Pure decision function: given the current PCS pause flag, the desired pause state, and |
| 982 | + * whether the partition still has any active Kafka subscription, return the transition that |
| 983 | + * needs to happen (if any). Side-effect-free and trivially unit-testable. |
| 984 | + * |
| 985 | + * <p>{@code RECONCILE_FORCE_UNSUBSCRIBE} fires when {@code shouldPause} is true and the PCS is |
| 986 | + * already flagged paused but {@code hasAnyActiveSubscription} reports a live subscription — |
| 987 | + * makes the reconcile loop idempotent against out-of-band re-subscriptions (L/F transitions, |
| 988 | + * topic switches) that would otherwise leave a paused store ingesting. |
| 989 | + * |
| 990 | + * <p>Returns NO_CHANGE for a {@code null} PCS — defensive guard mirroring |
| 991 | + * {@link StoreIngestionTask#shouldSkipQuotaCallbackForStoreLevelPause(PartitionConsumptionState)}. |
| 992 | + */ |
| 993 | + static PauseStateTransition decidePauseTransition( |
| 994 | + PartitionConsumptionState pcs, |
| 995 | + boolean shouldPause, |
| 996 | + boolean hasAnyActiveSubscription) { |
| 997 | + if (pcs == null) { |
| 998 | + return PauseStateTransition.NO_CHANGE; |
| 999 | + } |
| 1000 | + boolean isPaused = pcs.isStoreLevelPaused(); |
| 1001 | + if (shouldPause && !isPaused) { |
| 1002 | + return PauseStateTransition.ENTER_PAUSE; |
| 1003 | + } |
| 1004 | + if (shouldPause && isPaused && hasAnyActiveSubscription) { |
| 1005 | + return PauseStateTransition.RECONCILE_FORCE_UNSUBSCRIBE; |
| 1006 | + } |
| 1007 | + if (!shouldPause && pcs.isStoreLevelPaused()) { |
| 1008 | + return PauseStateTransition.EXIT_PAUSE; |
| 1009 | + } |
| 1010 | + return PauseStateTransition.NO_CHANGE; |
| 1011 | + } |
| 1012 | + |
874 | 1013 | protected static boolean checkWhetherToCloseUnusedVeniceWriter( |
875 | 1014 | Lazy<VeniceWriter<byte[], byte[], byte[]>> veniceWriterLazy, |
876 | 1015 | Lazy<VeniceWriter<byte[], byte[], byte[]>> veniceWriterForRealTimeLazy, |
@@ -3457,15 +3596,29 @@ protected void consumerBatchUnsubscribeAllTopics() { |
3457 | 3596 | */ |
3458 | 3597 | @Override |
3459 | 3598 | public void consumerUnSubscribeAllTopics(PartitionConsumptionState partitionConsumptionState) { |
3460 | | - PubSubTopic leaderTopic = partitionConsumptionState.getOffsetRecord().getLeaderTopic(pubSubTopicRepository); |
3461 | 3599 | int partitionId = partitionConsumptionState.getPartition(); |
3462 | | - if (partitionConsumptionState.getLeaderFollowerState().equals(LEADER) && leaderTopic != null) { |
3463 | | - aggKafkaConsumerService |
3464 | | - .unsubscribeConsumerFor(versionTopic, new PubSubTopicPartitionImpl(leaderTopic, partitionId)); |
3465 | | - } else { |
| 3600 | + PubSubTopic leaderTopic = partitionConsumptionState.getOffsetRecord().getLeaderTopic(pubSubTopicRepository); |
| 3601 | + |
| 3602 | + // During leader/follower transitions or bootstrap a partition can be transiently subscribed |
| 3603 | + // to BOTH the version topic and a separate leader topic (e.g., RT). Unsubscribe each that is |
| 3604 | + // currently subscribed; consumerHasSubscription() avoids redundant work when a topic isn't |
| 3605 | + // attached. |
| 3606 | + if (consumerHasSubscription(versionTopic, partitionConsumptionState)) { |
3466 | 3607 | aggKafkaConsumerService |
3467 | 3608 | .unsubscribeConsumerFor(versionTopic, new PubSubTopicPartitionImpl(versionTopic, partitionId)); |
3468 | 3609 | } |
| 3610 | + if (leaderTopic != null && !leaderTopic.equals(versionTopic) |
| 3611 | + && consumerHasSubscription(leaderTopic, partitionConsumptionState)) { |
| 3612 | + aggKafkaConsumerService |
| 3613 | + .unsubscribeConsumerFor(versionTopic, new PubSubTopicPartitionImpl(leaderTopic, partitionId)); |
| 3614 | + // Gate sep-RT unsubscribe on actual subscription to avoid WARN spam from the consumer |
| 3615 | + // delegator when sep-RT isn't currently attached (common during transitions). |
| 3616 | + if (isSeparatedRealtimeTopicEnabled() && leaderTopic.isRealTime() && separateRealTimeTopic != null |
| 3617 | + && consumerHasSubscription(separateRealTimeTopic, partitionConsumptionState)) { |
| 3618 | + aggKafkaConsumerService |
| 3619 | + .unsubscribeConsumerFor(versionTopic, new PubSubTopicPartitionImpl(separateRealTimeTopic, partitionId)); |
| 3620 | + } |
| 3621 | + } |
3469 | 3622 |
|
3470 | 3623 | /** |
3471 | 3624 | * Leader of the user partition should close all subPartitions it is producing to. |
|
0 commit comments