| Alexandr Lovtsov | d625443 | 2020-10-15 17:53:34 +0300 | [diff] [blame] | 1 | import org.jfrog.hudson.pipeline.common.types.ArtifactoryServer | 
|  | 2 | import java.util.concurrent.TimeoutException | 
| Alexandr Lovtsov | 5db4b0e | 2021-02-19 18:35:06 +0300 | [diff] [blame] | 3 | import groovy.time.TimeCategory | 
| Alexandr Lovtsov | d625443 | 2020-10-15 17:53:34 +0300 | [diff] [blame] | 4 |  | 
|  | 5 | class Lock { | 
|  | 6 | String  name, id, path | 
| Alexandr Lovtsov | 5db4b0e | 2021-02-19 18:35:06 +0300 | [diff] [blame] | 7 | String retryInterval, timeout, expiration | 
| Alexandr Lovtsov | d625443 | 2020-10-15 17:53:34 +0300 | [diff] [blame] | 8 | Boolean force | 
|  | 9 | Map lockExtraMetadata | 
|  | 10 | ArtifactoryServer artifactoryServer | 
|  | 11 |  | 
|  | 12 | private String lockFileContent | 
|  | 13 | private String lockFileContentCache | 
| Alexandr Lovtsov | 463efeb | 2020-10-29 18:28:40 +0300 | [diff] [blame] | 14 | private Boolean fileNotFound | 
| Alexandr Lovtsov | d625443 | 2020-10-15 17:53:34 +0300 | [diff] [blame] | 15 |  | 
|  | 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 Lovtsov | 5db4b0e | 2021-02-19 18:35:06 +0300 | [diff] [blame] | 28 | 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 Lovtsov | d625443 | 2020-10-15 17:53:34 +0300 | [diff] [blame] | 35 |  | 
|  | 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 Lovtsov | 463efeb | 2020-10-29 18:28:40 +0300 | [diff] [blame] | 58 | this.fileNotFound = false // file found | 
|  | 59 | } catch (java.io.FileNotFoundException e) { | 
| Alexandr Lovtsov | d625443 | 2020-10-15 17:53:34 +0300 | [diff] [blame] | 60 | this.lockFileContentCache = '' | 
| Alexandr Lovtsov | 463efeb | 2020-10-29 18:28:40 +0300 | [diff] [blame] | 61 | 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 Lovtsov | d625443 | 2020-10-15 17:53:34 +0300 | [diff] [blame] | 66 | } | 
|  | 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 Lovtsov | 5db4b0e | 2021-02-19 18:35:06 +0300 | [diff] [blame] | 88 | if (this.force || (this.id && this.id == lockMeta.getOrDefault('lockID', ''))) { | 
| Alexandr Lovtsov | d625443 | 2020-10-15 17:53:34 +0300 | [diff] [blame] | 89 | 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 Lovtsov | 5db4b0e | 2021-02-19 18:35:06 +0300 | [diff] [blame] | 99 | Date now = new Date() | 
|  | 100 | Date expiredAt = TimeCategory.plus(now, common.getDuration(this.expiration)) | 
| Alexandr Lovtsov | d625443 | 2020-10-15 17:53:34 +0300 | [diff] [blame] | 101 | Map lockMeta = [ | 
|  | 102 | 'lockID': this.id, | 
| Alexandr Lovtsov | 5db4b0e | 2021-02-19 18:35:06 +0300 | [diff] [blame] | 103 | 'createdAt': now.toString(), | 
|  | 104 | 'expiredAt': expiredAt.toString(), | 
| Alexandr Lovtsov | d625443 | 2020-10-15 17:53:34 +0300 | [diff] [blame] | 105 | ] | 
|  | 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 Lovtsov | 5db4b0e | 2021-02-19 18:35:06 +0300 | [diff] [blame] | 115 | Long timeoutMillis = common.getDuration(this.timeout).toMilliseconds() | 
|  | 116 | Long retryIntervalMillis = common.getDuration(this.retryInterval).toMilliseconds() | 
| Alexandr Lovtsov | d625443 | 2020-10-15 17:53:34 +0300 | [diff] [blame] | 117 | while (isLocked()) { | 
| Alexandr Lovtsov | 5db4b0e | 2021-02-19 18:35:06 +0300 | [diff] [blame] | 118 | if (System.currentTimeMillis() - startTime >= timeoutMillis) { | 
|  | 119 | throw new TimeoutException("Execution of waitLock timed out after ${this.timeout}") | 
| Alexandr Lovtsov | d625443 | 2020-10-15 17:53:34 +0300 | [diff] [blame] | 120 | } | 
| Alexandr Lovtsov | 5db4b0e | 2021-02-19 18:35:06 +0300 | [diff] [blame] | 121 | common.infoMsg("'${this.name}' is locked. Retry in ${this.retryInterval}") | 
| Alexandr Lovtsov | 7aca6e7 | 2020-10-20 13:00:11 +0300 | [diff] [blame] | 122 | // 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 Lovtsov | 5db4b0e | 2021-02-19 18:35:06 +0300 | [diff] [blame] | 126 | sleep(retryIntervalMillis) | 
| Alexandr Lovtsov | d625443 | 2020-10-15 17:53:34 +0300 | [diff] [blame] | 127 | } | 
|  | 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 Lovtsov | 463efeb | 2020-10-29 18:28:40 +0300 | [diff] [blame] | 142 | // 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 Lovtsov | d625443 | 2020-10-15 17:53:34 +0300 | [diff] [blame] | 145 | } | 
|  | 146 |  | 
|  | 147 | private Boolean isLockExpired() { | 
|  | 148 | if (!isLockFileExist()) { | 
|  | 149 | return true | 
|  | 150 | } | 
|  | 151 | Map lockMeta = common.readYaml2(text: this.lockFileContent ?: '{}') | 
| Alexandr Lovtsov | 5db4b0e | 2021-02-19 18:35:06 +0300 | [diff] [blame] | 152 | Date expirationTime = new Date(lockMeta.getOrDefault('expiredAt', '01/01/1970')) | 
| Alexandr Lovtsov | d625443 | 2020-10-15 17:53:34 +0300 | [diff] [blame] | 153 | Date currentTime = new Date() | 
|  | 154 | return currentTime.after(expirationTime) | 
|  | 155 | } | 
|  | 156 | } |