zuul/tests/unit/test_global_semaphores.py
James E. Blair 7fc94effe7 Add global semaphore support
This adds support for global semaphores which can be used by multiple tenants.
This supports the use case where they represent real-world resources which
operate independentyl of Zuul tenants.

This implements and removes the spec describing the feature.  One change from
the spec is that the configuration object in the tenant config file is
"global-semaphore" rather than "semaphore".  This makes it easier to distinguish
them in documentation (facilitating easier cross-references and deep links),
and may also make it easier for users to understand that they have distinct
behavoirs.

Change-Id: I5f2225a700d8f9bef0399189017f23b3f4caad17
2022-05-31 15:14:34 -07:00

170 lines
7.9 KiB
Python

# Copyright 2022 Acme Gating, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import zuul.configloader
from tests.base import ZuulTestCase
class TestGlobalSemaphoresConfig(ZuulTestCase):
tenant_config_file = 'config/global-semaphores-config/main.yaml'
def assertSemaphores(self, tenant, semaphores):
for k, v in semaphores.items():
self.assertEqual(
len(tenant.semaphore_handler.semaphoreHolders(k)),
v, k)
def assertSemaphoresMax(self, tenant, semaphores):
for k, v in semaphores.items():
abide = tenant.semaphore_handler.abide
semaphore = tenant.layout.getSemaphore(abide, k)
self.assertEqual(semaphore.max, v, k)
def test_semaphore_scope(self):
# This tests global and tenant semaphore scope
self.executor_server.hold_jobs_in_build = True
tenant1 = self.scheds.first.sched.abide.tenants.get('tenant-one')
tenant2 = self.scheds.first.sched.abide.tenants.get('tenant-two')
tenant3 = self.scheds.first.sched.abide.tenants.get('tenant-three')
# The different max values will tell us that we have the right
# semaphore objects. Each tenant has one tenant-scope
# semaphore in a tenant-specific project, and one tenant-scope
# semaphore with a common definition. Tenants 1 and 2 share a
# global-scope semaphore, and tenant 3 has a tenant-scope
# semaphore with the same name.
# Here is what is defined in each tenant:
# Tenant-one:
# * global-semaphore: scope:global max:100 definition:main.yaml
# * common-semaphore: scope:tenant max:10 definition:common-config
# * project1-semaphore: scope:tenant max:11 definition:project1
# * (global-semaphore): scope:tenant max:2 definition:project1
# [unused since it shadows the actual global-semaphore]
# Tenant-two:
# * global-semaphore: scope:global max:100 definition:main.yaml
# * common-semaphore: scope:tenant max:10 definition:common-config
# * project2-semaphore: scope:tenant max:12 definition:project2
# Tenant-three:
# * global-semaphore: scope:global max:999 definition:project3
# * common-semaphore: scope:tenant max:10 definition:common-config
# * project3-semaphore: scope:tenant max:13 definition:project3
self.assertSemaphoresMax(tenant1, {'global-semaphore': 100,
'common-semaphore': 10,
'project1-semaphore': 11,
'project2-semaphore': 1,
'project3-semaphore': 1})
self.assertSemaphoresMax(tenant2, {'global-semaphore': 100,
'common-semaphore': 10,
'project1-semaphore': 1,
'project2-semaphore': 12,
'project3-semaphore': 1})
# This "global" semaphore is really tenant-scoped, it just has
# the same name.
self.assertSemaphoresMax(tenant3, {'global-semaphore': 999,
'common-semaphore': 10,
'project1-semaphore': 1,
'project2-semaphore': 1,
'project3-semaphore': 13})
# We should have a config error in tenant1 due to the
# redefinition.
self.assertEquals(len(tenant1.layout.loading_errors), 1)
self.assertEquals(len(tenant2.layout.loading_errors), 0)
self.assertEquals(len(tenant3.layout.loading_errors), 0)
A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
B = self.fake_gerrit.addFakeChange('org/project2', 'master', 'B')
C = self.fake_gerrit.addFakeChange('org/project3', 'master', 'C')
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
self.fake_gerrit.addEvent(C.getPatchsetCreatedEvent(1))
self.waitUntilSettled()
# Checking the number of holders tells us whethere we are
# using global or tenant-scoped semaphores. Each in-use
# semaphore in a tenant should have only one holder except the
# global-scope semaphore shared between tenants 1 and 2.
self.assertSemaphores(tenant1, {'global-semaphore': 2,
'common-semaphore': 1,
'project1-semaphore': 1,
'project2-semaphore': 0,
'project3-semaphore': 0})
self.assertSemaphores(tenant2, {'global-semaphore': 2,
'common-semaphore': 1,
'project1-semaphore': 0,
'project2-semaphore': 1,
'project3-semaphore': 0})
self.assertSemaphores(tenant3, {'global-semaphore': 1,
'common-semaphore': 1,
'project1-semaphore': 0,
'project2-semaphore': 0,
'project3-semaphore': 1})
self.executor_server.hold_jobs_in_build = False
self.executor_server.release()
self.waitUntilSettled()
class TestGlobalSemaphoresBroken(ZuulTestCase):
validate_tenants = []
tenant_config_file = 'config/global-semaphores-config/broken.yaml'
# This test raises a config error during the startup of the test
# case which makes the first scheduler fail during its startup.
# The second (or any additional) scheduler won't even run as the
# startup is serialized in tests/base.py.
# Thus it doesn't make sense to execute this test with multiple
# schedulers.
scheduler_count = 1
def setUp(self):
self.assertRaises(zuul.configloader.GlobalSemaphoreNotFoundError,
super().setUp)
def test_broken_global_semaphore_config(self):
pass
class TestGlobalSemaphores(ZuulTestCase):
tenant_config_file = 'config/global-semaphores/main.yaml'
def test_global_semaphores(self):
# This tests that a job finishing in one tenant will correctly
# start a job in another tenant waiting on the semahpore.
self.executor_server.hold_jobs_in_build = True
A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
self.waitUntilSettled()
B = self.fake_gerrit.addFakeChange('org/project2', 'master', 'B')
self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
self.waitUntilSettled()
self.assertHistory([])
self.assertBuilds([
dict(name='test-global-semaphore', changes='1,1'),
])
self.executor_server.hold_jobs_in_build = False
self.executor_server.release()
self.waitUntilSettled()
self.assertHistory([
dict(name='test-global-semaphore',
result='SUCCESS', changes='1,1'),
dict(name='test-global-semaphore',
result='SUCCESS', changes='2,1'),
], ordered=False)