Completed
Pull Request — master (#488)
by Helpful
1295:51 queued 1292:33
created

RollbackStep::start()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 25
Code Lines 19

Duplication

Lines 25
Ratio 100 %
Metric Value
dl 25
loc 25
rs 8.439
cc 5
eloc 19
nc 5
nop 0
1
<?php
2
3
/**
4
 * Peforms rollback of a pipeline to a previous status.
5
 *
6
 * Note that this step would usually only be used in the special conditional rollback situation configured
7
 * on the Pipeline itself - see the Pipeline documentation for details.
8
 *
9
 * <code>
10
 * RollbackStep1:
11
 *   Class: RollbackStep
12
 *   RestoreDB: true
13
 *   MaxDuration: 3600
14
 * </code>
15
 *
16
 * @property string $Doing
17
 *
18
 * @property string $Title
19
 *
20
 * @method DNDeployment RollbackDeployment() The current rollback deployment
21
 * @property int $RollbackDeploymentID
22
 * @method DNDataTransfer RollbackDatabase()
23
 * @property int RollbackDatabaseID
24
 */
25
class RollbackStep extends LongRunningPipelineStep {
26
27
	/**
28
	 * @var array
29
	 */
30
	private static $db = array(
31
		'Doing' => "Enum('Deployment,Snapshot,Queued', 'Queued')"
32
	);
33
34
	/**
35
	 * @var array
36
	 */
37
	private static $has_one = array(
38
		'RollbackDeployment' => 'DNDeployment',
39
		'RollbackDatabase' => 'DNDataTransfer'
40
	);
41
42
	/**
43
	 * @return string
44
	 */
45
	public function getTitle() {
46
		// Make sure the title includes the subtask
47
		return parent::getTitle() . ":{$this->Doing}";
48
	}
49
50
	/**
51
	 * @return bool|null
52
	 */
53 View Code Duplication
	public function start() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
54
		parent::start();
55
56
		switch($this->Status) {
57
			case 'Started':
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
58
				// If we are doing a subtask, check which one to continue
59
				switch($this->Doing) {
60
					case 'Deployment':
61
						return $this->continueRevertDeploy();
62
					case 'Snapshot':
63
						return $this->continueRevertDatabase();
64
					default:
65
						$this->log("Unable to process {$this->Title} with subtask of {$this->Doing}");
66
						$this->markFailed();
67
						return false;
68
				}
69
			case 'Queued':
70
				// Begin rollback by initiating deployment
71
				return $this->startRevertDeploy();
72
			default:
73
				$this->log("Unable to process {$this->Title} with status of {$this->Status}");
74
				$this->markFailed();
75
				return false;
76
		}
77
	}
78
79
	/**
80
	 * Begin a new deployment
81
	 *
82
	 * @return boolean
83
	 */
84
	protected function startRevertDeploy() {
85
		$this->Status = 'Started';
86
		$this->Doing = 'Deployment';
87
		$this->log("{$this->Title} starting revert deployment");
88
89
		// Skip deployment for dry run
90
		if($this->Pipeline()->DryRun) {
91
			$this->log("[Skipped] Create DNDeployment");
92
			$this->write();
93
			return true;
94
		}
95
96
		// Get old deployment from pipeline
97
		$pipeline = $this->Pipeline();
98
		$previous = $pipeline->PreviousDeployment();
99
		if(empty($previous) || empty($previous->SHA)) {
100
			$this->log("No available SHA for {$this->Title}");
101
			$this->markFailed();
102
			return false;
103
		}
104
105
		// Initialise deployment
106
		$strategy = new DeploymentStrategy($pipeline->Environment(), array(
107
			'sha'=>$pipeline->SHA,
108
			// Leave the maintenance page up if we are restoring the DB
109
			'leaveMaintenancePage' => $this->doRestoreDB()
110
		));
111
		$deployment = $strategy->createDeployment();
112
113
		$deployment->DeployerID = $pipeline->AuthorID;
114
		$deployment->write();
115
		$deployment->start();
116
		$this->RollbackDeploymentID = $deployment->ID;
117
		$this->write();
118
119
		return true;
120
	}
121
122
	/**
123
	 * Create a snapshot of the db and store the ID on the Pipline
124
	 *
125
	 * @return bool True if success
126
	 */
127
	protected function startRevertDatabase() {
128
		// Mark self as creating a snapshot
129
		$this->Status = 'Started';
130
		$this->Doing = 'Snapshot';
131
		$this->log("{$this->Title} reverting database from snapshot");
132
133
		// Skip deployment for dry run
134
		if($this->Pipeline()->DryRun) {
135
			$this->write();
136
			$this->log("[Skipped] Create DNDataTransfer restore");
137
			return true;
138
		}
139
140
		// Get snapshot
141
		$pipeline = $this->Pipeline();
142
		$backup = $pipeline->PreviousSnapshot();
143
		if(empty($backup) || !$backup->exists()) {
144
			$this->log("No database to revert for {$this->Title}");
145
			$this->markFailed();
146
			return false;
147
		}
148
149
		// Create restore job
150
		$job = DNDataTransfer::create();
151
		$job->EnvironmentID = $pipeline->EnvironmentID;
152
		$job->Direction = 'push';
153
		$job->Mode = 'db';
154
		$job->DataArchiveID = $backup->DataArchiveID;
155
		$job->AuthorID = $pipeline->AuthorID;
156
		$job->EnvironmentID = $pipeline->EnvironmentID;
157
		$job->write();
158
		$job->start();
159
160
		// Save rollback
161
		$this->RollbackDatabaseID = $job->ID;
162
		$this->write();
163
		return true;
164
	}
165
166
	/**
167
	 * Check status of current snapshot
168
	 */
169 View Code Duplication
	protected function continueRevertDatabase() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
170
		$this->log("Checking status of {$this->Title}...");
171
172
		// Skip snapshot for dry run
173
		if($this->Pipeline()->DryRun) {
174
			$this->log("[Skipped] Checking progress of snapshot restore");
175
			return $this->finish();
176
		}
177
178
		// Get related snapshot
179
		$transfer = $this->RollbackDatabase();
180
		if(empty($transfer) || !$transfer->exists()) {
181
			$this->log("Missing database transfer for in-progress {$this->Title}");
182
			$this->markFailed();
183
			return false;
184
		}
185
186
		// Check finished state
187
		$status = $transfer->ResqueStatus();
188
		if($this->checkResqueStatus($status)) {
189
			// Re-enable the site by disabling the maintenance, since the DB restored successfully
190
			$this->Pipeline()->Environment()->disableMaintenance($this->Pipeline()->getLogger());
191
192
			// After revert is complete we are done
193
			return $this->finish();
194
		}
195
	}
196
197
	/**
198
	 * Check status of deployment and finish task if complete, or fail if timedout
199
	 *
200
	 * @return boolean
201
	 */
202
	protected function continueRevertDeploy() {
203
		$this->log("Checking status of {$this->Title}...");
204
205
		// Skip deployment for dry run
206
		if($this->Pipeline()->DryRun) {
207
			$this->log("[Skipped] Checking progress of deployment");
208
			if($this->getConfigSetting('RestoreDB')) {
209
				return $this->startRevertDatabase();
210
			} else {
211
				$this->finish();
212
				return true;
213
			}
214
		}
215
216
		// Get related deployment
217
		$deployment = $this->RollbackDeployment();
218
		if(empty($deployment) || !$deployment->exists()) {
219
			$this->log("Missing deployment for in-progress {$this->Title}");
220
			$this->markFailed();
221
			return false;
222
		}
223
224
		// Check finished state
225
		$status = $deployment->ResqueStatus();
226
		if($this->checkResqueStatus($status)) {
227
			// Since deployment is finished, check if we should also do a db restoration
228
			if($this->doRestoreDB()) {
229
				return $this->startRevertDatabase();
230
			} else {
231
				$this->finish();
232
			}
233
		}
234
		return !$this->isFailed();
235
	}
236
237
	/**
238
	 * Check if we are intending to restore the DB after this deployment
239
	 *
240
	 * @return boolean
241
	 */
242
	protected function doRestoreDB() {
243
		return $this->getConfigSetting('RestoreDB') && $this->Pipeline()->PreviousSnapshot();
244
	}
245
246
	/**
247
	 * Check the status of a resque sub-task
248
	 *
249
	 * @param string $status Resque task status
250
	 * @return boolean True if the task is finished successfully
251
	 */
252 View Code Duplication
	protected function checkResqueStatus($status) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
253
		switch($status) {
254
			case "Complete":
255
				return true;
256
			case "Failed":
257
			case "Invalid":
258
				$this->log("{$this->Title} failed with task status $status");
259
				$this->markFailed();
260
				return false;
261
			case "Queued":
262
			case "Running":
263
			default:
264
				// For running or queued tasks ensure that we have not exceeded
265
				// a reasonable time-elapsed to consider this job inactive
266
				if($this->isTimedOut()) {
267
					$this->log("{$this->Title} took longer than {$this->MaxDuration} seconds to run and has timed out");
268
					$this->markFailed();
269
					return false;
270
				} else {
271
					// While still running report no error, waiting for resque job to eventually finish
272
					// some time in the future
273
					$this->log("{$this->Title} is still in progress");
274
					return false;
275
				}
276
		}
277
	}
278
}
279