2022-07-29 06:29:21 +02:00
// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg.
// All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
2021-03-31 07:23:48 +02:00
package main
import (
2022-01-12 11:13:25 +01:00
"context"
"crypto/tls"
"errors"
2021-10-11 11:11:14 +02:00
"flag"
2021-12-08 15:50:03 +01:00
"fmt"
2022-01-31 15:14:37 +01:00
"io"
2022-01-12 11:13:25 +01:00
"net"
2021-03-31 07:23:48 +02:00
"net/http"
2022-02-09 15:03:12 +01:00
"net/url"
2021-03-31 07:23:48 +02:00
"os"
2022-01-12 11:13:25 +01:00
"os/signal"
2022-05-09 11:53:41 +02:00
"runtime"
2022-03-24 10:35:52 +01:00
"runtime/debug"
2022-01-12 11:13:25 +01:00
"strings"
"sync"
"syscall"
"time"
2021-03-31 07:23:48 +02:00
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
2022-06-21 17:52:36 +02:00
"github.com/ClusterCockpit/cc-backend/internal/api"
"github.com/ClusterCockpit/cc-backend/internal/auth"
"github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/internal/graph"
"github.com/ClusterCockpit/cc-backend/internal/graph/generated"
"github.com/ClusterCockpit/cc-backend/internal/metricdata"
"github.com/ClusterCockpit/cc-backend/internal/repository"
"github.com/ClusterCockpit/cc-backend/internal/routerConfig"
"github.com/ClusterCockpit/cc-backend/internal/runtimeEnv"
2022-09-05 17:46:38 +02:00
"github.com/ClusterCockpit/cc-backend/pkg/archive"
2022-06-21 17:52:36 +02:00
"github.com/ClusterCockpit/cc-backend/pkg/log"
2022-07-06 14:55:39 +02:00
"github.com/ClusterCockpit/cc-backend/web"
2022-03-14 08:45:17 +01:00
"github.com/google/gops/agent"
2021-03-31 07:23:48 +02:00
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
2022-01-20 10:00:55 +01:00
_ "github.com/go-sql-driver/mysql"
2021-03-31 07:23:48 +02:00
_ "github.com/mattn/go-sqlite3"
2022-09-13 15:20:07 +02:00
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
2021-03-31 07:23:48 +02:00
)
2021-12-08 10:15:25 +01:00
func main ( ) {
2022-09-11 07:08:28 +02:00
var flagReinitDB , flagServer , flagSyncLDAP , flagGops , flagDev bool
2022-09-13 15:20:07 +02:00
var flagNewUser , flagDelUser , flagGenJWT , flagConfigFile , flagImportJob string
2022-03-15 08:29:29 +01:00
flag . BoolVar ( & flagReinitDB , "init-db" , false , "Go through job-archive and re-initialize the 'job', 'tag', and 'jobtag' tables (all running jobs will be lost!)" )
flag . BoolVar ( & flagSyncLDAP , "sync-ldap" , false , "Sync the 'user' table with ldap" )
2022-09-11 07:08:28 +02:00
flag . BoolVar ( & flagServer , "server" , false , "Do not start a server, stop right after initialization and argument handling" )
2022-03-15 08:29:29 +01:00
flag . BoolVar ( & flagGops , "gops" , false , "Listen via github.com/google/gops/agent (for debugging)" )
2022-09-06 08:57:01 +02:00
flag . BoolVar ( & flagDev , "dev" , false , "Enable development components: GraphQL Playground and Swagger UI" )
2022-07-05 10:22:46 +02:00
flag . StringVar ( & flagConfigFile , "config" , "./config.json" , "Overwrite the global config options by those specified in `config.json`" )
2022-09-13 07:38:18 +02:00
flag . StringVar ( & flagNewUser , "add-user" , "" , "Add a new user. Argument format: `<username>:[admin,support,api,user]:<password>`" )
2022-03-15 08:29:29 +01:00
flag . StringVar ( & flagDelUser , "del-user" , "" , "Remove user by `username`" )
flag . StringVar ( & flagGenJWT , "jwt" , "" , "Generate and print a JWT for the user specified by its `username`" )
2022-09-13 15:20:07 +02:00
flag . StringVar ( & flagImportJob , "import-job" , "" , "Import a job. Argument format: `<path-to-meta.json>:<path-to-data.json>,...`" )
2021-12-08 10:15:25 +01:00
flag . Parse ( )
2021-03-31 07:23:48 +02:00
2022-03-15 08:29:29 +01:00
// See https://github.com/google/gops (Runtime overhead is almost zero)
2022-03-14 08:45:17 +01:00
if flagGops {
if err := agent . Listen ( agent . Options { } ) ; err != nil {
log . Fatalf ( "gops/agent.Listen failed: %s" , err . Error ( ) )
}
}
2022-06-21 17:52:36 +02:00
if err := runtimeEnv . LoadEnv ( "./.env" ) ; err != nil && ! os . IsNotExist ( err ) {
2022-01-12 11:13:25 +01:00
log . Fatalf ( "parsing './.env' file failed: %s" , err . Error ( ) )
}
2022-09-05 17:46:38 +02:00
// Initialize sub-modules and handle command line flags.
// The order here is important!
config . Init ( flagConfigFile )
2021-10-11 11:11:14 +02:00
2022-03-15 08:29:29 +01:00
// As a special case for `db`, allow using an environment variable instead of the value
// stored in the config. This can be done for people having security concerns about storing
2022-09-05 17:46:38 +02:00
// the password for their mysql database in config.json.
if strings . HasPrefix ( config . Keys . DB , "env:" ) {
envvar := strings . TrimPrefix ( config . Keys . DB , "env:" )
config . Keys . DB = os . Getenv ( envvar )
2022-01-31 15:14:37 +01:00
}
2022-09-05 17:46:38 +02:00
repository . Connect ( config . Keys . DBDriver , config . Keys . DB )
2022-06-21 17:52:36 +02:00
db := repository . GetConnection ( )
2021-12-16 09:35:03 +01:00
2022-03-02 10:50:08 +01:00
var authentication * auth . Authentication
2022-09-05 17:46:38 +02:00
if ! config . Keys . DisableAuthentication {
var err error
2022-07-07 14:08:37 +02:00
if authentication , err = auth . Init ( db . DB , map [ string ] interface { } {
2022-09-05 17:46:38 +02:00
"ldap" : config . Keys . LdapConfig ,
"jwt" : config . Keys . JwtConfig ,
2022-07-07 14:08:37 +02:00
} ) ; err != nil {
log . Fatal ( err )
2022-02-16 11:50:25 +01:00
}
2022-09-05 17:46:38 +02:00
if d , err := time . ParseDuration ( config . Keys . SessionMaxAge ) ; err != nil {
2022-07-07 14:08:37 +02:00
authentication . SessionMaxAge = d
2021-12-08 10:15:25 +01:00
}
2021-03-31 07:23:48 +02:00
2021-12-08 10:15:25 +01:00
if flagNewUser != "" {
2022-07-07 14:08:37 +02:00
parts := strings . SplitN ( flagNewUser , ":" , 3 )
if len ( parts ) != 3 || len ( parts [ 0 ] ) == 0 {
log . Fatal ( "invalid argument format for user creation" )
}
if err := authentication . AddUser ( & auth . User {
Username : parts [ 0 ] , Password : parts [ 2 ] , Roles : strings . Split ( parts [ 1 ] , "," ) ,
} ) ; err != nil {
2021-12-08 10:15:25 +01:00
log . Fatal ( err )
}
}
if flagDelUser != "" {
2022-02-14 14:22:44 +01:00
if err := authentication . DelUser ( flagDelUser ) ; err != nil {
2021-12-08 10:15:25 +01:00
log . Fatal ( err )
}
}
2021-11-26 10:34:29 +01:00
2021-12-08 10:15:25 +01:00
if flagSyncLDAP {
2022-07-07 14:08:37 +02:00
if authentication . LdapAuth == nil {
log . Fatal ( "cannot sync: LDAP authentication is not configured" )
}
if err := authentication . LdapAuth . Sync ( ) ; err != nil {
2022-02-14 14:22:44 +01:00
log . Fatal ( err )
}
2022-07-25 09:33:36 +02:00
log . Info ( "LDAP sync successfull" )
2021-12-08 10:15:25 +01:00
}
2022-01-10 16:14:54 +01:00
if flagGenJWT != "" {
2022-07-07 14:08:37 +02:00
user , err := authentication . GetUser ( flagGenJWT )
2022-01-10 16:14:54 +01:00
if err != nil {
log . Fatal ( err )
}
2022-01-27 09:29:11 +01:00
if ! user . HasRole ( auth . RoleApi ) {
2022-01-27 10:35:26 +01:00
log . Warn ( "that user does not have the API role" )
2022-01-10 16:14:54 +01:00
}
2022-07-07 14:08:37 +02:00
jwt , err := authentication . JwtAuth . ProvideJWT ( user )
2022-01-10 16:14:54 +01:00
if err != nil {
log . Fatal ( err )
}
fmt . Printf ( "JWT for '%s': %s\n" , user . Username , jwt )
}
2021-12-08 10:15:25 +01:00
} else if flagNewUser != "" || flagDelUser != "" {
2022-01-27 10:35:26 +01:00
log . Fatal ( "arguments --add-user and --del-user can only be used if authentication is enabled" )
2021-09-21 16:06:41 +02:00
}
2022-09-07 12:24:45 +02:00
if err := archive . Init ( config . Keys . Archive ) ; err != nil {
2021-12-08 10:15:25 +01:00
log . Fatal ( err )
}
2021-10-26 10:24:43 +02:00
2022-09-05 17:46:38 +02:00
if err := metricdata . Init ( config . Keys . DisableArchive ) ; err != nil {
2021-12-08 10:15:25 +01:00
log . Fatal ( err )
2021-10-26 10:24:43 +02:00
}
2021-12-08 10:15:25 +01:00
if flagReinitDB {
2022-09-05 17:46:38 +02:00
if err := repository . InitDB ( ) ; err != nil {
2021-12-08 10:15:25 +01:00
log . Fatal ( err )
2021-10-26 10:24:43 +02:00
}
2021-12-08 10:15:25 +01:00
}
2021-10-26 10:24:43 +02:00
2022-09-13 15:20:07 +02:00
if flagImportJob != "" {
if err := repository . HandleImportFlag ( flagImportJob ) ; err != nil {
log . Fatalf ( "import failed: %s" , err . Error ( ) )
}
}
2022-09-11 07:08:28 +02:00
if ! flagServer {
2021-12-08 10:15:25 +01:00
return
}
2021-10-26 10:24:43 +02:00
2022-03-15 08:29:29 +01:00
// Setup the http.Handler/Router used by the server
2022-09-05 17:46:38 +02:00
jobRepo := repository . GetJobRepository ( )
2022-06-21 17:52:36 +02:00
resolver := & graph . Resolver { DB : db . DB , Repo : jobRepo }
2021-12-08 15:50:03 +01:00
graphQLEndpoint := handler . NewDefaultServer ( generated . NewExecutableSchema ( generated . Config { Resolvers : resolver } ) )
2022-01-20 10:00:55 +01:00
if os . Getenv ( "DEBUG" ) != "1" {
2022-03-15 08:29:29 +01:00
// Having this handler means that a error message is returned via GraphQL instead of the connection simply beeing closed.
// The problem with this is that then, no more stacktrace is printed to stderr.
2022-01-20 10:00:55 +01:00
graphQLEndpoint . SetRecoverFunc ( func ( ctx context . Context , err interface { } ) error {
switch e := err . ( type ) {
case string :
return fmt . Errorf ( "panic: %s" , e )
case error :
return fmt . Errorf ( "panic caused by: %w" , e )
}
2022-01-10 16:14:54 +01:00
2022-01-20 10:00:55 +01:00
return errors . New ( "internal server error (panic)" )
} )
}
2022-01-10 16:14:54 +01:00
2021-12-17 15:49:22 +01:00
api := & api . RestApi {
2022-02-10 18:48:58 +01:00
JobRepository : jobRepo ,
2022-01-07 09:39:00 +01:00
Resolver : resolver ,
2022-09-05 17:46:38 +02:00
MachineStateDir : config . Keys . MachineStateDir ,
2022-03-02 10:50:08 +01:00
Authentication : authentication ,
2021-12-16 09:35:03 +01:00
}
2021-12-08 10:15:25 +01:00
r := mux . NewRouter ( )
2022-03-15 08:29:29 +01:00
r . HandleFunc ( "/login" , func ( rw http . ResponseWriter , r * http . Request ) {
2022-07-05 10:40:12 +02:00
rw . Header ( ) . Add ( "Content-Type" , "text/html; charset=utf-8" )
2022-07-06 15:00:08 +02:00
web . RenderTemplate ( rw , r , "login.tmpl" , & web . Page { Title : "Login" } )
2022-03-15 08:29:29 +01:00
} ) . Methods ( http . MethodGet )
2022-02-03 09:39:04 +01:00
r . HandleFunc ( "/imprint" , func ( rw http . ResponseWriter , r * http . Request ) {
2022-07-05 10:40:12 +02:00
rw . Header ( ) . Add ( "Content-Type" , "text/html; charset=utf-8" )
2022-07-06 15:00:08 +02:00
web . RenderTemplate ( rw , r , "imprint.tmpl" , & web . Page { Title : "Imprint" } )
2022-02-03 09:39:04 +01:00
} )
r . HandleFunc ( "/privacy" , func ( rw http . ResponseWriter , r * http . Request ) {
2022-07-05 10:40:12 +02:00
rw . Header ( ) . Add ( "Content-Type" , "text/html; charset=utf-8" )
2022-07-06 15:00:08 +02:00
web . RenderTemplate ( rw , r , "privacy.tmpl" , & web . Page { Title : "Privacy" } )
2022-02-03 09:39:04 +01:00
} )
2021-12-08 10:15:25 +01:00
2022-03-15 08:29:29 +01:00
// Some routes, such as /login or /query, should only be accessible to a user that is logged in.
// Those should be mounted to this subrouter. If authentication is enabled, a middleware will prevent
// any unauthenticated accesses.
2021-12-08 10:15:25 +01:00
secured := r . PathPrefix ( "/" ) . Subrouter ( )
2022-09-05 17:46:38 +02:00
if ! config . Keys . DisableAuthentication {
2022-02-14 14:22:44 +01:00
r . Handle ( "/login" , authentication . Login (
// On success:
http . RedirectHandler ( "/" , http . StatusTemporaryRedirect ) ,
// On failure:
2022-02-16 09:01:47 +01:00
func ( rw http . ResponseWriter , r * http . Request , err error ) {
2022-07-05 10:40:12 +02:00
rw . Header ( ) . Add ( "Content-Type" , "text/html; charset=utf-8" )
2022-02-14 14:22:44 +01:00
rw . WriteHeader ( http . StatusUnauthorized )
2022-07-06 15:00:08 +02:00
web . RenderTemplate ( rw , r , "login.tmpl" , & web . Page {
2022-02-14 14:22:44 +01:00
Title : "Login failed - ClusterCockpit" ,
2022-02-16 09:01:47 +01:00
Error : err . Error ( ) ,
2022-02-14 14:22:44 +01:00
} )
} ) ) . Methods ( http . MethodPost )
2022-02-16 09:01:47 +01:00
r . Handle ( "/logout" , authentication . Logout ( http . HandlerFunc ( func ( rw http . ResponseWriter , r * http . Request ) {
2022-07-05 10:40:12 +02:00
rw . Header ( ) . Add ( "Content-Type" , "text/html; charset=utf-8" )
2022-02-16 09:01:47 +01:00
rw . WriteHeader ( http . StatusOK )
2022-07-06 15:00:08 +02:00
web . RenderTemplate ( rw , r , "login.tmpl" , & web . Page {
2022-02-16 09:01:47 +01:00
Title : "Bye - ClusterCockpit" ,
Info : "Logout sucessful" ,
} )
} ) ) ) . Methods ( http . MethodPost )
2022-02-14 14:22:44 +01:00
secured . Use ( func ( next http . Handler ) http . Handler {
return authentication . Auth (
// On success;
next ,
// On failure:
2022-02-16 09:01:47 +01:00
func ( rw http . ResponseWriter , r * http . Request , err error ) {
2022-02-14 14:22:44 +01:00
rw . WriteHeader ( http . StatusUnauthorized )
2022-07-06 15:00:08 +02:00
web . RenderTemplate ( rw , r , "login.tmpl" , & web . Page {
2022-02-14 14:22:44 +01:00
Title : "Authentication failed - ClusterCockpit" ,
Error : err . Error ( ) ,
} )
} )
} )
2021-12-08 10:15:25 +01:00
}
2022-03-15 08:29:29 +01:00
2022-09-06 08:57:01 +02:00
if flagDev {
r . Handle ( "/playground" , playground . Handler ( "GraphQL playground" , "/query" ) )
}
2021-12-08 10:15:25 +01:00
secured . Handle ( "/query" , graphQLEndpoint )
2021-12-16 09:35:03 +01:00
2022-03-15 08:29:29 +01:00
// Send a searchId and then reply with a redirect to a user or job.
2022-02-09 15:03:12 +01:00
secured . HandleFunc ( "/search" , func ( rw http . ResponseWriter , r * http . Request ) {
if search := r . URL . Query ( ) . Get ( "searchId" ) ; search != "" {
job , username , err := api . JobRepository . FindJobOrUser ( r . Context ( ) , search )
if err == repository . ErrNotFound {
http . Redirect ( rw , r , "/monitoring/jobs/?jobId=" + url . QueryEscape ( search ) , http . StatusTemporaryRedirect )
return
} else if err != nil {
http . Error ( rw , err . Error ( ) , http . StatusInternalServerError )
return
}
if username != "" {
http . Redirect ( rw , r , "/monitoring/user/" + username , http . StatusTemporaryRedirect )
return
} else {
http . Redirect ( rw , r , fmt . Sprintf ( "/monitoring/job/%d" , job ) , http . StatusTemporaryRedirect )
return
}
} else {
http . Error ( rw , "'searchId' query parameter missing" , http . StatusBadRequest )
}
} )
2022-03-15 08:29:29 +01:00
// Mount all /monitoring/... and /api/... routes.
2022-06-21 17:52:36 +02:00
routerConfig . SetupRoutes ( secured )
2021-12-17 15:49:22 +01:00
api . MountRoutes ( secured )
2021-12-08 15:50:03 +01:00
2022-09-05 17:46:38 +02:00
if config . Keys . EmbedStaticFiles {
2022-07-06 14:55:39 +02:00
r . PathPrefix ( "/" ) . Handler ( web . ServeFiles ( ) )
} else {
2022-09-05 17:46:38 +02:00
r . PathPrefix ( "/" ) . Handler ( http . FileServer ( http . Dir ( config . Keys . StaticFiles ) ) )
2022-07-06 14:55:39 +02:00
}
2022-01-31 15:14:37 +01:00
r . Use ( handlers . CompressHandler )
2022-02-16 12:52:45 +01:00
r . Use ( handlers . RecoveryHandler ( handlers . PrintRecoveryStack ( true ) ) )
2022-01-31 15:14:37 +01:00
r . Use ( handlers . CORS (
2022-07-28 18:07:30 +02:00
handlers . AllowCredentials ( ) ,
handlers . AllowedHeaders ( [ ] string { "X-Requested-With" , "Content-Type" , "Authorization" , "Origin" } ) ,
2021-12-08 10:15:25 +01:00
handlers . AllowedMethods ( [ ] string { "GET" , "POST" , "HEAD" , "OPTIONS" } ) ,
2022-01-31 15:14:37 +01:00
handlers . AllowedOrigins ( [ ] string { "*" } ) ) )
2022-03-15 08:29:29 +01:00
handler := handlers . CustomLoggingHandler ( io . Discard , r , func ( _ io . Writer , params handlers . LogFormatterParams ) {
if strings . HasPrefix ( params . Request . RequestURI , "/api/" ) {
log . Infof ( "%s %s (%d, %.02fkb, %dms)" ,
params . Request . Method , params . URL . RequestURI ( ) ,
params . StatusCode , float32 ( params . Size ) / 1024 ,
time . Since ( params . TimeStamp ) . Milliseconds ( ) )
} else {
log . Debugf ( "%s %s (%d, %.02fkb, %dms)" ,
params . Request . Method , params . URL . RequestURI ( ) ,
params . StatusCode , float32 ( params . Size ) / 1024 ,
time . Since ( params . TimeStamp ) . Milliseconds ( ) )
}
2022-01-31 15:14:37 +01:00
} )
2021-12-08 10:15:25 +01:00
2022-01-12 11:13:25 +01:00
var wg sync . WaitGroup
server := http . Server {
ReadTimeout : 10 * time . Second ,
WriteTimeout : 10 * time . Second ,
Handler : handler ,
2022-09-05 17:46:38 +02:00
Addr : config . Keys . Addr ,
2022-01-12 11:13:25 +01:00
}
2021-12-08 10:15:25 +01:00
// Start http or https server
2022-09-05 17:46:38 +02:00
listener , err := net . Listen ( "tcp" , config . Keys . Addr )
2022-01-12 11:13:25 +01:00
if err != nil {
log . Fatal ( err )
}
2022-09-05 17:46:38 +02:00
if ! strings . HasSuffix ( config . Keys . Addr , ":80" ) && config . Keys . RedirectHttpTo != "" {
2022-03-14 08:45:17 +01:00
go func ( ) {
2022-09-05 17:46:38 +02:00
http . ListenAndServe ( ":80" , http . RedirectHandler ( config . Keys . RedirectHttpTo , http . StatusMovedPermanently ) )
2022-03-14 08:45:17 +01:00
} ( )
}
2022-09-05 17:46:38 +02:00
if config . Keys . HttpsCertFile != "" && config . Keys . HttpsKeyFile != "" {
cert , err := tls . LoadX509KeyPair ( config . Keys . HttpsCertFile , config . Keys . HttpsKeyFile )
2022-01-12 11:13:25 +01:00
if err != nil {
log . Fatal ( err )
}
listener = tls . NewListener ( listener , & tls . Config {
Certificates : [ ] tls . Certificate { cert } ,
2022-06-17 10:08:31 +02:00
CipherSuites : [ ] uint16 {
tls . TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 ,
tls . TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 ,
} ,
MinVersion : tls . VersionTLS12 ,
PreferServerCipherSuites : true ,
2022-01-12 11:13:25 +01:00
} )
2022-09-05 17:46:38 +02:00
log . Printf ( "HTTPS server listening at %s..." , config . Keys . Addr )
2021-12-08 10:15:25 +01:00
} else {
2022-09-05 17:46:38 +02:00
log . Printf ( "HTTP server listening at %s..." , config . Keys . Addr )
2022-01-12 11:13:25 +01:00
}
// Because this program will want to bind to a privileged port (like 80), the listener must
// be established first, then the user can be changed, and after that,
// the actuall http server can be started.
2022-09-05 17:46:38 +02:00
if err := runtimeEnv . DropPrivileges ( config . Keys . Group , config . Keys . User ) ; err != nil {
2022-01-12 11:13:25 +01:00
log . Fatalf ( "error while changing user: %s" , err . Error ( ) )
2021-12-08 10:15:25 +01:00
}
2022-01-12 11:13:25 +01:00
wg . Add ( 1 )
go func ( ) {
defer wg . Done ( )
if err := server . Serve ( listener ) ; err != nil && err != http . ErrServerClosed {
log . Fatal ( err )
}
} ( )
wg . Add ( 1 )
sigs := make ( chan os . Signal , 1 )
signal . Notify ( sigs , syscall . SIGINT , syscall . SIGTERM )
go func ( ) {
defer wg . Done ( )
<- sigs
2022-06-21 17:52:36 +02:00
runtimeEnv . SystemdNotifiy ( false , "shutting down" )
2022-01-12 11:13:25 +01:00
// First shut down the server gracefully (waiting for all ongoing requests)
server . Shutdown ( context . Background ( ) )
// Then, wait for any async archivings still pending...
api . OngoingArchivings . Wait ( )
} ( )
2022-09-05 17:46:38 +02:00
if config . Keys . StopJobsExceedingWalltime > 0 {
2022-05-09 11:53:41 +02:00
go func ( ) {
for range time . Tick ( 30 * time . Minute ) {
2022-09-05 17:46:38 +02:00
err := jobRepo . StopJobsExceedingWalltimeBy ( config . Keys . StopJobsExceedingWalltime )
2022-05-09 11:53:41 +02:00
if err != nil {
log . Errorf ( "error while looking for jobs exceeding theire walltime: %s" , err . Error ( ) )
}
runtime . GC ( )
}
} ( )
}
2022-04-07 09:50:32 +02:00
2022-03-24 10:35:52 +01:00
if os . Getenv ( "GOGC" ) == "" {
debug . SetGCPercent ( 25 )
}
2022-06-21 17:52:36 +02:00
runtimeEnv . SystemdNotifiy ( true , "running" )
2022-01-12 11:13:25 +01:00
wg . Wait ( )
log . Print ( "Gracefull shutdown completed!" )
2021-10-26 10:24:43 +02:00
}