@@ -120,7 +120,7 @@ export class KubernetesWorkloadManager implements WorkloadManager {
120120 } ,
121121 spec : {
122122 ...this . addPlacementTags ( this . #defaultPodSpec, opts . placementTags ) ,
123- affinity : this . #getAffinity( opts . machine , opts . projectId ) ,
123+ affinity : this . #getAffinity( opts ) ,
124124 terminationGracePeriodSeconds : 60 * 60 ,
125125 containers : [
126126 {
@@ -335,13 +335,22 @@ export class KubernetesWorkloadManager implements WorkloadManager {
335335 } ;
336336 }
337337
338+ #isScheduledRun( opts : WorkloadManagerCreateOptions ) : boolean {
339+ return opts . annotations ?. rootTriggerSource === "schedule" ;
340+ }
341+
338342 #getSharedLabels( opts : WorkloadManagerCreateOptions ) : Record < string , string > {
339343 return {
340344 env : opts . envId ,
341345 envtype : this . #envTypeToLabelValue( opts . envType ) ,
342346 org : opts . orgId ,
343347 project : opts . projectId ,
344348 machine : opts . machine . name ,
349+ // We intentionally use a boolean label rather than exposing the full trigger source
350+ // (e.g. sdk, api, cli, mcp, schedule) to keep label cardinality low in metrics.
351+ // The schedule vs non-schedule distinction is all we need for the current metrics
352+ // and pool-level scheduling decisions; finer-grained source breakdowns live in run annotations.
353+ scheduled : String ( this . #isScheduledRun( opts ) ) ,
345354 } ;
346355 }
347356
@@ -390,16 +399,37 @@ export class KubernetesWorkloadManager implements WorkloadManager {
390399 return preset . name . startsWith ( "large-" ) ;
391400 }
392401
393- #getAffinity( preset : MachinePreset , projectId : string ) : k8s . V1Affinity | undefined {
394- const nodeAffinity = this . #getNodeAffinityRules( preset ) ;
395- const podAffinity = this . #getProjectPodAffinity( projectId ) ;
396-
397- if ( ! nodeAffinity && ! podAffinity ) {
402+ #getAffinity( opts : WorkloadManagerCreateOptions ) : k8s . V1Affinity | undefined {
403+ const largeNodeAffinity = this . #getNodeAffinityRules( opts . machine ) ;
404+ const scheduleNodeAffinity = this . #getScheduleNodeAffinityRules( this . #isScheduledRun( opts ) ) ;
405+ const podAffinity = this . #getProjectPodAffinity( opts . projectId ) ;
406+
407+ // Merge node affinity rules from multiple sources
408+ const preferred = [
409+ ...( largeNodeAffinity ?. preferredDuringSchedulingIgnoredDuringExecution ?? [ ] ) ,
410+ ...( scheduleNodeAffinity ?. preferredDuringSchedulingIgnoredDuringExecution ?? [ ] ) ,
411+ ] ;
412+ // Only large machine affinity produces hard requirements (non-large runs must stay off the large pool).
413+ // Schedule affinity is soft both ways.
414+ const required = [
415+ ...( largeNodeAffinity ?. requiredDuringSchedulingIgnoredDuringExecution ?. nodeSelectorTerms ?? [ ] ) ,
416+ ] ;
417+
418+ const hasNodeAffinity = preferred . length > 0 || required . length > 0 ;
419+
420+ if ( ! hasNodeAffinity && ! podAffinity ) {
398421 return undefined ;
399422 }
400423
401424 return {
402- ...( nodeAffinity && { nodeAffinity } ) ,
425+ ...( hasNodeAffinity && {
426+ nodeAffinity : {
427+ ...( preferred . length > 0 && { preferredDuringSchedulingIgnoredDuringExecution : preferred } ) ,
428+ ...( required . length > 0 && {
429+ requiredDuringSchedulingIgnoredDuringExecution : { nodeSelectorTerms : required } ,
430+ } ) ,
431+ } ,
432+ } ) ,
403433 ...( podAffinity && { podAffinity } ) ,
404434 } ;
405435 }
@@ -447,6 +477,50 @@ export class KubernetesWorkloadManager implements WorkloadManager {
447477 } ;
448478 }
449479
480+ #getScheduleNodeAffinityRules( isScheduledRun : boolean ) : k8s . V1NodeAffinity | undefined {
481+ if ( ! env . KUBERNETES_SCHEDULE_AFFINITY_ENABLED || ! env . KUBERNETES_SCHEDULE_AFFINITY_POOL_LABEL_VALUE ) {
482+ return undefined ;
483+ }
484+
485+ if ( isScheduledRun ) {
486+ // soft preference for the schedule pool
487+ return {
488+ preferredDuringSchedulingIgnoredDuringExecution : [
489+ {
490+ weight : env . KUBERNETES_SCHEDULE_AFFINITY_WEIGHT ,
491+ preference : {
492+ matchExpressions : [
493+ {
494+ key : env . KUBERNETES_SCHEDULE_AFFINITY_POOL_LABEL_KEY ,
495+ operator : "In" ,
496+ values : [ env . KUBERNETES_SCHEDULE_AFFINITY_POOL_LABEL_VALUE ] ,
497+ } ,
498+ ] ,
499+ } ,
500+ } ,
501+ ] ,
502+ } ;
503+ }
504+
505+ // soft anti-affinity: non-schedule runs prefer to avoid the schedule pool
506+ return {
507+ preferredDuringSchedulingIgnoredDuringExecution : [
508+ {
509+ weight : env . KUBERNETES_SCHEDULE_ANTI_AFFINITY_WEIGHT ,
510+ preference : {
511+ matchExpressions : [
512+ {
513+ key : env . KUBERNETES_SCHEDULE_AFFINITY_POOL_LABEL_KEY ,
514+ operator : "NotIn" ,
515+ values : [ env . KUBERNETES_SCHEDULE_AFFINITY_POOL_LABEL_VALUE ] ,
516+ } ,
517+ ] ,
518+ } ,
519+ } ,
520+ ] ,
521+ } ;
522+ }
523+
450524 #getProjectPodAffinity( projectId : string ) : k8s . V1PodAffinity | undefined {
451525 if ( ! env . KUBERNETES_PROJECT_AFFINITY_ENABLED ) {
452526 return undefined ;
0 commit comments