7fc94effe7
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
170 lines
7.9 KiB
Python
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)
|