Wie kann ich eine Spring Boot-Anwendung korrekt herunterfahren?


120

Im Spring Boot-Dokument heißt es: "Jede SpringApplication registriert einen Shutdown-Hook bei der JVM, um sicherzustellen, dass der ApplicationContext beim Beenden ordnungsgemäß geschlossen wird."

Wenn ich ctrl+cauf den Shell-Befehl klicke, kann die Anwendung ordnungsgemäß heruntergefahren werden. Wenn ich die Anwendung auf einer Produktionsmaschine ausführe, muss ich den Befehl verwenden java -jar ProApplicaton.jar. Aber ich kann das Shell-Terminal nicht schließen, sonst wird der Prozess geschlossen.

Wenn ich einen Befehl wie ausführe nohup java -jar ProApplicaton.jar &, kann ich ihn nicht ordnungsgemäß ctrl+cherunterfahren.

Was ist der richtige Weg, um eine Spring Boot-Anwendung in der Produktionsumgebung zu starten und zu stoppen?


Abhängig von Ihrer Konfiguration wird die PID der Anwendung in die Datei geschrieben. Sie können ein Kill-Signal an diese PID senden. Siehe auch die Kommentare in dieser Ausgabe .
M. Deinum

Welches Signal soll ich verwenden, ich denke nicht, dass kill -9 eine gute Idee ist, oder?
Chris

5
Deshalb habe ich Sie auf diesen Thread hingewiesen ... Aber so etwas kill -SIGTERM <PID>sollte den Trick machen.
M. Deinum

töte mit pid, no -9
Dapeng

1
kill $ (lsof -ti tcp: <port>) - falls Sie keinen Aktuator verwenden möchten und einen schnellen Kill benötigen
Alex Nolasco

Antworten:


61

Wenn Sie das Aktuatormodul verwenden, können Sie die Anwendung über JMXoder HTTPwenn der Endpunkt aktiviert ist, herunterfahren .

hinzufügen zu application.properties:

endpoints.shutdown.enabled = true

Folgende URL wird verfügbar sein:

/actuator/shutdown - Ermöglicht das ordnungsgemäße Herunterfahren der Anwendung (standardmäßig nicht aktiviert).

Abhängig davon, wie ein Endpunkt verfügbar gemacht wird, kann der vertrauliche Parameter als Sicherheitshinweis verwendet werden.

Beispielsweise benötigen vertrauliche Endpunkte einen Benutzernamen / ein Kennwort, wenn auf sie zugegriffen wird HTTP(oder werden einfach deaktiviert, wenn die Websicherheit nicht aktiviert ist).

Aus der Spring-Boot-Dokumentation


1
Ich möchte kein Aktuatormodul einbinden. Ich habe jedoch festgestellt, dass Spring Boot in meinem Konsolenprotokoll die PID in der ersten Zeile druckt. Gibt es eine Möglichkeit, Spring Boot die PID in einer anderen Datei drucken zu lassen, ohne ein Aktuatormodul (ApplicationPidListener) hinzuzufügen?
Chris

2
Beachten Sie, dass dies ein http-Beitrag zu diesem Endpunkt sein muss. Sie können es mit
endpoints.shutdown.enabled

54

Hier ist eine weitere Option, bei der Sie den Code nicht ändern oder einen heruntergefahrenen Endpunkt verfügbar machen müssen. Erstellen Sie die folgenden Skripte und verwenden Sie sie, um Ihre App zu starten und zu stoppen.

start.sh

#!/bin/bash
java -jar myapp.jar & echo $! > ./pid.file &

Startet Ihre App und speichert die Prozess-ID in einer Datei

stop.sh

#!/bin/bash
kill $(cat ./pid.file)

Stoppt Ihre App mit der gespeicherten Prozess-ID

start_silent.sh

#!/bin/bash
nohup ./start.sh > foo.out 2> foo.err < /dev/null &

Wenn Sie die App mit ssh von einem Remotecomputer oder einer CI-Pipeline aus starten müssen, verwenden Sie stattdessen dieses Skript, um Ihre App zu starten. Wenn Sie start.sh direkt verwenden, kann die Shell hängen bleiben.

Nach z. Wenn Sie Ihre App erneut bereitstellen, können Sie sie neu starten, indem Sie:

sshpass -p password ssh -oStrictHostKeyChecking=no userName@www.domain.com 'cd /home/user/pathToApp; ./stop.sh; ./start_silent.sh'

Dies sollte die Antwort sein. Ich habe gerade bestätigt, dass ein Signal 15 Shutdown den Frühling anweist, anmutig zu scheißen.
Tschad

1
Warum rufen Sie nicht die Java-JAR-Ausführung mit nohup in start.sh auf, anstatt die Java-jar-Ausführung in start.sh aufzurufen, die mit nohup in einem anderen Skript der äußeren Shell aufgerufen wird?
Anand Varkey Philips

2
@AnandVarkeyPhilips Der einzige Grund ist, dass ich manchmal start.sh von der Kommandozeile zu Testzwecken aufrufe, aber wenn Sie immer brauchen nohup, können Sie einfach die Befehle zusammenführen
Jens

@Jens, Danke für die Info. Können Sie mir sagen, dass Sie dies tun: foo.out 2> foo.err </ dev / null &
Anand Varkey Philips

1
@jens, danke, ich habe es erfolgreich gemacht und mein Start- und Stopp-Skript hier unten veröffentlicht. ( stackoverflow.com/questions/26547532/… )
Anand Varkey Philips

52

Zur Antwort von @ Jean-Philippe Bond:

Hier ist ein kurzes Maven-Beispiel für Maven-Benutzer zum Konfigurieren des HTTP-Endpunkts zum Herunterfahren einer Spring-Boot-Web-App mithilfe des Spring-Boot-Starter-Aktuators, damit Sie Folgendes kopieren und einfügen können:

1.Maven pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

2.application.properties:

#No auth  protected 
endpoints.shutdown.sensitive=false

#Enable shutdown endpoint
endpoints.shutdown.enabled=true

Alle Endpunkte sind hier aufgelistet :

3.Senden Sie eine Post-Methode, um die App herunterzufahren:

curl -X POST localhost:port/shutdown

Sicherheitshinweis:

Wenn Sie die Shutdown-Methode auth protected benötigen, benötigen Sie möglicherweise auch

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Details konfigurieren :


Nach dem zweiten Schritt wurde beim Versuch der Bereitstellung die folgende Fehlermeldung angezeigt: Beschreibung: Für Parameter 0 der Methode setAuthenticationConfiguration in org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter ist eine Bean vom Typ 'org.springframework.security.config erforderlich .annotation.authentication.configuration.AuthenticationConfiguration ', die nicht gefunden werden konnte. Aktion: Definieren Sie in Ihrer Konfiguration eine Bean vom Typ 'org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration'.
Roberto

2
Bitte beachten Sie: Wenn ich etwas wie server.contextPath=/appNamein meine application.properties aufgenommen habe, würde der Befehl zum Herunterfahren jetzt lauten: Ich curl -X POST localhost:8080/appName/shutdown hoffe, es kann jemandem helfen. Ich musste wegen dieses Fehlers viel kämpfen.
Naveen Kumar

Mit Spring Boot 1.5.8 wird empfohlen, wenn ohne Sicherheit, in application.properties endpoints.shutdown.enabled=true management.security.enabled=false.
Stefano Scarpanti

Wenn Sie den Endpunkt nicht verfügbar machen
Anand Varkey Philips

27

Sie können die Springboot-Anwendung veranlassen, die PID in eine Datei zu schreiben, und Sie können die PID-Datei verwenden, um zu stoppen oder neu zu starten oder den Status mithilfe eines Bash-Skripts abzurufen. Um die PID in eine Datei zu schreiben, registrieren Sie einen Listener für SpringApplication mit ApplicationPidFileWriter wie folgt:

SpringApplication application = new SpringApplication(Application.class);
application.addListeners(new ApplicationPidFileWriter("./bin/app.pid"));
application.run();

Schreiben Sie dann ein Bash-Skript, um die Spring-Boot-Anwendung auszuführen. Referenz .

Jetzt können Sie das Skript zum Starten, Stoppen oder Neustarten verwenden.


Ein generisches und Jenkins-kompatibles vollständiges Start- und Stopp-Shell-Skript finden Sie hier ( stackoverflow.com/questions/26547532/… )
Anand Varkey Philips


14

Allen Antworten scheint die Tatsache zu fehlen, dass Sie möglicherweise einen Teil der Arbeit während des ordnungsgemäßen Herunterfahrens (z. B. in einer Unternehmensanwendung) koordiniert ausführen müssen.

@PreDestroyMit dieser Option können Sie den Shutdown-Code in den einzelnen Beans ausführen. Etwas Anspruchsvolleres würde so aussehen:

@Component
public class ApplicationShutdown implements ApplicationListener<ContextClosedEvent> {
     @Autowired ... //various components and services

     @Override
     public void onApplicationEvent(ContextClosedEvent event) {
         service1.changeHeartBeatMessage(); // allows loadbalancers & clusters to prepare for the impending shutdown
         service2.deregisterQueueListeners();
         service3.finishProcessingTasksAtHand();
         service2.reportFailedTasks();
         service4.gracefullyShutdownNativeSystemProcessesThatMayHaveBeenLaunched(); 
         service1.eventLogGracefulShutdownComplete();
     }
}

Das ist genau das, was ich brauchte. Nachdem Sie die Anwendung ausgeführt haben, drücken Sie Strg-C. Danke @Michal
Claudio Moscoso

8

Ich mache keine Endpunkte verfügbar und beginne ( mit nohup im Hintergrund und ohne durch nohup erstellte Out-Dateien ) und stoppe mit dem Shell-Skript (mit KILL PID anmutig und erzwinge das Töten, wenn die App nach 3 Minuten noch läuft ). Ich erstelle einfach eine ausführbare JAR-Datei und verwende den PID-Dateischreiber, um eine PID-Datei zu schreiben und Jar und Pid in einem Ordner mit demselben Namen wie der Anwendungsname zu speichern. Shell-Skripte haben auch denselben Namen mit Start und Stopp am Ende. Ich rufe diese Stoppskripte auf und starte die Skripte auch über die Jenkins-Pipeline. Bisher keine Probleme. Perfekt für 8 Anwendungen geeignet (Sehr allgemeine Skripte und einfach für jede App anzuwenden).

Hauptklasse

@SpringBootApplication
public class MyApplication {

    public static final void main(String[] args) {
        SpringApplicationBuilder app = new SpringApplicationBuilder(MyApplication.class);
        app.build().addListeners(new ApplicationPidFileWriter());
        app.run();
    }
}

YML-DATEI

spring.pid.fail-on-write-error: true
spring.pid.file: /server-path-with-folder-as-app-name-for-ID/appName/appName.pid

Hier ist das Startskript (start-appname.sh):

#Active Profile(YAML)
ACTIVE_PROFILE="preprod"
# JVM Parameters and Spring boot initialization parameters
JVM_PARAM="-Xms512m -Xmx1024m -Dspring.profiles.active=${ACTIVE_PROFILE} -Dcom.webmethods.jms.clientIDSharing=true"
# Base Folder Path like "/folder/packages"
CURRENT_DIR=$(readlink -f "$0")
BASE_PACKAGE="${CURRENT_DIR%/bin/*}"
# Shell Script file name after removing path like "start-yaml-validator.sh"
SHELL_SCRIPT_FILE_NAME=$(basename -- "$0")
# Shell Script file name after removing extension like "start-yaml-validator"
SHELL_SCRIPT_FILE_NAME_WITHOUT_EXT="${SHELL_SCRIPT_FILE_NAME%.sh}"
# App name after removing start/stop strings like "yaml-validator"
APP_NAME=${SHELL_SCRIPT_FILE_NAME_WITHOUT_EXT#start-}

PIDS=`ps aux |grep [j]ava.*-Dspring.profiles.active=$ACTIVE_PROFILE.*$APP_NAME.*jar | awk {'print $2'}`
if [ -z "$PIDS" ]; then
  echo "No instances of $APP_NAME with profile:$ACTIVE_PROFILE is running..." 1>&2
else
  for PROCESS_ID in $PIDS; do
        echo "Please stop the process($PROCESS_ID) using the shell script: stop-$APP_NAME.sh"
  done
  exit 1
fi

# Preparing the java home path for execution
JAVA_EXEC='/usr/bin/java'
# Java Executable - Jar Path Obtained from latest file in directory
JAVA_APP=$(ls -t $BASE_PACKAGE/apps/$APP_NAME/$APP_NAME*.jar | head -n1)
# To execute the application.
FINAL_EXEC="$JAVA_EXEC $JVM_PARAM -jar $JAVA_APP"
# Making executable command using tilde symbol and running completely detached from terminal
`nohup $FINAL_EXEC  </dev/null >/dev/null 2>&1 &`
echo "$APP_NAME start script is  completed."

Hier ist das Stoppskript (stop-appname.sh):

#Active Profile(YAML)
ACTIVE_PROFILE="preprod"
#Base Folder Path like "/folder/packages"
CURRENT_DIR=$(readlink -f "$0")
BASE_PACKAGE="${CURRENT_DIR%/bin/*}"
# Shell Script file name after removing path like "start-yaml-validator.sh"
SHELL_SCRIPT_FILE_NAME=$(basename -- "$0")
# Shell Script file name after removing extension like "start-yaml-validator"
SHELL_SCRIPT_FILE_NAME_WITHOUT_EXT="${SHELL_SCRIPT_FILE_NAME%.*}"
# App name after removing start/stop strings like "yaml-validator"
APP_NAME=${SHELL_SCRIPT_FILE_NAME_WITHOUT_EXT:5}

# Script to stop the application
PID_PATH="$BASE_PACKAGE/config/$APP_NAME/$APP_NAME.pid"

if [ ! -f "$PID_PATH" ]; then
   echo "Process Id FilePath($PID_PATH) Not found"
else
    PROCESS_ID=`cat $PID_PATH`
    if [ ! -e /proc/$PROCESS_ID -a /proc/$PROCESS_ID/exe ]; then
        echo "$APP_NAME was not running with PROCESS_ID:$PROCESS_ID.";
    else
        kill $PROCESS_ID;
        echo "Gracefully stopping $APP_NAME with PROCESS_ID:$PROCESS_ID..."
        sleep 5s
    fi
fi
PIDS=`/bin/ps aux |/bin/grep [j]ava.*-Dspring.profiles.active=$ACTIVE_PROFILE.*$APP_NAME.*jar | /bin/awk {'print $2'}`
if [ -z "$PIDS" ]; then
  echo "All instances of $APP_NAME with profile:$ACTIVE_PROFILE has has been successfully stopped now..." 1>&2
else
  for PROCESS_ID in $PIDS; do
    counter=1
    until [ $counter -gt 150 ]
        do
            if ps -p $PROCESS_ID > /dev/null; then
                echo "Waiting for the process($PROCESS_ID) to finish on it's own for $(( 300 - $(( $counter*5)) ))seconds..."
                sleep 2s
                ((counter++))
            else
                echo "$APP_NAME with PROCESS_ID:$PROCESS_ID is stopped now.."
                exit 0;
            fi
    done
    echo "Forcefully Killing $APP_NAME with PROCESS_ID:$PROCESS_ID."
    kill -9 $PROCESS_ID
  done
fi

7

Spring Boot stellte mehrere Anwendungslistener bereit, während versucht wurde, einen Anwendungskontext zu erstellen. Einer davon ist ApplicationFailedEvent. Wir können verwenden, um zu wissen, ob der Anwendungskontext initialisiert wurde oder nicht.

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.boot.context.event.ApplicationFailedEvent; 
    import org.springframework.context.ApplicationListener;

    public class ApplicationErrorListener implements 
                    ApplicationListener<ApplicationFailedEvent> {

        private static final Logger LOGGER = 
        LoggerFactory.getLogger(ApplicationErrorListener.class);

        @Override
        public void onApplicationEvent(ApplicationFailedEvent event) {
           if (event.getException() != null) {
                LOGGER.info("!!!!!!Looks like something not working as 
                                expected so stoping application.!!!!!!");
                         event.getApplicationContext().close();
                  System.exit(-1);
           } 
        }
    }

Zur obigen Listener-Klasse zu SpringApplication hinzufügen.

    new SpringApplicationBuilder(Application.class)
            .listeners(new ApplicationErrorListener())
            .run(args);  

Die beste Antwort, die ich gefunden habe! Danke dir!
Vagif

[@ user3137438], Wie unterscheidet es sich von der Protokollierung in Annotationen vor der Zerstörung?
Anand Varkey Philips

5

SpringApplication registriert implizit einen Shutdown-Hook bei der JVM, um sicherzustellen, dass ApplicationContext beim Beenden ordnungsgemäß geschlossen wird. Dadurch werden auch alle mit annotierten Bean-Methoden aufgerufen @PreDestroy. Das heißt, wir müssen die registerShutdownHook()Methode a ConfigurableApplicationContextin einer Boot-Anwendung nicht explizit verwenden , wie wir es in einer Spring Core-Anwendung tun müssen.

@SpringBootConfiguration
public class ExampleMain {
    @Bean
    MyBean myBean() {
        return new MyBean();
    }

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(ExampleMain.class, args);
        MyBean myBean = context.getBean(MyBean.class);
        myBean.doSomething();

        //no need to call context.registerShutdownHook();
    }

    private static class MyBean {

        @PostConstruct
        public void init() {
            System.out.println("init");
        }

        public void doSomething() {
            System.out.println("in doSomething()");
        }

        @PreDestroy
        public void destroy() {
            System.out.println("destroy");
        }
    }
}

Als Alternative zu @PostConstructund habe @PreDestroyich die Attribute initMethodund destroyMethodin der @BeanAnmerkung verwendet. Also für dieses Beispiel : @Bean(initMethod="init", destroyMethod="destroy").
Acker9

Ein Haken, den nur @PreDestroywenige Entwickler möglicherweise nicht kennen, ist, dass solche Methoden nur für Beans mit Singleton-Gültigkeitsbereich aufgerufen werden. Die Entwickler müssen den Bereinigungsteil des Bean-Lebenszyklus für andere Bereiche verwalten
wie

5

Ab Spring Boot 2.3 und höher ist ein ordnungsgemäßer Mechanismus zum ordnungsgemäßen Herunterfahren integriert .

Pre-Spring Boot 2.3 gibt es keinen sofort einsatzbereiten Mechanismus zum ordnungsgemäßen Herunterfahren. Einige Spring-Boot-Starter bieten diese Funktionalität:

  1. https://github.com/jihor/hiatus-spring-boot
  2. https://github.com/gesellix/graceful-shutdown-spring-boot
  3. https://github.com/corentin59/spring-boot-graceful-shutdown

Ich bin der Autor von nr. 1. Der Starter heißt "Hiatus for Spring Boot". Es funktioniert auf der Ebene des Lastenausgleichs, dh es markiert den Dienst einfach als OUT_OF_SERVICE und beeinträchtigt den Anwendungskontext in keiner Weise. Dies ermöglicht ein ordnungsgemäßes Herunterfahren und bedeutet, dass der Dienst bei Bedarf für einige Zeit außer Betrieb genommen und dann wieder zum Leben erweckt werden kann. Der Nachteil ist, dass die JVM nicht gestoppt wird. Sie müssen dies mit dem killBefehl tun . Da ich alles in Containern laufen lasse, war das für mich keine große Sache, da ich den Container sowieso anhalten und entfernen muss.

Nr. 2 und 3 basieren mehr oder weniger auf diesem Beitrag von Andy Wilkinson. Sie arbeiten in eine Richtung - sobald sie ausgelöst werden, schließen sie schließlich den Kontext.


2

Es gibt viele Möglichkeiten, eine Federanwendung abzuschalten. Eine ist, close () aufzurufen ApplicationContext:

ApplicationContext ctx =
    SpringApplication.run(HelloWorldApplication.class, args);
// ...
ctx.close()

Ihre Frage schlägt vor, dass Sie Ihre Anwendung schließen möchten, indem Sie dies tun Ctrl+C, was häufig zum Beenden eines Befehls verwendet wird. In diesem Fall...

Verwendung endpoints.shutdown.enabled=trueist nicht das beste Rezept. Dies bedeutet, dass Sie einen Endpunkt zum Beenden Ihrer Anwendung verfügbar machen. Abhängig von Ihrem Anwendungsfall und Ihrer Umgebung müssen Sie ihn also sichern ...

Ctrl+Csollte in Ihrem Fall sehr gut funktionieren. Ich gehe davon aus, dass Ihr Problem durch das kaufmännische Und (&) verursacht wird. Weitere Erläuterungen:

Ein Spring-Anwendungskontext hat möglicherweise einen Shutdown-Hook bei der JVM-Laufzeit registriert. Siehe ApplicationContext-Dokumentation .

Ich weiß nicht, ob Spring Boot diesen Hook automatisch konfiguriert, wie Sie sagten. Ich nehme an, es ist.

Ein Ctrl+C, Ihre Shell sendet ein INTSignal an die Vordergrundanwendung. Es bedeutet "Bitte unterbrechen Sie Ihre Ausführung". Die Anwendung kann dieses Signal abfangen und vor seiner Beendigung bereinigen (der von Spring registrierte Hook) oder es einfach ignorieren (schlecht).

nohupist ein Befehl, der das folgende Programm mit einem Trap ausführt, um das HUP-Signal zu ignorieren. HUP wird verwendet, um das Programm zu beenden, wenn Sie auflegen (schließen Sie beispielsweise Ihre SSH-Verbindung). Darüber hinaus werden die Ausgaben umgeleitet, um zu vermeiden, dass Ihr Programm auf einem verschwundenen TTY blockiert. nohupignoriert das INT-Signal NICHT. Es verhindert also NICHT, dass Ctrl+Ces funktioniert.

Ich gehe davon aus, dass Ihr Problem durch das kaufmännische Und (&) und nicht durch nohup verursacht wird. Ctrl+Csendet ein Signal an die Vordergrundprozesse. Das kaufmännische Und bewirkt, dass Ihre Anwendung im Hintergrund ausgeführt wird. Eine Lösung: tun

kill -INT pid

Verwenden Sie kill -9oder kill -KILList schlecht, weil die Anwendung (hier die JVM) es nicht abfangen kann, um ordnungsgemäß zu beenden.

Eine andere Lösung besteht darin, Ihre Anwendung wieder in den Vordergrund zu stellen. Dann Ctrl+Cwird es funktionieren. Schauen Sie sich die Bash Job-Steuerung genauer an fg.


2

Verwenden Sie die statische exit()Methode in der SpringApplication-Klasse, um Ihre Spring-Boot-Anwendung ordnungsgemäß zu schließen.

public class SomeClass {
    @Autowire
    private ApplicationContext context

    public void close() {
        SpringApplication.exit(context);
    }
}

Das funktioniert bei mir. Ich danke dir sehr.
Khachornchit Songsaen vor

1

Spring Boot unterstützt jetzt das ordnungsgemäße Herunterfahren (derzeit in den Vorabversionen 2.3.0.BUILD-SNAPSHOT).

Wenn diese Option aktiviert ist, umfasst das Herunterfahren der Anwendung eine Kulanzfrist von konfigurierbarer Dauer. Während dieser Nachfrist können vorhandene Anforderungen abgeschlossen werden, neue Anforderungen sind jedoch nicht zulässig

Sie können es aktivieren mit:

server.shutdown.grace-period=30s

https://docs.spring.io/spring-boot/docs/2.3.0.BUILD-SNAPSHOT/reference/html/spring-boot-features.html#boot-features-graceful-shutdown



0

Wenn Sie sich in einer Linux-Umgebung befinden, müssen Sie lediglich einen Symlink zu Ihrer JAR-Datei aus /etc/init.d/ erstellen.

sudo ln -s /path/to/your/myboot-app.jar /etc/init.d/myboot-app

Dann können Sie die Anwendung wie jeden anderen Dienst starten

sudo /etc/init.d/myboot-app start

So schließen Sie die Anwendung

sudo /etc/init.d/myboot-app stop

Auf diese Weise wird die Anwendung nicht beendet, wenn Sie das Terminal verlassen. Und die Anwendung wird ordnungsgemäß mit dem Befehl stop heruntergefahren.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.