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

caching the same repository avoid reload multiple time on the same build #340

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
8 changes: 4 additions & 4 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
- uses: eskatos/gradle-command-action@v1
with:
arguments: --no-daemon codenarc
- uses: actions/upload-artifact@v1
- uses: actions/upload-artifact@v4
if: failure()
with:
name: codenarc-results
Expand All @@ -45,7 +45,7 @@ jobs:
runs-on: ubuntu-latest
if: github.repository == 'jenkinsci/templating-engine-plugin'
container:
image: jdkato/vale:v2.18.0
image: jdkato/vale:v2.21.3
options: --user root
steps:
- uses: actions/checkout@v2
Expand All @@ -62,7 +62,7 @@ jobs:
- uses: eskatos/gradle-command-action@v1
with:
arguments: --no-daemon test jacocoTestReport
- uses: actions/upload-artifact@v1
- uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results
Expand All @@ -78,7 +78,7 @@ jobs:
- uses: eskatos/gradle-command-action@v1
with:
arguments: --no-daemon jpi
- uses: actions/upload-artifact@v1
- uses: actions/upload-artifact@v4
with:
name: jpi
path: build/libs/templating-engine.hpi
2 changes: 2 additions & 0 deletions docs/styles/Vocab/JTE/accept.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ Splunk
[Tt]emplat(e|es|ed|ing)\b
truthy
[Ww]alkthroughs?
JTE's
stageContext
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import hudson.ExtensionPoint
import jenkins.model.Jenkins
import org.boozallen.plugins.jte.init.governance.config.dsl.PipelineConfigurationObject
import org.boozallen.plugins.jte.util.AggregateException
import org.boozallen.plugins.jte.util.FileSystemWrapperFactory
import org.boozallen.plugins.jte.util.JTEException
import org.codehaus.groovy.reflection.CachedMethod
import org.jenkinsci.plugins.workflow.cps.CpsFlowExecution
Expand Down Expand Up @@ -84,14 +85,18 @@ abstract class TemplatePrimitiveInjector implements ExtensionPoint{
* @param config the aggregated pipeline configuration
*/
static void orchestrate(CpsFlowExecution exec, PipelineConfigurationObject config){
invoke("validateConfiguration", exec, config)
FlowExecutionOwner flowOwner = exec.getOwner()
WorkflowRun run = flowOwner.run()
TemplatePrimitiveCollector collector = new TemplatePrimitiveCollector()
run.addOrReplaceAction(collector) // may be used by one of the injectors
invoke(run, collector, "injectPrimitives", exec, config)
invoke("validatePrimitives", exec, config, collector)
run.addOrReplaceAction(collector)
try {
invoke("validateConfiguration", exec, config)
FlowExecutionOwner flowOwner = exec.getOwner()
WorkflowRun run = flowOwner.run()
TemplatePrimitiveCollector collector = new TemplatePrimitiveCollector()
run.addOrReplaceAction(collector) // may be used by one of the injectors
invoke(run, collector, "injectPrimitives", exec, config)
invoke("validatePrimitives", exec, config, collector)
run.addOrReplaceAction(collector)
} finally {
FileSystemWrapperFactory.clearCache(exec.getOwner())
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import org.jenkinsci.plugins.workflow.job.WorkflowJob
// load all the libraries
// this will copy their contents to ${jteDir} for the run
LinkedHashMap aggregatedConfig = config.getConfig()

aggregatedConfig[KEY].each{ libName, libConfig ->
LibraryProvider provider = providers.find{ provider ->
provider.hasLibrary(flowOwner, libName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ abstract class TemplateFlowDefinition extends FlowDefinition {
CpsFlowExecution exec = new CpsFlowExecution(template, true, owner, hint)
// Step 3: Invoke TemplatePrimitiveInjectors
TemplatePrimitiveInjector.orchestrate(exec, config)

return exec
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
Copyright 2018 Booz Allen Hamilton

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.
*/
package org.boozallen.plugins.jte.util

import hudson.scm.SCM
import jenkins.scm.api.SCMHead
import jenkins.scm.api.SCMRevision
import jenkins.scm.api.SCMSource
import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner

/**
* The FileSystemCacheKey class is a utility for creating cache keys based on various
* SCM (Source Code Management) related objects within the Jenkins Pipeline ecosystem.
* This key is intended to uniquely identify a cache entry based on SCM configurations
* and the owner of the pipeline execution.
*
* <p>It overrides the {@link #equals(Object)} and {@link #hashCode()} methods to ensure
* that cache keys are properly compared and can be used effectively in hash-based collections.</p>
*
* <p>This class is used in caching scenarios to avoid re-fetching or recalculating information
* that has already been retrieved from the SCM system during a Jenkins pipeline execution.</p>
*
* <h2>Fields:</h2>
* <ul>
* <li>{@code owner} - The {@link FlowExecutionOwner} that represents the owner of the flow execution.</li>
* <li>{@code scm} - The {@link SCM} instance that represents the source code management system in use.</li>
* <li>{@code scmSource} - The {@link SCMSource} which defines the SCM source for the project.</li>
* <li>{@code scmHead} - The {@link SCMHead} that identifies the branch, tag, or other head in the SCM repository.</li>
* <li>{@code scmRevision} - The {@link SCMRevision} that represents the revision of the SCM head.</li>
* </ul>
*
* <h2>Methods:</h2>
* <ul>
* <li>{@link #equals(Object)} - Compares this cache key with another object for equality, based on the owner, SCM, and related fields.</li>
* <li>{@link #hashCode()} - Generates a hash code based on the owner, SCM, and related fields for use in hash-based collections.</li>
* </ul>
*
* <p><b>Copyright:</b> 2018 Booz Allen Hamilton</p>
*
* <p><b>License:</b> This class is licensed under the Apache License, Version 2.0. See the LICENSE file or
* visit <a href="http://www.apache.org/licenses/LICENSE-2.0">http://www.apache.org/licenses/LICENSE-2.0</a> for details.</p>
*
* @see FlowExecutionOwner
* @see SCM
* @see SCMSource
* @see SCMHead
* @see SCMRevision
*/
class FileSystemCacheKey {

private static final int MULTIPLIER = 31

FlowExecutionOwner owner
SCM scm
SCMSource scmSource
SCMHead scmHead
SCMRevision scmRevision

/**
* Compares this FileSystemCacheKey instance with another object to determine if they are equal.
* The comparison is based on the equality of the following fields: {@code owner}, {@code scm},
* {@code scmSource}, {@code scmHead}, and {@code scmRevision}.
*
* <p>If the provided object is not an instance of {@code FileSystemCacheKey}, this method returns {@code false}.</p>
*
* @param o the object to be compared with this instance.
* @return {@code true} if the provided object is equal to this instance, {@code false} otherwise.
*/
@Override
boolean equals(Object o) {
if (this.is(o)) {
return true
}
if (!(o instanceof FileSystemCacheKey)) {
return false
}

FileSystemCacheKey that = (FileSystemCacheKey) o

boolean result = true

if (owner != that.owner) {
result = false
}
if (scm != that.scm) {
result = false
}
if (scmHead != that.scmHead) {
result = false
}
if (scmRevision != that.scmRevision) {
result = false
}
if (scmSource != that.scmSource) {
result = false
}

return result
}
/**
* Generates a hash code for this FileSystemCacheKey instance.
* The hash code is computed based on the {@code owner}, {@code scm}, {@code scmSource},
* {@code scmHead}, and {@code scmRevision} fields.
*
* <p>This method is used to efficiently store and retrieve instances of {@code FileSystemCacheKey}
* in hash-based collections such as {@link java.util.HashMap} or {@link java.util.HashSet}.</p>
*
* @return the hash code value for this instance.
*/
@Override
int hashCode() {
int result
result = (owner != null ? owner.hashCode() : 0)
result = MULTIPLIER * result + (scm != null ? scm.hashCode() : 0)
result = MULTIPLIER * result + (scmSource != null ? scmSource.hashCode() : 0)
result = MULTIPLIER * result + (scmHead != null ? scmHead.hashCode() : 0)
result = MULTIPLIER * result + (scmRevision != null ? scmRevision.hashCode() : 0)
return result
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,40 @@ import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject
*/
class FileSystemWrapperFactory {

/**
* A static cache that maps {@link FileSystemCacheKey} instances to their corresponding
* {@link FileSystemWrapper} objects.
*
* <p>This cache is used to avoid redundant filesystem operations by reusing {@code FileSystemWrapper}
* instances that have already been created for a given {@link FileSystemCacheKey}. It helps
* improve performance by preventing repeated lookups or calculations for the same SCM data
* during Jenkins pipeline executions.</p>
*
* <p>The cache is implemented as a {@code Map<FileSystemCacheKey, FileSystemWrapper>}, where:</p>
* <ul>
* <li>The key is an instance of {@link FileSystemCacheKey}, which uniquely identifies the SCM context.</li>
* <li>The value is an instance of {@link FileSystemWrapper}, which encapsulates file system operations for that context.</li>
* </ul>
*
* <p>This cache is a static attribute of the class, shared across all instances of {@code FileSystemCacheKey},
* and is initialized as an empty map, represented by {@code [:]}. It can grow dynamically as new entries are added during runtime.</p>
*/
private final static Map<FileSystemCacheKey, FileSystemWrapper> CACHE = [:]

static void clearCache(FlowExecutionOwner owner) {
if (!CACHE.isEmpty()) {
TemplateLogger logger = new TemplateLogger(owner.getListener())
ArrayList msg = [
"Values were found in the cache, so this helped reduce pipeline initiation time.",
]
CACHE.entrySet().find { entry -> entry.getKey().getOwner() == owner }.each { entry ->
msg.add("-- scm ${entry.getValue().getScmKey()}")
}
logger.print(msg)
CACHE.entrySet().removeIf { entry -> entry.getKey().getOwner() == owner }
}
}

/**
* Creates a FileSystemWrapper. Can either be provided an SCM directly
* or try to infer the SCM from the current job
Expand Down Expand Up @@ -73,16 +107,23 @@ class FileSystemWrapperFactory {
throw new IllegalStateException("Unable to build a FileSystemWrapper")
}

private static FileSystemWrapper fromSCM(FlowExecutionOwner owner, WorkflowJob job, SCM scm){
SCMFileSystem fs
fs = SCMFileSystem.of(job, scm)
FileSystemWrapper fsw = new FileSystemWrapper(fs: fs, scmKey: scm.getKey(), owner: owner)
private static FileSystemWrapper fromSCM(FlowExecutionOwner owner, WorkflowJob job, SCM scm) {
FileSystemWrapper fsw
FileSystemCacheKey cacheKey = new FileSystemCacheKey(owner: owner, scm: scm)
if (CACHE.containsKey(cacheKey)) {
fsw = CACHE.get(cacheKey)
} else {
SCMFileSystem fs
fs = SCMFileSystem.of(job, scm)
fsw = new FileSystemWrapper(fs: fs, scmKey: scm.getKey(), owner: owner)
CACHE.put(cacheKey, fsw)
}

return fsw
}

private static FileSystemWrapper fromMultiBranchProject(FlowExecutionOwner owner, WorkflowJob job, TaskListener listener){
ItemGroup<?> parent = job.getParent()

BranchJobProperty property = job.getProperty(BranchJobProperty)
if (!property) {
throw new JTEException("BranchJobProperty is somehow missing")
Expand All @@ -96,28 +137,48 @@ class FileSystemWrapperFactory {

SCMHead head = branch.getHead()
SCMRevision tip = scmSource.fetch(head, listener)

FileSystemWrapper fsw
SCMFileSystem fs
String scmKey
if (tip) {
scmKey = branch.getScm().getKey()
SCMRevision rev = scmSource.getTrustedRevision(tip, listener)
fs = SCMFileSystem.of(scmSource, head, rev)
FileSystemCacheKey cacheKey = new FileSystemCacheKey(owner: owner, scmSource: scmSource, scmHead: head, scmRevision: rev)
if (CACHE.containsKey(cacheKey)) {
fsw = CACHE.get(cacheKey)
} else {
fs = SCMFileSystem.of(scmSource, head, rev)
fsw = new FileSystemWrapper(fs: fs, scmKey: scmKey, owner: owner)
CACHE.put(cacheKey, fsw)
}
} else {
SCM jobSCM = branch.getScm()
fs = SCMFileSystem.of(job, jobSCM)
scmKey = jobSCM.getKey()
FileSystemCacheKey cacheKey = new FileSystemCacheKey(owner: owner, scm: jobSCM)
if (CACHE.containsKey(cacheKey)) {
fsw = CACHE.get(cacheKey)
} else {
fs = SCMFileSystem.of(job, jobSCM)
fsw = new FileSystemWrapper(fs: fs, scmKey: scmKey, owner: owner)
CACHE.put(cacheKey, fsw)
}
}
FileSystemWrapper fsw = new FileSystemWrapper(fs: fs, scmKey: scmKey, owner: owner)
return fsw
}

private static FileSystemWrapper fromPipelineJob(FlowExecutionOwner owner, WorkflowJob job){
FlowDefinition definition = job.getDefinition()
SCM jobSCM = definition.getScm()
String scmKey = jobSCM.getKey()
SCMFileSystem fs = SCMFileSystem.of(job, jobSCM)
FileSystemWrapper fsw = new FileSystemWrapper(fs: fs, scmKey: scmKey, owner: owner)
FileSystemCacheKey cacheKey = new FileSystemCacheKey(owner: owner, scm: jobSCM)
FileSystemWrapper fsw
if (CACHE.containsKey(cacheKey)) {
fsw = CACHE.get(cacheKey)
} else {
SCMFileSystem fs = SCMFileSystem.of(job, jobSCM)
fsw = new FileSystemWrapper(fs: fs, scmKey: scmKey, owner: owner)
CACHE.put(cacheKey, fsw)
}
return fsw
}

Expand Down
Loading