diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1bce9d2..8173c52 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,50 +1,45 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: // https://github.com/microsoft/vscode-dev-containers/tree/v0.154.2/containers/go { - "name": "Go", - "dockerComposeFile": "./docker-compose.yaml", - "service": "jobrunner_code", - "shutdownAction": "stopCompose", + "name": "Go", + "dockerComposeFile": "./docker-compose.yaml", + "service": "jobrunner_code", + "shutdownAction": "stopCompose", - // Set *default* container specific settings.json values on container create. - "settings": { - "terminal.integrated.shell.linux": "/bin/bash", - "go.toolsManagement.checkForUpdates": "local", - "go.useLanguageServer": true, - "go.gopath": "/go", - "go.goroot": "/usr/local/go", - "go.toolsGopath": "/go/bin", - "go.buildOnSave": "package", - "go.testOnSave": true, - "go.coverOnTestPackage": true, - "go.coverageDecorator": { - "type": "gutter", - "coveredHighlightColor": "rgba(64,128,128,0.5)", - "uncoveredHighlightColor": "rgba(128,64,64,0.25)", - "coveredGutterStyle": "blockgreen", - "uncoveredGutterStyle": "blockred" - }, - "go.coverOnSingleTest": true, - "go.coverOnSave": true, - "go.testFlags": ["-short"] - }, + // Set *default* container specific settings.json values on container create. + "settings": { + "go.toolsManagement.checkForUpdates": "local", + "go.useLanguageServer": true, + "go.gopath": "/go", + "go.goroot": "/usr/local/go", + "go.toolsGopath": "/go/bin", + "go.buildOnSave": "package", + "go.testOnSave": true, + "go.coverOnTestPackage": true, + "go.coverageDecorator": { + "type": "gutter", + "coveredHighlightColor": "rgba(64,128,128,0.5)", + "uncoveredHighlightColor": "rgba(128,64,64,0.25)", + "coveredGutterStyle": "blockgreen", + "uncoveredGutterStyle": "blockred" + }, + "go.coverOnSingleTest": true, + "go.coverOnSave": true, + "go.testFlags": ["-short"] + }, - // Mount to proper location - "workspaceFolder": "/go/src/github.com/zhongjie-cai/job-runner", + // Mount to proper location + "workspaceFolder": "/go/src/github.com/zhongjie-cai/job-runner", - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "golang.Go", - "eamodio.gitlens", - "yzhang.markdown-all-in-one" - ], + // Add the IDs of extensions you want installed when the container is created. + "extensions": ["golang.Go", "eamodio.gitlens", "yzhang.markdown-all-in-one"] - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "go version", + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "go version", - // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. - //"remoteUser": "vscode" + // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + //"remoteUser": "vscode" } diff --git a/0_functionPointers.go b/0_functionPointers.go index 064f3b9..f82be15 100644 --- a/0_functionPointers.go +++ b/0_functionPointers.go @@ -8,9 +8,7 @@ import ( "net/http" "net/url" "reflect" - "regexp" "runtime" - "runtime/debug" "sort" "strconv" "strings" @@ -44,7 +42,6 @@ var ( fmtPrintf = fmt.Printf fmtSprintf = fmt.Sprintf marshalIgnoreErrorFunc = marshalIgnoreError - debugStack = debug.Stack ) // func pointers for injection / testing: handler.go @@ -88,11 +85,6 @@ var ( stringsJoin = strings.Join ) -// func pointers for injection / testing: parameter.go -var ( - regexpMatchString = regexp.MatchString -) - // func pointers for injection / testing: pointerutil.go var ( reflectValueOf = reflect.ValueOf diff --git a/0_functionPointers_test.go b/0_functionPointers_test.go index cb74305..ec7b549 100644 --- a/0_functionPointers_test.go +++ b/0_functionPointers_test.go @@ -10,9 +10,7 @@ import ( "net/http" "net/url" "reflect" - "regexp" "runtime" - "runtime/debug" "sort" "strconv" "strings" @@ -62,8 +60,6 @@ var ( fmtSprintfCalled int marshalIgnoreErrorFuncExpected int marshalIgnoreErrorFuncCalled int - debugStackExpected int - debugStackCalled int stringsSplitExpected int stringsSplitCalled int strconvAtoiExpected int @@ -104,8 +100,6 @@ var ( sortStringsCalled int stringsJoinExpected int stringsJoinCalled int - regexpMatchStringExpected int - regexpMatchStringCalled int reflectValueOfExpected int reflectValueOfCalled int timeDateExpected int @@ -144,8 +138,6 @@ var ( ioutilNopCloserCalled int bytesNewBufferExpected int bytesNewBufferCalled int - constructResponseFuncExpected int - constructResponseFuncCalled int logProcessEnterFuncExpected int logProcessEnterFuncCalled int logProcessExitFuncExpected int @@ -276,7 +268,7 @@ func createMock(t *testing.T) { } handleSessionFuncExpected = 0 handleSessionFuncCalled = 0 - handleSessionFunc = func(app *application, index int) error { + handleSessionFunc = func(app *application, index int, reruns int) error { handleSessionFuncCalled++ return nil } @@ -329,12 +321,6 @@ func createMock(t *testing.T) { marshalIgnoreErrorFuncCalled++ return "" } - debugStackExpected = 0 - debugStackCalled = 0 - debugStack = func() []byte { - debugStackCalled++ - return nil - } stringsSplitExpected = 0 stringsSplitCalled = 0 stringsSplit = func(s, sep string) []string { @@ -349,7 +335,7 @@ func createMock(t *testing.T) { } initiateSessionFuncExpected = 0 initiateSessionFuncCalled = 0 - initiateSessionFunc = func(app *application, index int) *session { + initiateSessionFunc = func(app *application, index int, reruns int) *session { initiateSessionFuncCalled++ return nil } @@ -453,12 +439,6 @@ func createMock(t *testing.T) { stringsJoinCalled++ return "" } - regexpMatchStringExpected = 0 - regexpMatchStringCalled = 0 - regexpMatchString = func(pattern string, s string) (bool, error) { - regexpMatchStringCalled++ - return false, nil - } reflectValueOfExpected = 0 reflectValueOfCalled = 0 reflectValueOf = func(i interface{}) reflect.Value { @@ -825,8 +805,6 @@ func verifyAll(t *testing.T) { assert.Equal(t, fmtSprintfExpected, fmtSprintfCalled, "Unexpected number of calls to method fmtSprintf") marshalIgnoreErrorFunc = marshalIgnoreError assert.Equal(t, marshalIgnoreErrorFuncExpected, marshalIgnoreErrorFuncCalled, "Unexpected number of calls to method marshalIgnoreErrorFunc") - debugStack = debug.Stack - assert.Equal(t, debugStackExpected, debugStackCalled, "Unexpected number of calls to debugStack") stringsSplit = strings.Split assert.Equal(t, stringsSplitExpected, stringsSplitCalled, "Unexpected number of calls to method stringsSplit") strconvAtoi = strconv.Atoi @@ -867,8 +845,6 @@ func verifyAll(t *testing.T) { assert.Equal(t, sortStringsExpected, sortStringsCalled, "Unexpected number of calls to sortStrings") stringsJoin = strings.Join assert.Equal(t, stringsJoinExpected, stringsJoinCalled, "Unexpected number of calls to stringsJoin") - regexpMatchString = regexp.MatchString - assert.Equal(t, regexpMatchStringExpected, regexpMatchStringCalled, "Unexpected number of calls to regexpMatchString") reflectValueOf = reflect.ValueOf assert.Equal(t, reflectValueOfExpected, reflectValueOfCalled, "Unexpected number of calls to reflectValueOf") timeDate = time.Date @@ -1074,6 +1050,11 @@ func (session *dummySession) GetIndex() int { return 0 } +func (session *dummySession) GetReruns() int { + assert.Fail(session.t, "Unexpected call to GetReruns") + return 0 +} + func (session *dummySession) Attach(name string, value interface{}) bool { assert.Fail(session.t, "Unexpected call to Attach") return false diff --git a/application.go b/application.go index d208e4e..cc593b8 100644 --- a/application.go +++ b/application.go @@ -2,6 +2,7 @@ package jobrunner import ( "sync" + "sync/atomic" ) // Application is the interface for job runner application @@ -20,6 +21,7 @@ type application struct { name string version string instances int + reruns []int32 schedule Schedule overlap bool session *session @@ -27,6 +29,7 @@ type application struct { shutdown chan bool started bool lastErrors []error + waits sync.WaitGroup } // NewApplication creates a new application for job runner hosting @@ -45,21 +48,23 @@ func NewApplication( customization = customizationDefault } var application = &application{ - name, - version, - instances, - schedule, - overlap, - &session{ - uuidNew(), - 0, - map[string]interface{}{}, - customization, + name: name, + version: version, + instances: instances, + reruns: make([]int32, instances), + schedule: schedule, + overlap: overlap, + session: &session{ + id: uuidNew(), + index: 0, + attachment: map[string]interface{}{}, + customization: customization, }, - customization, - make(chan bool), - false, - []error{}, + customization: customization, + shutdown: make(chan bool), + started: false, + lastErrors: []error{}, + waits: sync.WaitGroup{}, } return application } @@ -197,10 +202,12 @@ func runInstances(app *application) { var waitGroup sync.WaitGroup for id := 0; id < app.instances; id++ { waitGroup.Add(1) - go func(index int) { + atomic.AddInt32(&app.reruns[id], 1) + go func(index int, reruns int) { var sessionError = handleSessionFunc( app, index, + reruns, ) if sessionError != nil { app.lastErrors = append( @@ -209,9 +216,12 @@ func runInstances(app *application) { ) } waitGroup.Done() - }(id) + }(id, int(app.reruns[id])) } waitGroup.Wait() + if app.overlap { + app.waits.Done() + } } func scheduleExecution(app *application) { @@ -223,6 +233,7 @@ func scheduleExecution(app *application) { break } if app.overlap { + app.waits.Add(1) go runInstancesFunc( app, ) @@ -232,6 +243,9 @@ func scheduleExecution(app *application) { ) } } + if app.overlap { + app.waits.Wait() + } } func runApplication(app *application) { diff --git a/application_test.go b/application_test.go index 3066ab6..7625f4a 100644 --- a/application_test.go +++ b/application_test.go @@ -17,7 +17,7 @@ func TestNewApplication_NilCustomization(t *testing.T) { // arrange var dummyName = "some name" var dummyVersion = "some version" - var dummyInstances = rand.Int() + var dummyInstances = rand.Intn(100) var dummySchedule Schedule var dummyOverlap = rand.Intn(100) > 50 var dummyCustomization Customization @@ -75,7 +75,7 @@ func TestNewApplication_HasCustomization(t *testing.T) { // arrange var dummyName = "some name" var dummyVersion = "some version" - var dummyInstances = rand.Int() + var dummyInstances = rand.Intn(100) var dummySchedule Schedule var dummyOverlap = rand.Intn(100) > 50 var dummyCustomization = &dummyCustomization{t: t} @@ -834,8 +834,10 @@ func TestRunInstances_ZeroInstance(t *testing.T) { func TestRunInstances_SingleInstance(t *testing.T) { // arrange + var dummyReruns = rand.Int31n(65535) var dummyApplication = &application{ instances: 1, + reruns: []int32{dummyReruns}, } var dummyError = errors.New("some error") @@ -844,10 +846,11 @@ func TestRunInstances_SingleInstance(t *testing.T) { // expect handleSessionFuncExpected = 1 - handleSessionFunc = func(app *application, index int) error { + handleSessionFunc = func(app *application, index int, reruns int) error { handleSessionFuncCalled++ assert.Equal(t, dummyApplication, app) assert.Equal(t, 0, index) + assert.Equal(t, int(dummyReruns)+1, reruns) return dummyError } @@ -873,6 +876,7 @@ func TestRunInstances_MultipleInstances(t *testing.T) { } var dummyApplication = &application{ instances: len(dummyErrors), + reruns: make([]int32, len(dummyErrors)), } var expectedIndex = map[int]bool{} var writeLock sync.Mutex @@ -882,7 +886,7 @@ func TestRunInstances_MultipleInstances(t *testing.T) { // expect handleSessionFuncExpected = dummyApplication.instances - handleSessionFunc = func(app *application, index int) error { + handleSessionFunc = func(app *application, index int, reruns int) error { handleSessionFuncCalled++ assert.Equal(t, dummyApplication, app) writeLock.Lock() @@ -908,6 +912,45 @@ func TestRunInstances_MultipleInstances(t *testing.T) { verifyAll(t) } +func TestRunInstances_Overlap(t *testing.T) { + // arrange + var dummyReruns = rand.Int31n(65535) + var dummyApplication = &application{ + instances: 1, + reruns: []int32{dummyReruns}, + overlap: true, + } + var dummyError = errors.New("some error") + + // stub + dummyApplication.waits.Add(1) + + // mock + createMock(t) + + // expect + handleSessionFuncExpected = 1 + handleSessionFunc = func(app *application, index int, reruns int) error { + handleSessionFuncCalled++ + assert.Equal(t, dummyApplication, app) + assert.Equal(t, 0, index) + assert.Equal(t, int(dummyReruns)+1, reruns) + return dummyError + } + + // SUT + act + runInstances( + dummyApplication, + ) + + // assert + assert.Equal(t, 1, len(dummyApplication.lastErrors)) + assert.Equal(t, dummyError, dummyApplication.lastErrors[0]) + + // verify + verifyAll(t) +} + func TestScheduleExecution_WithOverlap(t *testing.T) { // arrange var dummyApplication = &application{ diff --git a/handler.go b/handler.go index 7b7f7fe..d114450 100644 --- a/handler.go +++ b/handler.go @@ -7,12 +7,14 @@ import ( func initiateSession( app *application, index int, + reruns int, ) *session { return &session{ - uuidNew(), - index, - map[string]interface{}{}, - app.customization, + id: uuidNew(), + index: index, + reruns: reruns, + attachment: map[string]interface{}{}, + customization: app.customization, } } @@ -64,10 +66,12 @@ func processSession( func handleSession( app *application, index int, + reruns int, ) (err error) { var session = initiateSessionFunc( app, index, + reruns, ) logProcessEnterFunc( session, diff --git a/handler_test.go b/handler_test.go index 464f97a..5fbc923 100644 --- a/handler_test.go +++ b/handler_test.go @@ -17,6 +17,7 @@ func TestInitiateSession(t *testing.T) { customization: dummyCustomization, } var dummyIndex = rand.Intn(65536) + var dummyReruns = rand.Intn(65536) var dummySessionID = uuid.New() // mock @@ -33,12 +34,14 @@ func TestInitiateSession(t *testing.T) { var session = initiateSession( dummyApplication, dummyIndex, + dummyReruns, ) // assert assert.NotNil(t, session) assert.Equal(t, dummySessionID, session.id) assert.Equal(t, dummyIndex, session.index) + assert.Equal(t, dummyReruns, session.reruns) assert.Empty(t, session.attachment) assert.Equal(t, dummyCustomization, session.customization) @@ -384,6 +387,7 @@ func TestHandleSession_HappyPath(t *testing.T) { customization: dummyCustomization, } var dummyIndex = rand.Int() + var dummyReruns = rand.Int() var dummySession = &session{id: uuid.New()} var dummyTimeNow = time.Now() var dummyDuration = time.Duration(rand.Intn(100)) @@ -395,10 +399,11 @@ func TestHandleSession_HappyPath(t *testing.T) { // expect initiateSessionFuncExpected = 1 - initiateSessionFunc = func(app *application, index int) *session { + initiateSessionFunc = func(app *application, index int, reruns int) *session { initiateSessionFuncCalled++ assert.Equal(t, dummyApplication, app) assert.Equal(t, dummyIndex, index) + assert.Equal(t, dummyReruns, reruns) return dummySession } logProcessEnterFuncExpected = 1 @@ -471,6 +476,7 @@ func TestHandleSession_HappyPath(t *testing.T) { var err = handleSession( dummyApplication, dummyIndex, + dummyReruns, ) // assert diff --git a/jsonutil.go b/jsonutil.go index a3d92f7..04e9d70 100644 --- a/jsonutil.go +++ b/jsonutil.go @@ -20,7 +20,7 @@ func marshalIgnoreError(v interface{}) string { var encoder = jsonNewEncoder(buffer) encoder.SetEscapeHTML(false) encoder.Encode(v) - var result = string(buffer.Bytes()) + var result = buffer.String() return stringsTrimRight(result, "\n") } diff --git a/logType.go b/logType.go index 492a2ef..f765258 100644 --- a/logType.go +++ b/logType.go @@ -1,9 +1,5 @@ package jobrunner -import ( - "strings" -) - // LogType is the entry type of logging type LogType int @@ -148,7 +144,7 @@ func (logtype LogType) HasFlag(flag LogType) bool { // NewLogType converts a string representation of LogType flag to its strongly typed instance func NewLogType(value string) LogType { - var splitValues = strings.Split( + var splitValues = stringsSplit( value, "|", ) diff --git a/scheduleMaker.go b/scheduleMaker.go index 732444f..ffe4225 100644 --- a/scheduleMaker.go +++ b/scheduleMaker.go @@ -474,7 +474,7 @@ func determineScheduleIndex( ), nil } - schedule.second, schedule.secondIndex, increment, overflow = findValueMatchFunc( + schedule.second, schedule.secondIndex, _, overflow = findValueMatchFunc( start.Second(), schedule.seconds, ) diff --git a/session.go b/session.go index d07a3fb..d75f2a8 100644 --- a/session.go +++ b/session.go @@ -19,6 +19,9 @@ type SessionMeta interface { // GetIndex returns the instance index registered to session object for given session ID GetIndex() int + + // GetReruns returns the rerun count for the same instance since first scheduled + GetReruns() int } // SessionAttachment is a subset of Session interface, containing only attachment related methods @@ -63,6 +66,7 @@ type SessionWebcall interface { type session struct { id uuid.UUID index int + reruns int attachment map[string]interface{} customization Customization } @@ -83,6 +87,14 @@ func (session *session) GetIndex() int { return session.index } +// GetReruns returns the rerun count for the same instance since first scheduled +func (session *session) GetReruns() int { + if session == nil { + return 0 + } + return session.reruns +} + // Attach attaches any value object into the given session associated to the session ID func (session *session) Attach(name string, value interface{}) bool { if session == nil {