This repository has been archived by the owner on May 15, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add basic foundation of Service class
It works, but has lots of TODOs and needs more fleshing out.
- Loading branch information
Showing
4 changed files
with
339 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,260 @@ | ||
// Copyright 2014 Google Inc. All Rights Reserved. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package com.google.enterprise.adaptor; | ||
|
||
import com.sun.net.httpserver.HttpContext; | ||
import com.sun.net.httpserver.HttpServer; | ||
|
||
import java.io.File; | ||
import java.io.IOException; | ||
import java.net.URI; | ||
import java.util.Arrays; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
import java.util.concurrent.ConcurrentMap; | ||
import java.util.concurrent.TimeUnit; | ||
import java.util.logging.Level; | ||
import java.util.logging.Logger; | ||
|
||
/** | ||
* Provides environment for running multiple adaptors on the same port and | ||
* having each instance be managed. | ||
*/ | ||
// TODO(ejona): improve locking | ||
// TODO(ejona): improve shutdown while starting | ||
// TODO(ejona): improve handling of time limits | ||
// TODO(ejona): improve state pre-condition checking | ||
public final class Service { | ||
private static final Logger log = Logger.getLogger(Service.class.getName()); | ||
|
||
private final Config config; | ||
private final HttpServer server; | ||
private final HttpServer dashboardServer; | ||
private final ConcurrentMap<String, Instance> instances | ||
= new ConcurrentHashMap<String, Instance>(); | ||
private final Thread shutdownHook | ||
= new Thread(new ShutdownHook(), "service-shutdown"); | ||
private int index; | ||
|
||
private Service(Config config) throws IOException { | ||
this.config = config; | ||
this.server = Application.createHttpServer(config); | ||
this.dashboardServer = Application.createDashboardHttpServer(config); | ||
} | ||
|
||
public synchronized Instance createInstance(String name, File jar, | ||
File workingDir) { | ||
Instance instance = new Instance(name, jar, workingDir, index++); | ||
if (instances.putIfAbsent(name, instance) != null) { | ||
throw new IllegalArgumentException("Instance by name already present: " | ||
+ name); | ||
} | ||
instance.install(); | ||
return instance; | ||
} | ||
|
||
public synchronized void deleteInstance(String name, long time, | ||
TimeUnit unit) { | ||
Instance instance = instances.get(name); | ||
if (instance == null) { | ||
throw new IllegalArgumentException("No instance with name: " + name); | ||
} | ||
instance.stop(time, unit); | ||
instance.uninstall(); | ||
instances.remove(name, instance); | ||
} | ||
|
||
public synchronized void start() throws IOException { | ||
daemonInit(); | ||
// The shutdown hook is purposefully not part of the daemon methods, | ||
// because it should only be done when running from the command line. | ||
Runtime.getRuntime().addShutdownHook(shutdownHook); | ||
daemonStart(); | ||
} | ||
|
||
synchronized void daemonInit() { | ||
server.start(); | ||
dashboardServer.start(); | ||
} | ||
|
||
synchronized void daemonStart() { | ||
for (Instance instance : instances.values()) { | ||
instance.start(); | ||
} | ||
} | ||
|
||
public synchronized void stop(long time, TimeUnit unit) { | ||
daemonStop(time, unit); | ||
daemonDestroy(time, unit); | ||
try { | ||
Runtime.getRuntime().removeShutdownHook(shutdownHook); | ||
} catch (IllegalStateException ex) { | ||
// Already executing hook. | ||
} | ||
} | ||
|
||
synchronized void daemonStop(long time, TimeUnit unit) { | ||
for (Instance instance : instances.values()) { | ||
instance.stop(time, unit); | ||
} | ||
} | ||
|
||
synchronized void daemonDestroy(long time, TimeUnit unit) { | ||
Application.httpServerShutdown(server, time, unit); | ||
Application.httpServerShutdown(dashboardServer, time, unit); | ||
} | ||
|
||
static Service daemonMain(String[] args) throws IOException { | ||
Config config = new Config(); | ||
Application.autoConfig(config, args, | ||
new File(Application.DEFAULT_CONFIG_FILE)); | ||
return new Service(config); | ||
} | ||
|
||
public static void main(String[] args) throws IOException { | ||
Service service = daemonMain(args); | ||
// TODO(ejona): decide if we want the JAR to be relative to the parent | ||
// process or the child process (right now it is relative to the child). | ||
service.createInstance("adaptor1", new File("../AdaptorTemplate.jar"), | ||
new File("adaptor1")); | ||
service.createInstance("adaptor2", new File("../AdaptorTemplate.jar"), | ||
new File("adaptor2")); | ||
service.start(); | ||
} | ||
|
||
/** | ||
* Maybe use this as an interface to allow disabling/enabling and | ||
* starting/stopping of particular instances? | ||
*/ | ||
public final class Instance { | ||
private final String name; | ||
private final File jar; | ||
private final File workingDir; | ||
private final int index; | ||
private Thread running; | ||
private ShutdownWaiter waiter; | ||
|
||
private Instance(String name, File jar, File workingDir, int index) { | ||
if (name == null) { | ||
throw new NullPointerException(); | ||
} | ||
if (name.contains("/") || name.startsWith(".")) { | ||
// Prevent '.', '..', and things containing '/' from being names. | ||
throw new IllegalArgumentException( | ||
"Name must not contain / or start with ."); | ||
} | ||
if (jar == null) { | ||
throw new NullPointerException(); | ||
} | ||
if (index < 0 || index > 10000) { | ||
throw new IllegalArgumentException("Index too large or small: " | ||
+ index); | ||
} | ||
this.name = name; | ||
this.jar = jar; | ||
this.workingDir = workingDir; | ||
this.index = index; | ||
} | ||
|
||
private void install() { | ||
// TODO(ejona): add disable support, where we return Service Unavailable | ||
// instead of Not Found. | ||
} | ||
|
||
private void start() { | ||
if (running != null) { | ||
throw new IllegalStateException(); | ||
} | ||
final int port = config.getServerPort() + 2 * (index + 1); | ||
final int dashboardPort = config.getServerDashboardPort() | ||
+ 2 * (index + 1); | ||
final String scheme = config.isServerSecure() ? "https" : "http"; | ||
waiter = new ShutdownWaiter(); | ||
running = new Thread(new Runnable() { | ||
@Override | ||
public void run() { | ||
try { | ||
// TODO(ejona): Figure out how much configuration to share. | ||
int ret = JavaExec.exec(jar, workingDir, Arrays.<String>asList( | ||
"-Dserver.port=" + port, | ||
"-Dserver.dashboardPort=" + dashboardPort, | ||
// TODO(ejona): use same security for child | ||
"-Dserver.secure=false", | ||
// TODO(ejona): REMOVE THIS HACK. WE NEED TO DO PROPER SECURITY | ||
// COMMUNICATION AND HANDLE X-Forwarded-For AND SIMILAR. THIS | ||
// DISABLES ALL SECURITY. | ||
"-Dserver.fullAccessHosts=127.0.0.1", | ||
"-Dgsa.hostname=" + config.getGsaHostname(), | ||
"-Dserver.reverseProxyProtocol=" + scheme, | ||
"-Dserver.reverseProxyPort=" + config.getServerPort())); | ||
if (ret != 0) { | ||
log.log(Level.WARNING, "Error response code from child: " + ret); | ||
} | ||
} catch (IOException ex) { | ||
// TODO(ejona): add more info as to which one failed. | ||
log.log(Level.WARNING, "IOException in subprocess", ex); | ||
} catch (InterruptedException ex) { | ||
log.log(Level.WARNING, "Forced shutdown of child", ex); | ||
} | ||
} | ||
}); | ||
running.start(); | ||
|
||
HttpContext context = server.createContext( | ||
"/" + name + "/", new ReverseProxyHandler( | ||
URI.create("http://127.0.0.1:" + port + "/"))); | ||
context.getFilters().add(waiter.filter()); | ||
|
||
// TODO(ejona): When you end up visiting the dashboard, it redirects you | ||
// to its port. It would be nice to fix RedirectHandler to deal with that, | ||
// although it will require additional config parameters. | ||
HttpContext dashboardContext = dashboardServer.createContext( | ||
"/" + name + "/", new ReverseProxyHandler( | ||
URI.create("http://127.0.0.1:" + dashboardPort + "/"))); | ||
dashboardContext.getFilters().add(waiter.filter()); | ||
} | ||
|
||
private void stop(long time, TimeUnit unit) { | ||
if (running == null) { | ||
throw new IllegalStateException(); | ||
} | ||
server.removeContext("/" + name); | ||
dashboardServer.removeContext("/" + name); | ||
try { | ||
waiter.shutdown(time, unit); | ||
} catch (InterruptedException ex) { | ||
Thread.currentThread().interrupt(); | ||
} | ||
// TODO(ejona): Send graceful shutdown request and join with running | ||
// thread, only interrupting if it times out. | ||
running.interrupt(); | ||
try { | ||
running.join(unit.toMillis(time)); | ||
} catch (InterruptedException ex) { | ||
Thread.currentThread().interrupt(); | ||
} | ||
running = null; | ||
} | ||
|
||
private void uninstall() { | ||
} | ||
} | ||
|
||
private class ShutdownHook implements Runnable { | ||
@Override | ||
public void run() { | ||
stop(3, TimeUnit.SECONDS); | ||
} | ||
} | ||
} |
Oops, something went wrong.