blob: 80d6637b44d429ca3619a74bc08824f3a7b2436c [file] [log] [blame]
#!/usr/bin/env vpython
# Copyright 2022 The LUCI Authors. All rights reserved.
# Use of this source code is governed under the Apache License, Version 2.0
# that can be found in the LICENSE file.
import datetime
import json
import logging
import sys
import unittest
import test_env
test_env.setup_test_env()
import mock
from test_support import test_case
from components import utils
from components import datastore_utils
from server import pools_config
from server import rbe
from server import task_request
from server import task_to_run
from proto.config import pools_pb2
class RBETest(test_case.TestCase):
@staticmethod
def pool_config(rbe_instance, rbe_mode_percent=100, allocs=None):
rbe_mgration = None
if rbe_instance is not None:
rbe_mgration = pools_pb2.Pool.RBEMigration(
rbe_instance=rbe_instance,
rbe_mode_percent=rbe_mode_percent,
bot_mode_allocation=[
pools_pb2.Pool.RBEMigration.BotModeAllocation(
mode=mode,
percent=percent,
) for mode, percent in (allocs or {}).items()
],
)
return pools_config.init_pool_config(rbe_migration=rbe_mgration)
@mock.patch('server.pools_config.get_pool_config')
@mock.patch('server.rbe._quasi_random_100')
def test_get_rbe_config_for_bot(self, quasi_random_100, pool_config):
pools = {}
pool_config.side_effect = lambda pool: pools[pool]
# Pure Swarming pool.
pools['swarming'] = self.pool_config(None)
cfg = rbe.get_rbe_config_for_bot('bot-id', ['swarming'])
self.assertIsNone(cfg)
# A pool with mixed composition of bots.
pools['many-modes'] = self.pool_config('instance',
allocs={
'SWARMING': 10,
'HYBRID': 60,
'RBE': 30,
})
# Swarming mode.
quasi_random_100.return_value = 9
cfg = rbe.get_rbe_config_for_bot('bot-id', ['many-modes'])
self.assertIsNone(cfg)
# Hybrid mode.
quasi_random_100.return_value = 10
cfg = rbe.get_rbe_config_for_bot('bot-id', ['many-modes'])
self.assertEqual(cfg.instance, 'instance')
self.assertTrue(cfg.hybrid_mode)
# RBE mode.
quasi_random_100.return_value = 70
cfg = rbe.get_rbe_config_for_bot('bot-id', ['many-modes'])
self.assertEqual(cfg.instance, 'instance')
self.assertFalse(cfg.hybrid_mode)
# Older RBE migration config without allocations => use Swarming mode.
pools['old-config'] = self.pool_config('instance')
cfg = rbe.get_rbe_config_for_bot('bot-id', ['old-config'])
self.assertIsNone(cfg)
# Multi-pool bot.
pools['pool-a'] = self.pool_config('instance',
allocs={
'SWARMING': 50,
'RBE': 50,
})
pools['pool-b'] = self.pool_config('instance',
allocs={
'SWARMING': 80,
'RBE': 20,
})
# All pools agree on a mode.
quasi_random_100.return_value = 90
cfg = rbe.get_rbe_config_for_bot('bot-id', ['pool-a', 'pool-b'])
self.assertEqual(cfg.instance, 'instance')
self.assertFalse(cfg.hybrid_mode)
# Pools disagree on a mode.
quasi_random_100.return_value = 60
cfg = rbe.get_rbe_config_for_bot('bot-id', ['pool-a', 'pool-b'])
self.assertEqual(cfg.instance, 'instance')
self.assertTrue(cfg.hybrid_mode)
# Multi-pool bot, pools disagree on RBE instance.
pools['pool-c'] = self.pool_config('instance-c', allocs={
'RBE': 100,
})
pools['pool-d'] = self.pool_config('instance-d', allocs={
'RBE': 100,
})
# All pools agree on a mode.
quasi_random_100.return_value = 90
cfg = rbe.get_rbe_config_for_bot('bot-id', ['pool-c', 'pool-d'])
self.assertEqual(cfg.instance, 'instance-c')
self.assertFalse(cfg.hybrid_mode)
@mock.patch('random.uniform')
def test_get_rbe_instance_for_task(self, uniform):
call = rbe.get_rbe_instance_for_task
self.assertIsNone(call([], self.pool_config(None)))
self.assertIsNone(call([], self.pool_config('')))
self.assertEqual(call(['rbe:require'], self.pool_config('inst', 0)), 'inst')
self.assertIsNone(call(['rbe:prevent'], self.pool_config('inst', 100)))
self.assertIsNone(
call(['rbe:require', 'rbe:prevent'], self.pool_config('inst', 100)))
uniform.return_value = 5.0
self.assertEqual(call([], self.pool_config('inst', 6)), 'inst')
self.assertIsNone(call([], self.pool_config('inst', 4)))
def test_quasi_random_100(self):
for i in range(1000):
val = rbe._quasi_random_100(u'bot-%d' % i)
self.assertGreaterEqual(val, 0)
self.assertLess(val, 100)
class EnqueueTest(test_case.TestCase):
maxDiff = None
def make_request(self):
def make_slice(name):
return task_request.TaskSlice(
properties=task_request.TaskProperties(
dimensions_data={
u'id': [u'bot-id'],
u'dim1': [u'val1', u'val2|val3'],
u'dim2': [u'val4'],
u'name': [name],
},
execution_timeout_secs=700,
grace_period_secs=300,
),
expiration_secs=123,
)
req = task_request.TaskRequest(
key=task_request.new_request_key(),
created_ts=utils.utcnow(),
name='some-name',
rbe_instance='some-instance',
priority=123,
scheduling_algorithm=pools_pb2.Pool.SCHEDULING_ALGORITHM_LIFO,
task_slices=[
make_slice(u'0'),
make_slice(u'1'),
make_slice(u'2'),
],
)
return req, task_to_run.new_task_to_run(req, 2)
@mock.patch('components.utils.enqueue_task')
@mock.patch('components.utils.utcnow')
@mock.patch('random.getrandbits')
def test_enqueue_rbe_task(self, getrandbits, utcnow, enqueue_task):
getrandbits.return_value = 42
utcnow.return_value = datetime.datetime(2112, 1, 1, 1, 1, 1)
enqueue_task.return_value = True
req, ttr = self.make_request()
datastore_utils.transaction(lambda: rbe.enqueue_rbe_task(req, ttr))
args, kwargs = enqueue_task.call_args
kwargs['payload'] = json.loads(kwargs['payload'])
self.assertEqual(
args,
('/internal/tasks/t/rbe-enqueue/2ed6c6804c8002a10-2', 'rbe-enqueue'))
self.assertEqual(
kwargs, {
'payload': {
u'body': {
u'payload': {
u'reservationId': u'sample-app-2ed6c6804c8002a10-2',
u'taskId': u'2ed6c6804c8002a10',
u'sliceIndex': 2,
u'taskToRunId': u'33',
u'taskToRunShard': 15,
u'debugInfo': {
u'created': u'2112-01-01T01:01:01Z',
u'pySwarmingVersion': u'v1a',
u'taskName': u'some-name',
},
},
u'rbeInstance':
u'some-instance',
u'executionTimeout':
u'1030s',
u'expiry':
u'2112-01-01T01:07:10Z',
u'requestedBotId':
u'bot-id',
u'constraints': [
{
u'key': u'dim1',
u'allowedValues': [u'val1']
},
{
u'key': u'dim1',
u'allowedValues': [u'val2', u'val3']
},
{
u'key': u'dim2',
u'allowedValues': [u'val4']
},
{
u'key': u'name',
u'allowedValues': [u'2']
},
],
u'priority':
123,
u'schedulingAlgorithm':
u'SCHEDULING_ALGORITHM_LIFO',
},
u'class': u'rbe-enqueue',
},
'transactional': True,
'use_dedicated_module': False,
})
@mock.patch('components.utils.enqueue_task')
@mock.patch('components.utils.utcnow')
@mock.patch('random.getrandbits')
def test_enqueue_rbe_cancel(self, getrandbits, utcnow, enqueue_task):
getrandbits.return_value = 42
utcnow.return_value = datetime.datetime(2112, 1, 1, 1, 1, 1)
enqueue_task.return_value = True
req, ttr = self.make_request()
datastore_utils.transaction(lambda: rbe.enqueue_rbe_cancel(req, ttr))
args, kwargs = enqueue_task.call_args
kwargs['payload'] = json.loads(kwargs['payload'])
self.assertEqual(
args,
('/internal/tasks/t/rbe-cancel/2ed6c6804c8002a10-2', 'rbe-cancel'))
self.assertEqual(
kwargs, {
'payload': {
u'body': {
u'rbeInstance': u'some-instance',
u'reservationId': u'sample-app-2ed6c6804c8002a10-2',
u'debugInfo': {
u'created': u'2112-01-01T01:01:01Z',
u'pySwarmingVersion': u'v1a',
u'taskName': u'some-name'
},
},
u'class': u'rbe-cancel',
},
'transactional': True,
'use_dedicated_module': False,
})
if __name__ == '__main__':
if '-v' in sys.argv:
unittest.TestCase.maxDiff = None
logging.basicConfig(
level=logging.DEBUG if '-v' in sys.argv else logging.CRITICAL)
unittest.main()