Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge job started messages based on aggregation interval #313

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,15 @@ public class PluginConfig implements GerritWorkersConfig {
* Default number of sending worker threads.
*/
public static final int DEFAULT_NR_OF_SENDING_WORKER_THREADS = 1;
/**
* Default time for sending feedback aggregation.
*/
public static final int DEFAULT_AGGREGATION_TIME = 0;

private int numberOfReceivingWorkerThreads;
private int numberOfSendingWorkerThreads;
private int replicationCacheExpirationInMinutes;
private int aggregateJobStartedInterval;

/**
* Constructs a config with default data.
Expand All @@ -75,6 +80,7 @@ public PluginConfig(PluginConfig pluginConfig) {
numberOfReceivingWorkerThreads = pluginConfig.getNumberOfReceivingWorkerThreads();
numberOfSendingWorkerThreads = pluginConfig.getNumberOfSendingWorkerThreads();
replicationCacheExpirationInMinutes = pluginConfig.getReplicationCacheExpirationInMinutes();
aggregateJobStartedInterval = pluginConfig.getAggregateJobStartedInterval();
}

/**
Expand Down Expand Up @@ -112,6 +118,11 @@ public void setValues(JSONObject formData) {
if (replicationCacheExpirationInMinutes <= 0) {
replicationCacheExpirationInMinutes = ReplicationCache.DEFAULT_EXPIRATION_IN_MINUTES;
}

aggregateJobStartedInterval = formData.optInt("aggregateJobStartedInterval", DEFAULT_AGGREGATION_TIME);
if (aggregateJobStartedInterval <= 0) {
aggregateJobStartedInterval = DEFAULT_AGGREGATION_TIME;
}
}

/**
Expand Down Expand Up @@ -178,4 +189,26 @@ public int getReplicationCacheExpirationInMinutes() {
public void setReplicationCacheExpirationInMinutes(int replicationCacheExpirationInMinutes) {
this.replicationCacheExpirationInMinutes = replicationCacheExpirationInMinutes;
}

/**
* Aggregate job started messages for some interval.
* I.e. setting this value to 2 means
* "wait first 2 minutes and send all notification only if 2 minutes passed or lst build had started already"
*
* @param aggregateJobStartedInterval time to wait builds before sending start messages
*/
public void setAggregateJobStartedInterval(Integer aggregateJobStartedInterval) {
this.aggregateJobStartedInterval = aggregateJobStartedInterval;
}

/**
* Aggregate job started messages for some interval.
* I.e. "10" means
* "first wait 10 seconds and send all notification only if 10 seconds passed or lst build had started already"
*
* @return aggregation interval
*/
public int getAggregateJobStartedInterval() {
return aggregateJobStartedInterval;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

/**
* Start position that notifies Gerrit of events.
* @author Robert Sandell &lt;[email protected]&gt;
Expand Down Expand Up @@ -70,18 +72,24 @@ public GerritNotifier(IGerritHudsonTriggerConfig config, GerritCmdRunner cmdRunn

/**
* Generates the build-started command based on configured templates and build-values and sends it to Gerrit.
* @param build the build.
* @param builds the builds.
* @param taskListener the taskListener.
* @param event the event.
* @param stats the stats.
*/
public void buildStarted(Run build, TaskListener taskListener,
GerritTriggeredEvent event, BuildsStartedStats stats) {
public void buildStarted(List<Run> builds, TaskListener taskListener,
GerritTriggeredEvent event, BuildsStartedStats stats) {
try {
/* Without a change, it doesn't make sense to notify gerrit */
if (event instanceof ChangeBasedEvent) {
String command =
parameterExpander.getBuildStartedCommand(build, taskListener, (ChangeBasedEvent)event, stats);
String command;
if (builds.size() == 1) {
command = parameterExpander.getBuildStartedCommand(builds.get(0), taskListener,
(ChangeBasedEvent)event, stats);
} else {
command = parameterExpander.getBuildsStartedCommand(builds, taskListener,
(ChangeBasedEvent)event, stats);
}
if (command != null) {
if (!command.isEmpty()) {
logger.info("Notifying BuildStarted to gerrit: {}", command);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

/**
* A factory for creating notification entities.
* This factory is mainly created and used to ease unit testing.
Expand Down Expand Up @@ -183,24 +185,24 @@ private String getServerName(GerritTriggeredEvent event) {
/**
* Queues a build started command on the send-command queue.
*
* @param build the build.
* @param builds the build.
* @param listener a listener.
* @param event the event.
* @param stats the started stats.
* @see GerritSendCommandQueue#queue(com.sonymobile.tools.gerrit.gerritevents.workers.cmd.AbstractSendCommandJob)
* @see BuildStartedCommandJob
*/
public void queueBuildStarted(Run build, TaskListener listener,
public void queueBuildsStarted(List<Run> builds, TaskListener listener,
GerritTriggeredEvent event, BuildsStartedStats stats) {
String serverName = getServerName(event);
if (serverName != null) {
IGerritHudsonTriggerConfig config = getConfig(serverName);
if (config != null) {
if (config.isUseRestApi() && event instanceof ChangeBasedEvent) {
GerritSendCommandQueue.queue(new BuildStartedRestCommandJob(config, build, listener,
GerritSendCommandQueue.queue(new BuildStartedRestCommandJob(config, builds, listener,
(ChangeBasedEvent)event, stats));
} else {
GerritSendCommandQueue.queue(new BuildStartedCommandJob(config, build, listener, event, stats));
GerritSendCommandQueue.queue(new BuildStartedCommandJob(config, builds, listener, event, stats));
}
} else {
logger.warn("Nothing queued since there is no configuration for serverName: {}", serverName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@
import hudson.model.Run;
import hudson.model.TaskListener;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import jenkins.model.Jenkins;
Expand Down Expand Up @@ -105,7 +107,7 @@ public String getBuildStartedCommand(Run r, TaskListener taskListener,

GerritTrigger trigger = GerritTrigger.getTrigger(r.getParent());
String gerritCmd = config.getGerritCmdBuildStarted();
Map<String, String> parameters = createStandardParameters(r, event,
Map<String, String> parameters = createStandardParameters(event,
getBuildStartedCodeReviewValue(r),
getBuildStartedVerifiedValue(r),
Notify.ALL.name());
Expand All @@ -127,11 +129,76 @@ public String getBuildStartedCommand(Run r, TaskListener taskListener,
}
}

parameters.put("BUILDURL", jenkins.getRootUrl() + r.getUrl());
parameters.put("STARTED_STATS", startedStats.toString());

return expandParameters(gerritCmd, r, taskListener, parameters);
}

/**
* Gets the expanded string to send to Gerrit for a build-started event.
* @param runs the builds.
* @param taskListener the taskListener.
* @param event the event.
* @param stats the statistics.
* @return the "expanded" command string.
*/
public String getBuildsStartedCommand(List<Run> runs, TaskListener taskListener,
ChangeBasedEvent event, BuildsStartedStats stats) {

String gerritCmd = config.getGerritCmdBuildStarted();

Integer minCodeReview = Integer.MAX_VALUE;
Integer minVerified = Integer.MAX_VALUE;

StringBuilder aggregatedBuilds = new StringBuilder();

int offset = runs.size() - 1;

for (Run r : runs) {
GerritTrigger trigger = GerritTrigger.getTrigger(r.getParent());

minCodeReview = Math.min(minCodeReview, getBuildStartedCodeReviewValue(r));
minVerified = Math.min(minVerified, getBuildStartedVerifiedValue(r));

aggregatedBuilds.append("\n\n");

String buildStartMessage = trigger.getBuildStartMessage();
if (buildStartMessage != null && !buildStartMessage.isEmpty()) {
aggregatedBuilds.append("\n\n").append(buildStartMessage);
}

String buildUrl = jenkins.getRootUrl() + r.getUrl();
aggregatedBuilds.append(buildUrl);

if (stats.getTotalBuildsToStart() > 1) {
aggregatedBuilds.append(" ");
aggregatedBuilds.append(stats.stringWithOffset(offset));
aggregatedBuilds.append(" ");
offset--;
}

if (config.isEnablePluginMessages()) {
for (GerritMessageProvider messageProvider : emptyIfNull(GerritMessageProvider.all())) {
String extensionMessage = messageProvider.getBuildStartedMessage(r);
if (extensionMessage != null) {
aggregatedBuilds.append("\n\n").append(extensionMessage);
}
}
}
}

Map<String, String> parameters = createStandardParameters(event,
minCodeReview,
minVerified,
Notify.ALL.name());

parameters.put("BUILDURL", "");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it would have been better if we could change the command templates and separate the message part with the actual command part. And then in case of a compound message like this the message part would be just two messages correctly expanded with BUILDURL and all and added to the one command.
This seems a bit hacky to me as it omits certain variables in case an admin has changed the message format.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did it, because it was already like that implemented for Completed message (it just ignored BUILDURL) completely :). I don't want to touch template part in my PR, but I can add more details in help section.

parameters.put("STARTED_STATS", aggregatedBuilds.toString());

return expandParameters(gerritCmd, runs.get(0), taskListener, parameters);
}

/**
* Helper for ensuring no NPEs when iterating iterables.
*
Expand Down Expand Up @@ -213,14 +280,13 @@ private Integer getBuildStartedCodeReviewValue(Run r) {
* <li><strong>CODE_REVIEW</strong>: The code review vote.</li>
* <li><strong>NOTIFICATION_LEVEL</strong>: The notification level.</li>
* </ul>
* @param r the build.
* @param gerritEvent the event.
* @param codeReview the code review vote.
* @param verified the verified vote.
* @param notifyLevel the notify level.
* @return the parameters and their values.
*/
private Map<String, String> createStandardParameters(Run r, GerritTriggeredEvent gerritEvent,
private Map<String, String> createStandardParameters(GerritTriggeredEvent gerritEvent,
Integer codeReview, Integer verified, String notifyLevel) {
//<GERRIT_NAME> <BRANCH> <CHANGE> <PATCHSET> <PATCHSET_REVISION> <REFSPEC> <BUILDURL> VERIFIED CODE_REVIEW
Map<String, String> map = new HashMap<String, String>(DEFAULT_PARAMETERS_COUNT);
Expand All @@ -239,9 +305,7 @@ private Map<String, String> createStandardParameters(Run r, GerritTriggeredEvent
map.put("REFSPEC", StringUtil.makeRefSpec(event));
}
}
if (r != null) {
map.put("BUILDURL", jenkins.getRootUrl() + r.getUrl());
}

map.put("VERIFIED", String.valueOf(verified));
map.put("CODE_REVIEW", String.valueOf(codeReview));
map.put("NOTIFICATION_LEVEL", notifyLevel);
Expand Down Expand Up @@ -404,6 +468,24 @@ public Integer getMinimumVerifiedValue(MemoryImprint memoryImprint, boolean only
return verified;
}

/**
* Convert entries of memoryImprint object to list of builds
*
* @param memoryImprint the memory
* @return the list of run objects from memory
*/
private List<Run> fromMemoryImprintToBuilds(MemoryImprint memoryImprint) {
final List<Run> runs = new ArrayList<Run>(memoryImprint.getEntries().length);
for (Entry entry : memoryImprint.getEntries()) {
if (entry == null || entry.getBuild() == null) {
continue;
}
runs.add(entry.getBuild());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getBuild() can also be null if jobs have been triggered but not yet started.
Even though you take care of potential null entries below it is safest if we can omit them all together in the source in case some other caller isn't that foreseeing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

return runs;
}

/**
* Returns the minimum code review value for the build results in the memory.
* If no builds have contributed to code review value, this method returns null
Expand Down Expand Up @@ -449,21 +531,30 @@ public Integer getMinimumCodeReviewValue(MemoryImprint memoryImprint, boolean on
* @return the highest configured notification level.
*/
public Notify getHighestNotificationLevel(MemoryImprint memoryImprint, boolean onlyBuilt) {
return getHighestNotificationLevel(fromMemoryImprintToBuilds(memoryImprint), onlyBuilt);
}

/**
* Returns the highest configured notification level.
*
* @param builds the list of builds
* @param onlyBuilt only count builds that completed (no NOT_BUILT builds)
* @return the highest configured notification level.
*/
public Notify getHighestNotificationLevel(List<Run> builds, boolean onlyBuilt) {
Notify highestLevel = Notify.NONE;
for (Entry entry : memoryImprint.getEntries()) {
if (entry == null) {
continue;
}
Run build = entry.getBuild();

for (Run build : builds) {
if (build == null) {
continue;
}

Result result = build.getResult();
if (onlyBuilt && result == Result.NOT_BUILT) {
continue;
}

GerritTrigger trigger = GerritTrigger.getTrigger(entry.getProject());
GerritTrigger trigger = GerritTrigger.getTrigger(build.getParent());
if (trigger == null || shouldSkip(trigger.getSkipVote(), result)) {
continue;
}
Expand All @@ -473,6 +564,7 @@ public Notify getHighestNotificationLevel(MemoryImprint memoryImprint, boolean o
highestLevel = level;
}
}

return highestLevel;
}

Expand Down Expand Up @@ -530,7 +622,7 @@ public String getBuildCompletedCommand(MemoryImprint memoryImprint, TaskListener
notifyLevel = getHighestNotificationLevel(memoryImprint, onlyCountBuilt);
}

Map<String, String> parameters = createStandardParameters(null, memoryImprint.getEvent(),
Map<String, String> parameters = createStandardParameters(memoryImprint.getEvent(),
codeReview, verified, notifyLevel.name());
// escapes ' as '"'"' in order to avoid breaking command line param
// Details: http://stackoverflow.com/a/26165123/99834
Expand Down Expand Up @@ -668,15 +760,21 @@ public String getBuildCompletedMessage(MemoryImprint memoryImprint, TaskListener
/**
* Returns cover message to be send after build has been started.
*
* @param build build
* @param builds build
* @param listener listener
* @param event event
* @param stats stats
* @return the message for the build started command.
*/
public String getBuildStartedMessage(Run build, TaskListener listener, ChangeBasedEvent event,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still nervous of removing signatures like this, even though it should be safe in this context as only careless plugina uthors would rely on this code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want, I can restore this one and mark like @Depracated

BuildsStartedStats stats) {
String startedCommand = getBuildStartedCommand(build, listener, event, stats);
public String getBuildsStartedMessage(List<Run> builds, TaskListener listener, ChangeBasedEvent event,
BuildsStartedStats stats) {
String startedCommand;
if (builds.size() == 1) {
startedCommand = getBuildStartedCommand(builds.get(0), listener, event, stats);
} else {
startedCommand = getBuildsStartedCommand(builds, listener, event, stats);
}

return findMessage(startedCommand);
}

Expand Down
Loading