From 3f5ff3375e51abb5948714dd92db149925926759 Mon Sep 17 00:00:00 2001 From: Georgy Moshkin <gmoshkin@picodata.io> Date: Wed, 1 Feb 2023 16:08:48 +0300 Subject: [PATCH] refactor(main): move main_* functions back to src/main.rs --- src/lib.rs | 363 +--------------------------------------------- src/main.rs | 367 ++++++++++++++++++++++++++++++++++++++++++++++- src/tarantool.rs | 14 -- 3 files changed, 372 insertions(+), 372 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1edd4ccd8d..75a4e52bd6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,3 @@ -use nix::sys::signal; -use nix::sys::termios::{tcgetattr, tcsetattr, SetArg::TCSADRAIN}; -use nix::sys::wait::{waitpid, WaitStatus}; -use nix::unistd::{self, fork, ForkResult}; use serde::{Deserialize, Serialize}; use ::raft::prelude as raft; @@ -551,33 +547,16 @@ fn init_handlers() -> traft::Result<()> { Ok(()) } -fn rm_tarantool_files(data_dir: &str) { - std::fs::read_dir(data_dir) - .expect("[supervisor] failed reading data_dir") - .map(|entry| entry.expect("[supervisor] failed reading directory entry")) - .map(|entry| entry.path()) - .filter(|path| path.is_file()) - .filter(|f| { - f.extension() - .map(|ext| ext == "xlog" || ext == "snap") - .unwrap_or(false) - }) - .for_each(|f| { - println!("[supervisor] removing file: {}", f.to_string_lossy()); - std::fs::remove_file(f).unwrap(); - }); -} - #[allow(clippy::enum_variant_names)] #[derive(Debug, Serialize, Deserialize)] -enum Entrypoint { +pub enum Entrypoint { StartDiscover, StartBoot, StartJoin { leader_address: String }, } impl Entrypoint { - fn exec(self, args: args::Run, to_supervisor: ipc::Sender<IpcMessage>) { + pub fn exec(self, args: args::Run, to_supervisor: ipc::Sender<IpcMessage>) { match self { Self::StartDiscover => start_discover(&args, to_supervisor), Self::StartBoot => start_boot(&args), @@ -587,173 +566,9 @@ impl Entrypoint { } #[derive(Debug, Serialize, Deserialize)] -struct IpcMessage { - next_entrypoint: Entrypoint, - drop_db: bool, -} - -macro_rules! tarantool_main { - ( - $tt_args:expr, - callback_data: $cb_data:tt, - callback_data_type: $cb_data_ty:ty, - callback_body: $cb_body:expr - ) => {{ - let tt_args = $tt_args; - // `argv` is a vec of pointers to data owned by `tt_args`, so - // make sure `tt_args` outlives `argv`, because the compiler is not - // gonna do that for you - let argv = tt_args.iter().map(|a| a.as_ptr()).collect::<Vec<_>>(); - extern "C" fn trampoline(data: *mut libc::c_void) { - let args = unsafe { Box::from_raw(data as _) }; - let cb = |$cb_data: $cb_data_ty| $cb_body; - cb(*args) - } - let cb_data: $cb_data_ty = $cb_data; - unsafe { - tarantool::main( - argv.len() as _, - argv.as_ptr() as _, - Some(trampoline), - Box::into_raw(Box::new(cb_data)) as _, - ) - } - }}; -} - -pub fn main_run(args: args::Run) -> ! { - // Tarantool implicitly parses some environment variables. - // We don't want them to affect the behavior and thus filter them out. - for (k, _) in std::env::vars() { - if k.starts_with("TT_") || k.starts_with("TARANTOOL_") { - std::env::remove_var(k) - } - } - - // Tarantool running in a fork (or, to be more percise, the - // libreadline) modifies termios settings to intercept echoed text. - // - // After subprocess termination it's not always possible to - // restore the settings (e.g. in case of SIGSEGV). At least it - // tries to. To preserve tarantool console operable, we cache - // initial termios attributes and restore them manually. - // - let tcattr = tcgetattr(0).ok(); - - // Intercept and forward signals to the child. As for the child - // itself, one shouldn't worry about setting up signal handlers - - // Tarantool does that implicitly. - static mut CHILD_PID: Option<libc::c_int> = None; - static mut SIGNALLED: Option<libc::c_int> = None; - extern "C" fn sigh(sig: libc::c_int) { - unsafe { - // Only a few functions are allowed in signal handlers. - // Read twice `man 7 signal-safety`. - if let Some(pid) = CHILD_PID { - libc::kill(pid, sig); - } - SIGNALLED = Some(sig); - } - } - let sigaction = signal::SigAction::new( - signal::SigHandler::Handler(sigh), - // It's important to use SA_RESTART flag here. - // Otherwise, waitpid() could return EINTR, - // but we don't want dealing with it. - signal::SaFlags::SA_RESTART, - signal::SigSet::empty(), - ); - unsafe { - signal::sigaction(signal::SIGHUP, &sigaction).unwrap(); - signal::sigaction(signal::SIGINT, &sigaction).unwrap(); - signal::sigaction(signal::SIGTERM, &sigaction).unwrap(); - signal::sigaction(signal::SIGUSR1, &sigaction).unwrap(); - } - - let parent = unistd::getpid(); - let mut entrypoint = Entrypoint::StartDiscover {}; - loop { - println!("[supervisor:{parent}] running {entrypoint:?}"); - - let (from_child, to_parent) = - ipc::channel::<IpcMessage>().expect("ipc channel creation failed"); - let (from_parent, to_child) = ipc::pipe().expect("ipc pipe creation failed"); - - let pid = unsafe { fork() }; - match pid.expect("fork failed") { - ForkResult::Child => { - drop(from_child); - drop(to_child); - - let rc = tarantool_main!( - args.tt_args().unwrap(), - callback_data: (entrypoint, args, to_parent, from_parent), - callback_data_type: (Entrypoint, args::Run, ipc::Sender<IpcMessage>, ipc::Fd), - callback_body: { - // We don't want a child to live without a supervisor. - // - // Usually, supervisor waits for child forever and retransmits - // termination signals. But if the parent is killed with a SIGKILL - // there's no way to pass anything. - // - // This fiber serves as a fuse - it tries to read from a pipe - // (that supervisor never writes to), and if the writing end is - // closed, it means the supervisor has terminated. - let fuse = fiber::Builder::new() - .name("supervisor_fuse") - .func(move || { - use ::tarantool::ffi::tarantool::CoIOFlags; - use ::tarantool::coio::coio_wait; - coio_wait(*from_parent, CoIOFlags::READ, f64::INFINITY).ok(); - tlog!(Warning, "Supervisor terminated, exiting"); - std::process::exit(0); - }); - std::mem::forget(fuse.start()); - - entrypoint.exec(args, to_parent) - } - ); - std::process::exit(rc); - } - ForkResult::Parent { child } => { - unsafe { CHILD_PID = Some(child.into()) }; - drop(from_parent); - drop(to_parent); - - let msg = from_child.recv().ok(); - - let status = waitpid(child, None); - - // Restore termios configuration as planned - if let Some(tcattr) = tcattr.as_ref() { - tcsetattr(0, TCSADRAIN, tcattr).unwrap(); - } - - if let Some(sig) = unsafe { SIGNALLED } { - println!("[supervisor:{parent}] got signal {sig}"); - } - - println!("[supervisor:{parent}] ipc message from child: {msg:?}"); - - let status = status.unwrap(); - println!("[supervisor:{parent}] subprocess finished: {status:?}"); - - if let Some(msg) = msg { - entrypoint = msg.next_entrypoint; - if msg.drop_db { - rm_tarantool_files(&args.data_dir); - } - } else { - let rc = match status { - WaitStatus::Exited(_, rc) => rc, - WaitStatus::Signaled(_, sig, _) => sig as _, - s => unreachable!("unexpected exit status {:?}", s), - }; - std::process::exit(rc); - } - } - }; - } +pub struct IpcMessage { + pub next_entrypoint: Entrypoint, + pub drop_db: bool, } /// Performs tarantool initialization calling `box.cfg` for the first time. @@ -1143,38 +958,7 @@ fn postjoin(args: &args::Run, storage: Clusterwide, raft_storage: RaftSpaceAcces } } -pub fn main_tarantool(args: args::Tarantool) -> ! { - // XXX: `argv` is a vec of pointers to data owned by `tt_args`, so - // make sure `tt_args` outlives `argv`, because the compiler is not - // gonna do that for you - let tt_args = args.tt_args(); - let argv = tt_args.iter().map(|a| a.as_ptr()).collect::<Vec<_>>(); - - let rc = unsafe { - tarantool::main( - argv.len() as _, - argv.as_ptr() as _, - None, - std::ptr::null_mut(), - ) - }; - - std::process::exit(rc); -} - -pub fn main_expel(args: args::Expel) -> ! { - let rc = tarantool_main!( - args.tt_args().unwrap(), - callback_data: (args,), - callback_data_type: (args::Expel,), - callback_body: { - tt_expel(args) - } - ); - std::process::exit(rc); -} - -fn tt_expel(args: args::Expel) { +pub fn tt_expel(args: args::Expel) { let req = rpc::expel::Request { cluster_id: args.cluster_id, instance_id: args.instance_id, @@ -1195,138 +979,3 @@ fn tt_expel(args: args::Expel) { } } } - -macro_rules! color { - (@priv red) => { "\x1b[0;31m" }; - (@priv green) => { "\x1b[0;32m" }; - (@priv clear) => { "\x1b[0m" }; - (@priv $s:literal) => { $s }; - ($($s:tt)*) => { - ::std::concat![ $( color!(@priv $s) ),* ] - } -} - -pub fn main_test(args: args::Test) -> ! { - cleanup_env!(); - - const PASSED: &str = color![green "ok" clear]; - const FAILED: &str = color![red "FAILED" clear]; - let mut cnt_passed = 0u32; - let mut cnt_failed = 0u32; - let mut cnt_skipped = 0u32; - - let now = std::time::Instant::now(); - - println!(); - println!( - "total {} tests", - inventory::iter::<InnerTest>.into_iter().count() - ); - for t in inventory::iter::<InnerTest> { - if let Some(filter) = args.filter.as_ref() { - if !t.name.contains(filter) { - cnt_skipped += 1; - continue; - } - } - print!("test {} ... ", t.name); - - let (mut rx, tx) = ipc::pipe().expect("pipe creation failed"); - let pid = unsafe { fork() }; - match pid.expect("fork failed") { - ForkResult::Child => { - drop(rx); - unistd::close(0).ok(); // stdin - if !args.nocapture { - unistd::dup2(*tx, 1).ok(); // stdout - unistd::dup2(*tx, 2).ok(); // stderr - } - drop(tx); - - let rc = tarantool_main!( - args.tt_args().unwrap(), - callback_data: t, - callback_data_type: &InnerTest, - callback_body: test_one(t) - ); - std::process::exit(rc); - } - ForkResult::Parent { child } => { - drop(tx); - let log = { - let mut buf = Vec::new(); - use std::io::Read; - if !args.nocapture { - rx.read_to_end(&mut buf) - .map_err(|e| println!("error reading ipc pipe: {e}")) - .ok(); - } - buf - }; - - let mut rc: i32 = 0; - unsafe { - libc::waitpid( - child.into(), // pid_t - &mut rc as *mut libc::c_int, // int* - 0, // int options - ) - }; - - if rc == 0 { - println!("{PASSED}"); - cnt_passed += 1; - } else { - println!("{FAILED}"); - cnt_failed += 1; - - if args.nocapture { - continue; - } - - use std::io::Write; - println!(); - std::io::stderr() - .write_all(&log) - .map_err(|e| println!("error writing stderr: {e}")) - .ok(); - println!(); - } - } - }; - } - - let ok = cnt_failed == 0; - println!(); - print!("test result: {}.", if ok { PASSED } else { FAILED }); - print!(" {cnt_passed} passed;"); - print!(" {cnt_failed} failed;"); - print!(" {cnt_skipped} skipped;"); - println!(" finished in {:.2}s", now.elapsed().as_secs_f32()); - println!(); - - std::process::exit(!ok as _); -} - -fn test_one(t: &InnerTest) { - let temp = tempfile::tempdir().expect("Failed creating a temp directory"); - std::env::set_current_dir(temp.path()).expect("Failed chainging current directory"); - - let cfg = tarantool::Cfg { - listen: Some("127.0.0.1:0".into()), - read_only: false, - log_level: ::tarantool::log::SayLevel::Verbose as u8, - ..Default::default() - }; - - tarantool::set_cfg(&cfg); - tarantool::exec( - r#" - box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists = true}) - "#, - ) - .unwrap(); - - (t.body)(); - std::process::exit(0i32); -} diff --git a/src/main.rs b/src/main.rs index 4512dbb625..351b5a793a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,15 @@ use clap::Parser; +use nix::sys::signal; +use nix::sys::termios::{tcgetattr, tcsetattr, SetArg::TCSADRAIN}; +use nix::sys::wait::{waitpid, WaitStatus}; +use nix::unistd::{self, fork, ForkResult}; use picodata::args; -use picodata::{main_expel, main_run, main_tarantool, main_test}; +use picodata::ipc; +use picodata::tlog; +use picodata::Entrypoint; +use picodata::InnerTest; +use picodata::IpcMessage; +use tarantool::fiber; fn main() -> ! { match args::Picodata::parse() { @@ -10,3 +19,359 @@ fn main() -> ! { args::Picodata::Expel(args) => main_expel(args), } } + +macro_rules! tarantool_main { + ( + $tt_args:expr, + callback_data: $cb_data:tt, + callback_data_type: $cb_data_ty:ty, + callback_body: $cb_body:expr + ) => {{ + let tt_args = $tt_args; + // `argv` is a vec of pointers to data owned by `tt_args`, so + // make sure `tt_args` outlives `argv`, because the compiler is not + // gonna do that for you + let argv = tt_args.iter().map(|a| a.as_ptr()).collect::<Vec<_>>(); + extern "C" fn trampoline(data: *mut libc::c_void) { + let args = unsafe { Box::from_raw(data as _) }; + let cb = |$cb_data: $cb_data_ty| $cb_body; + cb(*args) + } + let cb_data: $cb_data_ty = $cb_data; + unsafe { + picodata::tarantool::main( + argv.len() as _, + argv.as_ptr() as _, + Some(trampoline), + Box::into_raw(Box::new(cb_data)) as _, + ) + } + }}; +} + +fn main_run(args: args::Run) -> ! { + // Tarantool implicitly parses some environment variables. + // We don't want them to affect the behavior and thus filter them out. + for (k, _) in std::env::vars() { + if k.starts_with("TT_") || k.starts_with("TARANTOOL_") { + std::env::remove_var(k) + } + } + + // Tarantool running in a fork (or, to be more percise, the + // libreadline) modifies termios settings to intercept echoed text. + // + // After subprocess termination it's not always possible to + // restore the settings (e.g. in case of SIGSEGV). At least it + // tries to. To preserve tarantool console operable, we cache + // initial termios attributes and restore them manually. + // + let tcattr = tcgetattr(0).ok(); + + // Intercept and forward signals to the child. As for the child + // itself, one shouldn't worry about setting up signal handlers - + // Tarantool does that implicitly. + static mut CHILD_PID: Option<libc::c_int> = None; + static mut SIGNALLED: Option<libc::c_int> = None; + extern "C" fn sigh(sig: libc::c_int) { + unsafe { + // Only a few functions are allowed in signal handlers. + // Read twice `man 7 signal-safety`. + if let Some(pid) = CHILD_PID { + libc::kill(pid, sig); + } + SIGNALLED = Some(sig); + } + } + let sigaction = signal::SigAction::new( + signal::SigHandler::Handler(sigh), + // It's important to use SA_RESTART flag here. + // Otherwise, waitpid() could return EINTR, + // but we don't want dealing with it. + signal::SaFlags::SA_RESTART, + signal::SigSet::empty(), + ); + unsafe { + signal::sigaction(signal::SIGHUP, &sigaction).unwrap(); + signal::sigaction(signal::SIGINT, &sigaction).unwrap(); + signal::sigaction(signal::SIGTERM, &sigaction).unwrap(); + signal::sigaction(signal::SIGUSR1, &sigaction).unwrap(); + } + + let parent = unistd::getpid(); + let mut entrypoint = Entrypoint::StartDiscover {}; + loop { + println!("[supervisor:{parent}] running {entrypoint:?}"); + + let (from_child, to_parent) = + ipc::channel::<IpcMessage>().expect("ipc channel creation failed"); + let (from_parent, to_child) = ipc::pipe().expect("ipc pipe creation failed"); + + let pid = unsafe { fork() }; + match pid.expect("fork failed") { + ForkResult::Child => { + drop(from_child); + drop(to_child); + + let rc = tarantool_main!( + args.tt_args().unwrap(), + callback_data: (entrypoint, args, to_parent, from_parent), + callback_data_type: (Entrypoint, args::Run, ipc::Sender<IpcMessage>, ipc::Fd), + callback_body: { + // We don't want a child to live without a supervisor. + // + // Usually, supervisor waits for child forever and retransmits + // termination signals. But if the parent is killed with a SIGKILL + // there's no way to pass anything. + // + // This fiber serves as a fuse - it tries to read from a pipe + // (that supervisor never writes to), and if the writing end is + // closed, it means the supervisor has terminated. + let fuse = fiber::Builder::new() + .name("supervisor_fuse") + .func(move || { + use ::tarantool::ffi::tarantool::CoIOFlags; + use ::tarantool::coio::coio_wait; + coio_wait(*from_parent, CoIOFlags::READ, f64::INFINITY).ok(); + tlog!(Warning, "Supervisor terminated, exiting"); + std::process::exit(0); + }); + std::mem::forget(fuse.start()); + + entrypoint.exec(args, to_parent) + } + ); + std::process::exit(rc); + } + ForkResult::Parent { child } => { + unsafe { CHILD_PID = Some(child.into()) }; + drop(from_parent); + drop(to_parent); + + let msg = from_child.recv().ok(); + + let status = waitpid(child, None); + + // Restore termios configuration as planned + if let Some(tcattr) = tcattr.as_ref() { + tcsetattr(0, TCSADRAIN, tcattr).unwrap(); + } + + if let Some(sig) = unsafe { SIGNALLED } { + println!("[supervisor:{parent}] got signal {sig}"); + } + + println!("[supervisor:{parent}] ipc message from child: {msg:?}"); + + let status = status.unwrap(); + println!("[supervisor:{parent}] subprocess finished: {status:?}"); + + if let Some(msg) = msg { + entrypoint = msg.next_entrypoint; + if msg.drop_db { + rm_tarantool_files(&args.data_dir); + } + } else { + let rc = match status { + WaitStatus::Exited(_, rc) => rc, + WaitStatus::Signaled(_, sig, _) => sig as _, + s => unreachable!("unexpected exit status {:?}", s), + }; + std::process::exit(rc); + } + } + }; + } +} + +fn rm_tarantool_files(data_dir: &str) { + std::fs::read_dir(data_dir) + .expect("[supervisor] failed reading data_dir") + .map(|entry| entry.expect("[supervisor] failed reading directory entry")) + .map(|entry| entry.path()) + .filter(|path| path.is_file()) + .filter(|f| { + f.extension() + .map(|ext| ext == "xlog" || ext == "snap") + .unwrap_or(false) + }) + .for_each(|f| { + println!("[supervisor] removing file: {}", f.to_string_lossy()); + std::fs::remove_file(f).unwrap(); + }); +} + +fn main_tarantool(args: args::Tarantool) -> ! { + // XXX: `argv` is a vec of pointers to data owned by `tt_args`, so + // make sure `tt_args` outlives `argv`, because the compiler is not + // gonna do that for you + let tt_args = args.tt_args(); + let argv = tt_args.iter().map(|a| a.as_ptr()).collect::<Vec<_>>(); + + let rc = unsafe { + picodata::tarantool::main( + argv.len() as _, + argv.as_ptr() as _, + None, + std::ptr::null_mut(), + ) + }; + + std::process::exit(rc); +} + +fn main_expel(args: args::Expel) -> ! { + let rc = tarantool_main!( + args.tt_args().unwrap(), + callback_data: (args,), + callback_data_type: (args::Expel,), + callback_body: { + picodata::tt_expel(args) + } + ); + std::process::exit(rc); +} + +macro_rules! color { + (@priv red) => { "\x1b[0;31m" }; + (@priv green) => { "\x1b[0;32m" }; + (@priv clear) => { "\x1b[0m" }; + (@priv $s:literal) => { $s }; + ($($s:tt)*) => { + ::std::concat![ $( color!(@priv $s) ),* ] + } +} + +pub fn main_test(args: args::Test) -> ! { + // Tarantool implicitly parses some environment variables. + // We don't want them to affect the behavior and thus filter them out. + + for (k, _) in std::env::vars() { + if k.starts_with("TT_") || k.starts_with("TARANTOOL_") { + std::env::remove_var(k) + } + } + + const PASSED: &str = color![green "ok" clear]; + const FAILED: &str = color![red "FAILED" clear]; + let mut cnt_passed = 0u32; + let mut cnt_failed = 0u32; + let mut cnt_skipped = 0u32; + + let now = std::time::Instant::now(); + + println!(); + println!( + "total {} tests", + inventory::iter::<InnerTest>.into_iter().count() + ); + for t in inventory::iter::<InnerTest> { + if let Some(filter) = args.filter.as_ref() { + if !t.name.contains(filter) { + cnt_skipped += 1; + continue; + } + } + print!("test {} ... ", t.name); + + let (mut rx, tx) = ipc::pipe().expect("pipe creation failed"); + let pid = unsafe { fork() }; + match pid.expect("fork failed") { + ForkResult::Child => { + drop(rx); + unistd::close(0).ok(); // stdin + if !args.nocapture { + unistd::dup2(*tx, 1).ok(); // stdout + unistd::dup2(*tx, 2).ok(); // stderr + } + drop(tx); + + let rc = tarantool_main!( + args.tt_args().unwrap(), + callback_data: t, + callback_data_type: &InnerTest, + callback_body: test_one(t) + ); + std::process::exit(rc); + } + ForkResult::Parent { child } => { + drop(tx); + let log = { + let mut buf = Vec::new(); + use std::io::Read; + if !args.nocapture { + rx.read_to_end(&mut buf) + .map_err(|e| println!("error reading ipc pipe: {e}")) + .ok(); + } + buf + }; + + let mut rc: i32 = 0; + unsafe { + libc::waitpid( + child.into(), // pid_t + &mut rc as *mut libc::c_int, // int* + 0, // int options + ) + }; + + if rc == 0 { + println!("{PASSED}"); + cnt_passed += 1; + } else { + println!("{FAILED}"); + cnt_failed += 1; + + if args.nocapture { + continue; + } + + use std::io::Write; + println!(); + std::io::stderr() + .write_all(&log) + .map_err(|e| println!("error writing stderr: {e}")) + .ok(); + println!(); + } + } + }; + } + + let ok = cnt_failed == 0; + println!(); + print!("test result: {}.", if ok { PASSED } else { FAILED }); + print!(" {cnt_passed} passed;"); + print!(" {cnt_failed} failed;"); + print!(" {cnt_skipped} skipped;"); + println!(" finished in {:.2}s", now.elapsed().as_secs_f32()); + println!(); + + std::process::exit(!ok as _); +} + +fn test_one(t: &InnerTest) { + use picodata::tarantool; + + let temp = tempfile::tempdir().expect("Failed creating a temp directory"); + std::env::set_current_dir(temp.path()).expect("Failed chainging current directory"); + + let cfg = tarantool::Cfg { + listen: Some("127.0.0.1:0".into()), + read_only: false, + log_level: ::tarantool::log::SayLevel::Verbose as u8, + ..Default::default() + }; + + tarantool::set_cfg(&cfg); + tarantool::exec( + r#" + box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists = true}) + "#, + ) + .unwrap(); + + (t.body)(); + std::process::exit(0i32); +} diff --git a/src/tarantool.rs b/src/tarantool.rs index 027ed6835a..5239f99eb4 100644 --- a/src/tarantool.rs +++ b/src/tarantool.rs @@ -31,20 +31,6 @@ macro_rules! stringify_cfunc { }}; } -#[macro_export] -macro_rules! cleanup_env { - () => { - // Tarantool implicitly parses some environment variables. - // We don't want them to affect the behavior and thus filter them out. - - for (k, _) in std::env::vars() { - if k.starts_with("TT_") || k.starts_with("TARANTOOL_") { - std::env::remove_var(k) - } - } - }; -} - mod ffi { use libc::c_char; use libc::c_int; -- GitLab