diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/config/properties/queue-properties.ts b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/config/properties/queue-properties.ts index 6fb1c87c7b6638..79552d09ac146d 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/config/properties/queue-properties.ts +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/config/properties/queue-properties.ts @@ -548,7 +548,7 @@ export const queuePropertyDefinitions: PropertyDescriptor[] = [ name: 'auto-queue-creation-v2.enabled', displayName: 'Flexible Queue Auto-Creation', description: - 'Enable flexible queue auto-creation (parent and leaf queues). In legacy queue mode, root queue requires all child queues to use weight-based capacity.', + 'Enable flexible queue auto-creation (parent and leaf queues). In legacy queue mode, root queue requires all child queues to use weight-based capacity; enabling or disabling on root automatically converts root capacity between 100% and 1w.', type: 'boolean' as PropertyType, category: 'dynamic-queues' as PropertyCategory, defaultValue: '', diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/hooks/usePropertyEditor.test.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/hooks/usePropertyEditor.test.tsx index 32657c2f39f0f9..856b456a3b6292 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/hooks/usePropertyEditor.test.tsx +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/hooks/usePropertyEditor.test.tsx @@ -34,6 +34,7 @@ vi.mock('sonner', () => ({ error: vi.fn(), success: vi.fn(), warning: vi.fn(), + info: vi.fn(), }, })); diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/hooks/usePropertyEditor.ts b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/hooks/usePropertyEditor.ts index 08de908fcba2f9..e654578ba032fb 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/hooks/usePropertyEditor.ts +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/hooks/usePropertyEditor.ts @@ -142,8 +142,14 @@ export function usePropertyEditor({ queuePath, properties = queuePropertyDefinitions, }: UsePropertyEditorOptions) { - const { getQueuePropertyValue, stageQueueChange, clearQueueChanges, schedulerData, configData } = - useSchedulerStore(); + const { + getQueuePropertyValue, + stageQueueChange, + clearQueueChanges, + schedulerData, + configData, + resolveRootCapacityStagingForFlexibleAutoCreation, + } = useSchedulerStore(); const stagedChanges = useSchedulerStore((state) => state.stagedChanges); const cleanResetRef = useRef(false); @@ -461,6 +467,14 @@ export function usePropertyEditor({ } }); + const stagedRootCapacity = resolveRootCapacityStagingForFlexibleAutoCreation( + queuePath, + changedData, + ); + if (stagedRootCapacity) { + changedData.capacity = stagedRootCapacity.capacity; + } + const pendingEntries = Object.entries(changedData); const previewConfigData = new Map(configData); @@ -550,6 +564,12 @@ export function usePropertyEditor({ message: `${stagedCount} change${stagedCount !== 1 ? 's' : ''} staged successfully!`, }; + if (stagedRootCapacity?.direction === 'to-weight') { + toast.info('Root queue capacity was automatically converted from 100% to 1w for weight mode.'); + } else if (stagedRootCapacity?.direction === 'to-percentage') { + toast.info('Root queue capacity was automatically converted from 1w to 100%.'); + } + if (stagedCount > 0) { const latestValues = form.getValues(); reset(latestValues, { diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/stores/slices/__tests__/rootCapacityAutoStaging.test.ts b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/stores/slices/__tests__/rootCapacityAutoStaging.test.ts new file mode 100644 index 00000000000000..1c3676a95ee370 --- /dev/null +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/stores/slices/__tests__/rootCapacityAutoStaging.test.ts @@ -0,0 +1,124 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect } from 'vitest'; +import { resolveRootCapacityStagingWhenAutoQueueCreationIsToggled } from '~/stores/slices/rootCapacityAutoStaging'; +import { AUTO_CREATION_PROPS } from '~/types/constants/auto-creation'; +import { SPECIAL_VALUES } from '~/types/constants/special-values'; + +describe('resolveRootCapacityStagingWhenAutoQueueCreationIsToggled', () => { + const createGetQueuePropertyValue = + (values: Record) => (_queuePath: string, propertyName: string) => ({ + value: values[propertyName] ?? '', + isStaged: false, + }); + + const legacyConfig = new Map([[SPECIAL_VALUES.LEGACY_MODE_PROPERTY, 'true']]); + + it('returns 1w when enabling flexible auto-creation on root with percentage capacity', () => { + const result = resolveRootCapacityStagingWhenAutoQueueCreationIsToggled({ + queuePath: SPECIAL_VALUES.ROOT_QUEUE_NAME, + changedData: { [AUTO_CREATION_PROPS.FLEXIBLE_ENABLED]: 'true' }, + getQueuePropertyValue: createGetQueuePropertyValue({ capacity: '100' }), + configData: legacyConfig, + }); + + expect(result).toEqual({ + capacity: '1w', + direction: 'to-weight', + }); + }); + + it('returns 100% when disabling flexible auto-creation on root with weight capacity', () => { + const result = resolveRootCapacityStagingWhenAutoQueueCreationIsToggled({ + queuePath: SPECIAL_VALUES.ROOT_QUEUE_NAME, + changedData: { [AUTO_CREATION_PROPS.FLEXIBLE_ENABLED]: 'false' }, + getQueuePropertyValue: createGetQueuePropertyValue({ + capacity: '1w', + [AUTO_CREATION_PROPS.FLEXIBLE_ENABLED]: 'true', + }), + configData: legacyConfig, + }); + + expect(result).toEqual({ + capacity: '100%', + direction: 'to-percentage', + }); + }); + + it('returns null for non-root queues', () => { + const result = resolveRootCapacityStagingWhenAutoQueueCreationIsToggled({ + queuePath: 'root.default', + changedData: { [AUTO_CREATION_PROPS.FLEXIBLE_ENABLED]: 'true' }, + getQueuePropertyValue: createGetQueuePropertyValue({ capacity: '2w' }), + configData: legacyConfig, + }); + + expect(result).toBeNull(); + }); + + it('returns null when legacy mode is disabled', () => { + const result = resolveRootCapacityStagingWhenAutoQueueCreationIsToggled({ + queuePath: SPECIAL_VALUES.ROOT_QUEUE_NAME, + changedData: { [AUTO_CREATION_PROPS.FLEXIBLE_ENABLED]: 'true' }, + getQueuePropertyValue: createGetQueuePropertyValue({ capacity: '100' }), + configData: new Map([[SPECIAL_VALUES.LEGACY_MODE_PROPERTY, 'false']]), + }); + + expect(result).toBeNull(); + }); + + it('returns null when root already uses weight capacity while enabling', () => { + const result = resolveRootCapacityStagingWhenAutoQueueCreationIsToggled({ + queuePath: SPECIAL_VALUES.ROOT_QUEUE_NAME, + changedData: { [AUTO_CREATION_PROPS.FLEXIBLE_ENABLED]: 'true' }, + getQueuePropertyValue: createGetQueuePropertyValue({ capacity: '2w' }), + configData: legacyConfig, + }); + + expect(result).toBeNull(); + }); + + it('returns null when flexible auto-creation was not enabled while disabling', () => { + const result = resolveRootCapacityStagingWhenAutoQueueCreationIsToggled({ + queuePath: SPECIAL_VALUES.ROOT_QUEUE_NAME, + changedData: { [AUTO_CREATION_PROPS.FLEXIBLE_ENABLED]: 'false' }, + getQueuePropertyValue: createGetQueuePropertyValue({ + capacity: '1w', + [AUTO_CREATION_PROPS.FLEXIBLE_ENABLED]: 'false', + }), + configData: legacyConfig, + }); + + expect(result).toBeNull(); + }); + + it('returns null when root already uses percentage capacity while disabling', () => { + const result = resolveRootCapacityStagingWhenAutoQueueCreationIsToggled({ + queuePath: SPECIAL_VALUES.ROOT_QUEUE_NAME, + changedData: { [AUTO_CREATION_PROPS.FLEXIBLE_ENABLED]: 'false' }, + getQueuePropertyValue: createGetQueuePropertyValue({ + capacity: '100', + [AUTO_CREATION_PROPS.FLEXIBLE_ENABLED]: 'true', + }), + configData: legacyConfig, + }); + + expect(result).toBeNull(); + }); +}); diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/stores/slices/rootCapacityAutoStaging.ts b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/stores/slices/rootCapacityAutoStaging.ts new file mode 100644 index 00000000000000..f2f540463c9fb6 --- /dev/null +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/stores/slices/rootCapacityAutoStaging.ts @@ -0,0 +1,119 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AUTO_CREATION_PROPS } from '~/types/constants/auto-creation'; +import { SPECIAL_VALUES } from '~/types/constants/special-values'; +import { getCapacityType } from '~/utils/capacityUtils'; + +export interface RootCapacityAutoStagingResult { + capacity: string; + direction: 'to-weight' | 'to-percentage'; +} + +export interface RootCapacityAutoStagingContext { + queuePath: string; + changedData: Record; + getQueuePropertyValue: ( + queuePath: string, + propertyName: string, + ) => { value: string; isStaged: boolean }; + configData: Map; +} + +function canAutoStageRootCapacity({ + queuePath, + changedData, + configData, +}: RootCapacityAutoStagingContext): boolean { + if (queuePath !== SPECIAL_VALUES.ROOT_QUEUE_NAME) { + return false; + } + + if (changedData.capacity !== undefined) { + return false; + } + + return configData.get(SPECIAL_VALUES.LEGACY_MODE_PROPERTY) !== 'false'; +} + +function resolveRootCapacityStagingWhenEnablingFlexibleAutoCreation( + options: RootCapacityAutoStagingContext, +): RootCapacityAutoStagingResult | null { + if (!canAutoStageRootCapacity(options)) { + return null; + } + + if (options.changedData[AUTO_CREATION_PROPS.FLEXIBLE_ENABLED] !== 'true') { + return null; + } + + const { value: currentCapacity } = options.getQueuePropertyValue(options.queuePath, 'capacity'); + if (getCapacityType(currentCapacity) !== 'percentage') { + return null; + } + + return { + capacity: '1w', + direction: 'to-weight', + }; +} + +function resolveRootCapacityStagingWhenDisablingFlexibleAutoCreation( + options: RootCapacityAutoStagingContext, +): RootCapacityAutoStagingResult | null { + if (!canAutoStageRootCapacity(options)) { + return null; + } + + const flexibleEnabledChange = options.changedData[AUTO_CREATION_PROPS.FLEXIBLE_ENABLED]; + if (flexibleEnabledChange === undefined || flexibleEnabledChange === 'true') { + return null; + } + + const { value: currentFlexibleEnabled } = options.getQueuePropertyValue( + options.queuePath, + AUTO_CREATION_PROPS.FLEXIBLE_ENABLED, + ); + if (currentFlexibleEnabled !== 'true') { + return null; + } + + const { value: currentCapacity } = options.getQueuePropertyValue(options.queuePath, 'capacity'); + if (getCapacityType(currentCapacity) !== 'weight') { + return null; + } + + return { + capacity: '100%', + direction: 'to-percentage', + }; +} + +/** + * Restore root capacity when flexible auto-queue-creation is toggled. + * Convert 100% -> 1w when the toggle is turned on. + * Convert 1w -> 100% when the toggle is turned off. + */ +export function resolveRootCapacityStagingWhenAutoQueueCreationIsToggled( + context: RootCapacityAutoStagingContext, +): RootCapacityAutoStagingResult | null { + return ( + resolveRootCapacityStagingWhenEnablingFlexibleAutoCreation(context) ?? + resolveRootCapacityStagingWhenDisablingFlexibleAutoCreation(context) + ); +} diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/stores/slices/stagedChangesSlice.ts b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/stores/slices/stagedChangesSlice.ts index cd9b02bccc7c18..dc4e6e2dd92ebc 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/stores/slices/stagedChangesSlice.ts +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/stores/slices/stagedChangesSlice.ts @@ -47,6 +47,7 @@ import { assertWritable } from '~/lib/errors/readOnlyGuard'; import type { StagedChangesSlice, SchedulerStore } from './types'; import { getAffectedQueuesForValidation } from '~/features/validation/utils/affectedQueues'; import { validateStagedChanges, validatePropertyChange } from '~/features/validation/crossQueue'; +import { resolveRootCapacityStagingWhenAutoQueueCreationIsToggled } from './rootCapacityAutoStaging'; type MutationErrorState = Pick; const clearMutationError = (state: MutationErrorState) => { @@ -582,6 +583,14 @@ export const createStagedChangesSlice: StateCreator< }); }, + resolveRootCapacityStagingForFlexibleAutoCreation: (queuePath, changedData) => + resolveRootCapacityStagingWhenAutoQueueCreationIsToggled({ + queuePath, + changedData, + getQueuePropertyValue: get().getQueuePropertyValue, + configData: get().configData, + }), + refreshAffectedValidationErrors: (triggeringQueuePath: string, triggeringProperty: string) => { const { stagedChanges, schedulerData, configData } = get(); diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/stores/slices/types.ts b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/stores/slices/types.ts index 02fb6b7900115b..cf28c51ec179dc 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/stores/slices/types.ts +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/stores/slices/types.ts @@ -34,6 +34,7 @@ import type { } from '~/types'; import type { PlacementRulesSlice } from './placementRulesSlice'; import type { CapacityEditorSlice } from './capacityEditorSlice'; +import type { RootCapacityAutoStagingResult } from './rootCapacityAutoStaging'; export interface BaseStoreSlice { apiClient: YarnApiClient; @@ -107,6 +108,10 @@ export interface StagedChangesSlice { triggeringQueuePath: string, triggeringProperty: string, ) => void; + resolveRootCapacityStagingForFlexibleAutoCreation: ( + queuePath: string, + changedData: Record, + ) => RootCapacityAutoStagingResult | null; } export interface QueueSelectionSlice {