1   /*
2    * Copyright 2017 LINE Corporation
3    *
4    * LINE Corporation licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  
17  package com.linecorp.centraldogma.server.internal.mirror;
18  
19  import static com.linecorp.centraldogma.server.mirror.MirrorUtil.normalizePath;
20  import static java.util.Objects.requireNonNull;
21  
22  import java.io.File;
23  import java.net.URI;
24  import java.time.Instant;
25  import java.time.ZonedDateTime;
26  import java.time.temporal.ChronoUnit;
27  import java.util.Objects;
28  import java.util.Optional;
29  
30  import javax.annotation.Nullable;
31  
32  import com.cronutils.descriptor.CronDescriptor;
33  import com.cronutils.model.Cron;
34  import com.cronutils.model.time.ExecutionTime;
35  import com.google.common.annotations.VisibleForTesting;
36  import com.google.common.base.MoreObjects;
37  import com.google.common.base.MoreObjects.ToStringHelper;
38  
39  import com.linecorp.centraldogma.common.Author;
40  import com.linecorp.centraldogma.common.MirrorException;
41  import com.linecorp.centraldogma.server.command.CommandExecutor;
42  import com.linecorp.centraldogma.server.credential.Credential;
43  import com.linecorp.centraldogma.server.mirror.Mirror;
44  import com.linecorp.centraldogma.server.mirror.MirrorDirection;
45  import com.linecorp.centraldogma.server.mirror.MirrorResult;
46  import com.linecorp.centraldogma.server.mirror.MirrorStatus;
47  import com.linecorp.centraldogma.server.storage.repository.Repository;
48  
49  public abstract class AbstractMirror implements Mirror {
50  
51      private static final CronDescriptor CRON_DESCRIPTOR = CronDescriptor.instance();
52  
53      protected static final Author MIRROR_AUTHOR = new Author("Mirror", "mirror@localhost.localdomain");
54  
55      private final String id;
56      private final boolean enabled;
57      private final MirrorDirection direction;
58      private final Credential credential;
59      private final Repository localRepo;
60      private final String localPath;
61      private final URI remoteRepoUri;
62      private final String remotePath;
63      private final String remoteBranch;
64      @Nullable
65      private final String gitignore;
66      @Nullable
67      private final String zone;
68      @Nullable
69      private final Cron schedule;
70      @Nullable
71      private final ExecutionTime executionTime;
72      private final long jitterMillis;
73  
74      protected AbstractMirror(String id, boolean enabled, @Nullable Cron schedule, MirrorDirection direction,
75                               Credential credential, Repository localRepo, String localPath,
76                               URI remoteRepoUri, String remotePath, String remoteBranch,
77                               @Nullable String gitignore, @Nullable String zone) {
78          this.id = requireNonNull(id, "id");
79          this.enabled = enabled;
80          this.direction = requireNonNull(direction, "direction");
81          this.credential = requireNonNull(credential, "credential");
82          this.localRepo = requireNonNull(localRepo, "localRepo");
83          this.localPath = normalizePath(requireNonNull(localPath, "localPath"));
84          this.remoteRepoUri = requireNonNull(remoteRepoUri, "remoteRepoUri");
85          this.remotePath = normalizePath(requireNonNull(remotePath, "remotePath"));
86          this.remoteBranch = requireNonNull(remoteBranch, "remoteBranch");
87          this.gitignore = gitignore;
88          this.zone = zone;
89  
90          if (schedule != null) {
91              this.schedule = requireNonNull(schedule, "schedule");
92              executionTime = ExecutionTime.forCron(this.schedule);
93  
94              // Pre-calculate a constant jitter value up to 1 minute for a mirror.
95              // Use the properties' hash code so that the same properties result in the same jitter.
96              jitterMillis = Math.abs(Objects.hash(this.schedule.asString(), this.direction,
97                                                   this.localRepo.parent().name(), this.localRepo.name(),
98                                                   this.remoteRepoUri, this.remotePath, this.remoteBranch) /
99                                      (Integer.MAX_VALUE / 60000));
100         } else {
101             this.schedule = null;
102             executionTime = null;
103             jitterMillis = -1;
104         }
105     }
106 
107     @Override
108     public String id() {
109         return id;
110     }
111 
112     @Override
113     public final Cron schedule() {
114         return schedule;
115     }
116 
117     @Override
118     public final ZonedDateTime nextExecutionTime(ZonedDateTime lastExecutionTime) {
119         return nextExecutionTime(lastExecutionTime, jitterMillis);
120     }
121 
122     @VisibleForTesting
123     ZonedDateTime nextExecutionTime(ZonedDateTime lastExecutionTime, long jitterMillis) {
124         requireNonNull(lastExecutionTime, "lastExecutionTime");
125         final Optional<ZonedDateTime> next = executionTime.nextExecution(
126                 lastExecutionTime.minus(jitterMillis, ChronoUnit.MILLIS));
127         if (next.isPresent()) {
128             return next.get().plus(jitterMillis, ChronoUnit.MILLIS);
129         }
130         throw new IllegalArgumentException(
131                 "no next execution time for " + CRON_DESCRIPTOR.describe(schedule) + ", lastExecutionTime: " +
132                 lastExecutionTime);
133     }
134 
135     @Override
136     public MirrorDirection direction() {
137         return direction;
138     }
139 
140     @Override
141     public final Credential credential() {
142         return credential;
143     }
144 
145     @Override
146     public final Repository localRepo() {
147         return localRepo;
148     }
149 
150     @Override
151     public final String localPath() {
152         return localPath;
153     }
154 
155     @Override
156     public final URI remoteRepoUri() {
157         return remoteRepoUri;
158     }
159 
160     @Override
161     public final String remotePath() {
162         return remotePath;
163     }
164 
165     @Override
166     public final String remoteBranch() {
167         return remoteBranch;
168     }
169 
170     @Override
171     public final String gitignore() {
172         return gitignore;
173     }
174 
175     @Override
176     public final boolean enabled() {
177         return enabled;
178     }
179 
180     @Nullable
181     @Override
182     public String zone() {
183         return zone;
184     }
185 
186     @Override
187     public final MirrorResult mirror(File workDir, CommandExecutor executor, int maxNumFiles,
188                                      long maxNumBytes, Instant triggeredTime) {
189         try {
190             switch (direction()) {
191                 case LOCAL_TO_REMOTE:
192                     return mirrorLocalToRemote(workDir, maxNumFiles, maxNumBytes, triggeredTime);
193                 case REMOTE_TO_LOCAL:
194                     return mirrorRemoteToLocal(workDir, executor, maxNumFiles, maxNumBytes, triggeredTime);
195                 default:
196                     throw new Error("Should never reach here");
197             }
198         } catch (InterruptedException e) {
199             // Propagate the interruption.
200             Thread.currentThread().interrupt();
201             throw new MirrorException(e);
202         } catch (MirrorException e) {
203             throw e;
204         } catch (Exception e) {
205             final String message = e.getMessage();
206             if (message != null) {
207                 throw new MirrorException(message, e);
208             } else {
209                 throw new MirrorException(e);
210             }
211         }
212     }
213 
214     protected abstract MirrorResult mirrorLocalToRemote(
215             File workDir, int maxNumFiles, long maxNumBytes, Instant triggeredTime) throws Exception;
216 
217     protected abstract MirrorResult mirrorRemoteToLocal(
218             File workDir, CommandExecutor executor, int maxNumFiles, long maxNumBytes, Instant triggeredTime)
219             throws Exception;
220 
221     protected final MirrorResult newMirrorResult(MirrorStatus mirrorStatus, @Nullable String description,
222                                                  Instant triggeredTime) {
223         return new MirrorResult(id, localRepo.parent().name(), localRepo.name(), mirrorStatus, description,
224                                 triggeredTime, Instant.now(), zone);
225     }
226 
227     @Override
228     public String toString() {
229         final ToStringHelper helper = MoreObjects.toStringHelper("")
230                                                  .omitNullValues()
231                                                  .add("direction", direction)
232                                                  .add("localProj", localRepo.parent().name())
233                                                  .add("localRepo", localRepo.name())
234                                                  .add("localPath", localPath)
235                                                  .add("remoteRepo", remoteRepoUri)
236                                                  .add("remotePath", remotePath)
237                                                  .add("remoteBranch", remoteBranch)
238                                                  .add("gitignore", gitignore)
239                                                  .add("credential", credential);
240         if (schedule != null) {
241             helper.add("schedule", CronDescriptor.instance().describe(schedule));
242         }
243         return helper.toString();
244     }
245 }