volcano.sh/volcano@v1.9.0/pkg/scheduler/plugins/extender/extender.go (about)

     1  /*
     2  Copyright 2022 The Volcano Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package extender
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"net/http"
    25  	"strings"
    26  	"time"
    27  
    28  	"k8s.io/klog/v2"
    29  
    30  	"volcano.sh/volcano/pkg/scheduler/api"
    31  	"volcano.sh/volcano/pkg/scheduler/framework"
    32  	"volcano.sh/volcano/pkg/scheduler/plugins/util"
    33  )
    34  
    35  const (
    36  	// PluginName indicates name of volcano scheduler plugin.
    37  	PluginName = "extender"
    38  
    39  	// ExtenderURLPrefix is the key for providing extender endpoint address
    40  	ExtenderURLPrefix = "extender.urlPrefix"
    41  	// ExtenderHTTPTimeout is the timeout for extender http calls
    42  	ExtenderHTTPTimeout = "extender.httpTimeout"
    43  	// ExtenderOnSessionOpenVerb is the verb of OnSessionOpen method
    44  	ExtenderOnSessionOpenVerb = "extender.onSessionOpenVerb"
    45  	// ExtenderOnSessionCloseVerb is the verb of OnSessionClose method
    46  	ExtenderOnSessionCloseVerb = "extender.onSessionCloseVerb"
    47  	// ExtenderPredicateVerb is the verb of Predicate method
    48  	ExtenderPredicateVerb = "extender.predicateVerb"
    49  	// ExtenderPrioritizeVerb is the verb of Prioritize method
    50  	ExtenderPrioritizeVerb = "extender.prioritizeVerb"
    51  	// ExtenderPreemptableVerb is the verb of Preemptable method
    52  	ExtenderPreemptableVerb = "extender.preemptableVerb"
    53  	// ExtenderReclaimableVerb is the verb of Reclaimable method
    54  	ExtenderReclaimableVerb = "extender.reclaimableVerb"
    55  	// ExtenderQueueOverusedVerb is the verb of QueueOverused method
    56  	ExtenderQueueOverusedVerb = "extender.queueOverusedVerb"
    57  	// ExtenderJobEnqueueableVerb is the verb of JobEnqueueable method
    58  	ExtenderJobEnqueueableVerb = "extender.jobEnqueueableVerb"
    59  	// ExtenderJobReadyVerb is the verb of JobReady method
    60  	ExtenderJobReadyVerb = "extender.jobReadyVerb"
    61  	// ExtenderIgnorable indicates whether the extender can ignore unexpected errors
    62  	ExtenderIgnorable = "extender.ignorable"
    63  )
    64  
    65  type extenderConfig struct {
    66  	urlPrefix          string
    67  	httpTimeout        time.Duration
    68  	onSessionOpenVerb  string
    69  	onSessionCloseVerb string
    70  	predicateVerb      string
    71  	prioritizeVerb     string
    72  	preemptableVerb    string
    73  	reclaimableVerb    string
    74  	queueOverusedVerb  string
    75  	jobEnqueueableVerb string
    76  	jobReadyVerb       string
    77  	ignorable          bool
    78  }
    79  
    80  type extenderPlugin struct {
    81  	client http.Client
    82  	config *extenderConfig
    83  }
    84  
    85  func parseExtenderConfig(arguments framework.Arguments) *extenderConfig {
    86  	/*
    87  		   actions: "reclaim, allocate, backfill, preempt"
    88  		   tiers:
    89  		   - plugins:
    90  		     - name: priority
    91  		     - name: gang
    92  		     - name: conformance
    93  		   - plugins:
    94  		     - name: drf
    95  		     - name: predicates
    96  			 - name: extender
    97  		       arguments:
    98  				   extender.urlPrefix: http://127.0.0.1
    99  				   extender.httpTimeout: 100ms
   100  				   extender.onSessionOpenVerb: onSessionOpen
   101  				   extender.onSessionCloseVerb: onSessionClose
   102  				   extender.predicateVerb: predicate
   103  				   extender.prioritizeVerb: prioritize
   104  				   extender.preemptableVerb: preemptable
   105  				   extender.reclaimableVerb: reclaimable
   106  				   extender.queueOverusedVerb: queueOverused
   107  				   extender.jobEnqueueableVerb: jobEnqueueable
   108  				   extender.ignorable: true
   109  		     - name: proportion
   110  		     - name: nodeorder
   111  	*/
   112  	ec := &extenderConfig{}
   113  	ec.urlPrefix, _ = arguments[ExtenderURLPrefix].(string)
   114  	ec.onSessionOpenVerb, _ = arguments[ExtenderOnSessionOpenVerb].(string)
   115  	ec.onSessionCloseVerb, _ = arguments[ExtenderOnSessionCloseVerb].(string)
   116  	ec.predicateVerb, _ = arguments[ExtenderPredicateVerb].(string)
   117  	ec.prioritizeVerb, _ = arguments[ExtenderPrioritizeVerb].(string)
   118  	ec.preemptableVerb, _ = arguments[ExtenderPreemptableVerb].(string)
   119  	ec.reclaimableVerb, _ = arguments[ExtenderReclaimableVerb].(string)
   120  	ec.queueOverusedVerb, _ = arguments[ExtenderQueueOverusedVerb].(string)
   121  	ec.jobEnqueueableVerb, _ = arguments[ExtenderJobEnqueueableVerb].(string)
   122  	ec.jobReadyVerb, _ = arguments[ExtenderJobReadyVerb].(string)
   123  
   124  	arguments.GetBool(&ec.ignorable, ExtenderIgnorable)
   125  
   126  	ec.httpTimeout = time.Second
   127  	if httpTimeout, _ := arguments[ExtenderHTTPTimeout].(string); httpTimeout != "" {
   128  		if timeoutDuration, err := time.ParseDuration(httpTimeout); err == nil {
   129  			ec.httpTimeout = timeoutDuration
   130  		}
   131  	}
   132  
   133  	return ec
   134  }
   135  
   136  func New(arguments framework.Arguments) framework.Plugin {
   137  	cfg := parseExtenderConfig(arguments)
   138  	klog.V(4).Infof("Initialize extender plugin with endpoint address %s", cfg.urlPrefix)
   139  	return &extenderPlugin{client: http.Client{Timeout: cfg.httpTimeout}, config: cfg}
   140  }
   141  
   142  func (ep *extenderPlugin) Name() string {
   143  	return PluginName
   144  }
   145  
   146  func (ep *extenderPlugin) OnSessionOpen(ssn *framework.Session) {
   147  	if ep.config.onSessionOpenVerb != "" {
   148  		err := ep.send(ep.config.onSessionOpenVerb, &OnSessionOpenRequest{
   149  			Jobs:           ssn.Jobs,
   150  			Nodes:          ssn.Nodes,
   151  			Queues:         ssn.Queues,
   152  			NamespaceInfo:  ssn.NamespaceInfo,
   153  			RevocableNodes: ssn.RevocableNodes,
   154  		}, nil)
   155  		if err != nil {
   156  			klog.Warningf("OnSessionClose failed with error %v", err)
   157  		}
   158  		if err != nil && !ep.config.ignorable {
   159  			return
   160  		}
   161  	}
   162  
   163  	if ep.config.predicateVerb != "" {
   164  		ssn.AddPredicateFn(ep.Name(), func(task *api.TaskInfo, node *api.NodeInfo) ([]*api.Status, error) {
   165  			resp := &PredicateResponse{}
   166  			err := ep.send(ep.config.predicateVerb, &PredicateRequest{Task: task, Node: node}, resp)
   167  			if err != nil {
   168  				klog.Warningf("Predicate failed with error %v", err)
   169  
   170  				if ep.config.ignorable {
   171  					return nil, nil
   172  				}
   173  				return nil, err
   174  			}
   175  
   176  			predicateStatus := resp.Status
   177  			return predicateStatus, nil
   178  		})
   179  	}
   180  
   181  	if ep.config.prioritizeVerb != "" {
   182  		ssn.AddBatchNodeOrderFn(ep.Name(), func(task *api.TaskInfo, nodes []*api.NodeInfo) (map[string]float64, error) {
   183  			resp := &PrioritizeResponse{}
   184  			err := ep.send(ep.config.prioritizeVerb, &PrioritizeRequest{Task: task, Nodes: nodes}, resp)
   185  			if err != nil {
   186  				klog.Warningf("Prioritize failed with error %v", err)
   187  
   188  				if ep.config.ignorable {
   189  					return nil, nil
   190  				}
   191  				return nil, err
   192  			}
   193  
   194  			if resp.ErrorMessage == "" && resp.NodeScore != nil {
   195  				return resp.NodeScore, nil
   196  			}
   197  			return nil, errors.New(resp.ErrorMessage)
   198  		})
   199  	}
   200  
   201  	if ep.config.preemptableVerb != "" {
   202  		ssn.AddPreemptableFn(ep.Name(), func(evictor *api.TaskInfo, evictees []*api.TaskInfo) ([]*api.TaskInfo, int) {
   203  			resp := &PreemptableResponse{}
   204  			err := ep.send(ep.config.preemptableVerb, &PreemptableRequest{Evictor: evictor, Evictees: evictees}, resp)
   205  			if err != nil {
   206  				klog.Warningf("Preemptable failed with error %v", err)
   207  
   208  				if ep.config.ignorable {
   209  					return nil, util.Permit
   210  				}
   211  				return nil, util.Reject
   212  			}
   213  
   214  			return resp.Victims, resp.Status
   215  		})
   216  	}
   217  
   218  	if ep.config.reclaimableVerb != "" {
   219  		ssn.AddReclaimableFn(ep.Name(), func(evictor *api.TaskInfo, evictees []*api.TaskInfo) ([]*api.TaskInfo, int) {
   220  			resp := &ReclaimableResponse{}
   221  			err := ep.send(ep.config.reclaimableVerb, &ReclaimableRequest{Evictor: evictor, Evictees: evictees}, resp)
   222  			if err != nil {
   223  				klog.Warningf("Reclaimable failed with error %v", err)
   224  
   225  				if ep.config.ignorable {
   226  					return nil, util.Permit
   227  				}
   228  				return nil, util.Reject
   229  			}
   230  
   231  			return resp.Victims, resp.Status
   232  		})
   233  	}
   234  
   235  	if ep.config.jobEnqueueableVerb != "" {
   236  		ssn.AddJobEnqueueableFn(ep.Name(), func(obj interface{}) int {
   237  			job := obj.(*api.JobInfo)
   238  			resp := &JobEnqueueableResponse{}
   239  			err := ep.send(ep.config.jobEnqueueableVerb, &JobEnqueueableRequest{Job: job}, resp)
   240  			if err != nil {
   241  				klog.Warningf("JobEnqueueable failed with error %v", err)
   242  
   243  				if ep.config.ignorable {
   244  					return util.Permit
   245  				}
   246  				return util.Reject
   247  			}
   248  
   249  			return resp.Status
   250  		})
   251  	}
   252  
   253  	if ep.config.queueOverusedVerb != "" {
   254  		ssn.AddOverusedFn(ep.Name(), func(obj interface{}) bool {
   255  			queue := obj.(*api.QueueInfo)
   256  			resp := &QueueOverusedResponse{}
   257  			err := ep.send(ep.config.queueOverusedVerb, &QueueOverusedRequest{Queue: queue}, resp)
   258  			if err != nil {
   259  				klog.Warningf("QueueOverused failed with error %v", err)
   260  
   261  				return !ep.config.ignorable
   262  			}
   263  
   264  			return resp.Overused
   265  		})
   266  	}
   267  
   268  	if ep.config.jobReadyVerb != "" {
   269  		ssn.AddJobReadyFn(ep.Name(), func(obj interface{}) bool {
   270  			job := obj.(*api.JobInfo)
   271  			resp := &JobReadyResponse{}
   272  			err := ep.send(ep.config.jobReadyVerb, &JobReadyRequest{Job: job}, resp)
   273  			if err != nil {
   274  				klog.Warningf("JobReady failed with error %v", err)
   275  
   276  				return !ep.config.ignorable
   277  			}
   278  
   279  			return resp.Status
   280  		})
   281  	}
   282  }
   283  
   284  func (ep *extenderPlugin) OnSessionClose(ssn *framework.Session) {
   285  	if ep.config.onSessionCloseVerb != "" {
   286  		if err := ep.send(ep.config.onSessionCloseVerb, &OnSessionCloseRequest{}, nil); err != nil {
   287  			klog.Warningf("OnSessionClose failed with error %v", err)
   288  		}
   289  	}
   290  }
   291  
   292  func (ep *extenderPlugin) send(action string, args interface{}, result interface{}) error {
   293  	out, err := json.Marshal(args)
   294  	if err != nil {
   295  		return err
   296  	}
   297  
   298  	url := strings.TrimRight(ep.config.urlPrefix, "/") + "/" + action
   299  
   300  	req, err := http.NewRequest("POST", url, bytes.NewReader(out))
   301  	if err != nil {
   302  		return err
   303  	}
   304  
   305  	req.Header.Set("Content-Type", "application/json")
   306  
   307  	resp, err := ep.client.Do(req)
   308  	if err != nil {
   309  		return err
   310  	}
   311  	defer resp.Body.Close()
   312  
   313  	if resp.StatusCode != http.StatusOK {
   314  		return fmt.Errorf("failed %v with extender at URL %v, code %v", action, url, resp.StatusCode)
   315  	}
   316  
   317  	if result != nil {
   318  		return json.NewDecoder(resp.Body).Decode(result)
   319  	}
   320  	return nil
   321  }