88import shlex
99import argparse
1010from ..config import add_parser_args
11+ from ..utils import normalize_command
1112import traceback
1213import datetime
1314import re
@@ -47,143 +48,16 @@ def do_orchestrate(self, group):
4748
4849 agents = self .fetch_worker_group_agents (group )
4950
50- desired_workers = self .get_desired_workers_for_group (group , agents )
51-
5251 # Evaluate what workers are currently, rightfully there. They won't be touched.
53- current_workers = defaultdict (int )
5452 for agent in agents :
55- agent ["free_memory" ] = agent ["total_memory" ]
56- agent ["free_cpu" ] = agent ["total_cpu" ]
53+ desired_workers = self .get_desired_workers_for_agent (group , agent )
5754 agent ["new_desired_workers" ] = []
58- for worker in agent .get ("desired_workers" , []):
59- if worker in desired_workers :
60- cpu = desired_workers [worker ]["cpu" ]
61- memory = desired_workers [worker ]["memory" ]
62-
63- # If no more memory for currently existing workers: their requirements must have changed.
64- # We need to schedule it somewhere else
65- if cpu <= agent ["free_cpu" ] and memory <= agent ["free_memory" ]:
66- current_workers [worker ] += 1
67- agent ["free_cpu" ] -= cpu
68- agent ["free_memory" ] -= memory
69- agent ["new_desired_workers" ].append (worker )
70-
71- # What changes need to be made in worker count
72- deltas = {
73- worker : (worker_info ["desired_count" ] - current_workers [worker ])
74- for worker , worker_info in desired_workers .items ()
75- if worker_info ["desired_count" ] != current_workers [worker ]
76- }
77-
78- # Remove workers from the most loaded machines (TODO improve)
79- for worker , delta in deltas .items ():
80- if delta >= 0 :
81- continue
82-
83- for _ in range (delta , 0 ):
84- found = False
85- for agent in sorted (agents , key = lambda a : float (a ["free_cpu" ]) / a ["total_cpu" ]):
86- for i in range (len (agent ["new_desired_workers" ])):
87- if agent ["new_desired_workers" ][i ] == worker :
88- agent ["new_desired_workers" ].pop (i )
89- agent ["free_cpu" ] += desired_workers [worker ]["cpu" ]
90- agent ["free_memory" ] += desired_workers [worker ]["memory" ]
91- found = True
92- break
93- if found :
94- break
95-
96- assert found
97-
98- needs_new_agents = False
99-
100- # Add new workers to the least loaded machines
101- for worker , delta in deltas .items ():
102- if delta <= 0 :
103- continue
104-
105- cpu = desired_workers [worker ]["cpu" ]
106- memory = desired_workers [worker ]["memory" ]
107-
108- for _ in range (delta ):
109- found = False
110- for agent in sorted (agents , key = lambda a : float (a ["free_cpu" ]) / a ["total_cpu" ], reverse = True ):
111- if cpu <= agent ["free_cpu" ] and memory <= agent ["free_memory" ]:
112- agent ["new_desired_workers" ].append (worker )
113- agent ["free_cpu" ] -= cpu
114- agent ["free_memory" ] -= memory
115- found = True
116- break
117-
118- if not found :
119- log .debug ("Worker orchestration: no agent had enough CPU & memory (%s & %s) to schedule a new worker" % (cpu , memory ))
120- needs_new_agents = True
121- break
122-
123- # Worker diversity enforcement: if we are under-scaled, make sure that at least one worker
124- # of each profile is launched. if not, make space forcefully on other workers.
125- if needs_new_agents :
126-
127- all_profiles = defaultdict (int )
128- for agent in agents :
129- for w in agent ["new_desired_workers" ]:
130- all_profiles [w ] += 1
131-
132- removable = sorted ([list (x ) for x in all_profiles .items () if x [1 ] > 1 ], key = lambda item : item [1 ], reverse = True )
133-
134- for profile in desired_workers :
135- if desired_workers [profile ]["desired_count" ] == 0 :
136- continue
137- if profile not in all_profiles :
138- found = False
139- # This profile is not currently represented in the workers. Make some space for it.
140- for rem in removable :
141- for agent in agents :
142- for i in range (len (agent ["new_desired_workers" ])):
143- if agent ["new_desired_workers" ][i ] == rem [0 ] and rem [1 ] > 1 :
144- agent ["new_desired_workers" ][i ] = None
145- agent ["free_cpu" ] += desired_workers [rem [0 ]]["cpu" ]
146- agent ["free_memory" ] += desired_workers [rem [0 ]]["memory" ]
147- rem [1 ] -= 1
148- if desired_workers [profile ]["cpu" ] <= agent ["free_cpu" ] and desired_workers [profile ]["memory" ] <= agent ["free_memory" ]:
149- log .debug ("Orchestration: enforcing worker diversity with %s => %s" % (rem [0 ], profile ))
150- agent ["new_desired_workers" ].append (profile )
151- agent ["free_cpu" ] -= desired_workers [profile ]["cpu" ]
152- agent ["free_memory" ] -= desired_workers [profile ]["memory" ]
153- found = True
154- break
155-
156- agent ["new_desired_workers" ] = [x for x in agent ["new_desired_workers" ] if x is not None ]
157- if found :
158- break
159-
160- if found :
161- break
162-
163- if not found :
164- log .debug ("Orchestration couldn't enforce worker diversity for profile '%s'" % profile )
165-
166- # User-provided autoscaling task
167- if self .config .get ("autoscaling_taskpath" ):
168- result = run_task (self .config ["autoscaling_taskpath" ], {
169- "agents" : agents ,
170- "needs_new_agents" : needs_new_agents ,
171- "worker_group" : group ["_id" ]
172- })
173- needs_new_agents = result ["needs_new_agents" ]
174- agents = result ["agents" ]
175-
176- # Save the new values in the DB. They will be applied by each agent process.
177- connections .mongodb_jobs .worker_groups .update_one ({"_id" : group ["_id" ]}, {"$set" : {
178- "needs_new_agents" : needs_new_agents
179- }})
55+ agent ["new_desired_workers" ] = desired_workers
18056
18157 for agent in agents :
18258 if sorted (agent ["new_desired_workers" ]) != sorted (agent .get ("desired_workers" , [])):
18359 connections .mongodb_jobs .mrq_agents .update_one ({"_id" : agent ["_id" ]}, {"$set" : {
184- "desired_workers" : agent ["new_desired_workers" ],
185- "free_cpu" : agent ["free_cpu" ],
186- "free_memory" : agent ["free_memory" ]
60+ "desired_workers" : agent ["new_desired_workers" ]
18761 }})
18862
18963 # Remember the date of the last successful orchestration (will be reported)
@@ -195,95 +69,8 @@ def redis_queuestats_key(self):
19569 """ Returns the global HSET redis key used to store queue stats """
19670 return "%s:queuestats" % (get_current_config ()["redis_prefix" ])
19771
198- def get_desired_workers_for_group (self , group , agents ):
199-
200- workers = {}
201-
202- def unpack (v ):
203- s = v .split (" " )
204- return (int (s [0 ]), None if s [1 ] == "N" else float (s [1 ]), int (s [2 ]))
205-
206- # count_jobs, eta, last_time
207- etas = {
208- k : unpack (v )
209- for k , v in connections .redis .hgetall (self .redis_queuestats_key ).items ()
210- }
211-
212- # Compute average usage of each worker profile
213- worker_reports = self .fetch_worker_group_reports (group , projection = [
214- "_id" , "config.worker_profile" , "usage_avg" , "status" , "datestarted" # , "process.cpu", "process.mem"
215- ])
216-
217- worker_warmups_by_profile = defaultdict (int )
218- worker_usages_by_profile = defaultdict (list )
219- for rep in worker_reports :
220- profileid = rep ["config" ].get ("worker_profile" )
221- if not profileid or rep .get ("status" ) not in ("wait" , "spawn" , "full" ):
222- continue
223- # Don't take brand new workers into account yet.
224- age = (datetime .datetime .utcnow () - rep ["datestarted" ]).total_seconds ()
225- if age < group .get ("profiles" , {}).get (profileid , {}).get ("warmup" , 60 ):
226- worker_warmups_by_profile [profileid ] += 1
227- continue
228- worker_usages_by_profile [profileid ].append (rep ["usage_avg" ])
229-
230- worker_count_by_profile = defaultdict (int )
231- for agent in agents :
232- for command in agent .get ("desired_workers" , []):
233- profile = re .search (r"^MRQ_WORKER_PROFILE=([^\s]+)" , command )
234- if profile :
235- profile = profile .group (1 )
236- worker_count_by_profile [profile ] += 1
237-
238- # Compute the desired count for each profile
239- # This is the real "autoscaling" part.
240- for profileid , profile in group .get ("profiles" , {}).items ():
241-
242- cfg = self .get_config_for_profile (profile )
243- eta_queues = [q for q in cfg .queues if (q in etas and etas [q ][1 ] is not None )]
244- worker_usage = sum (worker_usages_by_profile .get (profileid , []))
245- report_count = len (worker_usages_by_profile .get (profileid , []))
246- current_desired_count = worker_count_by_profile [profileid ]
247- total_greenlets = (cfg .greenlets or 1 ) * (cfg .processes or 1 )
248- warmups = worker_warmups_by_profile [profileid ]
249-
250- # By default, try not to change count
251- desired_count = current_desired_count
252-
253- # If the queue is doing OK, are all instances necessary?
254- if warmups == 0 and report_count > 0 :
255- desired_count = math .ceil (worker_usage * (float (current_desired_count ) / report_count ))
256-
257- if len (eta_queues ) > 0 :
258-
259- total_jobs = sum (etas [q ][0 ] for q in eta_queues )
260- max_eta = max (etas [q ][1 ] for q in eta_queues )
261-
262- max_allowed_eta = profile .get ("max_eta" , 3600 )
263-
264- # Queue is taking too long, try to add an instance.
265- if max_eta > max_allowed_eta or any (etas [q ][1 ] < 0 for q in eta_queues ):
266-
267- # Don't add a worker if the absolute number of remaining jobs is less than the number of
268- # greenlets. (They may become available right now)
269- # Also don't add a worker if the reported, warmed-up worker count is not yet the desired one.
270- if total_jobs > total_greenlets and report_count == current_desired_count :
271- desired_count = current_desired_count + 1
272-
273- final_count = min (max (desired_count , profile .get ("min_count" , 0 )), profile .get ("max_count" , 100 ))
274-
275- if final_count != current_desired_count :
276- log .debug ("Autoscaling: Changing worker profile %s count from %s to %s" % (
277- profileid , current_desired_count , final_count
278- ))
279-
280- workers [profile ["command" ]] = {
281- "desired_count" : final_count ,
282- "memory" : profile ["memory" ],
283- "cpu" : profile ["cpu" ]
284- }
285-
286- return workers
72+ def get_desired_workers_for_agent (self , group , agent ):
73+ return group .get ("commands" , [])
28774
28875 def fetch_worker_group_reports (self , worker_group , projection = None ):
28976 return list (connections .mongodb_jobs .mrq_workers .find ({
@@ -295,9 +82,12 @@ def fetch_worker_group_definitions(self):
29582 definitions = list (connections .mongodb_jobs .mrq_workergroups .find ())
29683
29784 for definition in definitions :
298- # Prepend all commands by their worker profile.
299- for profileid , profile in (definition or {}).get ("profiles" , {}).items ():
300- profile ["command" ] = "MRQ_WORKER_PROFILE=%s %s" % (profileid , profile ["command" ])
85+ commands = []
86+ # Prepend all commands by their worker group.
87+ for command in definition .get ("commands" , []):
88+ simplified_command , worker_count = normalize_command (command , definition ["_id" ])
89+ commands .extend ([simplified_command ] * worker_count )
90+ definition ["commands" ] = commands
30191
30292 return definitions
30393
0 commit comments