Im Changsu
Jenkins Workspace 동시성 문제
WorkspaceList

개요

Jenkins Pipeline을 사용해서 잡 스케줄러를 실행하기 위해 triggers directive를 사용했다.

pipeline {
    agent any

    triggers {
        cron("* * * * *") // HERE
    }

    stages {...}

    post {
        always {
            cleanWs(cleanWhenNotBuilt: false,
                    deleteDirs: true,
                    disableDeferredWipeout: true,
                    notFailBuild: true,
                    patterns: [
                        [pattern: '.git/**', type: 'EXCLUDE'],
                        [pattern: '.gitignore', type: 'EXCLUDE'],
                        [pattern: '.meta/**', type: 'EXCLUDE'],
                    ]
            )
        }
    }
}

해당 Job은 빌드 간 메타데이터(.meta/)를 공유해야 했기 때문에 cleanWs 플러그인에서도 .git 디렉토리와 함께 삭제되지 않도록 설정했다.

하지만 무슨 이유인지 메타데이터가 간헐적으로 누락되었고, 작업도 원하는대로 동작하지 않고 있었다.

WorkspaceList

Jenkins에서 Job을 실행할 경우 말그대로 작업 공간을 위한 Workspace($JENKINS_HOME/workspace) 디렉토리가 생성된다.

// hudson.slaves.WorkspaceList
public synchronized Lease allocate(@NonNull FilePath base, Object context) throws InterruptedException {
    for (int i = 1; ; i++) {
        FilePath candidate = i == 1 ? base : base.withSuffix("@" + i);
        Entry e = inUse.get(candidate.getRemote());
        if (e != null && !e.quick && e.context != context)
            continue;
        return acquire(candidate, false, context);
    }
}

Jenkins는 Workspace 목록을 별도의 메타데이터 파일에 저장해서 관리하지 않는다. Jenkins 런타임의 WorkspaceList 객체에 전체 Workspace 목록을 저장한다.

// hudson.slaves.WorkspaceList
/**
 * Used by {@link Computer} to keep track of workspaces that are actively in use.
 */
public final class WorkspaceList {

    private static final class AllocationAt extends Exception {...}
    
    /**
      * Book keeping for workspace allocation.
      */
    public static final class Entry {...}
    
    /**
     * Represents a leased workspace that needs to be returned later.
     */
    public abstract static class Lease implements /*Auto*/Closeable {...}

    // ...
}

문제

만약 파이프라인에서 Concurrent Build 옵션을 허용한 채 여러 개의 빌드를 동시에 실행하면 간혹 job_name workspace에서 실행되지 않고 job_name@2 에서만 실행되는 경우가 있다. 그런데 메타데이터 파일을 공유해서 사용해야 하는 경우 job_name workspace에서 실행되기를 보장해야 한다.

해결

Jenkins Master 프로세스를 재시작해서 WorkspaceList를 초기화하거나 새로운 이름의 Job을 생성하면 새로운 이름의 workspace에서 빌드할 수 있다. 이후 스레드 안전성을 보장하기 위해 Concurrent Build 옵션을 허용하지 않은 채 빌드한다.

pipeline {
    agent any

    triggers {
        cron("* * * * *")
    }
    
    // https://www.jenkins.io/doc/book/pipeline/syntax/#options
    options {
        // cron 설정에 따라 빌드 간 겹치지 않도록 타임아웃을 설정한다.
        timeout(time: 50, unit: 'SECONDS')
    
        // 빌드 스케줄이 2개 생성되면 'job_name', 'job_name@2' workspace가 생성되고
        // metadata를 각각 관리하게 된다. abortPrevious 값을 true로 설정하면
        // 이미 빌드 중인 프로세스와 겹쳐서 'job_name@2' workspace가 생성되더라도
        // 이후 빌드부터는 기존 빌드 프로세스가 제거되고 'job_name' workspace에서 실행된다.
        disableConcurrentBuilds(abortPrevious: true)
    }

    stages {...}
}

timeout과 cron

Crontab(Unix의 Job Scheduler)처럼 Jenkins는 cron 설정을 통해 잡 스케줄러를 만들 수 있다. Jenkins cron의 최소 간격은 1분(* * * * *)이다.

문제

timeout 설정도 cron 설정과 같이 1분으로 두면 timeout abort 되기 전 job_name@2 workspace 디렉토리가 생성되고 별도의 메타데이터를 갖는 Job이 실행될 수 있다.

해결

만약 cron 간격을 1분으로 설정했다면 timeout을 50초로 설정하는 등 차이를 둔다. (55초는 살짝 겹쳤다…)

timeout(time: 50, unit: 'SECONDS')