1919 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
2020 DEALINGS IN THE SOFTWARE.
2121 */
22- package net.gleske.scmfilter.impl.trait ;
22+ package net.gleske.scmfilter.impl.trait
2323
24+ import static net.gleske.jervis.remotes.SimpleRestService.objToJson
2425import static net.gleske.jervis.tools.AutoRelease.getScriptFromTemplate
26+ import static net.gleske.jervis.tools.SecurityIO.sha256Sum
2527import net.gleske.jervis.remotes.GitHubGraphQL
2628import net.gleske.jervis.tools.YamlOperator
2729import net.gleske.scmfilter.credential.GraphQLTokenCredential
@@ -45,12 +47,36 @@ import org.jenkinsci.Symbol
4547import org.jenkinsci.plugins.github_branch_source.GitHubSCMSource
4648import org.kohsuke.stapler.DataBoundConstructor
4749
48- import groovy.text.SimpleTemplateEngine
49- import java.util.logging.Level
50+ import java.time.Instant
51+ import java.util.concurrent.ThreadLocalRandom
5052import java.util.logging.Logger
5153import java.util.regex.Pattern
5254
55+ /**
56+ This trait is responsible for determining if a branch, PR, or Tag is
57+ buildable in a MultiBranch pipeline.
5358
59+ This class has tunable properties for making network requests to GitHub. It
60+ will introduce a random delay (min 1000ms to max 3000ms) between retrying.
61+ By default it will retry GitHub API requests up to 60 times. Time-wise
62+ retrying can take anywhere from 1-3 minutes based on the randomness before
63+ this class gives up raising an exception.
64+
65+ To change minimum range of random delay, start Jenkins with the following
66+ property. Value is an Integer (milliseconds).
67+
68+ -Dnet.gleske.scmfilter.impl.trait.JervisFilterTrait.minRetryInterval=1000
69+
70+ To change maximum range of random delay, start Jenkins with the following
71+ property. Value is an Integer (milliseconds).
72+
73+ -Dnet.gleske.scmfilter.impl.trait.JervisFilterTrait.maxRetryInterval=3000
74+
75+ To change the amount of times retry is attempted, start Jenkins with the
76+ following property. Value is an Integer (count of retrying).
77+
78+ -Dnet.gleske.scmfilter.impl.trait.JervisFilterTrait.retryLimit=60
79+ */
5480public class JervisFilterTrait extends SCMSourceTrait {
5581
5682 private static final long serialVersionUID = 1L
@@ -75,6 +101,87 @@ public class JervisFilterTrait extends SCMSourceTrait {
75101
76102 @NonNull
77103 private final String yamlFileName
104+
105+ /**
106+ Get .jervis.yml file from GitHub retrying with a random delay if GitHub
107+ request fails.
108+
109+ @param options Instead of parameters you pass parameters by name. [client, query, yamlFiles, log_trace_id]
110+ @return A response from GitHub with .jervis.yml with keys [yamlFile, yamlText]. Key-values can be Strings, empty string or null.
111+ */
112+ private Map getYamlWithRetry (Map options ) throws Exception {
113+ // method options
114+ GitHubGraphQL github = options. client
115+ String query = options. query
116+ List yamlFiles = options. yamlFiles
117+ String log_trace_id = options. log_trace_id
118+
119+ // system properties
120+ Integer minInterval = Integer . getInteger(JervisFilterTrait . name + " .minRetryInterval" , 1000 )
121+ Integer maxInterval = Integer . getInteger(JervisFilterTrait . name + " .maxRetryInterval" , 3000 ) + 1
122+ Integer retryLimit = Integer . getInteger(JervisFilterTrait . name + " .retryLimit" , 30 )
123+
124+ // internal values
125+ List errors = []
126+ String yamlText = ' '
127+ String yamlFile = ' '
128+ Integer retryCount = 0
129+ Map response = [:]
130+ while ({->
131+ if (retryCount > 0 ) {
132+ LOGGER . finer(" (trace-${ log_trace_id} ) Retrying GraphQL after failure (retryCount ${ retryCount} )" )
133+ }
134+ if (retryCount > retryLimit && errors) {
135+ LOGGER . finer(" (trace-${ log_trace_id} ) Retry limit reached with GQL errors ${ objToJson(errors: errors)} " )
136+ throw new Exception (" (trace-${ log_trace_id} ) Retry limit reached with GQL errors ${ objToJson(errors: errors)} " )
137+ } else {
138+ errors = []
139+ }
140+ try {
141+ response = github. sendGQL(query)
142+ } catch (Exception httpError) {
143+ if (retryCount > retryLimit) {
144+ LOGGER . finer(" (trace-${ log_trace_id} ) GraphQL HTTP Error: ${ httpError.getMessage()} " )
145+ throw httpError
146+ }
147+ // random delay for sleep
148+ sleep(ThreadLocalRandom . current(). nextLong(minInterval, maxInterval))
149+ retryCount++
150+ // retry while loop
151+ return true
152+ }
153+
154+ // look for GQL errors
155+ if (' errors' in response. keySet()) {
156+ errors = response. errors
157+ // random delay for sleep
158+ sleep(ThreadLocalRandom . current(). nextLong(minInterval, maxInterval))
159+ retryCount++
160+ // retry while loop
161+ return true
162+ }
163+
164+ // get data from response
165+ response?. data?. repository?. with { Map repoData ->
166+ yamlFiles. eachWithIndex { yamlFileEntry , i ->
167+ if (yamlText) {
168+ // exit eachWithIndex loop
169+ return
170+ }
171+ yamlText = (repoData?. get(" jervisYaml${ i} " . toString())?. text?. trim())
172+ if (yamlText) {
173+ yamlFile = yamlFiles[i]
174+ }
175+ }
176+ }
177+ // exit the do-while loop
178+ return false
179+ }()) continue
180+
181+ // return Map
182+ [yamlFile : yamlFile, yamlText : yamlText]
183+ }
184+
78185 /**
79186 Returns the YAML file name to search for filters.
80187
@@ -86,10 +193,10 @@ public class JervisFilterTrait extends SCMSourceTrait {
86193
87194 @DataBoundConstructor
88195 JervisFilterTrait (@CheckForNull String yamlFileName ) {
89- this . yamlFileName = StringUtils . defaultIfBlank(yamlFileName, DEFAULT_YAML_FILE );
196+ this . yamlFileName = StringUtils . defaultIfBlank(yamlFileName, DEFAULT_YAML_FILE )
90197 }
91198
92- private static shouldExclude (def filters_obj , String target_ref ) {
199+ private static shouldExclude (def filters_obj , String target_ref , String log_trace_id ) {
93200 List filters = []
94201 String filter_type = ' only'
95202 if (filters_obj instanceof List ) {
@@ -105,7 +212,7 @@ public class JervisFilterTrait extends SCMSourceTrait {
105212 }
106213 }
107214 if (! filters) {
108- LOGGER . fine(" Malformed filter found on git reference ${ target_ref} so we will allow by default" )
215+ LOGGER . fine(" (trace- ${ log_trace_id } ) Malformed filter found on git reference ${ target_ref} so we will allow by default" )
109216 return false
110217 }
111218 String regex = filters. collect {
@@ -129,6 +236,13 @@ public class JervisFilterTrait extends SCMSourceTrait {
129236 // wrong type of SCM source so skipping without excluding
130237 return false
131238 }
239+ // ISO-8601 instant timestamp which provides uniqueness
240+ // http://www.iso.org/iso/home/standards/iso8601.htm
241+ String log_trace_timestamp = Instant . now(). toString()
242+ String trace_target = (head in ChangeRequestSCMHead ) ? head. target. name : head. name
243+ // Unique trace ID so that associated log messages can be
244+ // followed in Jenkins debug logs
245+ String log_trace_id = sha256Sum(log_trace_timestamp + source. repoOwner + source. repository + trace_target)
132246 def github = new GitHubGraphQL ()
133247 // set credentials for GraphQL API interaction
134248 github. credential = new GraphQLTokenCredential (source. owner, source. credentialsId)
@@ -152,44 +266,35 @@ public class JervisFilterTrait extends SCMSourceTrait {
152266 // pull request
153267 binding[' git_ref' ] = " refs/pull/${ head.id} /head"
154268 target_ref = head. target. name
155- LOGGER . fine(" Scanning pull request ${ head.name} ." )
269+ LOGGER . fine(" (trace- ${ log_trace_id } ) Scanning pull request ${ head.name} ." )
156270 }
157271 else if (head instanceof TagSCMHead ) {
158272 // tag
159273 binding[' git_ref' ] = " refs/tags/${ head.name} "
160274 target_ref = head. name
161- LOGGER . fine(" Scanning tag ${ head.name} ." )
275+ LOGGER . fine(" (trace- ${ log_trace_id } ) Scanning tag ${ head.name} ." )
162276 }
163277 else {
164278 // branch
165279 binding[' git_ref' ] = " refs/heads/${ head.name} "
166280 target_ref = head. name
167- LOGGER . fine(" Scanning branch ${ head.name} ." )
281+ LOGGER . fine(" (trace- ${ log_trace_id } ) Scanning branch ${ head.name} ." )
168282 }
169283
170284 String graphql_query = getScriptFromTemplate(graphql_expr_template, binding)
171- LOGGER . finer(" GraphQL query for target ref ${ target_ref} :\n ${ graphql_query} " )
172- Map response = github. sendGQL(graphql_query)
173- String yamlText = ' '
285+ LOGGER . finer(" (trace-${ log_trace_id} ) GraphQL query for target ref ${ target_ref} :\n ${ graphql_query} " )
174286 // try to get all requested yaml files from the comma
175287 // separated paths provided by the admin configuration
176- String yamlFile = ' '
177- response?. get(' data' )?. get(' repository' )?. with { Map repoData ->
178- for (int i = 0 ; i < yamlFiles. size(); i++ ) {
179- yamlText = (repoData?. get(" jervisYaml${ i} " . toString())?. get(' text' )?. trim())
180- if (yamlText) {
181- yamlFile = yamlFiles[i]
182- break
183- }
184- }
185- }
288+ Map response = getYamlWithRetry(client : github, query : graphql_query, yamlFiles : yamlFiles, log_trace_id : log_trace_id)
289+ String yamlFile = response. yamlFile ?: ' '
290+ String yamlText = response. yamlText ?: ' '
186291
187292 if (! yamlText) {
188293 // could not find YAML or file was empty so should not build
189- LOGGER . finer(" On target ref ${ target_ref} , could not find yaml file(s): ${ yamlFileName} " )
294+ LOGGER . finer(" (trace- ${ log_trace_id } ) On target ref ${ target_ref} , could not find yaml file(s): ${ yamlFileName} " )
190295 return true
191296 }
192- LOGGER . fine(" On target ref ${ target_ref} , found ${ yamlFile} :\n ${ ['='*80, yamlText, '='*80].join('\n')} \n END YAML FILE" )
297+ LOGGER . fine(" (trace- ${ log_trace_id } ) On target ref ${ target_ref} , found ${ yamlFile} :\n ${ ['='*80, yamlText, '='*80].join('\n')} \n END YAML FILE" )
193298
194299 // parse the YAML for filtering
195300 Map jervis_yaml = YamlOperator . loadYamlFrom(yamlText)
@@ -199,15 +304,15 @@ public class JervisFilterTrait extends SCMSourceTrait {
199304 // allow all by default
200305 return false
201306 }
202- return shouldExclude(jervis_yaml[' tags' ], target_ref)
307+ return shouldExclude(jervis_yaml[' tags' ], target_ref, log_trace_id )
203308 }
204309 else {
205310 // branch or pull request
206311 if (! (' branches' in jervis_yaml)) {
207312 // allow all by default
208313 return false
209314 }
210- return shouldExclude(jervis_yaml[' branches' ], target_ref)
315+ return shouldExclude(jervis_yaml[' branches' ], target_ref, log_trace_id )
211316 }
212317 }
213318 })
0 commit comments