1   /*
2    * Copyright 2019 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  package com.linecorp.centraldogma.server;
17  
18  import static com.google.common.collect.ImmutableList.toImmutableList;
19  import static java.util.Objects.requireNonNull;
20  
21  import java.util.List;
22  import java.util.Optional;
23  import java.util.ServiceLoader;
24  import java.util.concurrent.CompletableFuture;
25  import java.util.concurrent.CompletionStage;
26  import java.util.concurrent.Executor;
27  import java.util.concurrent.Executors;
28  import java.util.concurrent.ScheduledExecutorService;
29  
30  import javax.annotation.Nullable;
31  
32  import org.slf4j.Logger;
33  import org.slf4j.LoggerFactory;
34  
35  import com.google.common.collect.ImmutableList;
36  import com.google.common.collect.ImmutableList.Builder;
37  import com.spotify.futures.CompletableFutures;
38  
39  import com.linecorp.armeria.common.util.StartStopSupport;
40  import com.linecorp.centraldogma.server.command.CommandExecutor;
41  import com.linecorp.centraldogma.server.plugin.Plugin;
42  import com.linecorp.centraldogma.server.plugin.PluginContext;
43  import com.linecorp.centraldogma.server.plugin.PluginTarget;
44  import com.linecorp.centraldogma.server.storage.project.ProjectManager;
45  
46  import io.micrometer.core.instrument.MeterRegistry;
47  import io.netty.util.concurrent.DefaultThreadFactory;
48  
49  /**
50   * Provides asynchronous start-stop life cycle support for the {@link Plugin}s.
51   */
52  final class PluginGroup {
53  
54      private static final Logger logger = LoggerFactory.getLogger(PluginGroup.class);
55  
56      /**
57       * Returns a new {@link PluginGroup} which holds the {@link Plugin}s loaded from the classpath.
58       * {@code null} is returned if there is no {@link Plugin} whose target equals to the specified
59       * {@code target}.
60       *
61       * @param target the {@link PluginTarget} which would be loaded
62       */
63      @Nullable
64      static PluginGroup loadPlugins(PluginTarget target, CentralDogmaConfig config) {
65          return loadPlugins(PluginGroup.class.getClassLoader(), target, config);
66      }
67  
68      /**
69       * Returns a new {@link PluginGroup} which holds the {@link Plugin}s loaded from the classpath.
70       * {@code null} is returned if there is no {@link Plugin} whose target equals to the specified
71       * {@code target}.
72       *
73       * @param classLoader which is used to load the {@link Plugin}s
74       * @param target the {@link PluginTarget} which would be loaded
75       */
76      @Nullable
77      static PluginGroup loadPlugins(ClassLoader classLoader, PluginTarget target, CentralDogmaConfig config) {
78          requireNonNull(classLoader, "classLoader");
79          requireNonNull(target, "target");
80          requireNonNull(config, "config");
81  
82          final ServiceLoader<Plugin> loader = ServiceLoader.load(Plugin.class, classLoader);
83          final Builder<Plugin> plugins = new Builder<>();
84          for (Plugin plugin : loader) {
85              if (target == plugin.target() && plugin.isEnabled(config)) {
86                  plugins.add(plugin);
87              }
88          }
89  
90          final List<Plugin> list = plugins.build();
91          if (list.isEmpty()) {
92              return null;
93          }
94  
95          return new PluginGroup(list, Executors.newSingleThreadExecutor(new DefaultThreadFactory(
96                  "plugins-for-" + target.name().toLowerCase().replace("_", "-"), true)));
97      }
98  
99      private final List<Plugin> plugins;
100     private final PluginGroupStartStop startStop;
101 
102     private PluginGroup(Iterable<Plugin> plugins, Executor executor) {
103         this.plugins = ImmutableList.copyOf(requireNonNull(plugins, "plugins"));
104         startStop = new PluginGroupStartStop(requireNonNull(executor, "executor"));
105     }
106 
107     /**
108      * Returns the {@link Plugin}s managed by this {@link PluginGroup}.
109      */
110     List<Plugin> plugins() {
111         return plugins;
112     }
113 
114     /**
115      * Returns the first {@link Plugin} of the specified {@code clazz} as wrapped by an {@link Optional}.
116      */
117     <T extends Plugin> Optional<T> findFirstPlugin(Class<T> clazz) {
118         requireNonNull(clazz, "clazz");
119         return plugins.stream().filter(clazz::isInstance).map(clazz::cast).findFirst();
120     }
121 
122     /**
123      * Starts the {@link Plugin}s managed by this {@link PluginGroup}.
124      */
125     CompletableFuture<Void> start(CentralDogmaConfig config, ProjectManager projectManager,
126                                   CommandExecutor commandExecutor, MeterRegistry meterRegistry,
127                                   ScheduledExecutorService purgeWorker) {
128         final PluginContext context = new PluginContext(config, projectManager, commandExecutor, meterRegistry,
129                                                         purgeWorker);
130         return startStop.start(context, context, true);
131     }
132 
133     /**
134      * Stops the {@link Plugin}s managed by this {@link PluginGroup}.
135      */
136     CompletableFuture<Void> stop(CentralDogmaConfig config, ProjectManager projectManager,
137                                  CommandExecutor commandExecutor, MeterRegistry meterRegistry,
138                                  ScheduledExecutorService purgeWorker) {
139         return startStop.stop(
140                 new PluginContext(config, projectManager, commandExecutor, meterRegistry, purgeWorker));
141     }
142 
143     private class PluginGroupStartStop extends StartStopSupport<PluginContext, PluginContext, Void, Void> {
144 
145         PluginGroupStartStop(Executor executor) {
146             super(executor);
147         }
148 
149         @Override
150         protected CompletionStage<Void> doStart(@Nullable PluginContext arg) throws Exception {
151             assert arg != null;
152             final List<CompletionStage<Void>> futures = plugins.stream().map(
153                     plugin -> plugin.start(arg)
154                                     .thenAccept(unused -> logger.info("Plugin started: {}", plugin))
155                                     .exceptionally(cause -> {
156                                         logger.info("Failed to start plugin: {}", plugin, cause);
157                                         return null;
158                                     })).collect(toImmutableList());
159             return CompletableFutures.allAsList(futures).thenApply(unused -> null);
160         }
161 
162         @Override
163         protected CompletionStage<Void> doStop(@Nullable PluginContext arg) throws Exception {
164             assert arg != null;
165             final List<CompletionStage<Void>> futures = plugins.stream().map(
166                     plugin -> plugin.stop(arg)
167                                     .thenAccept(unused -> logger.info("Plugin stopped: {}", plugin))
168                                     .exceptionally(cause -> {
169                                         logger.info("Failed to stop plugin: {}", plugin, cause);
170                                         return null;
171                                     })).collect(toImmutableList());
172             return CompletableFutures.allAsList(futures).thenApply(unused -> null);
173         }
174     }
175 }