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.ZonedDateTime;
25  import java.time.temporal.ChronoUnit;
26  import java.util.Objects;
27  import java.util.Optional;
28  
29  import javax.annotation.Nullable;
30  
31  import com.cronutils.descriptor.CronDescriptor;
32  import com.cronutils.model.Cron;
33  import com.cronutils.model.time.ExecutionTime;
34  import com.google.common.annotations.VisibleForTesting;
35  import com.google.common.base.MoreObjects;
36  import com.google.common.base.MoreObjects.ToStringHelper;
37  
38  import com.linecorp.centraldogma.common.Author;
39  import com.linecorp.centraldogma.server.MirrorException;
40  import com.linecorp.centraldogma.server.command.CommandExecutor;
41  import com.linecorp.centraldogma.server.mirror.Mirror;
42  import com.linecorp.centraldogma.server.mirror.MirrorCredential;
43  import com.linecorp.centraldogma.server.mirror.MirrorDirection;
44  import com.linecorp.centraldogma.server.storage.repository.Repository;
45  
46  public abstract class AbstractMirror implements Mirror {
47  
48      private static final CronDescriptor CRON_DESCRIPTOR = CronDescriptor.instance();
49  
50      protected static final Author MIRROR_AUTHOR = new Author("Mirror", "mirror@localhost.localdomain");
51  
52      private final Cron schedule;
53      private final MirrorDirection direction;
54      private final MirrorCredential credential;
55      private final Repository localRepo;
56      private final String localPath;
57      private final URI remoteRepoUri;
58      private final String remotePath;
59      @Nullable
60      private final String remoteBranch;
61      @Nullable
62      private final String gitignore;
63      private final ExecutionTime executionTime;
64      private final long jitterMillis;
65  
66      protected AbstractMirror(Cron schedule, MirrorDirection direction, MirrorCredential credential,
67                               Repository localRepo, String localPath,
68                               URI remoteRepoUri, String remotePath, @Nullable String remoteBranch,
69                               @Nullable String gitignore) {
70  
71          this.schedule = requireNonNull(schedule, "schedule");
72          this.direction = requireNonNull(direction, "direction");
73          this.credential = requireNonNull(credential, "credential");
74          this.localRepo = requireNonNull(localRepo, "localRepo");
75          this.localPath = normalizePath(requireNonNull(localPath, "localPath"));
76          this.remoteRepoUri = requireNonNull(remoteRepoUri, "remoteRepoUri");
77          this.remotePath = normalizePath(requireNonNull(remotePath, "remotePath"));
78          this.remoteBranch = remoteBranch;
79          this.gitignore = gitignore;
80  
81          executionTime = ExecutionTime.forCron(this.schedule);
82  
83          // Pre-calculate a constant jitter value up to 1 minute for a mirror.
84          // Use the properties' hash code so that the same properties result in the same jitter.
85          jitterMillis = Math.abs(Objects.hash(this.schedule.asString(), this.direction,
86                                               this.localRepo.parent().name(), this.localRepo.name(),
87                                               this.remoteRepoUri, this.remotePath, this.remoteBranch) /
88                                  (Integer.MAX_VALUE / 60000));
89      }
90  
91      @Override
92      public final Cron schedule() {
93          return schedule;
94      }
95  
96      @Override
97      public final ZonedDateTime nextExecutionTime(ZonedDateTime lastExecutionTime) {
98          return nextExecutionTime(lastExecutionTime, jitterMillis);
99      }
100 
101     @VisibleForTesting
102     ZonedDateTime nextExecutionTime(ZonedDateTime lastExecutionTime, long jitterMillis) {
103         requireNonNull(lastExecutionTime, "lastExecutionTime");
104         final Optional<ZonedDateTime> next = executionTime.nextExecution(
105                 lastExecutionTime.minus(jitterMillis, ChronoUnit.MILLIS));
106         if (next.isPresent()) {
107             return next.get().plus(jitterMillis, ChronoUnit.MILLIS);
108         }
109         throw new IllegalArgumentException(
110                 "no next execution time for " + CRON_DESCRIPTOR.describe(schedule) + ", lastExecutionTime: " +
111                 lastExecutionTime);
112     }
113 
114     @Override
115     public MirrorDirection direction() {
116         return direction;
117     }
118 
119     @Override
120     public final MirrorCredential credential() {
121         return credential;
122     }
123 
124     @Override
125     public final Repository localRepo() {
126         return localRepo;
127     }
128 
129     @Override
130     public final String localPath() {
131         return localPath;
132     }
133 
134     @Override
135     public final URI remoteRepoUri() {
136         return remoteRepoUri;
137     }
138 
139     @Override
140     public final String remotePath() {
141         return remotePath;
142     }
143 
144     @Override
145     public final String remoteBranch() {
146         return remoteBranch;
147     }
148 
149     @Override
150     public final String gitignore() {
151         return gitignore;
152     }
153 
154     @Override
155     public final void mirror(File workDir, CommandExecutor executor, int maxNumFiles, long maxNumBytes) {
156         try {
157             switch (direction()) {
158                 case LOCAL_TO_REMOTE:
159                     mirrorLocalToRemote(workDir, maxNumFiles, maxNumBytes);
160                     break;
161                 case REMOTE_TO_LOCAL:
162                     mirrorRemoteToLocal(workDir, executor, maxNumFiles, maxNumBytes);
163                     break;
164             }
165         } catch (InterruptedException e) {
166             // Propagate the interruption.
167             Thread.currentThread().interrupt();
168         } catch (MirrorException e) {
169             throw e;
170         } catch (Exception e) {
171             throw new MirrorException(e);
172         }
173     }
174 
175     protected abstract void mirrorLocalToRemote(
176             File workDir, int maxNumFiles, long maxNumBytes) throws Exception;
177 
178     protected abstract void mirrorRemoteToLocal(
179             File workDir, CommandExecutor executor, int maxNumFiles, long maxNumBytes) throws Exception;
180 
181     @Override
182     public String toString() {
183         final ToStringHelper helper = MoreObjects.toStringHelper("")
184                                                  .omitNullValues()
185                                                  .add("schedule", CronDescriptor.instance().describe(schedule))
186                                                  .add("direction", direction)
187                                                  .add("localProj", localRepo.parent().name())
188                                                  .add("localRepo", localRepo.name())
189                                                  .add("localPath", localPath)
190                                                  .add("remoteRepo", remoteRepoUri)
191                                                  .add("remotePath", remotePath)
192                                                  .add("remoteBranch", remoteBranch)
193                                                  .add("gitignore", gitignore)
194                                                  .add("credential", credential);
195 
196         return helper.toString();
197     }
198 }