| #!/usr/bin/env vpython |
| # Copyright 2014 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 hashlib |
| import logging |
| import sys |
| import unittest |
| |
| import mock |
| from parameterized import parameterized |
| |
| import test_env |
| |
| test_env.setup_test_env() |
| |
| from google.protobuf import struct_pb2 |
| from google.protobuf import timestamp_pb2 |
| |
| from google.appengine.api import memcache |
| from google.appengine.ext import ndb |
| |
| from components import utils |
| from test_support import test_case |
| |
| from proto.api import swarming_pb2 # pylint: disable=no-name-in-module |
| from server import bot_management |
| from server import config |
| from server import task_queues |
| |
| |
| _VERSION = unicode(hashlib.sha256().hexdigest()) |
| |
| |
| def _bot_event( |
| event_type, |
| bot_id=None, |
| external_ip="8.8.4.4", |
| authenticated_as=None, |
| dimensions=None, |
| state=None, |
| version=_VERSION, |
| quarantined=False, |
| maintenance_msg=None, |
| task_id=None, |
| task_name=None, |
| **kwargs, |
| ): |
| """Calls bot_management.bot_event with default arguments.""" |
| if not bot_id: |
| bot_id = "id1" |
| if not dimensions: |
| dimensions = { |
| "id": [bot_id], |
| "os": ["Ubuntu", "Ubuntu-16.04"], |
| "pool": ["default"], |
| } |
| if not authenticated_as: |
| authenticated_as = "bot:%s.domain" % bot_id |
| register_dimensions = event_type.startswith("request_") or event_type in ( |
| "bot_idle", |
| "bot_polling", |
| ) |
| return bot_management.bot_event( |
| event_type=event_type, |
| bot_id=bot_id, |
| external_ip=external_ip, |
| authenticated_as=authenticated_as, |
| dimensions=dimensions, |
| state=state or {"ram": 65}, |
| version=version, |
| quarantined=quarantined, |
| maintenance_msg=maintenance_msg, |
| task_id=task_id, |
| task_name=task_name, |
| register_dimensions=register_dimensions, |
| **kwargs, |
| ) |
| |
| |
| def _ensure_bot_info(bot_id="id1", **kwargs): |
| _bot_event(bot_id=bot_id, event_type="request_sleep", **kwargs) |
| return bot_management.get_info_key(bot_id).get() |
| |
| |
| def _gen_bot_info(**kwargs): |
| now = kwargs.get("last_seen_ts") or utils.utcnow() |
| out = { |
| "authenticated_as": "bot:id1.domain", |
| "composite": [ |
| bot_management.BotInfo.NOT_IN_MAINTENANCE, |
| bot_management.BotInfo.ALIVE, |
| bot_management.BotInfo.HEALTHY, |
| bot_management.BotInfo.IDLE, |
| ], |
| "dimensions": { |
| "id": ["id1"], |
| "os": ["Ubuntu", "Ubuntu-16.04"], |
| "pool": ["default"], |
| }, |
| "expire_at": now + bot_management._OLD_BOT_INFO_CUT_OFF, |
| "external_ip": "8.8.4.4", |
| "first_seen_ts": now, |
| "id": "id1", |
| "idle_since_ts": None, |
| "is_dead": False, |
| "last_finished_task": None, |
| "last_seen_ts": now, |
| "lease_id": None, |
| "lease_expiration_ts": None, |
| "leased_indefinitely": None, |
| "machine_lease": None, |
| "machine_type": None, |
| "quarantined": False, |
| "maintenance_msg": None, |
| "owners": [], |
| "session_id": None, |
| "state": {"ram": 65}, |
| "task_id": None, |
| "task_flags": None, |
| "task_name": None, |
| "termination_task_id": None, |
| "version": _VERSION, |
| "rbe_effective_bot_id": None, |
| "last_abandoned_task": None, |
| } |
| out.update(kwargs) |
| return out |
| |
| |
| def _gen_bot_event(**kwargs): |
| ts = kwargs.get("ts") or utils.utcnow() |
| out = { |
| "authenticated_as": "bot:id1.domain", |
| "dimensions": { |
| "id": ["id1"], |
| "os": ["Ubuntu", "Ubuntu-16.04"], |
| "pool": ["default"], |
| }, |
| "expire_at": ts + bot_management._OLD_BOT_EVENTS_CUT_OFF, |
| "external_ip": "8.8.4.4", |
| "idle_since_ts": None, |
| "last_seen_ts": kwargs.get("ts") or utils.utcnow(), |
| "lease_id": None, |
| "lease_expiration_ts": None, |
| "leased_indefinitely": None, |
| "machine_lease": None, |
| "machine_type": None, |
| "message": None, |
| "quarantined": False, |
| "maintenance_msg": None, |
| "owners": [], |
| "session_id": None, |
| "state": {"ram": 65}, |
| "task_id": None, |
| "ts": ts, |
| "version": _VERSION, |
| } |
| out.update(kwargs) |
| return out |
| |
| |
| class BotManagementTest(test_case.TestCase): |
| APP_DIR = test_env.APP_DIR |
| |
| def setUp(self): |
| super(BotManagementTest, self).setUp() |
| self.now = datetime.datetime(2010, 1, 2, 3, 4, 5, 6) |
| self.mock_now(self.now) |
| |
| def test_all_apis_are_tested(self): |
| actual = frozenset(i[5:] for i in dir(self) if i.startswith("test_")) |
| # Contains the list of all public APIs. |
| expected = frozenset( |
| i |
| for i in dir(bot_management) |
| if i[0] != "_" and hasattr(getattr(bot_management, i), "func_name") |
| ) |
| missing = expected - actual |
| self.assertFalse(missing) |
| |
| def test_BotEvent_proto_idle(self): |
| event_key = _bot_event( |
| event_type="request_sleep", |
| bot_id="id1", |
| dimensions={ |
| "id": ["id1"], |
| }, |
| ) |
| actual = swarming_pb2.BotEvent() |
| event_key.get().to_proto(actual) |
| self.assertEqual(swarming_pb2.INSTRUCT_IDLE, actual.event) |
| self.assertEqual(swarming_pb2.IDLE, actual.bot.status) |
| |
| def test_BotEvent_proto_busy(self): |
| event_key = _bot_event( |
| event_type="request_task", |
| task_id="task1", |
| bot_id="id1", |
| dimensions={ |
| "id": ["id1"], |
| }, |
| ) |
| actual = swarming_pb2.BotEvent() |
| event_key.get().to_proto(actual) |
| self.assertEqual(swarming_pb2.INSTRUCT_START_TASK, actual.event) |
| self.assertEqual(swarming_pb2.BUSY, actual.bot.status) |
| self.assertEqual("task1", actual.bot.current_task_id) |
| |
| def test_BotEvent_proto_empty(self): |
| # Assert that it doesn't throw on empty entity. |
| actual = swarming_pb2.BotEvent() |
| bot_management.BotEvent().to_proto(actual) |
| self.assertEqual(swarming_pb2.BotEvent(), actual) |
| |
| def test_BotEvent_proto_events(self): |
| # Ensures all bot event states can be converted to a proto. |
| dimensions = { |
| "id": ["id1"], |
| "os": ["Ubuntu", "Ubuntu-16.04"], |
| "pool": ["default"], |
| } |
| for name in bot_management.BotEvent.ALLOWED_EVENTS: |
| event_key = _bot_event( |
| event_type=name, bot_id="id1", dimensions=dimensions |
| ) |
| if name in ("task_update", "bot_polling"): |
| # TODO(maruel): Store request_sleep IFF the state changed. |
| self.assertIsNone(event_key, name) |
| continue |
| # Just asserts it doesn't crash. |
| actual = swarming_pb2.BotEvent() |
| event_key.get().to_proto(actual) |
| |
| def test_BotEvent_proto_maintenance(self): |
| # Also test a misconfigured bot not in a pool. |
| event_key = _bot_event( |
| event_type="bot_connected", |
| bot_id="id1", |
| dimensions={"id": ["id1"]}, |
| maintenance_msg="Too hot", |
| ) |
| actual = swarming_pb2.BotEvent() |
| event_key.get().to_proto(actual) |
| expected = swarming_pb2.BotEvent( |
| event=swarming_pb2.BOT_NEW_SESSION, |
| bot=swarming_pb2.Bot( |
| bot_id="id1", |
| dimensions=[ |
| swarming_pb2.StringListPair(key="id", values=["id1"]), |
| ], |
| status=swarming_pb2.OVERHEAD_MAINTENANCE_EXTERNAL, |
| status_msg="Too hot", |
| info=swarming_pb2.BotInfo( |
| supplemental=struct_pb2.Struct( |
| fields={ |
| "ram": struct_pb2.Value(number_value=65), |
| } |
| ), |
| version=_VERSION, |
| external_ip="8.8.4.4", |
| authenticated_as="bot:id1.domain", |
| ), |
| ), |
| ) |
| expected.event_time.FromDatetime(self.now) |
| self.assertEqual(unicode(expected), unicode(actual)) |
| |
| def test_BotEvent_proto_quarantine(self): |
| # Also test that a bot can belong to two pools. |
| event_key = _bot_event( |
| event_type="bot_connected", |
| bot_id="id1", |
| dimensions={"id": ["id1"], "pool": ["next", "previous"]}, |
| state={"ram": 65, "quarantined": "sad bot"}, |
| quarantined=True, |
| ) |
| actual = swarming_pb2.BotEvent() |
| event_key.get().to_proto(actual) |
| expected = swarming_pb2.BotEvent( |
| event=swarming_pb2.BOT_NEW_SESSION, |
| bot=swarming_pb2.Bot( |
| bot_id="id1", |
| pools=["next", "previous"], |
| dimensions=[ |
| swarming_pb2.StringListPair(key="id", values=["id1"]), |
| swarming_pb2.StringListPair(key="pool", values=["next", "previous"]), |
| ], |
| status=swarming_pb2.QUARANTINED_BY_BOT, |
| status_msg="sad bot", |
| info=swarming_pb2.BotInfo( |
| supplemental=struct_pb2.Struct( |
| fields={ |
| "quarantined": struct_pb2.Value(string_value="sad bot"), |
| "ram": struct_pb2.Value(number_value=65), |
| } |
| ), |
| version=_VERSION, |
| external_ip="8.8.4.4", |
| authenticated_as="bot:id1.domain", |
| ), |
| ), |
| ) |
| expected.event_time.FromDatetime(self.now) |
| self.assertEqual(unicode(expected), unicode(actual)) |
| |
| def test_bot_event(self): |
| # connected. |
| d = { |
| "id": ["id1"], |
| "os": ["Ubuntu", "Ubuntu-16.04"], |
| "pool": ["default"], |
| } |
| event = "request_sleep" |
| _bot_event(event_type=event, bot_id="id1", dimensions=d) |
| |
| expected = _gen_bot_info(idle_since_ts=self.now) |
| self.assertEqual( |
| expected, bot_management.get_info_key("id1").get().to_dict() |
| ) |
| |
| @parameterized.expand( |
| [ |
| # Starting and finishing normal tasks. |
| ("request_task", "task_completed", True, False), |
| ("request_task", "task_error", True, False), |
| ("request_task", "task_killed", True, False), |
| ("request_task", "request_sleep", True, True), |
| ("request_task", "task_update", False, True), |
| # Starting and finishing a termination task. |
| ("bot_terminate", "task_completed", True, False), |
| ] |
| ) |
| def test_bot_event_reset_task( |
| self, start_event, event, reset_task, skip_store_event |
| ): |
| bot_id = "id1" |
| task_id = "12311" |
| task_name = "yo" |
| |
| task_flags = 0 |
| if start_event == "bot_terminate": |
| task_flags = bot_management.TASK_FLAG_TERMINATION |
| |
| d = { |
| "id": ["id1"], |
| "os": ["Ubuntu", "Ubuntu-16.04"], |
| "pool": ["default"], |
| } |
| |
| # This creates request_sleep event and an idle bot. |
| t1 = self.mock_now(self.now, 1) |
| bot_info = _ensure_bot_info(bot_id=bot_id, dimensions=d) |
| |
| # This moves the bot into "executing a task now" state. |
| _bot_event( |
| event_type=start_event, |
| bot_id=bot_id, |
| dimensions=d, |
| task_id=task_id, |
| task_name=task_name, |
| ) |
| |
| # This performs the event being tested that can reset the task. |
| t2 = self.mock_now(self.now, 2) |
| _bot_event( |
| event_type=event, |
| bot_id=bot_id, |
| dimensions=d, |
| task_id=None if event == "request_sleep" else task_id, |
| task_name=None if event == "request_sleep" else task_name, |
| ) |
| |
| # check bot_info |
| composite = [ |
| bot_management.BotInfo.NOT_IN_MAINTENANCE, |
| bot_management.BotInfo.ALIVE, |
| bot_management.BotInfo.HEALTHY, |
| ] |
| if event == "request_sleep": |
| composite += [bot_management.BotInfo.IDLE] |
| idle_since_ts = t2 |
| else: |
| composite += [bot_management.BotInfo.BUSY] |
| idle_since_ts = None |
| |
| if reset_task: |
| # bot_info.task_id and bot_info.task_name should be reset |
| expected = _gen_bot_info( |
| first_seen_ts=t1, |
| composite=composite, |
| id=bot_id, |
| task_name=None, |
| idle_since_ts=idle_since_ts, |
| last_finished_task={ |
| "finished_due": event, |
| "task_flags": task_flags, |
| "task_id": task_id, |
| "task_name": task_name, |
| }, |
| ) |
| else: |
| # bot_info.task_id and bot_info.task_name should be kept |
| expected = _gen_bot_info( |
| first_seen_ts=t1, |
| composite=composite, |
| id=bot_id, |
| task_id=task_id, |
| task_name=task_name, |
| task_flags=task_flags, |
| idle_since_ts=None, |
| ) |
| |
| self.assertEqual(expected, bot_info.key.get().to_dict()) |
| |
| # bot_event should have task_id |
| if not skip_store_event: |
| expected_event = _gen_bot_event(event_type=event, task_id=task_id, ts=t2) |
| last_event = bot_management.get_events_query(bot_id).get() |
| self.assertEqual(expected_event, last_event.to_dict()) |
| |
| def test_get_events_query(self): |
| _bot_event(event_type="bot_connected") |
| expected = [_gen_bot_event(event_type="bot_connected")] |
| self.assertEqual( |
| expected, [i.to_dict() for i in bot_management.get_events_query("id1")] |
| ) |
| |
| def test_get_latest_info(self): |
| stored = _ensure_bot_info("bot-id") |
| fetched, alive = bot_management.get_latest_info("bot-id") |
| self.assertEqual(stored.dimensions_flat, fetched.dimensions_flat) |
| self.assertTrue(alive) |
| |
| def test_get_latest_info_dead(self): |
| stored = _ensure_bot_info("bot-id") |
| stored.key.delete() |
| fetched, alive = bot_management.get_latest_info("bot-id") |
| self.assertEqual(stored.dimensions_flat, fetched.dimensions_flat) |
| self.assertFalse(alive) |
| |
| def test_get_latest_info_missing(self): |
| fetched, alive = bot_management.get_latest_info("bot-id") |
| self.assertIsNone(fetched) |
| self.assertFalse(alive) |
| |
| def test_get_latest_info_race_condition_crbug_1407381(self): |
| stored = _ensure_bot_info("bot-id") |
| stored.key.delete() |
| |
| # The bot is considered dead now. |
| fetched, alive = bot_management.get_latest_info("bot-id") |
| self.assertIsNotNone(fetched) |
| self.assertFalse(alive) |
| |
| # Has an event history. |
| self.assertTrue(bot_management.get_events_query("bot-id").fetch(1)) |
| |
| get_events_query = bot_management.get_events_query |
| |
| # A bot reappears when the history is being fetched. |
| def get_events_query_mock(bot_id): |
| self.assertEqual(bot_id, "bot-id") |
| _ensure_bot_info(bot_id) |
| return get_events_query(bot_id) |
| |
| with mock.patch( |
| "server.bot_management.get_events_query", |
| side_effect=get_events_query_mock, |
| ) as m: |
| fetched, alive = bot_management.get_latest_info("bot-id") |
| self.assertEqual(stored.dimensions_flat, fetched.dimensions_flat) |
| self.assertTrue(alive) # considered alive now |
| m.assert_called_once() |
| |
| def test_get_bot_pools(self): |
| _ensure_bot_info(bot_id="bot-id", dimensions={"pool": ["p1", "p2"]}) |
| self.assertEqual(bot_management.get_bot_pools("bot-id"), ["p1", "p2"]) |
| |
| def test_get_bot_pools_dead(self): |
| e = _ensure_bot_info(bot_id="bot-id", dimensions={"pool": ["p1", "p2"]}) |
| e.key.delete() |
| self.assertEqual(bot_management.get_bot_pools("bot-id"), ["p1", "p2"]) |
| |
| def test_get_bot_pools_missing(self): |
| self.assertEqual(bot_management.get_bot_pools("bot-id"), []) |
| |
| @parameterized.expand( |
| [ |
| ("request_sleep",), |
| ("bot_idle",), |
| ] |
| ) |
| def test_bot_event_poll_sleep(self, event_type): |
| _bot_event(event_type=event_type) |
| |
| # Assert that BotInfo was updated too. |
| expected = _gen_bot_info( |
| idle_since_ts=self.now, |
| composite=[ |
| bot_management.BotInfo.NOT_IN_MAINTENANCE, |
| bot_management.BotInfo.ALIVE, |
| bot_management.BotInfo.HEALTHY, |
| bot_management.BotInfo.IDLE, |
| ], |
| ) |
| |
| bot_info = bot_management.get_info_key("id1").get() |
| self.assertEqual(expected, bot_info.to_dict()) |
| |
| # BotEvent is registered for poll when BotInfo creates |
| expected_event = _gen_bot_event( |
| event_type=event_type, idle_since_ts=utils.utcnow() |
| ) |
| bot_events = bot_management.get_events_query("id1") |
| self.assertEqual([expected_event], [e.to_dict() for e in bot_events]) |
| |
| # flush bot events |
| ndb.delete_multi(e.key for e in bot_events) |
| |
| # BotEvent is not registered for poll when no dimensions change |
| _bot_event(event_type=event_type) |
| self.assertEqual([], bot_management.get_events_query("id1").fetch()) |
| |
| # BotEvent is registered for poll when dimensions change |
| dims = {"foo": ["bar"]} |
| _bot_event(event_type=event_type, dimensions=dims) |
| expected_event["dimensions"] = dims |
| bot_events = bot_management.get_events_query("id1").fetch() |
| self.assertEqual([expected_event], [e.to_dict() for e in bot_events]) |
| |
| def test_bot_event_busy(self): |
| ticker = test_case.Ticker(self.now) |
| t1 = self.mock_now(ticker()) |
| _bot_event(event_type="bot_connected") |
| t2 = self.mock_now(ticker()) |
| _bot_event(event_type="request_task", task_id="12311", task_name="yo") |
| expected_events = [ |
| bot_management.BotInfo.NOT_IN_MAINTENANCE, |
| bot_management.BotInfo.ALIVE, |
| bot_management.BotInfo.HEALTHY, |
| bot_management.BotInfo.BUSY, |
| ] |
| expected = _gen_bot_info( |
| composite=expected_events, |
| task_id="12311", |
| task_name="yo", |
| task_flags=0, |
| first_seen_ts=t1, |
| ) |
| bot_info = bot_management.get_info_key("id1").get() |
| self.assertEqual(expected, bot_info.to_dict()) |
| |
| expected = [ |
| _gen_bot_event(event_type="request_task", task_id="12311", ts=t2), |
| _gen_bot_event(event_type="bot_connected", ts=t1), |
| ] |
| self.assertEqual( |
| expected, [e.to_dict() for e in bot_management.get_events_query("id1")] |
| ) |
| |
| def test_bot_event_update_dimensions(self): |
| bot_id = "id1" |
| bot_info_key = bot_management.get_info_key(bot_id) |
| |
| # bot dimensions generated without injected bot_config.py. |
| dimensions_invalid = {"id": ["id1"], "os": ["Ubuntu"], "pool": ["default"]} |
| |
| # 'bot_connected' event creates BotInfo only with id and pool dimensions. |
| _bot_event( |
| bot_id=bot_id, event_type="bot_connected", dimensions=dimensions_invalid |
| ) |
| self.assertEqual( |
| bot_info_key.get().dimensions_flat, ["id:id1", "pool:default"] |
| ) |
| |
| # 'bot_error' event does not register dimensions other than id and pool. |
| _bot_event( |
| bot_id=bot_id, event_type="bot_error", dimensions=dimensions_invalid |
| ) |
| self.assertEqual( |
| bot_info_key.get().dimensions_flat, ["id:id1", "pool:default"] |
| ) |
| |
| # 'request_sleep' registers given dimensions to BotInfo. |
| _bot_event( |
| bot_id=bot_id, |
| event_type="request_sleep", |
| dimensions={"id": ["id1"], "os": ["Android"], "pool": ["default"]}, |
| ) |
| self.assertEqual( |
| bot_info_key.get().dimensions_flat, |
| ["id:id1", "os:Android", "pool:default"], |
| ) |
| |
| # 'bot_connected' doesn't update dimensions since bot_config isn't injected. |
| _bot_event( |
| bot_id=bot_id, event_type="bot_connected", dimensions=dimensions_invalid |
| ) |
| self.assertEqual( |
| bot_info_key.get().dimensions_flat, |
| ["id:id1", "os:Android", "pool:default"], |
| ) |
| |
| def test_bot_event_set_effective_bot_id(self): |
| bot_id = "id1" |
| bot_info_key = bot_management.get_info_key(bot_id) |
| _bot_event(bot_id=bot_id, event_type="bot_connected") |
| self.assertIsNotNone(bot_info_key.get()) |
| |
| # rbe_effective_bot_id is not set when set_rbe_effective_bot_id is False. |
| _bot_event( |
| bot_id=bot_id, |
| event_type="bot_polling", |
| rbe_effective_bot_id="pool:dut_id:dut", |
| set_rbe_effective_bot_id=False, |
| ) |
| self.assertIsNone(bot_info_key.get().rbe_effective_bot_id) |
| |
| # rbe_effective_bot_id is set when set_rbe_effective_bot_id is True. |
| _bot_event( |
| bot_id=bot_id, |
| event_type="bot_polling", |
| rbe_effective_bot_id="pool:dut_id:dut", |
| set_rbe_effective_bot_id=True, |
| ) |
| self.assertEqual(bot_info_key.get().rbe_effective_bot_id, "pool:dut_id:dut") |
| |
| def test_get_info_key(self): |
| self.assertEqual( |
| ndb.Key(bot_management.BotRoot, "foo", bot_management.BotInfo, "info"), |
| bot_management.get_info_key("foo"), |
| ) |
| |
| def test_get_root_key(self): |
| self.assertEqual( |
| ndb.Key(bot_management.BotRoot, "foo"), bot_management.get_root_key("foo") |
| ) |
| |
| def test_get_settings_key(self): |
| expected = ndb.Key( |
| bot_management.BotRoot, "foo", bot_management.BotSettings, "settings" |
| ) |
| self.assertEqual(expected, bot_management.get_settings_key("foo")) |
| |
| def test_has_capacity(self): |
| # The bot can service this dimensions. |
| d = {"pool": ["default"], "os": ["Ubuntu-16.04"]} |
| |
| # The bot can service one of 'or' dimensions. |
| or_dimensions = { |
| "pool": ["default"], |
| "os": ["Ubuntu-14.04|Ubuntu-16.04"], |
| } |
| # By default, nothing has capacity. |
| self.assertEqual(False, bot_management.has_capacity(d)) |
| # By default, nothing has capacity. |
| self.assertEqual(False, bot_management.has_capacity(or_dimensions)) |
| |
| # A bot comes online. There's some capacity now. |
| _bot_event( |
| event_type="request_sleep", |
| dimensions={ |
| "id": ["id1"], |
| "pool": ["default"], |
| "os": ["Ubuntu", "Ubuntu-16.04"], |
| }, |
| ) |
| self.assertEqual(1, bot_management.BotInfo.query().count()) |
| self.assertEqual(True, bot_management.has_capacity(d)) |
| |
| self.assertEqual(True, bot_management.has_capacity(or_dimensions)) |
| |
| # Disable the memcache code path to confirm the DB based behavior. |
| self.mock(task_queues, "probably_has_capacity", lambda *_: None) |
| self.assertEqual(True, bot_management.has_capacity(d)) |
| |
| self.assertEqual(True, bot_management.has_capacity(or_dimensions)) |
| |
| def test_has_capacity_BotEvent(self): |
| # Disable the memcache code path to confirm the DB based behavior. |
| self.mock(task_queues, "probably_has_capacity", lambda *_: None) |
| |
| d = {"pool": ["default"], "os": ["Ubuntu-16.04"]} |
| botid = "id1" |
| _bot_event( |
| event_type="request_sleep", |
| dimensions={ |
| "id": [botid], |
| "pool": ["default"], |
| "os": ["Ubuntu", "Ubuntu-16.04"], |
| }, |
| ) |
| self.assertEqual(True, bot_management.has_capacity(d)) |
| |
| or_dimensions = { |
| "pool": ["default"], |
| "os": ["Ubuntu-14.04|Ubuntu-16.04"], |
| } |
| |
| # Delete the BotInfo, so the bot will disappear. |
| bot_management.get_info_key(botid).delete() |
| # The capacity is still found due to a recent BotEvent with this dimension. |
| self.assertEqual(True, bot_management.has_capacity(d)) |
| self.assertEqual(True, bot_management.has_capacity(or_dimensions)) |
| |
| self.mock_now(self.now, config.settings().bot_death_timeout_secs - 1) |
| self.assertEqual(True, bot_management.has_capacity(d)) |
| self.assertEqual(True, bot_management.has_capacity(or_dimensions)) |
| |
| self.mock_now(self.now, config.settings().bot_death_timeout_secs) |
| self.assertEqual(False, bot_management.has_capacity(d)) |
| self.assertEqual(False, bot_management.has_capacity(or_dimensions)) |
| |
| def test_get_pools_from_dimensions_flat(self): |
| pools = bot_management.get_pools_from_dimensions_flat( |
| ["id:id1", "os:Linux", "pool:pool1", "pool:pool2"] |
| ) |
| self.assertEqual(pools, ["pool1", "pool2"]) |
| |
| def test_cron_update_bot_info(self): |
| # Create two bots, one becomes dead, updating the cron job fixes composite. |
| timeout = bot_management.config.settings().bot_death_timeout_secs |
| |
| def check_dead(bots): |
| q = bot_management.filter_availability( |
| bot_management.BotInfo.query(), |
| quarantined=None, |
| in_maintenance=None, |
| is_dead=True, |
| is_busy=None, |
| ) |
| self.assertEqual(bots, [t.to_dict() for t in q]) |
| |
| def check_alive(bots): |
| q = bot_management.filter_availability( |
| bot_management.BotInfo.query(), |
| quarantined=None, |
| in_maintenance=None, |
| is_dead=False, |
| is_busy=None, |
| ) |
| self.assertEqual(bots, [t.to_dict() for t in q]) |
| |
| _bot_event(event_type="request_sleep") |
| # One second before the timeout value. |
| then = self.mock_now(self.now, timeout - 1) |
| _bot_event( |
| event_type="request_sleep", |
| bot_id="id2", |
| external_ip="8.8.4.4", |
| authenticated_as="bot:id2.domain", |
| dimensions={"id": ["id2"], "foo": ["bar"]}, |
| ) |
| |
| bot1_alive = _gen_bot_info( |
| first_seen_ts=self.now, idle_since_ts=self.now, last_seen_ts=self.now |
| ) |
| bot1_dead = _gen_bot_info( |
| first_seen_ts=self.now, |
| last_seen_ts=self.now, |
| composite=[ |
| bot_management.BotInfo.NOT_IN_MAINTENANCE, |
| bot_management.BotInfo.DEAD, |
| bot_management.BotInfo.HEALTHY, |
| bot_management.BotInfo.BUSY, |
| ], |
| is_dead=True, |
| ) |
| bot2_alive = _gen_bot_info( |
| authenticated_as="bot:id2.domain", |
| dimensions={"foo": ["bar"], "id": ["id2"]}, |
| first_seen_ts=then, |
| id="id2", |
| idle_since_ts=then, |
| last_seen_ts=then, |
| ) |
| check_dead([]) |
| check_alive([bot1_alive, bot2_alive]) |
| self.assertEqual(0, bot_management.cron_update_bot_info()) |
| check_dead([]) |
| check_alive([bot1_alive, bot2_alive]) |
| |
| # Just stale enough to trigger the dead logic. |
| now = self.mock_now(self.now, timeout) |
| # The cron job didn't run yet, so it still has ALIVE bit. |
| check_dead([]) |
| check_alive([bot1_alive, bot2_alive]) |
| self.assertEqual(1, bot_management.cron_update_bot_info()) |
| # The cron job ran, so it's now correct. This also bumped expiry time of |
| # the dead bot's BotInfo (when emitting the `bot_missing` event). |
| bot1_dead["expire_at"] = now + bot_management._OLD_BOT_INFO_CUT_OFF |
| check_dead([bot1_dead]) |
| check_alive([bot2_alive]) |
| |
| # the last event should be bot_missing |
| events = list(bot_management.get_events_query("id1")) |
| event = events[0] |
| bq_event = swarming_pb2.BotEvent() |
| event.to_proto(bq_event) |
| |
| self.assertEqual(event.event_type, "bot_missing") |
| self.assertEqual(event.last_seen_ts, bot1_dead["last_seen_ts"]) |
| self.assertEqual(bq_event.event, swarming_pb2.BOT_MISSING) |
| self.assertEqual(bq_event.bot.status, swarming_pb2.MISSING) |
| last_seen_ts = timestamp_pb2.Timestamp() |
| last_seen_ts.FromDatetime(bot1_dead["last_seen_ts"]) |
| self.assertEqual(bq_event.bot.info.last_seen_ts, last_seen_ts) |
| |
| def test_filter_dimensions(self): |
| pass # Tested in handlers_endpoints_test |
| |
| def test_filter_availability(self): |
| pass # Tested in handlers_endpoints_test |
| |
| def test_check_bot_alive_async(self): |
| _ensure_bot_info("alive") |
| self.assertTrue(bot_management.check_bot_alive_async("alive").get_result()) |
| |
| info = _ensure_bot_info("dead") |
| info.last_seen_ts = self.now - datetime.timedelta(seconds=3000) |
| info.put() |
| self.assertFalse(bot_management.check_bot_alive_async("dead").get_result()) |
| |
| self.assertFalse(bot_management.check_bot_alive_async("gone").get_result()) |
| |
| |
| if __name__ == "__main__": |
| logging.basicConfig( |
| level=logging.DEBUG if "-v" in sys.argv else logging.ERROR |
| ) |
| if "-v" in sys.argv: |
| unittest.TestCase.maxDiff = None |
| unittest.main() |