blob: 6b34dbea971c81fda6005d2c3e2b5deebfda9b28 [file] [log] [blame]
Alexandr Lovtsovd6254432020-10-15 17:53:34 +03001import org.jfrog.hudson.pipeline.common.types.ArtifactoryServer
2import java.util.concurrent.TimeoutException
Alexandr Lovtsov5db4b0e2021-02-19 18:35:06 +03003import groovy.time.TimeCategory
Alexandr Lovtsovd6254432020-10-15 17:53:34 +03004
5class Lock {
6 String name, id, path
Alexandr Lovtsov5db4b0e2021-02-19 18:35:06 +03007 String retryInterval, timeout, expiration
Alexandr Lovtsovd6254432020-10-15 17:53:34 +03008 Boolean force
9 Map lockExtraMetadata
10 ArtifactoryServer artifactoryServer
11
12 private String lockFileContent
13 private String lockFileContentCache
Alexandr Lovtsov463efeb2020-10-29 18:28:40 +030014 private Boolean fileNotFound
Alexandr Lovtsovd6254432020-10-15 17:53:34 +030015
16 final private String fileUri
17
18 final private def common = new com.mirantis.mk.Common()
19 final private def artifactoryTools = new com.mirantis.mk.Artifactory()
20
21 // Constructor
22 public Lock(Map args) {
23 // Mandatory
24 this.name = args.name
25 this.artifactoryServer = args.artifactoryServer
26
27 // Defaults
Alexandr Lovtsov5db4b0e2021-02-19 18:35:06 +030028 this.id = args.getOrDefault('id', '')
29 this.path = args.getOrDefault('path', 'binary-dev-local/locks')
30 this.retryInterval = args.getOrDefault('retryInterval', '5m')
31 this.timeout = args.getOrDefault('timeout', '3h')
32 this.expiration = args.getOrDefault('expiration', '24h')
33 this.force = args.getOrDefault('force', false)
34 this.lockExtraMetadata = args.getOrDefault('lockExtraMetadata', [:])
Alexandr Lovtsovd6254432020-10-15 17:53:34 +030035
36 // Internal
37 this.fileUri = "/${path}/${name}.yaml".toLowerCase()
38 }
39
40 final private Map artObj
41 // getPasswordCredentials() is CPS-transformed function and cannot be used in constructor
42 final private Map getArtObj() {
43 def artifactoryCreds = common.getPasswordCredentials(artifactoryServer.getCredentialsId())
44 return [
45 'url': "${artifactoryServer.getUrl()}/artifactory",
46 'creds': [
47 'username': artifactoryCreds['username'],
48 'password': artifactoryCreds['password'],
49 ]
50 ]
51 }
52
53 // getter for lockFileContent
54 final private String getLockFileContent() {
55 if (this.lockFileContentCache == null) {
56 try {
57 this.lockFileContentCache = artifactoryTools.restCall(this.artObj, this.fileUri, 'GET', null, [:], '')
Alexandr Lovtsov463efeb2020-10-29 18:28:40 +030058 this.fileNotFound = false // file found
59 } catch (java.io.FileNotFoundException e) {
Alexandr Lovtsovd6254432020-10-15 17:53:34 +030060 this.lockFileContentCache = ''
Alexandr Lovtsov463efeb2020-10-29 18:28:40 +030061 this.fileNotFound = true // file not found
62 } catch (Exception e) {
63 common.errorMsg(e.message)
64 this.lockFileContentCache = ''
65 this.fileNotFound = null // we don't know about file existence
Alexandr Lovtsovd6254432020-10-15 17:53:34 +030066 }
67 }
68 return this.lockFileContentCache
69 }
70
71 public void lock() {
72 if (this.force) {
73 common.infoMsg("Ignore lock checking due 'force' flag presence")
74 } else {
75 waitLockReleased()
76 }
77 createLockFile()
78 }
79
80 public void unlock() {
81 if (!isLockFileExist()) {
82 common.infoMsg("Lock file '${this.artObj['url']}${this.fileUri}' does not exist. No need to remove it")
83 // No need to continue if file does not exist
84 return
85 }
86
87 Map lockMeta = common.readYaml2(text: this.lockFileContent ?: '{}')
Alexandr Lovtsov5db4b0e2021-02-19 18:35:06 +030088 if (this.force || (this.id && this.id == lockMeta.getOrDefault('lockID', ''))) {
Alexandr Lovtsovd6254432020-10-15 17:53:34 +030089 artifactoryTools.restCall(this.artObj, this.fileUri, 'DELETE', null, [:], '')
90 common.infoMsg("Lock file '${this.artObj['url']}${this.fileUri}' has been removed")
91 } else {
92 throw new RuntimeException("Given lock ID '${this.id}' is not equal to '${lockMeta.get('lockID')}' ID in lock file")
93 }
94 }
95
96 private void createLockFile() {
97 this.id = UUID.randomUUID().toString()
98
Alexandr Lovtsov5db4b0e2021-02-19 18:35:06 +030099 Date now = new Date()
100 Date expiredAt = TimeCategory.plus(now, common.getDuration(this.expiration))
Alexandr Lovtsovd6254432020-10-15 17:53:34 +0300101 Map lockMeta = [
102 'lockID': this.id,
Alexandr Lovtsov5db4b0e2021-02-19 18:35:06 +0300103 'createdAt': now.toString(),
104 'expiredAt': expiredAt.toString(),
Alexandr Lovtsovd6254432020-10-15 17:53:34 +0300105 ]
106 lockMeta.putAll(this.lockExtraMetadata)
107
108 def commonMCP = new com.mirantis.mcp.Common()
109 artifactoryTools.restCall(this.artObj, this.fileUri, 'PUT', commonMCP.dumpYAML(lockMeta), [:], '')
110 common.infoMsg("Lock file '${this.artObj['url']}${this.fileUri}' has been created")
111 }
112
113 private void waitLockReleased() {
114 Long startTime = System.currentTimeMillis()
Alexandr Lovtsov5db4b0e2021-02-19 18:35:06 +0300115 Long timeoutMillis = common.getDuration(this.timeout).toMilliseconds()
116 Long retryIntervalMillis = common.getDuration(this.retryInterval).toMilliseconds()
Alexandr Lovtsovd6254432020-10-15 17:53:34 +0300117 while (isLocked()) {
Alexandr Lovtsov5db4b0e2021-02-19 18:35:06 +0300118 if (System.currentTimeMillis() - startTime >= timeoutMillis) {
119 throw new TimeoutException("Execution of waitLock timed out after ${this.timeout}")
Alexandr Lovtsovd6254432020-10-15 17:53:34 +0300120 }
Alexandr Lovtsov5db4b0e2021-02-19 18:35:06 +0300121 common.infoMsg("'${this.name}' is locked. Retry in ${this.retryInterval}")
Alexandr Lovtsov7aca6e72020-10-20 13:00:11 +0300122 // Reset the cache so it will re-retrieve the file and its content
123 // otherwise it cannot determine that file has been removed on artifactory
124 // in the middle of waiting
125 this.lockFileContentCache = null
Alexandr Lovtsov5db4b0e2021-02-19 18:35:06 +0300126 sleep(retryIntervalMillis)
Alexandr Lovtsovd6254432020-10-15 17:53:34 +0300127 }
128 }
129
130 private Boolean isLocked() {
131 if (!isLockFileExist()) {
132 common.infoMsg("Lock file for '${this.name}' does not exist")
133 return false
134 } else if (isLockExpired()) {
135 common.infoMsg("Lock '${this.name}' has been expired")
136 return false
137 }
138 return true
139 }
140
141 private Boolean isLockFileExist() {
Alexandr Lovtsov463efeb2020-10-29 18:28:40 +0300142 // If there is something in file's content that it definitly exists
143 // If we don't know about file existence (fileNotFound == null) we assume it exists
144 return !this.lockFileContent.isEmpty() || !this.fileNotFound
Alexandr Lovtsovd6254432020-10-15 17:53:34 +0300145 }
146
147 private Boolean isLockExpired() {
148 if (!isLockFileExist()) {
149 return true
150 }
151 Map lockMeta = common.readYaml2(text: this.lockFileContent ?: '{}')
Alexandr Lovtsov5db4b0e2021-02-19 18:35:06 +0300152 Date expirationTime = new Date(lockMeta.getOrDefault('expiredAt', '01/01/1970'))
Alexandr Lovtsovd6254432020-10-15 17:53:34 +0300153 Date currentTime = new Date()
154 return currentTime.after(expirationTime)
155 }
156}