/*
* The MIT License
*
* Copyright (c) 2016 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
package jenkins.branch;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.model.Action;
import hudson.model.Cause;
import hudson.model.FreeStyleBuild;
import hudson.model.FreeStyleProject;
import hudson.model.Job;
import hudson.model.ParameterDefinition;
import hudson.model.ParameterValue;
import hudson.model.ParametersAction;
import hudson.model.Queue;
import hudson.model.Run;
import hudson.model.StringParameterDefinition;
import hudson.model.StringParameterValue;
import hudson.model.TopLevelItem;
import hudson.model.queue.QueueTaskFuture;
import integration.harness.BasicMultiBranchProject;
import java.io.IOException;
import java.util.Collections;
import java.util.concurrent.Future;
import jenkins.scm.impl.mock.MockSCMController;
import jenkins.scm.impl.mock.MockSCMSource;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.TestExtension;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
public class RateLimitBranchPropertyTest {
/**
* All tests in this class only create items and do not affect other global configuration, thus we trade test
* execution time for the restriction on only touching items.
*/
@ClassRule
public static JenkinsRule r = new JenkinsRule();
@Before
public void cleanOutAllItems() throws Exception {
for (TopLevelItem i : r.getInstance().getItems()) {
i.delete();
}
}
@Test
public void getCount() throws Exception {
for (int i = 1; i < 1001; i++) {
assertThat(new RateLimitBranchProperty(i, "hour").getCount(), is(i));
}
}
@Test
public void getCount_lowerBound() throws Exception {
assertThat(new RateLimitBranchProperty(0, "hour").getCount(), is(1));
}
@Test
public void getCount_upperBound() throws Exception {
assertThat(new RateLimitBranchProperty(1001, "hour").getCount(), is(1000));
}
@Test
public void getDurationName() throws Exception {
assertThat(new RateLimitBranchProperty(10, "hour").getDurationName(), is("hour"));
assertThat(new RateLimitBranchProperty(10, "year").getDurationName(), is("year"));
}
@Test
public void rateLimitsBlockBuilds_maxRate() throws Exception {
rateLimitsBlockBuilds(1000);
}
@Test
public void rateLimitsBlockBuilds_medRate() throws Exception {
rateLimitsBlockBuilds(500);
}
// we run this test at two rates which have more than the error margin (500ms) in expected delay to ensure
// that the delay is doubled when the rate is halved and thus rule out a false positive where there is a general
// delay on all builds of more than the expected delay.
public void rateLimitsBlockBuilds(int rate) throws Exception {
try (MockSCMController c = MockSCMController.create()) {
c.createRepository("foo");
BasicMultiBranchProject prj = r.jenkins.createProject(BasicMultiBranchProject.class, "foo");
prj.setCriteria(null);
BranchSource source = new BranchSource(new MockSCMSource(null, c, "foo", true, false, false));
source.setStrategy(new DefaultBranchPropertyStrategy(new BranchProperty[]{
new RateLimitBranchProperty(rate, "hour")
}));
prj.getSourcesList().add(source);
prj.scheduleBuild2(0).getFuture().get();
r.waitUntilNoActivity();
FreeStyleProject master = prj.getItem("master");
master.setQuietPeriod(0);
assertThat(master.getProperties(),
hasEntry(
instanceOf(RateLimitBranchProperty.JobPropertyImpl.DescriptorImpl.class),
allOf(
instanceOf(RateLimitBranchProperty.JobPropertyImpl.class),
hasProperty("count", is(rate)),
hasProperty("durationName", is("hour"))
)
)
);
assertThat(master.isInQueue(), is(false));
assertThat(master.getQueueItem(), nullValue());
QueueTaskFuture<FreeStyleBuild> future = master.scheduleBuild2(0);
// let the item get added to the queue
while (!master.isInQueue()) {
Thread.yield();
}
long startTime = System.currentTimeMillis();
assertThat(master.isInQueue(), is(true));
// while it is in the queue, until queue maintenance takes place, it will not be flagged as blocked
// since we cannot know when queue maintenance happens from the periodic task
// we cannot assert any value of isBlocked() on this side of maintenance
Queue.getInstance().maintain();
assertThat(master.getQueueItem().isBlocked(), is(true));
assertThat(master.getQueueItem().getCauseOfBlockage().getShortDescription().toLowerCase(),
containsString("throttle"));
// now we wait for the start... invoking queue maintain every 100ms so that the queue
// will pick up more responsively than the default 5s
Future<FreeStyleBuild> startCondition = future.getStartCondition();
long endTime = startTime + 60*60/rate*1000L*5; // at least 5 times the expected delay
while (!startCondition.isDone() && System.currentTimeMillis() < endTime) {
Queue.getInstance().maintain();
Thread.sleep(100);
}
assertThat(startCondition.isDone(), is(true));
// it can take more than the requested delay... that's ok, but it should not be
// more than 500ms longer (i.e. 5 of our Queue.maintain loops above)
assertThat("At least the rate implied delay but no more than 500ms longer",
System.currentTimeMillis() - startTime,
allOf(
greaterThanOrEqualTo(60 * 60 / rate * 1000L - 200L),
lessThanOrEqualTo(60 * 60 / rate * 1000L * 500L)
)
);
future.get();
}
}
@Test
public void rateLimitsConcurrentBuilds() throws Exception {
int rate = 1000;
try (final MockSCMController c = MockSCMController.create()) {
c.createRepository("foo");
BasicMultiBranchProject prj = r.jenkins.createProject(BasicMultiBranchProject.class, "foo");
prj.setCriteria(null);
BranchSource source = new BranchSource(new MockSCMSource(null, c, "foo", true, false, false));
BasicParameterDefinitionBranchProperty p = new BasicParameterDefinitionBranchProperty();
p.setParameterDefinitions(Collections.<ParameterDefinition>singletonList(new StringParameterDefinition("FOO", "BAR")));
source.setStrategy(new DefaultBranchPropertyStrategy(new BranchProperty[]{
new RateLimitBranchProperty(rate, "hour"),
new ConcurrentBuildBranchProperty(),
p
}));
prj.getSourcesList().add(source);
prj.scheduleBuild2(0).getFuture().get();
r.waitUntilNoActivity();
FreeStyleProject master = prj.getItem("master");
master.setQuietPeriod(0);
assertThat(master.getProperties(),
hasEntry(
instanceOf(RateLimitBranchProperty.JobPropertyImpl.DescriptorImpl.class),
allOf(
instanceOf(RateLimitBranchProperty.JobPropertyImpl.class),
hasProperty("count", is(rate)),
hasProperty("durationName", is("hour"))
)
)
);
assertThat(master.isInQueue(), is(false));
assertThat(master.getQueueItem(), nullValue());
QueueTaskFuture<FreeStyleBuild> future = master.scheduleBuild2(0);
QueueTaskFuture<FreeStyleBuild> future2 = master.scheduleBuild2(0, (Cause) null,
(Action) new ParametersAction(
Collections.<ParameterValue>singletonList(new StringParameterValue("FOO", "MANCHU"))));
assertThat(future, not(is(future2)));
// let the item get added to the queue
while (!master.isInQueue()) {
Thread.yield();
}
long startTime = System.currentTimeMillis();
assertThat(master.isInQueue(), is(true));
// while it is in the queue, until queue maintenance takes place, it will not be flagged as blocked
// since we cannot know when queue maintenance happens from the periodic task
// we cannot assert any value of isBlocked() on this side of maintenance
Queue.getInstance().maintain();
assertThat(master.getQueueItem().isBlocked(), is(true));
assertThat(master.getQueueItem().getCauseOfBlockage().getShortDescription().toLowerCase(),
containsString("throttle"));
// now we wait for the start... invoking queue maintain every 100ms so that the queue
// will pick up more responsively than the default 5s
Future<FreeStyleBuild> startCondition = future.getStartCondition();
long midTime = startTime + 60*60/rate*1000L*5; // at least 5 times the expected delay
while (!startCondition.isDone() && System.currentTimeMillis() < midTime) {
Queue.getInstance().maintain();
Thread.sleep(100);
}
assertThat(startCondition.isDone(), is(true));
assertThat(master.isInQueue(), is(true));
FreeStyleBuild firstBuild = startCondition.get();
// now we wait for the start... invoking queue maintain every 100ms so that the queue
// will pick up more responsively than the default 5s
startCondition = future2.getStartCondition();
long endTime = startTime + 60*60/rate*1000L*5; // at least 5 times the expected delay
while (!startCondition.isDone() && System.currentTimeMillis() < midTime) {
Queue.getInstance().maintain();
Thread.sleep(100);
}
assertThat(startCondition.isDone(), is(true));
assertThat(master.isInQueue(), is(false));
FreeStyleBuild secondBuild = startCondition.get();
// it can take more than the requested delay... that's ok, but it should not be
// more than 500ms longer (i.e. 5 of our Queue.maintain loops above)
assertThat("At least the rate implied delay but no more than 500ms longer",
secondBuild.getStartTimeInMillis() - firstBuild.getStartTimeInMillis(),
allOf(
greaterThanOrEqualTo(60 * 60 / rate * 1000L - 200L),
lessThanOrEqualTo(60 * 60 / rate * 1000L * 500L)
)
);
future.get();
}
}
@Test
public void configRoundtrip() throws Exception {
try (MockSCMController c = MockSCMController.create()) {
c.createRepository("foo");
BasicMultiBranchProject prj = r.jenkins.createProject(BasicMultiBranchProject.class, "foo");
prj.setCriteria(null);
BranchSource source = new BranchSource(new MockSCMSource(null, c, "foo", true, false, false));
source.setStrategy(new DefaultBranchPropertyStrategy(new BranchProperty[]{
new RateLimitBranchProperty(10, "day")
}));
prj.getSourcesList().add(source);
r.configRoundtrip(prj);
assertThat(prj.getSources().get(0).getStrategy(), instanceOf(DefaultBranchPropertyStrategy.class));
DefaultBranchPropertyStrategy strategy =
(DefaultBranchPropertyStrategy) prj.getSources().get(0).getStrategy();
assertThat(strategy.getProps().get(0), instanceOf(RateLimitBranchProperty.class));
RateLimitBranchProperty property = (RateLimitBranchProperty)strategy.getProps().get(0);
assertThat(property.getCount(), is(10));
assertThat(property.getDurationName(), is("day"));
}
}
public static class ConcurrentBuildBranchProperty extends BranchProperty {
@Override
public <P extends Job<P, B>, B extends Run<P, B>> JobDecorator<P, B> jobDecorator(
Class<P> clazz) {
if (FreeStyleProject.class.isAssignableFrom(clazz)) {
return (JobDecorator<P, B>) new ProjectDecorator<FreeStyleProject, FreeStyleBuild>(){
@NonNull
@Override
public FreeStyleProject project(@NonNull FreeStyleProject project) {
try {
project.setConcurrentBuild(true);
} catch (IOException e) {
// ignore
}
return super.project(project);
}
};
}
return null;
}
@TestExtension
public static class DescriptorImpl extends BranchPropertyDescriptor {
@Override
protected boolean isApplicable(@NonNull MultiBranchProjectDescriptor projectDescriptor) {
return projectDescriptor instanceof BasicMultiBranchProject.DescriptorImpl;
}
}
}
public static class BasicParameterDefinitionBranchProperty extends ParameterDefinitionBranchProperty {
public BasicParameterDefinitionBranchProperty() {
}
@TestExtension
public static class DescriptorImpl extends BranchPropertyDescriptor {
@Override
protected boolean isApplicable(@NonNull MultiBranchProjectDescriptor projectDescriptor) {
return projectDescriptor instanceof BasicMultiBranchProject.DescriptorImpl;
}
}
}
}