Full Code of go-co-op/gocron for AI

v2 5b307f56dd52 cached
44 files
298.0 KB
87.9k tokens
479 symbols
1 requests
Download .txt
Showing preview only (312K chars total). Download the full file or copy to clipboard to get everything.
Repository: go-co-op/gocron
Branch: v2
Commit: 5b307f56dd52
Files: 44
Total size: 298.0 KB

Directory structure:
gitextract_1w8mopnk/

├── .github/
│   ├── FUNDING.yml
│   ├── copilot-instructions.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── codeql-analysis.yml
│       ├── file_formatting.yml
│       └── go_test.yml
├── .gitignore
├── .golangci.yaml
├── .pre-commit-config.yaml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── SECURITY.md
├── distributed.go
├── errors.go
├── example_test.go
├── examples/
│   └── elector/
│       └── main.go
├── executor.go
├── go.mod
├── go.sum
├── gocron-monitor-test/
│   ├── debug_restart.go
│   ├── go.mod
│   └── go.sum
├── job.go
├── job_test.go
├── logger.go
├── logger_test.go
├── migration_v1_to_v2.md
├── mocks/
│   ├── README.md
│   ├── distributed.go
│   ├── go.mod
│   ├── go.sum
│   ├── job.go
│   ├── logger.go
│   └── scheduler.go
├── monitor.go
├── scheduler.go
├── scheduler_monitor.go
├── scheduler_monitor_test.go
├── scheduler_test.go
├── util.go
└── util_test.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: go-co-op # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: gocron
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']


================================================
FILE: .github/copilot-instructions.md
================================================
# gocron: Go Job Scheduling Library

Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.

## Working Effectively

### Bootstrap and Build Commands
- Install dependencies: `go mod tidy`
- Build the library: `go build -v ./...`
- Install required tools:
  - `go install go.uber.org/mock/mockgen@latest` 
  - `export PATH=$PATH:$(go env GOPATH)/bin` (add to shell profile)
- Generate mocks: `make mocks`
- Format code: `make fmt`

### Testing Commands  
- Run all tests: `make test` -- takes 50 seconds. NEVER CANCEL. Set timeout to 90+ seconds.
- Run CI tests: `make test_ci` -- takes 50 seconds. NEVER CANCEL. Set timeout to 90+ seconds.
- Run with coverage: `make test_coverage` -- takes 50 seconds. NEVER CANCEL. Set timeout to 90+ seconds.
- Run specific tests: `go test -v -race -count=1 ./...`

### Linting Commands
- Format verification: `grep "^func [a-zA-Z]" example_test.go | sort -c`
- Full linting: `make lint` -- MAY FAIL due to golangci-lint config compatibility issues. This is a known issue.
- Alternative basic linting: `go vet ./...` and `gofmt -d .`

## Validation

### Required Validation Steps
- ALWAYS run `make test` before submitting changes. Tests must pass.
- ALWAYS run `make fmt` to ensure proper formatting.
- ALWAYS run `make mocks` if you change interface definitions.
- ALWAYS verify examples still work by running them: `cd examples/elector && go run main.go`

### Manual Testing Scenarios
Since this is a library, not an application, testing involves:
1. **Basic Scheduler Creation**: Verify you can create a scheduler with `gocron.NewScheduler()`
2. **Job Creation**: Verify you can create jobs with various `JobDefinition` types
3. **Scheduler Lifecycle**: Verify Start() and Shutdown() work correctly
4. **Example Validation**: Run examples in `examples/` directory to ensure functionality

Example validation script:
```go
package main
import (
    "fmt"
    "time"
    "github.com/go-co-op/gocron/v2"
)
func main() {
    s, err := gocron.NewScheduler()
    if err != nil { panic(err) }
    j, err := s.NewJob(
        gocron.DurationJob(2*time.Second),
        gocron.NewTask(func() { fmt.Println("Working!") }),
    )
    if err != nil { panic(err) }
    fmt.Printf("Job ID: %s\n", j.ID())
    s.Start()
    time.Sleep(6 * time.Second)
    s.Shutdown()
}
```

### CI Requirements
The CI will fail if:
- Tests don't pass (`make test_ci`)
- Function order in `example_test.go` is incorrect
- golangci-lint finds issues (though config compatibility varies)

## Common Tasks

### Repository Structure
```
.
├── README.md              # Main documentation
├── CONTRIBUTING.md        # Contribution guidelines  
├── SECURITY.md           # Security policy
├── Makefile              # Build automation
├── go.mod               # Go module definition
├── .github/             # GitHub workflows and configs
├── .golangci.yaml       # Linting configuration
├── examples/            # Usage examples
│   └── elector/         # Distributed elector example
├── mocks/               # Generated mock files
├── *.go                # Library source files
└── *_test.go           # Test files
```

### Key Source Files
- `scheduler.go` - Main scheduler implementation
- `job.go` - Job definitions and scheduling logic
- `executor.go` - Job execution engine
- `logger.go` - Logging interfaces and implementations  
- `distributed.go` - Distributed scheduling support
- `monitor.go` - Job monitoring interfaces
- `util.go` - Utility functions
- `errors.go` - Error definitions

### Dependencies and Versions
- Requires Go 1.23.0+
- Key dependencies automatically managed via `go mod`:
  - `github.com/google/uuid` - UUID generation
  - `github.com/jonboulle/clockwork` - Time mocking for tests
  - `github.com/robfig/cron/v3` - Cron expression parsing
  - `github.com/stretchr/testify` - Testing utilities
  - `go.uber.org/goleak` - Goroutine leak detection

### Testing Patterns
- Uses table-driven tests following Go best practices
- Extensive use of goroutine leak detection (may be skipped in CI via TEST_ENV)
- Mock-based testing for interfaces
- Race condition detection enabled (`-race` flag)
- 93.8% test coverage expected

### Build and Release
- No application to build - this is a library
- Version managed via Git tags (v2.x.x)
- Distribution via Go module system
- CI tests on Go 1.23 and 1.24

## Troubleshooting

### Common Issues
1. **mockgen not found**: Install with `go install go.uber.org/mock/mockgen@latest`
2. **golangci-lint config errors**: Known compatibility issue - use `go vet` instead
3. **Test timeouts**: Tests can take 50+ seconds, always set adequate timeouts
4. **PATH issues**: Ensure `$(go env GOPATH)/bin` is in PATH
5. **Import errors in examples**: Run `go mod tidy` to resolve dependencies

### Expected Timings
- `make test`: ~50 seconds
- `make test_coverage`: ~50 seconds  
- `make test_ci`: ~50 seconds
- `go build`: ~5 seconds
- `make mocks`: ~2 seconds
- `make fmt`: <1 second

### Known Limitations
- golangci-lint configuration may have compatibility issues with certain versions
- Some tests are skipped in CI environments (controlled by TEST_ENV variable)
- Examples directory has no tests but should be manually validated

================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates

version: 2
updates:
  # Maintain dependencies for GitHub Actions
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"

  # Maintain Go dependencies
  - package-ecosystem: "gomod"
    directory: "/"
    schedule:
      interval: "weekly"


================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"

on:
  push:
    branches: [ v2 ]
    branches-ignore:
      - "dependabot/**"
  pull_request:
    paths-ignore:
      - '**.md'
    # The branches below must be a subset of the branches above
    branches: [ v2 ]
  schedule:
    - cron: '34 7 * * 1'

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest

    strategy:
      fail-fast: false
      matrix:
        language: [ 'go' ]
        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
        # Learn more:
        # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed

    steps:
    - name: Checkout repository
      uses: actions/checkout@v6

    # Initializes the CodeQL tools for scanning.
    - name: Initialize CodeQL
      uses: github/codeql-action/init@v4
      with:
        languages: ${{ matrix.language }}
        # If you wish to specify custom queries, you can do so here or in a config file.
        # By default, queries listed here will override any specified in a config file.
        # Prefix the list here with "+" to use these queries and those in the config file.
        # queries: ./path/to/local/query, your-org/your-repo/queries@main

    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
    # If this step fails, then you should remove it and run the build manually (see below)
    - name: Autobuild
      uses: github/codeql-action/autobuild@v4

    # ℹ️ Command-line programs to run using the OS shell.
    # 📚 https://git.io/JvXDl

    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
    #    and modify them (or add more) to build your code if your project
    #    uses a compiled language

    #- run: |
    #   make bootstrap
    #   make release

    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v4


================================================
FILE: .github/workflows/file_formatting.yml
================================================
on:
  push:
    branches:
      - v2
  pull_request:
    branches:
      - v2

name: formatting
jobs:
  check-sorted:
    name: check sorted
    runs-on: ubuntu-latest
    steps:
      - name: checkout code
        uses: actions/checkout@v6
      - name: verify example_test.go
        run: |
          grep "^func [a-z-A-Z]" example_test.go | sort -c


================================================
FILE: .github/workflows/go_test.yml
================================================
on:
  push:
    branches:
      - v2
  pull_request:
    branches:
      - v2

name: lint and test
jobs:
  golangci:
    strategy:
      matrix:
        go-version:
          - "1.24"
          - "1.25"
    name: lint and test
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v6
      - name: Install Go
        uses: actions/setup-go@v6
        with:
          go-version: ${{ matrix.go-version }}
      - name: golangci-lint
        uses: golangci/golangci-lint-action@v9.2.0
        with:
          version: v2.4.0
      - name: test
        run: make test_ci


================================================
FILE: .gitignore
================================================
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test
local_testing
coverage.out

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
vendor/

# IDE project files
.idea


================================================
FILE: .golangci.yaml
================================================
version: "2"
run:
  issues-exit-code: 1
  tests: true
output:
  formats:
    text:
      path: stdout
      print-linter-name: true
      print-issued-lines: true
  path-prefix: ""
linters:
  enable:
    - bodyclose
    - copyloopvar
    - misspell
    - revive
    - whitespace
  exclusions:
    generated: lax
    presets:
      - common-false-positives
      - legacy
      - std-error-handling
    rules:
      - linters:
          - revive
        path: example_test.go
        text: seems to be unused
      - linters:
          - revive
        text: package-comments
    paths:
      - local
      - third_party$
      - builtin$
      - examples$
issues:
  max-same-issues: 100
  fix: true
formatters:
  enable:
    - gofumpt
    - goimports
  exclusions:
    generated: lax
    paths:
      - local
      - third_party$
      - builtin$
      - examples$


================================================
FILE: .pre-commit-config.yaml
================================================
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: check-added-large-files
      - id: check-case-conflict
      - id: check-merge-conflict
      - id: check-yaml
      - id: detect-private-key
      - id: end-of-file-fixer
      - id: trailing-whitespace
  - repo: https://github.com/golangci/golangci-lint
    rev: v2.4.0
    hooks:
      - id: golangci-lint
  - repo: https://github.com/TekWizely/pre-commit-golang
    rev: v1.0.0-rc.1
    hooks:
      - id: go-fumpt
        args:
          - -w
      - id: go-mod-tidy


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone. And we mean everyone!

## Our Standards

Examples of behavior that contributes to creating a positive environment
include:

* Using welcoming and kind language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or
 advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
 address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
 professional setting

## Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.

## Scope

This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team initially on Slack to coordinate private communication. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html

[homepage]: https://www.contributor-covenant.org

For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to gocron

Thank you for coming to contribute to gocron! We welcome new ideas, PRs and general feedback.

## Reporting Bugs

If you find a bug then please let the project know by opening an issue after doing the following:

- Do a quick search of the existing issues to make sure the bug isn't already reported
- Try and make a minimal list of steps that can reliably reproduce the bug you are experiencing
- Collect as much information as you can to help identify what the issue is (project version, configuration files, etc)

## Suggesting Enhancements

If you have a use case that you don't see a way to support yet, we would welcome the feedback in an issue. Before opening the issue, please consider:

- Is this a common use case?
- Is it simple to understand?

You can help us out by doing the following before raising a new issue:

- Check that the feature hasn't been requested already by searching existing issues
- Try and reduce your enhancement into a single, concise and deliverable request, rather than a general idea
- Explain your own use cases as the basis of the request

## Adding Features

Pull requests are always welcome. However, before going through the trouble of implementing a change it's worth creating a bug or feature request issue.
This allows us to discuss the changes and make sure they are a good fit for the project.

Please always make sure a pull request has been:

- Unit tested with `make test`
- Linted with `make lint`

## Writing Tests

Tests should follow the [table driven test pattern](https://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go). See other tests in the code base for additional examples.


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2014, 辣椒面

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: Makefile
================================================
.PHONY: fmt lint test mocks test_coverage test_ci

GO_PKGS   := $(shell go list -f {{.Dir}} ./...)

fmt:
	@go list -f {{.Dir}} ./... | xargs -I{} gofmt -w -s {}

lint:
	@grep "^func [a-zA-Z]" example_test.go | sort -c
	@golangci-lint run

test:
	@go test -race -v $(GO_FLAGS) -count=1 $(GO_PKGS)

test_coverage:
	@go test -race -v $(GO_FLAGS) -count=1 -coverprofile=coverage.out -covermode=atomic $(GO_PKGS)

test_ci:
	@go test -race -v $(GO_FLAGS) -count=1 $(GO_PKGS)

mocks:
	@go generate ./...


================================================
FILE: README.md
================================================
# gocron: A Golang Job Scheduling Package

[![CI State](https://github.com/go-co-op/gocron/actions/workflows/go_test.yml/badge.svg?branch=v2&event=push)](https://github.com/go-co-op/gocron/actions)
![Go Report Card](https://goreportcard.com/badge/github.com/go-co-op/gocron) [![Go Doc](https://godoc.org/github.com/go-co-op/gocron/v2?status.svg)](https://pkg.go.dev/github.com/go-co-op/gocron/v2)

gocron is a job scheduling package which lets you run Go functions at pre-determined intervals.

> Looking for a visual interface?  
> Check out [**gocron-ui**](https://github.com/go-co-op/gocron-ui) — a lightweight web dashboard to monitor, trigger, and manage your `gocron` jobs in real time.

If you want to chat, you can find us on Slack at
[<img src="https://img.shields.io/badge/gophers-gocron-brightgreen?logo=slack">](https://gophers.slack.com/archives/CQ7T0T1FW)

## Quick Start

```
go get github.com/go-co-op/gocron/v2
```

```golang
package main

import (
	"fmt"
	"time"

	"github.com/go-co-op/gocron/v2"
)

func main() {
	// create a scheduler
	s, err := gocron.NewScheduler()
	if err != nil {
		// handle error
	}

	// add a job to the scheduler
	j, err := s.NewJob(
		gocron.DurationJob(
			10*time.Second,
		),
		gocron.NewTask(
			func(a string, b int) {
				// do things
			},
			"hello",
			1,
		),
	)
	if err != nil {
		// handle error
	}
	// each job has a unique id
	fmt.Println(j.ID())

	// start the scheduler
	s.Start()

	// block until you are ready to shut down
	select {
	case <-time.After(time.Minute):
	}

	// when you're done, shut it down
	err = s.Shutdown()
	if err != nil {
		// handle error
	}
}
```

## Examples

- [Go doc examples](https://pkg.go.dev/github.com/go-co-op/gocron/v2#pkg-examples)
- [Examples directory](examples)

## Articles & Blog Posts

Community articles and tutorials about using gocron:

- [Building a dynamic, highly available scheduler in Go](https://tech.efg.gg/posts/2025/highly-available-scheduler-in-go/) - A deep dive into building a highly available scheduler using gocron, MongoDB change streams, and leader election patterns for the FACEIT Watch platform.

## Concepts

- **Job**: The job encapsulates a "task", which is made up of a go function and any function parameters. The Job then
  provides the scheduler with the time the job should next be scheduled to run.
- **Scheduler**: The scheduler keeps track of all the jobs and sends each job to the executor when
  it is ready to be run.
- **Executor**: The executor calls the job's task and manages the complexities of different job
  execution timing requirements (e.g. singletons that shouldn't overrun each other, limiting the max number of jobs running)


## Features

### Job types
Jobs can be run at various intervals.
- [**Duration**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#DurationJob):
Jobs can be run at a fixed `time.Duration`.
- [**Random duration**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#DurationRandomJob):
Jobs can be run at a random `time.Duration` between a min and max.
- [**Cron**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#CronJob):
Jobs can be run using a crontab.
- [**Daily**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#DailyJob):
Jobs can be run every x days at specific times.
- [**Weekly**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WeeklyJob):
Jobs can be run every x weeks on specific days of the week and at specific times.
- [**Monthly**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#MonthlyJob):
Jobs can be run every x months on specific days of the month and at specific times.
- [**One time**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#OneTimeJob):
Jobs can be run at specific time(s) (either once or many times).

### Interval Timing
Jobs can be scheduled with different interval timing modes.
- [**Interval from scheduled time (default)**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#DurationJob):
By default, jobs calculate their next run time from when they were scheduled to start, resulting in fixed intervals 
regardless of execution time. Good for cron-like scheduling at predictable times.
- [**Interval from completion time**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithIntervalFromCompletion):
Jobs can calculate their next run time from when they complete, ensuring consistent rest periods between executions.
Ideal for rate-limited APIs, resource-intensive jobs, and scenarios where execution time varies.

### Concurrency Limits
Jobs can be limited individually or across the entire scheduler.
- [**Per job limiting with singleton mode**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithSingletonMode):
Jobs can be limited to a single concurrent execution that either reschedules (skips overlapping executions)
or queues (waits for the previous execution to finish).
- [**Per scheduler limiting with limit mode**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithLimitConcurrentJobs):
Jobs can be limited to a certain number of concurrent executions across the entire scheduler
using either reschedule (skip when the limit is met) or queue (jobs are added to a queue to
wait for the limit to be available).
- **Note:** A scheduler limit and a job limit can both be enabled.

### Distributed instances of gocron
Multiple instances of gocron can be run.
- [**Elector**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithDistributedElector):
An elector can be used to elect a single instance of gocron to run as the primary with the
other instances checking to see if a new leader needs to be elected.
  - Implementations: [go-co-op electors](https://github.com/go-co-op?q=-elector&type=all&language=&sort=)
    (don't see what you need? request on slack to get a repo created to contribute it!)
- [**Locker**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithDistributedLocker):
A locker can be used to lock each run of a job to a single instance of gocron.
Locker can be at job or scheduler, if it is defined both at job and scheduler then locker of job will take precedence.
  - See Notes in the doc for [Locker](https://pkg.go.dev/github.com/go-co-op/gocron/v2#Locker) for
    details and limitations of the locker design.
  - Implementations: [go-co-op lockers](https://github.com/go-co-op?q=-lock&type=all&language=&sort=)
    (don't see what you need? request on slack to get a repo created to contribute it!)

### Events
Job events can trigger actions.
- [**Listeners**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithEventListeners):
Can be added to a job, with [event listeners](https://pkg.go.dev/github.com/go-co-op/gocron/v2#EventListener),
or all jobs across the
[scheduler](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithGlobalJobOptions)
to listen for job events and trigger actions.

### Options
Many job and scheduler options are available.
- [**Job options**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#JobOption):
Job options can be set when creating a job using `NewJob`.
- [**Global job options**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithGlobalJobOptions):
Global job options can be set when creating a scheduler using `NewScheduler`
and the `WithGlobalJobOptions` option.
- [**Scheduler options**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#SchedulerOption):
Scheduler options can be set when creating a scheduler using `NewScheduler`.

### Logging
Logs can be enabled.
- [Logger](https://pkg.go.dev/github.com/go-co-op/gocron/v2#Logger):
The Logger interface can be implemented with your desired logging library.
The provided NewLogger uses the standard library's log package.

### Metrics
Metrics may be collected from the execution of each job and scheduler lifecycle events.
- [**Monitor**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#Monitor):
A monitor can be used to collect metrics for each job from a scheduler.
  - Implementations: There are currently no open source implementations of the Monitor interface available.
    We'd love for you to be the first to contribute one! Check out the [Monitor interface documentation](https://pkg.go.dev/github.com/go-co-op/gocron/v2#Monitor)
    to get started, or reach out on [Slack](https://gophers.slack.com/archives/CQ7T0T1FW) if you'd like to discuss your implementation.
- [**MonitorStatus**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#MonitorStatus):
Extends Monitor with status and error tracking for each job.
  - Implementations: There are currently no open source implementations of the MonitorStatus interface available.
    We'd love for you to be the first to contribute one! Check out the [MonitorStatus interface documentation](https://pkg.go.dev/github.com/go-co-op/gocron/v2#MonitorStatus)
    to get started, or reach out on [Slack](https://gophers.slack.com/archives/CQ7T0T1FW) if you'd like to discuss your implementation.
- [**SchedulerMonitor**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#SchedulerMonitor):
A scheduler monitor provides comprehensive observability into scheduler and job lifecycle events.

  **Available Metrics:**
  - **Scheduler Lifecycle**: `SchedulerStarted`, `SchedulerStopped`, `SchedulerShutdown`
  - **Job Management**: `JobRegistered`, `JobUnregistered` - track jobs added/removed from scheduler
  - **Job Execution**: `JobStarted`, `JobRunning`, `JobCompleted`, `JobFailed` - monitor job execution flow
  - **Performance**: `JobExecutionTime`, `JobSchedulingDelay` - measure job duration and scheduling lag
  - **Concurrency**: `ConcurrencyLimitReached` - detect when singleton or limit mode constraints are hit

  **Derived Metrics** (calculable from events):
  - Error rate: `JobFailed / (JobCompleted + JobFailed)`
  - Average execution time: from `JobExecutionTime` events
  - Active jobs: `JobRegistered - JobUnregistered`
  - Current queue depth: `JobStarted - (JobCompleted + JobFailed)`

  **Example - Prometheus Integration:**
  ```go
  type PrometheusMonitor struct {
      jobsCompleted   prometheus.Counter
      jobsFailed      prometheus.Counter
      executionTime   prometheus.Histogram
      schedulingDelay prometheus.Histogram
  }

  func (p *PrometheusMonitor) JobExecutionTime(job gocron.Job, duration time.Duration) {
      p.executionTime.Observe(duration.Seconds())
  }

  func (p *PrometheusMonitor) JobSchedulingDelay(job gocron.Job, scheduled, actual time.Time) {
      if delay := actual.Sub(scheduled); delay > 0 {
          p.schedulingDelay.Observe(delay.Seconds())
      }
  }

  // Initialize scheduler with monitor
  s, _ := gocron.NewScheduler(gocron.WithSchedulerMonitor(monitor))
  ```

  **Use Cases:** Prometheus metrics, custom dashboards, alerting systems, performance monitoring

  - Implementations: There are currently no open source implementations of the SchedulerMonitor interface available.
    We'd love for you to be the first to contribute one! Check out the [SchedulerMonitor interface documentation](https://pkg.go.dev/github.com/go-co-op/gocron/v2#SchedulerMonitor)
    to get started, or reach out on [Slack](https://gophers.slack.com/archives/CQ7T0T1FW) if you'd like to discuss your implementation.

### Testing
The gocron library is set up to enable testing.
- Mocks are provided in [the mock package](mocks) using [gomock](https://github.com/uber-go/mock).
- Time can be mocked by passing in a [FakeClock](https://pkg.go.dev/github.com/jonboulle/clockwork#FakeClock)
to [WithClock](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithClock) -
see the [example on WithClock](https://pkg.go.dev/github.com/go-co-op/gocron/v2#example-WithClock).

## Supporters

We appreciate the support for free and open source software!

This project is supported by:

[JetBrains](https://www.jetbrains.com/?from=gocron)

<a href="https://www.jetbrains.com/?from=gocron">
 <picture>
   <source media="(prefers-color-scheme: dark)" srcset="assets/jetbrains-mono-white.png" />
   <source media="(prefers-color-scheme: light)" srcset="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.png" />
   <img alt="JetBrains logo" src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.png" />
 </picture>
</a>

[Sentry](https://sentry.io/welcome/)

<a href="https://sentry.io/?utm_source=github&utm_medium=logo">
 <picture>
   <source media="(prefers-color-scheme: dark)" srcset="assets/sentry-wordmark-light-280x84.png" />
   <source media="(prefers-color-scheme: light)" srcset="https://sentry-brand.storage.googleapis.com/sentry-wordmark-dark-280x84.png" />
   <img alt="Sentry logo" src="https://sentry-brand.storage.googleapis.com/sentry-wordmark-dark-280x84.png" />
 </picture>
</a>

## Star History

<a href="https://www.star-history.com/#go-co-op/gocron&Date">
 <picture>
   <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=go-co-op/gocron&type=Date&theme=dark" />
   <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=go-co-op/gocron&type=Date" />
   <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=go-co-op/gocron&type=Date" />
 </picture>
</a>


================================================
FILE: SECURITY.md
================================================
# Security Policy

## Supported Versions

The current plan is to maintain version 2 as long as possible incorporating any necessary security patches. Version 1 is deprecated and will no longer be patched.

| Version | Supported          |
| ------- | ------------------ |
| 1.x.x   | :heavy_multiplication_x: |
| 2.x.x   | :white_check_mark: |

## Reporting a Vulnerability

Vulnerabilities can be reported by [opening an issue](https://github.com/go-co-op/gocron/issues/new/choose) or reaching out on Slack: [<img src="https://img.shields.io/badge/gophers-gocron-brightgreen?logo=slack">](https://gophers.slack.com/archives/CQ7T0T1FW)

We will do our best to address any vulnerabilities in an expeditious manner.


================================================
FILE: distributed.go
================================================
//go:generate mockgen -destination=mocks/distributed.go -package=gocronmocks . Elector,Locker,Lock
package gocron

import (
	"context"
)

// Elector determines the leader from instances asking to be the leader. Only
// the leader runs jobs. If the leader goes down, a new leader will be elected.
type Elector interface {
	// IsLeader should return  nil if the job should be scheduled by the instance
	// making the request and an error if the job should not be scheduled.
	IsLeader(context.Context) error
}

// Locker represents the required interface to lock jobs when running multiple schedulers.
// The lock is held for the duration of the job's run, and it is expected that the
// locker implementation handles time splay between schedulers.
// The lock key passed is the job's name - which, if not set, defaults to the
// go function's name, e.g. "pkg.myJob" for func myJob() {} in pkg
//
// Notes: The locker and scheduler do not handle synchronization of run times across
// schedulers.
//
//  1. If you are using duration based jobs (DurationJob), you can utilize the JobOption
//     WithStartAt to set a start time for the job to the nearest time rounded to your
//     duration. For example, if you have a job that runs every 5 minutes, you can set
//     the start time to the nearest 5 minute e.g. 12:05, 12:10.
//
//  2. For all jobs, the implementation is still vulnerable to clockskew between scheduler
//     instances. This may result in a single scheduler instance running the majority of the
//     jobs.
//
// For distributed jobs, consider utilizing the Elector option if these notes are not acceptable
// to your use case.
type Locker interface {
	// Lock if an error is returned by lock, the job will not be scheduled.
	Lock(ctx context.Context, key string) (Lock, error)
}

// Lock represents an obtained lock. The lock is released after the execution of the job
// by the scheduler.
type Lock interface {
	Unlock(ctx context.Context) error
}


================================================
FILE: errors.go
================================================
package gocron

import (
	"errors"
)

// Public error definitions
var (
	ErrCronJobInvalid                = errors.New("gocron: CronJob: invalid crontab")
	ErrCronJobParse                  = errors.New("gocron: CronJob: crontab parse failure")
	ErrDailyJobAtTimeNil             = errors.New("gocron: DailyJob: atTime within atTimes must not be nil")
	ErrDailyJobAtTimesNil            = errors.New("gocron: DailyJob: atTimes must not be nil")
	ErrDailyJobHours                 = errors.New("gocron: DailyJob: atTimes hours must be between 0 and 23 inclusive")
	ErrDailyJobZeroInterval          = errors.New("gocron: DailyJob: interval must be greater than 0")
	ErrDailyJobMinutesSeconds        = errors.New("gocron: DailyJob: atTimes minutes and seconds must be between 0 and 59 inclusive")
	ErrDurationJobIntervalZero       = errors.New("gocron: DurationJob: time interval is 0")
	ErrDurationJobIntervalNegative   = errors.New("gocron: DurationJob: time interval must be greater than 0")
	ErrDurationRandomJobPositive     = errors.New("gocron: DurationRandomJob: minimum and maximum durations must be greater than 0")
	ErrDurationRandomJobMinMax       = errors.New("gocron: DurationRandomJob: minimum duration must be less than maximum duration")
	ErrEventListenerFuncNil          = errors.New("gocron: eventListenerFunc must not be nil")
	ErrJobNotFound                   = errors.New("gocron: job not found")
	ErrJobRunNowFailed               = errors.New("gocron: Job: RunNow: scheduler unreachable")
	ErrMonthlyJobDays                = errors.New("gocron: MonthlyJob: daysOfTheMonth must be between 31 and -31 inclusive, and not 0")
	ErrMonthlyJobAtTimeNil           = errors.New("gocron: MonthlyJob: atTime within atTimes must not be nil")
	ErrMonthlyJobAtTimesNil          = errors.New("gocron: MonthlyJob: atTimes must not be nil")
	ErrMonthlyJobDaysNil             = errors.New("gocron: MonthlyJob: daysOfTheMonth must not be nil")
	ErrMonthlyJobHours               = errors.New("gocron: MonthlyJob: atTimes hours must be between 0 and 23 inclusive")
	ErrMonthlyJobZeroInterval        = errors.New("gocron: MonthlyJob: interval must be greater than 0")
	ErrMonthlyJobMinutesSeconds      = errors.New("gocron: MonthlyJob: atTimes minutes and seconds must be between 0 and 59 inclusive")
	ErrNewJobTaskNil                 = errors.New("gocron: NewJob: Task must not be nil")
	ErrNewJobTaskNotFunc             = errors.New("gocron: NewJob: Task.Function must be of kind reflect.Func")
	ErrNewJobWrongNumberOfParameters = errors.New("gocron: NewJob: Number of provided parameters does not match expected")
	ErrNewJobWrongTypeOfParameters   = errors.New("gocron: NewJob: Type of provided parameters does not match expected")
	ErrOneTimeJobStartDateTimePast   = errors.New("gocron: OneTimeJob: start must not be in the past")
	ErrStopExecutorTimedOut          = errors.New("gocron: timed out waiting for executor to stop")
	ErrStopJobsTimedOut              = errors.New("gocron: timed out waiting for jobs to finish")
	ErrStopSchedulerTimedOut         = errors.New("gocron: timed out waiting for scheduler to stop")
	ErrWeeklyJobAtTimeNil            = errors.New("gocron: WeeklyJob: atTime within atTimes must not be nil")
	ErrWeeklyJobAtTimesNil           = errors.New("gocron: WeeklyJob: atTimes must not be nil")
	ErrWeeklyJobDaysOfTheWeekNil     = errors.New("gocron: WeeklyJob: daysOfTheWeek must not be nil")
	ErrWeeklyJobHours                = errors.New("gocron: WeeklyJob: atTimes hours must be between 0 and 23 inclusive")
	ErrWeeklyJobZeroInterval         = errors.New("gocron: WeeklyJob: interval must be greater than 0")
	ErrWeeklyJobMinutesSeconds       = errors.New("gocron: WeeklyJob: atTimes minutes and seconds must be between 0 and 59 inclusive")
	ErrPanicRecovered                = errors.New("gocron: panic recovered")
	ErrWithClockNil                  = errors.New("gocron: WithClock: clock must not be nil")
	ErrWithContextNil                = errors.New("gocron: WithContext: context must not be nil")
	ErrWithDistributedElectorNil     = errors.New("gocron: WithDistributedElector: elector must not be nil")
	ErrWithDistributedLockerNil      = errors.New("gocron: WithDistributedLocker: locker must not be nil")
	ErrWithDistributedJobLockerNil   = errors.New("gocron: WithDistributedJobLocker: locker must not be nil")
	ErrWithIdentifierNil             = errors.New("gocron: WithIdentifier: identifier must not be nil")
	ErrSchedulerMonitorNil           = errors.New("gocron: WithSchedulerMonitor: monitor must not be nil")
	ErrWithLimitConcurrentJobsZero   = errors.New("gocron: WithLimitConcurrentJobs: limit must be greater than 0")
	ErrWithLocationNil               = errors.New("gocron: WithLocation: location must not be nil")
	ErrWithLoggerNil                 = errors.New("gocron: WithLogger: logger must not be nil")
	ErrWithMonitorNil                = errors.New("gocron: WithMonitor: monitor must not be nil")
	ErrWithNameEmpty                 = errors.New("gocron: WithName: name must not be empty")
	ErrWithStartDateTimePast         = errors.New("gocron: WithStartDateTime: start must not be in the past")
	ErrWithStartDateTimePastZero     = errors.New("gocron: WithStartDateTime: start must not be zero")
	ErrWithStopDateTimePast          = errors.New("gocron: WithStopDateTime: end must not be in the past")
	ErrStartTimeLaterThanEndTime     = errors.New("gocron: WithStartDateTime: start must not be later than end")
	ErrStopTimeEarlierThanStartTime  = errors.New("gocron: WithStopDateTime: end must not be earlier than start")
	ErrWithStopTimeoutZeroOrNegative = errors.New("gocron: WithStopTimeout: timeout must be greater than 0")
	ErrWithSchedulerMonitorNil       = errors.New("gocron: WithSchedulerMonitor: scheduler monitor cannot be nil")
	ErrWithLimitedRunsZero           = errors.New("gocron: WithLimitedRuns: limit must be greater than 0")
)

// internal errors
var (
	errAtTimeNil    = errors.New("errAtTimeNil")
	errAtTimesNil   = errors.New("errAtTimesNil")
	errAtTimeHours  = errors.New("errAtTimeHours")
	errAtTimeMinSec = errors.New("errAtTimeMinSec")
)


================================================
FILE: example_test.go
================================================
package gocron_test

import (
	"context"
	"errors"
	"fmt"
	"sync"
	"time"

	"github.com/go-co-op/gocron/v2"
	"github.com/google/uuid"
	"github.com/jonboulle/clockwork"
)

func ExampleAfterJobRuns() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	_, _ = s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func() {},
		),
		gocron.WithEventListeners(
			gocron.AfterJobRuns(
				func(jobID uuid.UUID, jobName string) {
					// do something after the job completes
				},
			),
		),
	)
}

func ExampleAfterJobRunsWithError() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	_, _ = s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func() {},
		),
		gocron.WithEventListeners(
			gocron.AfterJobRunsWithError(
				func(jobID uuid.UUID, jobName string, err error) {
					// do something when the job returns an error
				},
			),
		),
	)
}

var _ gocron.Locker = new(errorLocker)

type errorLocker struct{}

func (e errorLocker) Lock(_ context.Context, _ string) (gocron.Lock, error) {
	return nil, errors.New("locked")
}

func ExampleAfterLockError() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	_, _ = s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func() {},
		),
		gocron.WithDistributedJobLocker(&errorLocker{}),
		gocron.WithEventListeners(
			gocron.AfterLockError(
				func(jobID uuid.UUID, jobName string, err error) {
					// do something immediately before the job is run
				},
			),
		),
	)
}

func ExampleBeforeJobRuns() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	_, _ = s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func() {},
		),
		gocron.WithEventListeners(
			gocron.BeforeJobRuns(
				func(jobID uuid.UUID, jobName string) {
					// do something immediately before the job is run
				},
			),
		),
	)
}

func ExampleBeforeJobRunsSkipIfBeforeFuncErrors() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	_, _ = s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func() {
				fmt.Println("Will never run, because before job func errors")
			},
		),
		gocron.WithEventListeners(
			gocron.BeforeJobRunsSkipIfBeforeFuncErrors(
				func(jobID uuid.UUID, jobName string) error {
					return errors.New("error")
				},
			),
		),
	)
}

func ExampleCronJob() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	_, _ = s.NewJob(
		gocron.CronJob(
			// standard cron tab parsing
			"1 * * * *",
			false,
		),
		gocron.NewTask(
			func() {},
		),
	)
	_, _ = s.NewJob(
		gocron.CronJob(
			// optionally include seconds as the first field
			"* 1 * * * *",
			true,
		),
		gocron.NewTask(
			func() {},
		),
	)
}

func ExampleDailyJob() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	_, _ = s.NewJob(
		gocron.DailyJob(
			1,
			gocron.NewAtTimes(
				gocron.NewAtTime(10, 30, 0),
				gocron.NewAtTime(14, 0, 0),
			),
		),
		gocron.NewTask(
			func(a, b string) {},
			"a",
			"b",
		),
	)
}

func ExampleDurationJob() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	_, _ = s.NewJob(
		gocron.DurationJob(
			time.Second*5,
		),
		gocron.NewTask(
			func() {},
		),
	)
}

func ExampleDurationRandomJob() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	_, _ = s.NewJob(
		gocron.DurationRandomJob(
			time.Second,
			5*time.Second,
		),
		gocron.NewTask(
			func() {},
		),
	)
}

func ExampleJob_id() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	j, _ := s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func() {},
		),
	)

	fmt.Println(j.ID())
}

func ExampleJob_lastRun() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	j, _ := s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func() {},
		),
	)

	fmt.Println(j.LastRun())
}

func ExampleJob_name() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	j, _ := s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func() {},
		),
		gocron.WithName("foobar"),
	)

	fmt.Println(j.Name())
	// Output:
	// foobar
}

func ExampleJob_nextRun() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	j, _ := s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func() {},
		),
	)

	// NextRun is only available after the scheduler has been started.
	s.Start()

	nextRun, _ := j.NextRun()
	fmt.Println(nextRun)
}

func ExampleJob_nextRuns() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	j, _ := s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func() {},
		),
	)

	// NextRuns is only available after the scheduler has been started.
	s.Start()

	nextRuns, _ := j.NextRuns(5)
	fmt.Println(nextRuns)
}

func ExampleJob_runNow() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	j, _ := s.NewJob(
		gocron.MonthlyJob(
			1,
			gocron.NewDaysOfTheMonth(3, -5, -1),
			gocron.NewAtTimes(
				gocron.NewAtTime(10, 30, 0),
				gocron.NewAtTime(11, 15, 0),
			),
		),
		gocron.NewTask(
			func() {},
		),
	)
	s.Start()
	// Runs the job one time now, without impacting the schedule
	_ = j.RunNow()
}

func ExampleJob_tags() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	j, _ := s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func() {},
		),
		gocron.WithTags("foo", "bar"),
	)

	fmt.Println(j.Tags())
	// Output:
	// [foo bar]
}

func ExampleMonthlyJob() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	_, _ = s.NewJob(
		gocron.MonthlyJob(
			1,
			gocron.NewDaysOfTheMonth(3, -5, -1),
			gocron.NewAtTimes(
				gocron.NewAtTime(10, 30, 0),
				gocron.NewAtTime(11, 15, 0),
			),
		),
		gocron.NewTask(
			func() {},
		),
	)
}

func ExampleNewDefaultCron() {
	c := gocron.NewDefaultCron(true)
	err := c.IsValid("* * * * * *", time.Local, time.Now())
	if err != nil {
		panic(err)
	}
}

func ExampleNewScheduler() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	fmt.Println(s.Jobs())
}

func ExampleNewTask() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	_, _ = s.NewJob(
		gocron.DurationJob(time.Second),
		gocron.NewTask(
			func(ctx context.Context) {
				// gocron will pass in a context (either the default Job context, or one
				// provided via WithContext) to the job and will cancel the context on shutdown.
				// This allows you to listen for and handle cancellation within your job.
			},
		),
	)
}

func ExampleOneTimeJob() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	// run a job once, immediately
	_, _ = s.NewJob(
		gocron.OneTimeJob(
			gocron.OneTimeJobStartImmediately(),
		),
		gocron.NewTask(
			func() {},
		),
	)
	// run a job once in 10 seconds
	_, _ = s.NewJob(
		gocron.OneTimeJob(
			gocron.OneTimeJobStartDateTime(time.Now().Add(10*time.Second)),
		),
		gocron.NewTask(
			func() {},
		),
	)
	// run job twice - once in 10 seconds and once in 55 minutes
	n := time.Now()
	_, _ = s.NewJob(
		gocron.OneTimeJob(
			gocron.OneTimeJobStartDateTimes(
				n.Add(10*time.Second),
				n.Add(55*time.Minute),
			),
		),
		gocron.NewTask(func() {}),
	)

	s.Start()
}

func ExampleScheduler_jobs() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	_, _ = s.NewJob(
		gocron.DurationJob(
			10*time.Second,
		),
		gocron.NewTask(
			func() {},
		),
	)
	fmt.Println(len(s.Jobs()))
	// Output:
	// 1
}

func ExampleScheduler_newJob() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	j, err := s.NewJob(
		gocron.DurationJob(
			10*time.Second,
		),
		gocron.NewTask(
			func() {},
		),
	)
	if err != nil {
		panic(err)
	}
	fmt.Println(j.ID())
}

func ExampleScheduler_removeByTags() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	_, _ = s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func() {},
		),
		gocron.WithTags("tag1"),
	)
	_, _ = s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func() {},
		),
		gocron.WithTags("tag2"),
	)
	fmt.Println(len(s.Jobs()))

	s.RemoveByTags("tag1", "tag2")

	fmt.Println(len(s.Jobs()))
	// Output:
	// 2
	// 0
}

func ExampleScheduler_removeJob() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	j, _ := s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func() {},
		),
	)

	fmt.Println(len(s.Jobs()))

	_ = s.RemoveJob(j.ID())

	fmt.Println(len(s.Jobs()))
	// Output:
	// 1
	// 0
}

func ExampleScheduler_shutdown() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()
}

func ExampleScheduler_start() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	_, _ = s.NewJob(
		gocron.CronJob(
			"* * * * *",
			false,
		),
		gocron.NewTask(
			func() {},
		),
	)

	s.Start()
}

func ExampleScheduler_stopJobs() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	_, _ = s.NewJob(
		gocron.CronJob(
			"* * * * *",
			false,
		),
		gocron.NewTask(
			func() {},
		),
	)

	s.Start()

	_ = s.StopJobs()
}

func ExampleScheduler_update() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	j, _ := s.NewJob(
		gocron.CronJob(
			"* * * * *",
			false,
		),
		gocron.NewTask(
			func() {},
		),
	)

	s.Start()

	// after some time, need to change the job

	j, _ = s.Update(
		j.ID(),
		gocron.DurationJob(
			5*time.Second,
		),
		gocron.NewTask(
			func() {},
		),
	)
}

func ExampleWeeklyJob() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	_, _ = s.NewJob(
		gocron.WeeklyJob(
			2,
			gocron.NewWeekdays(time.Tuesday, time.Wednesday, time.Saturday),
			gocron.NewAtTimes(
				gocron.NewAtTime(1, 30, 0),
				gocron.NewAtTime(12, 0, 30),
			),
		),
		gocron.NewTask(
			func() {},
		),
	)
}

func ExampleWithClock() {
	fakeClock := clockwork.NewFakeClock()
	s, _ := gocron.NewScheduler(
		gocron.WithClock(fakeClock),
	)
	var wg sync.WaitGroup
	wg.Add(1)
	_, _ = s.NewJob(
		gocron.DurationJob(
			time.Second*5,
		),
		gocron.NewTask(
			func(one string, two int) {
				fmt.Printf("%s, %d\n", one, two)
				wg.Done()
			},
			"one", 2,
		),
	)
	s.Start()
	_ = fakeClock.BlockUntilContext(context.Background(), 1)
	fakeClock.Advance(time.Second * 5)
	wg.Wait()
	_ = s.StopJobs()
	// Output:
	// one, 2
}

func ExampleWithContext() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	_, _ = s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func(ctx context.Context) {
				// gocron will pass in the context provided via WithContext
				// to the job and will cancel the context on shutdown.
				// This allows you to listen for and handle cancellation within your job.
			},
		),
		gocron.WithContext(ctx),
	)
}

var _ gocron.Cron = (*customCron)(nil)

type customCron struct{}

func (c customCron) IsValid(crontab string, location *time.Location, now time.Time) error {
	return nil
}

func (c customCron) Next(lastRun time.Time) time.Time {
	return time.Now().Add(time.Second)
}

func ExampleWithCronImplementation() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()
	_, _ = s.NewJob(
		gocron.CronJob(
			"* * * * *",
			false,
		),
		gocron.NewTask(
			func() {},
		),
		gocron.WithCronImplementation(
			&customCron{},
		),
	)
}

func ExampleWithDisabledDistributedJobLocker() {
	// var _ gocron.Locker = (*myLocker)(nil)
	//
	// type myLocker struct{}
	//
	// func (m myLocker) Lock(ctx context.Context, key string) (Lock, error) {
	//     return &testLock{}, nil
	// }
	//
	// var _ gocron.Lock = (*testLock)(nil)
	//
	// type testLock struct{}
	//
	// func (t testLock) Unlock(_ context.Context) error {
	//     return nil
	// }

	locker := &myLocker{}

	s, _ := gocron.NewScheduler(
		gocron.WithDistributedLocker(locker),
	)

	_, _ = s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func() {},
		),
		gocron.WithDisabledDistributedJobLocker(true),
	)
}

var _ gocron.Elector = (*myElector)(nil)

type myElector struct{}

func (m myElector) IsLeader(_ context.Context) error {
	return nil
}

func ExampleWithDistributedElector() {
	// var _ gocron.Elector = (*myElector)(nil)
	//
	// type myElector struct{}
	//
	// func (m myElector) IsLeader(_ context.Context) error {
	//     return nil
	// }
	//
	elector := &myElector{}

	_, _ = gocron.NewScheduler(
		gocron.WithDistributedElector(elector),
	)
}

var _ gocron.Locker = (*myLocker)(nil)

type myLocker struct{}

func (m myLocker) Lock(ctx context.Context, key string) (gocron.Lock, error) {
	return &testLock{}, nil
}

var _ gocron.Lock = (*testLock)(nil)

type testLock struct{}

func (t testLock) Unlock(_ context.Context) error {
	return nil
}

func ExampleWithDistributedLocker() {
	// var _ gocron.Locker = (*myLocker)(nil)
	//
	// type myLocker struct{}
	//
	// func (m myLocker) Lock(ctx context.Context, key string) (Lock, error) {
	//     return &testLock{}, nil
	// }
	//
	// var _ gocron.Lock = (*testLock)(nil)
	//
	// type testLock struct{}
	//
	// func (t testLock) Unlock(_ context.Context) error {
	//     return nil
	// }

	locker := &myLocker{}

	_, _ = gocron.NewScheduler(
		gocron.WithDistributedLocker(locker),
	)
}

func ExampleWithEventListeners() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	_, _ = s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func() {},
		),
		gocron.WithEventListeners(
			gocron.AfterJobRuns(
				func(jobID uuid.UUID, jobName string) {
					// do something after the job completes
				},
			),
			gocron.AfterJobRunsWithError(
				func(jobID uuid.UUID, jobName string, err error) {
					// do something when the job returns an error
				},
			),
			gocron.BeforeJobRuns(
				func(jobID uuid.UUID, jobName string) {
					// do something immediately before the job is run
				},
			),
		),
	)
}

func ExampleWithGlobalJobOptions() {
	s, _ := gocron.NewScheduler(
		gocron.WithGlobalJobOptions(
			gocron.WithTags("tag1", "tag2", "tag3"),
		),
	)

	j, _ := s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func(one string, two int) {
				fmt.Printf("%s, %d", one, two)
			},
			"one", 2,
		),
	)
	// The job will have the globally applied tags
	fmt.Println(j.Tags())

	s2, _ := gocron.NewScheduler(
		gocron.WithGlobalJobOptions(
			gocron.WithTags("tag1", "tag2", "tag3"),
		),
	)
	j2, _ := s2.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func(one string, two int) {
				fmt.Printf("%s, %d", one, two)
			},
			"one", 2,
		),
		gocron.WithTags("tag4", "tag5", "tag6"),
	)
	// The job will have the tags set specifically on the job
	// overriding those set globally by the scheduler
	fmt.Println(j2.Tags())
	// Output:
	// [tag1 tag2 tag3]
	// [tag4 tag5 tag6]
}

func ExampleWithIdentifier() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	j, _ := s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func(one string, two int) {
				fmt.Printf("%s, %d", one, two)
			},
			"one", 2,
		),
		gocron.WithIdentifier(uuid.MustParse("87b95dfc-3e71-11ef-9454-0242ac120002")),
	)
	fmt.Println(j.ID())
	// Output:
	// 87b95dfc-3e71-11ef-9454-0242ac120002
}

func ExampleWithIntervalFromCompletion() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	_, _ = s.NewJob(
		gocron.DurationJob(
			5*time.Minute,
		),
		gocron.NewTask(
			func() {
				time.Sleep(30 * time.Second)
			},
		),
		gocron.WithIntervalFromCompletion(),
	)

	// Without WithIntervalFromCompletion (default behavior):
	// If the job starts at 00:00 and completes at 00:00:30,
	// the next job starts at 00:05:00 (only 4m30s rest).

	// With WithIntervalFromCompletion:
	// If the job starts at 00:00 and completes at 00:00:30,
	// the next job starts at 00:05:30 (full 5m rest).
}

func ExampleWithLimitConcurrentJobs() {
	_, _ = gocron.NewScheduler(
		gocron.WithLimitConcurrentJobs(
			1,
			gocron.LimitModeReschedule,
		),
	)
}

func ExampleWithLimitedRuns() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	_, _ = s.NewJob(
		gocron.DurationJob(
			time.Millisecond,
		),
		gocron.NewTask(
			func(one string, two int) {
				fmt.Printf("%s, %d\n", one, two)
			},
			"one", 2,
		),
		gocron.WithLimitedRuns(1),
	)
	s.Start()

	time.Sleep(100 * time.Millisecond)
	_ = s.StopJobs()
	fmt.Printf("no jobs in scheduler: %v\n", s.Jobs())
	// Output:
	// one, 2
	// no jobs in scheduler: []
}

func ExampleWithLocation() {
	location, _ := time.LoadLocation("Asia/Kolkata")

	_, _ = gocron.NewScheduler(
		gocron.WithLocation(location),
	)
}

func ExampleWithLogger() {
	_, _ = gocron.NewScheduler(
		gocron.WithLogger(
			gocron.NewLogger(gocron.LogLevelDebug),
		),
	)
}

func ExampleWithMonitor() {
	//type exampleMonitor struct {
	//	mu      sync.Mutex
	//	counter map[string]int
	//	time    map[string][]time.Duration
	//}
	//
	//func newExampleMonitor() *exampleMonitor {
	//	return &exampleMonitor{
	//	counter: make(map[string]int),
	//	time:    make(map[string][]time.Duration),
	//}
	//}
	//
	//func (t *exampleMonitor) IncrementJob(_ uuid.UUID, name string, _ []string, _ JobStatus) {
	//	t.mu.Lock()
	//	defer t.mu.Unlock()
	//	_, ok := t.counter[name]
	//	if !ok {
	//		t.counter[name] = 0
	//	}
	//	t.counter[name]++
	//}
	//
	//func (t *exampleMonitor) RecordJobTiming(startTime, endTime time.Time, _ uuid.UUID, name string, _ []string) {
	//	t.mu.Lock()
	//	defer t.mu.Unlock()
	//	_, ok := t.time[name]
	//	if !ok {
	//		t.time[name] = make([]time.Duration, 0)
	//	}
	//	t.time[name] = append(t.time[name], endTime.Sub(startTime))
	//}
	//
	//monitor := newExampleMonitor()
	//s, _ := NewScheduler(
	//	WithMonitor(monitor),
	//)
	//name := "example"
	//_, _ = s.NewJob(
	//	DurationJob(
	//		time.Second,
	//	),
	//	NewTask(
	//		func() {
	//			time.Sleep(1 * time.Second)
	//		},
	//	),
	//	WithName(name),
	//	WithStartAt(
	//		WithStartImmediately(),
	//	),
	//)
	//s.Start()
	//time.Sleep(5 * time.Second)
	//_ = s.Shutdown()
	//
	//fmt.Printf("Job %q total execute count: %d\n", name, monitor.counter[name])
	//for i, val := range monitor.time[name] {
	//	fmt.Printf("Job %q execute #%d elapsed %.4f seconds\n", name, i+1, val.Seconds())
	//}
}

func ExampleWithName() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	j, _ := s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func(one string, two int) {
				fmt.Printf("%s, %d", one, two)
			},
			"one", 2,
		),
		gocron.WithName("job 1"),
	)
	fmt.Println(j.Name())
	// Output:
	// job 1
}

func ExampleWithSingletonMode() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	_, _ = s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func() {
				// this job will skip half it's executions
				// and effectively run every 2 seconds
				time.Sleep(1500 * time.Second)
			},
		),
		gocron.WithSingletonMode(gocron.LimitModeReschedule),
	)
}

func ExampleWithStartAt() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	start := time.Date(9999, 9, 9, 9, 9, 9, 9, time.UTC)

	j, _ := s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func(one string, two int) {
				fmt.Printf("%s, %d", one, two)
			},
			"one", 2,
		),
		gocron.WithStartAt(
			gocron.WithStartDateTime(start),
		),
	)
	s.Start()

	next, _ := j.NextRun()
	fmt.Println(next)

	_ = s.StopJobs()
	// Output:
	// 9999-09-09 09:09:09.000000009 +0000 UTC
}

func ExampleWithStartDateTime() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	start := time.Date(9999, 9, 9, 9, 9, 9, 9, time.UTC)

	j, _ := s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func(one string, two int) {
				fmt.Printf("%s, %d", one, two)
			},
			"one", 2,
		),
		gocron.WithStartAt(
			gocron.WithStartDateTime(start),
		),
	)
	s.Start()

	next, _ := j.NextRun()
	fmt.Println(next)

	_ = s.StopJobs()
	// Output:
	// 9999-09-09 09:09:09.000000009 +0000 UTC
}

func ExampleWithStartDateTimePast() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	start := time.Now().Add(-time.Minute)

	j, _ := s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func(one string, two int) {
				fmt.Printf("%s, %d", one, two)
			},
			"one", 2,
		),
		gocron.WithStartAt(
			gocron.WithStartDateTimePast(start),
		),
	)
	s.Start()

	time.Sleep(100 * time.Millisecond)

	_, _ = j.NextRun()

	_ = s.StopJobs()
}

func ExampleWithStartImmediately() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	j, _ := s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func(one string, two int) {
				fmt.Printf("%s, %d", one, two)
			},
			"one", 2,
		),
		gocron.WithStartAt(
			gocron.WithStartImmediately(),
		),
	)
	s.Start()

	_, _ = j.NextRun()

	_ = s.StopJobs()
}

func ExampleWithStopTimeout() {
	_, _ = gocron.NewScheduler(
		gocron.WithStopTimeout(time.Second * 5),
	)
}

func ExampleWithTags() {
	s, _ := gocron.NewScheduler()
	defer func() { _ = s.Shutdown() }()

	j, _ := s.NewJob(
		gocron.DurationJob(
			time.Second,
		),
		gocron.NewTask(
			func(one string, two int) {
				fmt.Printf("%s, %d", one, two)
			},
			"one", 2,
		),
		gocron.WithTags("tag1", "tag2", "tag3"),
	)
	fmt.Println(j.Tags())
	// Output:
	// [tag1 tag2 tag3]
}


================================================
FILE: examples/elector/main.go
================================================
package main

import (
	"context"
	"errors"
	"log"
	"time"

	"github.com/go-co-op/gocron/v2"
)

var _ gocron.Elector = (*myElector)(nil)

type myElector struct {
	num    int
	leader bool
}

func (m myElector) IsLeader(_ context.Context) error {
	if m.leader {
		log.Printf("node %d is leader", m.num)
		return nil
	}
	log.Printf("node %d is not leader", m.num)
	return errors.New("not leader")
}

func main() {
	log.SetFlags(log.LstdFlags | log.Lmicroseconds)

	for i := 0; i < 3; i++ {
		go func(i int) {
			elector := &myElector{
				num: i,
			}
			if i == 0 {
				elector.leader = true
			}

			scheduler, err := gocron.NewScheduler(
				gocron.WithDistributedElector(elector),
			)
			if err != nil {
				log.Println(err)
				return
			}

			_, err = scheduler.NewJob(
				gocron.DurationJob(time.Second),
				gocron.NewTask(func() {
					log.Println("run job")
				}),
			)
			if err != nil {
				log.Println(err)
				return
			}
			scheduler.Start()

			if i == 0 {
				time.Sleep(5 * time.Second)
				elector.leader = false
			}
			if i == 1 {
				time.Sleep(5 * time.Second)
				elector.leader = true
			}
		}(i)
	}

	select {} // wait forever
}


================================================
FILE: executor.go
================================================
package gocron

import (
	"context"
	"fmt"
	"strconv"
	"sync"
	"time"

	"github.com/jonboulle/clockwork"

	"github.com/google/uuid"
)

type executor struct {
	// context used for shutting down
	ctx context.Context
	// cancel used by the executor to signal a stop of it's functions
	cancel context.CancelFunc
	// clock used for regular time or mocking time
	clock clockwork.Clock
	// the executor's logger
	logger Logger

	// receives jobs scheduled to execute
	jobsIn chan jobIn
	// sends out jobs for rescheduling
	jobsOutForRescheduling chan uuid.UUID
	// sends out jobs once completed
	jobsOutCompleted chan uuid.UUID
	// used to request jobs from the scheduler
	jobOutRequest chan *jobOutRequest

	// sends out job needs to update the next runs
	jobUpdateNextRuns chan uuid.UUID

	// used by the executor to receive a stop signal from the scheduler
	stopCh chan struct{}
	// ensure that stop runs before the next call to start and only runs once
	stopOnce *sync.Once
	// the timeout value when stopping
	stopTimeout time.Duration
	// used to signal that the executor has completed shutdown
	done chan error

	// runners for any singleton type jobs
	// map[uuid.UUID]singletonRunner
	singletonRunners *sync.Map
	// config for limit mode
	limitMode *limitModeConfig
	// the elector when running distributed instances
	elector Elector
	// the locker when running distributed instances
	locker Locker
	// monitor for reporting metrics
	monitor Monitor
	// monitorStatus for reporting metrics
	monitorStatus MonitorStatus
	// reference to parent scheduler for lifecycle notifications
	scheduler *scheduler
}

type jobIn struct {
	id            uuid.UUID
	shouldSendOut bool
}

type singletonRunner struct {
	in                chan jobIn
	rescheduleLimiter chan struct{}
}

type limitModeConfig struct {
	started           bool
	mode              LimitMode
	limit             uint
	rescheduleLimiter chan struct{}
	in                chan jobIn
	// singletonJobs is used to track singleton jobs that are running
	// in the limit mode runner. This is used to prevent the same job
	// from running multiple times across limit mode runners when both
	// a limit mode and singleton mode are enabled.
	singletonJobs   map[uuid.UUID]struct{}
	singletonJobsMu sync.Mutex
}

func (e *executor) start() {
	e.logger.Debug("gocron: executor started")

	// creating the executor's context here as the executor
	// is the only goroutine that should access this context
	// any other uses within the executor should create a context
	// using the executor context as parent.
	e.ctx, e.cancel = context.WithCancel(context.Background())
	e.stopOnce = &sync.Once{}

	// the standardJobsWg tracks
	standardJobsWg := &waitGroupWithMutex{}

	singletonJobsWg := &waitGroupWithMutex{}

	limitModeJobsWg := &waitGroupWithMutex{}

	// create a fresh map for tracking singleton runners
	e.singletonRunners = &sync.Map{}

	// start the for leap that is the executor
	// selecting on channels for work to do
	for {
		select {
		// job ids in are sent from 1 of 2 places:
		// 1. the scheduler sends directly when jobs
		//    are run immediately.
		// 2. sent from time.AfterFuncs in which job schedules
		// 	  are spun up by the scheduler
		case jIn := <-e.jobsIn:
			select {
			case <-e.stopCh:
				e.stop(standardJobsWg, singletonJobsWg, limitModeJobsWg)
				return
			default:
			}
			// this context is used to handle cancellation of the executor
			// on requests for a job to the scheduler via requestJobCtx
			ctx, cancel := context.WithCancel(e.ctx)

			if e.limitMode != nil && !e.limitMode.started {
				// check if we are already running the limit mode runners
				// if not, spin up the required number i.e. limit!
				e.limitMode.started = true
				for i := e.limitMode.limit; i > 0; i-- {
					limitModeJobsWg.Add(1)
					go e.limitModeRunner("limitMode-"+strconv.Itoa(int(i)), e.limitMode.in, limitModeJobsWg, e.limitMode.mode, e.limitMode.rescheduleLimiter)
				}
			}

			// spin off into a goroutine to unblock the executor and
			// allow for processing for more work
			go func(executorCtx context.Context) {
				// make sure to cancel the above context per the docs
				// // Canceling this context releases resources associated with it, so code should
				// // call cancel as soon as the operations running in this Context complete.
				defer cancel()

				// check for limit mode - this spins up a separate runner which handles
				// limiting the total number of concurrently running jobs
				if e.limitMode != nil {
					if e.limitMode.mode == LimitModeReschedule {
						select {
						// rescheduleLimiter is a channel the size of the limit
						// this blocks publishing to the channel and keeps
						// the executor from building up a waiting queue
						// and forces rescheduling
						case e.limitMode.rescheduleLimiter <- struct{}{}:
							e.limitMode.in <- jIn
						default:
							// all runners are busy, reschedule the work for later
							// which means we just skip it here and do nothing
							// TODO when metrics are added, this should increment a rescheduled metric
							// Notify concurrency limit reached if monitor is configured
							if e.scheduler != nil && e.scheduler.schedulerMonitor != nil {
								ctx2, cancel2 := context.WithCancel(executorCtx)
								job := requestJobCtx(ctx2, jIn.id, e.jobOutRequest)
								cancel2()
								if job != nil {
									e.scheduler.notifyConcurrencyLimitReached("limit", e.scheduler.jobFromInternalJob(*job))
								}
							}
							e.sendOutForRescheduling(&jIn)
						}
					} else {
						// since we're not using LimitModeReschedule, but instead using LimitModeWait
						// we do want to queue up the work to the limit mode runners and allow them
						// to work through the channel backlog. A hard limit of 1000 is in place
						// at which point this call would block.
						// TODO when metrics are added, this should increment a wait metric
						e.sendOutForRescheduling(&jIn)
						e.limitMode.in <- jIn
					}
				} else {
					// no limit mode, so we're either running a regular job or
					// a job with a singleton mode
					//
					// get the job, so we can figure out what kind it is and how
					// to execute it
					j := requestJobCtx(ctx, jIn.id, e.jobOutRequest)
					if j == nil {
						// safety check as it'd be strange bug if this occurred
						return
					}
					if j.singletonMode {
						// for singleton mode, get the existing runner for the job
						// or spin up a new one
						runner := &singletonRunner{}
						runnerSrc, ok := e.singletonRunners.Load(jIn.id)
						if !ok {
							runner.in = make(chan jobIn, 1000)
							if j.singletonLimitMode == LimitModeReschedule {
								runner.rescheduleLimiter = make(chan struct{}, 1)
							}
							e.singletonRunners.Store(jIn.id, runner)
							singletonJobsWg.Add(1)
							go e.singletonModeRunner("singleton-"+jIn.id.String(), runner.in, singletonJobsWg, j.singletonLimitMode, runner.rescheduleLimiter)
						} else {
							runner = runnerSrc.(*singletonRunner)
						}

						if j.singletonLimitMode == LimitModeReschedule {
							// reschedule mode uses the limiter channel to check
							// for a running job and reschedules if the channel is full.
							select {
							case runner.rescheduleLimiter <- struct{}{}:
								runner.in <- jIn
								// For intervalFromCompletion, skip rescheduling here - it will happen after job completes
								if !j.intervalFromCompletion {
									e.sendOutForRescheduling(&jIn)
								}
							default:
								// runner is busy, reschedule the work for later
								// which means we just skip it here and do nothing
								e.incrementJobCounter(*j, SingletonRescheduled)
								e.sendOutForRescheduling(&jIn)
								// Notify concurrency limit reached if monitor is configured
								if e.scheduler != nil && e.scheduler.schedulerMonitor != nil {
									e.scheduler.notifyConcurrencyLimitReached("singleton", e.scheduler.jobFromInternalJob(*j))
								}
							}
						} else {
							// wait mode, fill up that queue (buffered channel, so it's ok)
							runner.in <- jIn
							// For intervalFromCompletion, skip rescheduling here - it will happen after job completes
							if !j.intervalFromCompletion {
								e.sendOutForRescheduling(&jIn)
							}
						}
					} else {
						select {
						case <-executorCtx.Done():
							return
						default:
						}
						// we've gotten to the basic / standard jobs --
						// the ones without anything special that just want
						// to be run. Add to the WaitGroup so that
						// stopping or shutting down can wait for the jobs to
						// complete.
						standardJobsWg.Add(1)
						go func(j internalJob) {
							e.runJob(j, jIn)
							standardJobsWg.Done()
						}(*j)
					}
				}
			}(e.ctx)
		case <-e.stopCh:
			e.stop(standardJobsWg, singletonJobsWg, limitModeJobsWg)
			return
		}
	}
}

func (e *executor) sendOutForRescheduling(jIn *jobIn) {
	if jIn.shouldSendOut {
		select {
		case e.jobsOutForRescheduling <- jIn.id:
		case <-e.ctx.Done():
			return
		}
	}
	// we need to set this to false now, because to handle
	// non-limit jobs, we send out from the e.runJob function
	// and in this case we don't want to send out twice.
	jIn.shouldSendOut = false
}

func (e *executor) sendOutForNextRunUpdate(jIn *jobIn) {
	select {
	case e.jobUpdateNextRuns <- jIn.id:
	case <-e.ctx.Done():
		return
	}
}

func (e *executor) limitModeRunner(name string, in chan jobIn, wg *waitGroupWithMutex, limitMode LimitMode, rescheduleLimiter chan struct{}) {
	e.logger.Debug("gocron: limitModeRunner starting", "name", name)
	for {
		select {
		case jIn := <-in:
			select {
			case <-e.ctx.Done():
				e.logger.Debug("gocron: limitModeRunner shutting down", "name", name)
				wg.Done()
				return
			default:
			}

			ctx, cancel := context.WithCancel(e.ctx)
			j := requestJobCtx(ctx, jIn.id, e.jobOutRequest)
			cancel()
			if j != nil {
				if j.singletonMode {
					e.limitMode.singletonJobsMu.Lock()
					_, ok := e.limitMode.singletonJobs[jIn.id]
					if ok {
						// this job is already running, so don't run it
						// but instead reschedule it
						e.limitMode.singletonJobsMu.Unlock()
						if jIn.shouldSendOut {
							select {
							case <-e.ctx.Done():
								return
							case <-j.ctx.Done():
								return
							case e.jobsOutForRescheduling <- j.id:
							}
						}
						// remove the limiter block, as this particular job
						// was a singleton already running, and we want to
						// allow another job to be scheduled
						if limitMode == LimitModeReschedule {
							<-rescheduleLimiter
						}
						continue
					}
					e.limitMode.singletonJobs[jIn.id] = struct{}{}
					e.limitMode.singletonJobsMu.Unlock()
				}
				e.runJob(*j, jIn)

				if j.singletonMode {
					e.limitMode.singletonJobsMu.Lock()
					delete(e.limitMode.singletonJobs, jIn.id)
					e.limitMode.singletonJobsMu.Unlock()
				}
			}

			// remove the limiter block to allow another job to be scheduled
			if limitMode == LimitModeReschedule {
				<-rescheduleLimiter
			}
		case <-e.ctx.Done():
			e.logger.Debug("limitModeRunner shutting down", "name", name)
			wg.Done()
			return
		}
	}
}

func (e *executor) singletonModeRunner(name string, in chan jobIn, wg *waitGroupWithMutex, limitMode LimitMode, rescheduleLimiter chan struct{}) {
	e.logger.Debug("gocron: singletonModeRunner starting", "name", name)
	for {
		select {
		case jIn := <-in:
			select {
			case <-e.ctx.Done():
				e.logger.Debug("gocron: singletonModeRunner shutting down", "name", name)
				wg.Done()
				return
			default:
			}

			ctx, cancel := context.WithCancel(e.ctx)
			j := requestJobCtx(ctx, jIn.id, e.jobOutRequest)
			cancel()
			if j != nil {
				// need to set shouldSendOut = false here, as there is a duplicative call to sendOutForRescheduling
				// inside the runJob function that needs to be skipped. sendOutForRescheduling is previously called
				// when the job is sent to the singleton mode runner.
				// Exception: for intervalFromCompletion, we want rescheduling to happen AFTER job completion
				if !j.intervalFromCompletion {
					jIn.shouldSendOut = false
				}
				e.runJob(*j, jIn)
			}

			// remove the limiter block to allow another job to be scheduled
			if limitMode == LimitModeReschedule {
				<-rescheduleLimiter
			}
		case <-e.ctx.Done():
			e.logger.Debug("singletonModeRunner shutting down", "name", name)
			wg.Done()
			return
		}
	}
}

func (e *executor) runJob(j internalJob, jIn jobIn) {
	if j.ctx == nil {
		return
	}
	select {
	case <-e.ctx.Done():
		return
	case <-j.ctx.Done():
		return
	default:
	}

	if j.stopTimeReached(e.clock.Now()) {
		return
	}

	if e.elector != nil {
		if err := e.elector.IsLeader(j.ctx); err != nil {
			e.sendOutForRescheduling(&jIn)
			e.incrementJobCounter(j, Skip)
			return
		}
	} else if !j.disabledLocker && j.locker != nil {
		lock, err := j.locker.Lock(j.ctx, j.name)
		if err != nil {
			_ = callJobFuncWithParams(j.afterLockError, j.id, j.name, err)
			e.sendOutForRescheduling(&jIn)
			e.incrementJobCounter(j, Skip)
			e.sendOutForNextRunUpdate(&jIn)
			return
		}
		defer func() { _ = lock.Unlock(j.ctx) }()
	} else if !j.disabledLocker && e.locker != nil {
		lock, err := e.locker.Lock(j.ctx, j.name)
		if err != nil {
			_ = callJobFuncWithParams(j.afterLockError, j.id, j.name, err)
			e.sendOutForRescheduling(&jIn)
			e.incrementJobCounter(j, Skip)
			e.sendOutForNextRunUpdate(&jIn)
			return
		}
		defer func() { _ = lock.Unlock(j.ctx) }()
	}

	_ = callJobFuncWithParams(j.beforeJobRuns, j.id, j.name)

	//  Notify job started
	actualStartTime := time.Now()
	if e.scheduler != nil && e.scheduler.schedulerMonitor != nil {
		jobObj := e.scheduler.jobFromInternalJob(j)
		e.scheduler.notifyJobStarted(jobObj)
		// Notify scheduling delay if job had a scheduled time
		if len(j.nextScheduled) > 0 {
			e.scheduler.notifyJobSchedulingDelay(jobObj, j.nextScheduled[0], actualStartTime)
		}
	}

	err := callJobFuncWithParams(j.beforeJobRunsSkipIfBeforeFuncErrors, j.id, j.name)
	if err != nil {
		e.sendOutForRescheduling(&jIn)
		select {
		case e.jobsOutCompleted <- j.id:
		case <-e.ctx.Done():
		}
		// Notify job failed (before actual run)
		if e.scheduler != nil && e.scheduler.schedulerMonitor != nil {
			e.scheduler.notifyJobFailed(e.scheduler.jobFromInternalJob(j), err)
		}
		return
	}

	// Notify job running
	if e.scheduler != nil && e.scheduler.schedulerMonitor != nil {
		e.scheduler.notifyJobRunning(e.scheduler.jobFromInternalJob(j))
	}

	// For intervalFromCompletion, we need to reschedule AFTER the job completes,
	// not before. For regular jobs, we reschedule before execution (existing behavior).
	if !j.intervalFromCompletion {
		e.sendOutForRescheduling(&jIn)
		select {
		case e.jobsOutCompleted <- j.id:
		case <-e.ctx.Done():
		}
	}

	startTime := time.Now()
	if j.afterJobRunsWithPanic != nil {
		err = e.callJobWithRecover(j)
	} else {
		err = callJobFuncWithParams(j.function, j.parameters...)
	}
	e.recordJobTiming(startTime, time.Now(), j)
	if err != nil {
		_ = callJobFuncWithParams(j.afterJobRunsWithError, j.id, j.name, err)
		e.incrementJobCounter(j, Fail)
		endTime := time.Now()
		e.recordJobTimingWithStatus(startTime, endTime, j, Fail, err)
		// Notify job failed
		if e.scheduler != nil && e.scheduler.schedulerMonitor != nil {
			jobObj := e.scheduler.jobFromInternalJob(j)
			e.scheduler.notifyJobFailed(jobObj, err)
			e.scheduler.notifyJobExecutionTime(jobObj, endTime.Sub(startTime))
		}
	} else {
		_ = callJobFuncWithParams(j.afterJobRuns, j.id, j.name)
		e.incrementJobCounter(j, Success)
		endTime := time.Now()
		e.recordJobTimingWithStatus(startTime, endTime, j, Success, nil)
		// Notify job completed
		if e.scheduler != nil && e.scheduler.schedulerMonitor != nil {
			jobObj := e.scheduler.jobFromInternalJob(j)
			e.scheduler.notifyJobCompleted(jobObj)
			e.scheduler.notifyJobExecutionTime(jobObj, endTime.Sub(startTime))
		}
	}

	// For intervalFromCompletion, reschedule AFTER the job completes
	if j.intervalFromCompletion {
		select {
		case e.jobsOutCompleted <- j.id:
		case <-e.ctx.Done():
		}
		e.sendOutForRescheduling(&jIn)
	}
}

func (e *executor) callJobWithRecover(j internalJob) (err error) {
	defer func() {
		if recoverData := recover(); recoverData != nil {
			_ = callJobFuncWithParams(j.afterJobRunsWithPanic, j.id, j.name, recoverData)

			// if panic is occurred, we should return an error
			err = fmt.Errorf("%w from %v", ErrPanicRecovered, recoverData)
		}
	}()

	return callJobFuncWithParams(j.function, j.parameters...)
}

func (e *executor) recordJobTiming(start time.Time, end time.Time, j internalJob) {
	if e.monitor != nil {
		e.monitor.RecordJobTiming(start, end, j.id, j.name, j.tags)
	}
}

func (e *executor) recordJobTimingWithStatus(start time.Time, end time.Time, j internalJob, status JobStatus, err error) {
	if e.monitorStatus != nil {
		e.monitorStatus.RecordJobTimingWithStatus(start, end, j.id, j.name, j.tags, status, err)
	}
}

func (e *executor) incrementJobCounter(j internalJob, status JobStatus) {
	if e.monitor != nil {
		e.monitor.IncrementJob(j.id, j.name, j.tags, status)
	}
}

func (e *executor) stop(standardJobsWg, singletonJobsWg, limitModeJobsWg *waitGroupWithMutex) {
	e.stopOnce.Do(func() {
		e.logger.Debug("gocron: stopping executor")
		// we've been asked to stop. This is either because the scheduler has been told
		// to stop all jobs or the scheduler has been asked to completely shutdown.
		//
		// cancel tells all the functions to stop their work and send in a done response
		e.cancel()

		// the wait for job channels are used to report back whether we successfully waited
		// for all jobs to complete or if we hit the configured timeout.
		waitForJobs := make(chan struct{}, 1)
		waitForSingletons := make(chan struct{}, 1)
		waitForLimitMode := make(chan struct{}, 1)

		// the waiter context is used to cancel the functions waiting on jobs.
		// this is done to avoid goroutine leaks.
		waiterCtx, waiterCancel := context.WithCancel(context.Background())

		// wait for standard jobs to complete
		go func() {
			e.logger.Debug("gocron: waiting for standard jobs to complete")
			go func() {
				// this is done in a separate goroutine, so we aren't
				// blocked by the WaitGroup's Wait call in the event
				// that the waiter context is cancelled.
				// This particular goroutine could leak in the event that
				// some long-running standard job doesn't complete.
				standardJobsWg.Wait()
				e.logger.Debug("gocron: standard jobs completed")
				waitForJobs <- struct{}{}
			}()
			<-waiterCtx.Done()
		}()

		// wait for per job singleton limit mode runner jobs to complete
		go func() {
			e.logger.Debug("gocron: waiting for singleton jobs to complete")
			go func() {
				singletonJobsWg.Wait()
				e.logger.Debug("gocron: singleton jobs completed")
				waitForSingletons <- struct{}{}
			}()
			<-waiterCtx.Done()
		}()

		// wait for limit mode runners to complete
		go func() {
			e.logger.Debug("gocron: waiting for limit mode jobs to complete")
			go func() {
				limitModeJobsWg.Wait()
				e.logger.Debug("gocron: limitMode jobs completed")
				waitForLimitMode <- struct{}{}
			}()
			<-waiterCtx.Done()
		}()

		// now either wait for all the jobs to complete,
		// or hit the timeout.
		var count int
		timeout := time.Now().Add(e.stopTimeout)
		for time.Now().Before(timeout) && count < 3 {
			select {
			case <-waitForJobs:
				count++
			case <-waitForSingletons:
				count++
			case <-waitForLimitMode:
				count++
			default:
			}
		}
		if count < 3 {
			e.done <- ErrStopJobsTimedOut
			e.logger.Debug("gocron: executor stopped - timed out")
		} else {
			e.done <- nil
			e.logger.Debug("gocron: executor stopped")
		}
		waiterCancel()

		if e.limitMode != nil {
			e.limitMode.started = false
		}
	})
}


================================================
FILE: go.mod
================================================
module github.com/go-co-op/gocron/v2

go 1.21.4

require (
	github.com/google/uuid v1.6.0
	github.com/jonboulle/clockwork v0.5.0
	github.com/robfig/cron/v3 v3.0.1
	github.com/stretchr/testify v1.11.1
	go.uber.org/goleak v1.3.0
)

require (
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/kr/text v0.2.0 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)


================================================
FILE: go.sum
================================================
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=


================================================
FILE: gocron-monitor-test/debug_restart.go
================================================
package main

import (
	"fmt"
	"time"

	"github.com/go-co-op/gocron/v2"
)

type DebugMonitor struct {
	startCount      int
	stopCount       int
	jobRegCount     int
	jobUnregCount   int
	jobStartCount   int
	jobRunningCount int
	jobCompletCount int
	jobFailCount    int
}

func (m *DebugMonitor) SchedulerStarted() {
	m.startCount++
	fmt.Printf("✓ SchedulerStarted() called (total: %d)\n", m.startCount)
}

func (m *DebugMonitor) SchedulerShutdown() {
	m.stopCount++
	fmt.Printf("✓ SchedulerShutdown() called (total: %d)\n", m.stopCount)
}

func (m *DebugMonitor) JobRegistered(job *gocron.Job) {
	m.jobRegCount++
	fmt.Printf("✓ JobRegistered() called (total: %d) - Job ID: %s\n", m.jobRegCount, (*job).ID())
}

func (m *DebugMonitor) JobUnregistered(job *gocron.Job) {
	m.jobUnregCount++
	fmt.Printf("✓ JobUnregistered() called (total: %d) - Job ID: %s\n", m.jobUnregCount, (*job).ID())
}

func (m *DebugMonitor) JobStarted(job *gocron.Job) {
	m.jobStartCount++
	fmt.Printf("✓ JobStarted() called (total: %d) - Job ID: %s\n", m.jobStartCount, (*job).ID())
}

func (m *DebugMonitor) JobRunning(job *gocron.Job) {
	m.jobRunningCount++
	fmt.Printf("✓ JobRunning() called (total: %d) - Job ID: %s\n", m.jobRunningCount, (*job).ID())
}

func (m *DebugMonitor) JobCompleted(job *gocron.Job) {
	m.jobCompletCount++
	fmt.Printf("✓ JobCompleted() called (total: %d) - Job ID: %s\n", m.jobCompletCount, (*job).ID())
}

func (m *DebugMonitor) JobFailed(job *gocron.Job, err error) {
	m.jobFailCount++
	fmt.Printf("✓ JobFailed() called (total: %d) - Job ID: %s, Error: %v\n", m.jobFailCount, (*job).ID(), err)
}

func main() {
	// ONE monitor, multiple scheduler instances
	monitor := &DebugMonitor{}

	fmt.Println("=== Cycle 1 (Scheduler Instance 1) ===")
	s1, err := gocron.NewScheduler(
		gocron.WithSchedulerMonitor(monitor),
	)
	if err != nil {
		panic(err)
	}

	// Create and register some test jobs
	fmt.Println("Creating jobs...")
	_, err = s1.NewJob(
		gocron.DurationJob(1*time.Second),
		gocron.NewTask(func() { fmt.Println("Job 1 running") }),
	)
	if err != nil {
		panic(err)
	}

	_, err = s1.NewJob(
		gocron.DurationJob(2*time.Second),
		gocron.NewTask(func() error {
			fmt.Println("Job 2 executing and returning error")
			return fmt.Errorf("simulated job failure")
		}), // This job will fail with error
	)
	if err != nil {
		panic(err)
	}

	fmt.Println("Calling Start()...")
	s1.Start()
	time.Sleep(3 * time.Second) // Wait for jobs to execute

	fmt.Println("Calling Shutdown()...")
	err = s1.Shutdown()
	if err != nil {
		fmt.Printf("Shutdown error: %v\n", err)
	}

	fmt.Println("\n=== Cycle 2 (Job Updates) ===")
	s2, err := gocron.NewScheduler(
		gocron.WithSchedulerMonitor(monitor),
	)
	if err != nil {
		panic(err)
	}

	fmt.Println("Creating and updating jobs...")
	job3, err := s2.NewJob(
		gocron.DurationJob(1*time.Second),
		gocron.NewTask(func() { fmt.Println("Job 3 running") }),
	)
	if err != nil {
		panic(err)
	}

	// Update the job
	_, err = s2.Update(
		job3.ID(),
		gocron.DurationJob(2*time.Second),
		gocron.NewTask(func() { fmt.Println("Job 3 updated") }),
	)
	if err != nil {
		panic(err)
	}

	fmt.Println("Calling Start()...")
	s2.Start()
	time.Sleep(3 * time.Second)

	fmt.Println("Calling Shutdown()...")
	err = s2.Shutdown()
	if err != nil {
		fmt.Printf("Shutdown error: %v\n", err)
	}

	fmt.Println("\n=== Summary ===")
	fmt.Printf("Total Scheduler Starts: %d\n", monitor.startCount)
	fmt.Printf("Total Scheduler Stops: %d\n", monitor.stopCount)
	fmt.Printf("Total Jobs Registered: %d\n", monitor.jobRegCount)
	fmt.Printf("Total Jobs Unregistered: %d\n", monitor.jobUnregCount)
	fmt.Printf("Total Jobs Started: %d\n", monitor.jobStartCount)
	fmt.Printf("Total Jobs Running: %d\n", monitor.jobRunningCount)
	fmt.Printf("Total Jobs Completed: %d\n", monitor.jobCompletCount)
	fmt.Printf("Total Jobs Failed: %d\n", monitor.jobFailCount)
}


================================================
FILE: gocron-monitor-test/go.mod
================================================
module test

go 1.21.4

require github.com/go-co-op/gocron/v2 v2.17.0

require (
	github.com/google/uuid v1.6.0 // indirect
	github.com/jonboulle/clockwork v0.5.0 // indirect
	github.com/robfig/cron/v3 v3.0.1 // indirect
)

replace github.com/go-co-op/gocron/v2 => ../


================================================
FILE: gocron-monitor-test/go.sum
================================================
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=


================================================
FILE: job.go
================================================
//go:generate mockgen -destination=mocks/job.go -package=gocronmocks . Job
package gocron

import (
	"context"
	"errors"
	"fmt"
	"math/rand"
	"slices"
	"strings"
	"time"

	"github.com/google/uuid"
	"github.com/jonboulle/clockwork"
	"github.com/robfig/cron/v3"
)

// internalJob stores the information needed by the scheduler
// to manage scheduling, starting and stopping the job
type internalJob struct {
	ctx       context.Context
	parentCtx context.Context
	cancel    context.CancelFunc
	id        uuid.UUID
	name      string
	tags      []string
	cron      Cron
	jobSchedule

	// as some jobs may queue up, it's possible to
	// have multiple nextScheduled times
	nextScheduled []time.Time

	lastRun                time.Time
	function               any
	parameters             []any
	timer                  clockwork.Timer
	singletonMode          bool
	singletonLimitMode     LimitMode
	limitRunsTo            *limitRunsTo
	startTime              time.Time
	startImmediately       bool
	stopTime               time.Time
	intervalFromCompletion bool
	// event listeners
	afterJobRuns                        func(jobID uuid.UUID, jobName string)
	beforeJobRuns                       func(jobID uuid.UUID, jobName string)
	beforeJobRunsSkipIfBeforeFuncErrors func(jobID uuid.UUID, jobName string) error
	afterJobRunsWithError               func(jobID uuid.UUID, jobName string, err error)
	afterJobRunsWithPanic               func(jobID uuid.UUID, jobName string, recoverData any)
	afterLockError                      func(jobID uuid.UUID, jobName string, err error)
	disabledLocker                      bool

	locker Locker
}

// stop is used to stop the job's timer and cancel the context
// stopping the timer is critical for cleaning up jobs that are
// sleeping in a time.AfterFunc timer when the job is being stopped.
// cancelling the context keeps the executor from continuing to try
// and run the job.
func (j *internalJob) stop() {
	if j.timer != nil {
		j.timer.Stop()
	}
	j.cancel()
}

func (j *internalJob) stopTimeReached(now time.Time) bool {
	if j.stopTime.IsZero() {
		return false
	}
	return j.stopTime.Before(now)
}

// task stores the function and parameters
// that are actually run when the job is executed.
type task struct {
	function   any
	parameters []any
}

// Task defines a function that returns the task
// function and parameters.
type Task func() task

// NewTask provides the job's task function and parameters.
// If you set the first argument of your Task func to be a context.Context,
// gocron will pass in a context (either the default Job context, or one
// provided via WithContext) to the job and will cancel the context on shutdown.
// This allows you to listen for and handle cancellation within your job.
func NewTask(function any, parameters ...any) Task {
	return func() task {
		return task{
			function:   function,
			parameters: parameters,
		}
	}
}

// limitRunsTo is used for managing the number of runs
// when the user only wants the job to run a certain
// number of times and then be removed from the scheduler.
type limitRunsTo struct {
	limit    uint
	runCount uint
}

// -----------------------------------------------
// -----------------------------------------------
// --------------- Custom Cron -------------------
// -----------------------------------------------
// -----------------------------------------------

// Cron defines the interface that must be
// implemented to provide a custom cron implementation for
// the job. Pass in the implementation using the JobOption WithCronImplementation.
type Cron interface {
	IsValid(crontab string, location *time.Location, now time.Time) error
	Next(lastRun time.Time) time.Time
}

// -----------------------------------------------
// -----------------------------------------------
// --------------- Job Variants ------------------
// -----------------------------------------------
// -----------------------------------------------

// JobDefinition defines the interface that must be
// implemented to create a job from the definition.
type JobDefinition interface {
	setup(j *internalJob, l *time.Location, now time.Time) error
}

// Default cron implementation

func newDefaultCronImplementation(withSeconds bool) Cron {
	return &defaultCron{
		withSeconds: withSeconds,
	}
}

// NewDefaultCron returns the default cron implementation for use outside the
// scheduling of a job. For example, validating crontab syntax before passing to the
// NewJob function.
func NewDefaultCron(cronStatementsIncludeSeconds bool) Cron {
	return &defaultCron{
		withSeconds: cronStatementsIncludeSeconds,
	}
}

var _ Cron = (*defaultCron)(nil)

type defaultCron struct {
	cronSchedule cron.Schedule
	withSeconds  bool
}

func (c *defaultCron) IsValid(crontab string, location *time.Location, now time.Time) error {
	var withLocation string
	if strings.HasPrefix(crontab, "TZ=") || strings.HasPrefix(crontab, "CRON_TZ=") {
		withLocation = crontab
	} else {
		// since the user didn't provide a timezone default to the location
		// passed in by the scheduler. Default: time.Local
		withLocation = fmt.Sprintf("CRON_TZ=%s %s", location.String(), crontab)
	}

	var (
		cronSchedule cron.Schedule
		err          error
	)

	if c.withSeconds {
		p := cron.NewParser(cron.SecondOptional | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
		cronSchedule, err = p.Parse(withLocation)
	} else {
		cronSchedule, err = cron.ParseStandard(withLocation)
	}
	if err != nil {
		return errors.Join(ErrCronJobParse, err)
	}
	if cronSchedule.Next(now).IsZero() {
		return ErrCronJobInvalid
	}
	c.cronSchedule = cronSchedule
	return nil
}

func (c *defaultCron) Next(lastRun time.Time) time.Time {
	return c.cronSchedule.Next(lastRun)
}

// default cron job implementation
var _ JobDefinition = (*cronJobDefinition)(nil)

type cronJobDefinition struct {
	crontab string
	cron    Cron
}

func (c cronJobDefinition) setup(j *internalJob, location *time.Location, now time.Time) error {
	if j.cron != nil {
		c.cron = j.cron
	}

	if err := c.cron.IsValid(c.crontab, location, now); err != nil {
		return err
	}

	j.jobSchedule = &cronJob{crontab: c.crontab, cronSchedule: c.cron}
	return nil
}

// CronJob defines a new job using the crontab syntax: `* * * * *`.
// An optional 6th field can be used at the beginning if withSeconds
// is set to true: `* * * * * *`.
// The timezone can be set on the Scheduler using WithLocation, or in the
// crontab in the form `TZ=America/Chicago * * * * *` or
// `CRON_TZ=America/Chicago * * * * *`
func CronJob(crontab string, withSeconds bool) JobDefinition {
	return cronJobDefinition{
		crontab: crontab,
		cron:    newDefaultCronImplementation(withSeconds),
	}
}

var _ JobDefinition = (*durationJobDefinition)(nil)

type durationJobDefinition struct {
	duration time.Duration
}

func (d durationJobDefinition) setup(j *internalJob, _ *time.Location, _ time.Time) error {
	if d.duration == 0 {
		return ErrDurationJobIntervalZero
	}
	if d.duration < 0 {
		return ErrDurationJobIntervalNegative
	}
	j.jobSchedule = &durationJob{duration: d.duration}
	return nil
}

// DurationJob defines a new job using time.Duration
// for the interval.
func DurationJob(duration time.Duration) JobDefinition {
	return durationJobDefinition{
		duration: duration,
	}
}

var _ JobDefinition = (*durationRandomJobDefinition)(nil)

type durationRandomJobDefinition struct {
	min, max time.Duration
}

func (d durationRandomJobDefinition) setup(j *internalJob, _ *time.Location, _ time.Time) error {
	if d.min >= d.max {
		return ErrDurationRandomJobMinMax
	}

	if d.min <= 0 || d.max <= 0 {
		return ErrDurationRandomJobPositive
	}

	j.jobSchedule = &durationRandomJob{
		min:  d.min,
		max:  d.max,
		rand: rand.New(rand.NewSource(time.Now().UnixNano())), // nolint:gosec
	}
	return nil
}

// DurationRandomJob defines a new job that runs on a random interval
// between the min and max duration values provided.
//
// To achieve a similar behavior as tools that use a splay/jitter technique
// consider the median value as the baseline and the difference between the
// max-median or median-min as the splay/jitter.
//
// For example, if you want a job to run every 5 minutes, but want to add
// up to 1 min of jitter to the interval, you could use
// DurationRandomJob(4*time.Minute, 6*time.Minute)
func DurationRandomJob(minDuration, maxDuration time.Duration) JobDefinition {
	return durationRandomJobDefinition{
		min: minDuration,
		max: maxDuration,
	}
}

// DailyJob runs the job on the interval of days, and at the set times.
// By default, the job will start the next available day, considering the last run to be now,
// and the time and day based on the interval and times you input. This means, if you
// select an interval greater than 1, your job by default will run X (interval) days from now
// if there are no atTimes left in the current day. You can use WithStartAt to tell the
// scheduler to start the job sooner.
func DailyJob(interval uint, atTimes AtTimes) JobDefinition {
	return dailyJobDefinition{
		interval: interval,
		atTimes:  atTimes,
	}
}

var _ JobDefinition = (*dailyJobDefinition)(nil)

type dailyJobDefinition struct {
	interval uint
	atTimes  AtTimes
}

func (d dailyJobDefinition) setup(j *internalJob, location *time.Location, _ time.Time) error {
	atTimesDate, err := convertAtTimesToDateTime(d.atTimes, location)
	switch {
	case errors.Is(err, errAtTimesNil):
		return ErrDailyJobAtTimesNil
	case errors.Is(err, errAtTimeNil):
		return ErrDailyJobAtTimeNil
	case errors.Is(err, errAtTimeHours):
		return ErrDailyJobHours
	case errors.Is(err, errAtTimeMinSec):
		return ErrDailyJobMinutesSeconds
	}

	if d.interval == 0 {
		return ErrDailyJobZeroInterval
	}

	ds := dailyJob{
		interval: d.interval,
		atTimes:  atTimesDate,
	}
	j.jobSchedule = ds
	return nil
}

var _ JobDefinition = (*weeklyJobDefinition)(nil)

type weeklyJobDefinition struct {
	interval      uint
	daysOfTheWeek Weekdays
	atTimes       AtTimes
}

func (w weeklyJobDefinition) setup(j *internalJob, location *time.Location, _ time.Time) error {
	var ws weeklyJob
	if w.interval == 0 {
		return ErrWeeklyJobZeroInterval
	}
	ws.interval = w.interval

	if w.daysOfTheWeek == nil {
		return ErrWeeklyJobDaysOfTheWeekNil
	}

	daysOfTheWeek := w.daysOfTheWeek()

	slices.Sort(daysOfTheWeek)
	ws.daysOfWeek = daysOfTheWeek

	atTimesDate, err := convertAtTimesToDateTime(w.atTimes, location)
	switch {
	case errors.Is(err, errAtTimesNil):
		return ErrWeeklyJobAtTimesNil
	case errors.Is(err, errAtTimeNil):
		return ErrWeeklyJobAtTimeNil
	case errors.Is(err, errAtTimeHours):
		return ErrWeeklyJobHours
	case errors.Is(err, errAtTimeMinSec):
		return ErrWeeklyJobMinutesSeconds
	}
	ws.atTimes = atTimesDate

	j.jobSchedule = ws
	return nil
}

// Weekdays defines a function that returns a list of week days.
type Weekdays func() []time.Weekday

// NewWeekdays provide the days of the week the job should run.
func NewWeekdays(weekday time.Weekday, weekdays ...time.Weekday) Weekdays {
	return func() []time.Weekday {
		return append([]time.Weekday{weekday}, weekdays...)
	}
}

// WeeklyJob runs the job on the interval of weeks, on the specific days of the week
// specified, and at the set times.
//
// By default, the job will start the next available day, considering the last run to be now,
// and the time and day based on the interval, days and times you input. This means, if you
// select an interval greater than 1, your job by default will run X (interval) weeks from now
// if there are no daysOfTheWeek left in the current week. You can use WithStartAt to tell the
// scheduler to start the job sooner.
func WeeklyJob(interval uint, daysOfTheWeek Weekdays, atTimes AtTimes) JobDefinition {
	return weeklyJobDefinition{
		interval:      interval,
		daysOfTheWeek: daysOfTheWeek,
		atTimes:       atTimes,
	}
}

var _ JobDefinition = (*monthlyJobDefinition)(nil)

type monthlyJobDefinition struct {
	interval       uint
	daysOfTheMonth DaysOfTheMonth
	atTimes        AtTimes
}

func (m monthlyJobDefinition) setup(j *internalJob, location *time.Location, _ time.Time) error {
	var ms monthlyJob
	if m.interval == 0 {
		return ErrMonthlyJobZeroInterval
	}
	ms.interval = m.interval

	if m.daysOfTheMonth == nil {
		return ErrMonthlyJobDaysNil
	}

	var daysStart, daysEnd []int
	for _, day := range m.daysOfTheMonth() {
		if day > 31 || day == 0 || day < -31 {
			return ErrMonthlyJobDays
		}
		if day > 0 {
			daysStart = append(daysStart, day)
		} else {
			daysEnd = append(daysEnd, day)
		}
	}
	daysStart = removeSliceDuplicatesInt(daysStart)
	ms.days = daysStart

	daysEnd = removeSliceDuplicatesInt(daysEnd)
	ms.daysFromEnd = daysEnd

	atTimesDate, err := convertAtTimesToDateTime(m.atTimes, location)
	switch {
	case errors.Is(err, errAtTimesNil):
		return ErrMonthlyJobAtTimesNil
	case errors.Is(err, errAtTimeNil):
		return ErrMonthlyJobAtTimeNil
	case errors.Is(err, errAtTimeHours):
		return ErrMonthlyJobHours
	case errors.Is(err, errAtTimeMinSec):
		return ErrMonthlyJobMinutesSeconds
	}
	ms.atTimes = atTimesDate

	j.jobSchedule = ms
	return nil
}

type days []int

// DaysOfTheMonth defines a function that returns a list of days.
type DaysOfTheMonth func() days

// NewDaysOfTheMonth provide the days of the month the job should
// run. The days can be positive 1 to 31 and/or negative -31 to -1.
// Negative values count backwards from the end of the month.
// For example: -1 == the last day of the month.
//
//	-5 == 5 days before the end of the month.
func NewDaysOfTheMonth(day int, moreDays ...int) DaysOfTheMonth {
	return func() days {
		return append([]int{day}, moreDays...)
	}
}

type atTime struct {
	hours, minutes, seconds uint
}

func (a atTime) time(location *time.Location) time.Time {
	return time.Date(0, 0, 0, int(a.hours), int(a.minutes), int(a.seconds), 0, location)
}

// TimeFromAtTime is a helper function to allow converting AtTime into a time.Time value
// Note: the time.Time value will have zero values for all Time fields except Hours, Minutes, Seconds.
//
//	For example: time.Date(0, 0, 0, 1, 1, 1, 0, time.UTC)
func TimeFromAtTime(at AtTime, loc *time.Location) time.Time {
	return at().time(loc)
}

// AtTime defines a function that returns the internal atTime
type AtTime func() atTime

// NewAtTime provide the hours, minutes and seconds at which
// the job should be run
func NewAtTime(hours, minutes, seconds uint) AtTime {
	return func() atTime {
		return atTime{hours: hours, minutes: minutes, seconds: seconds}
	}
}

// AtTimes define a list of AtTime
type AtTimes func() []AtTime

// NewAtTimes provide the hours, minutes and seconds at which
// the job should be run
func NewAtTimes(atTime AtTime, atTimes ...AtTime) AtTimes {
	return func() []AtTime {
		return append([]AtTime{atTime}, atTimes...)
	}
}

// MonthlyJob runs the job on the interval of months, on the specific days of the month
// specified, and at the set times. Days of the month can be 1 to 31 or negative (-1 to -31), which
// count backwards from the end of the month. E.g. -1 is the last day of the month.
//
// If a day of the month is selected that does not exist in all months (e.g. 31st)
// any month that does not have that day will be skipped.
//
// By default, the job will start the next available day, considering the last run to be now,
// and the time and month based on the interval, days and times you input.
// This means, if you select an interval greater than 1, your job by default will run
// X (interval) months from now if there are no daysOfTheMonth left in the current month.
// You can use WithStartAt to tell the scheduler to start the job sooner.
//
// Carefully consider your configuration!
//   - For example: an interval of 2 months on the 31st of each month, starting 12/31
//     would skip Feb, April, June, and next run would be in August.
func MonthlyJob(interval uint, daysOfTheMonth DaysOfTheMonth, atTimes AtTimes) JobDefinition {
	return monthlyJobDefinition{
		interval:       interval,
		daysOfTheMonth: daysOfTheMonth,
		atTimes:        atTimes,
	}
}

var _ JobDefinition = (*oneTimeJobDefinition)(nil)

type oneTimeJobDefinition struct {
	startAt OneTimeJobStartAtOption
}

func (o oneTimeJobDefinition) setup(j *internalJob, _ *time.Location, now time.Time) error {
	sortedTimes := o.startAt(j)
	slices.SortStableFunc(sortedTimes, ascendingTime)
	// deduplicate the times
	sortedTimes = removeSliceDuplicatesTimeOnSortedSlice(sortedTimes)
	// keep only schedules that are in the future
	idx, found := slices.BinarySearchFunc(sortedTimes, now, ascendingTime)
	if found {
		idx++
	}
	sortedTimes = sortedTimes[idx:]
	if !j.startImmediately && len(sortedTimes) == 0 {
		return ErrOneTimeJobStartDateTimePast
	}
	j.jobSchedule = oneTimeJob{sortedTimes: sortedTimes}
	return nil
}

func removeSliceDuplicatesTimeOnSortedSlice(times []time.Time) []time.Time {
	ret := make([]time.Time, 0, len(times))
	for i, t := range times {
		if i == 0 || t != times[i-1] {
			ret = append(ret, t)
		}
	}
	return ret
}

// OneTimeJobStartAtOption defines when the one time job is run
type OneTimeJobStartAtOption func(*internalJob) []time.Time

// OneTimeJobStartImmediately tells the scheduler to run the one time job immediately.
func OneTimeJobStartImmediately() OneTimeJobStartAtOption {
	return func(j *internalJob) []time.Time {
		j.startImmediately = true
		return []time.Time{}
	}
}

// OneTimeJobStartDateTime sets the date & time at which the job should run.
// This datetime must be in the future (according to the scheduler clock).
func OneTimeJobStartDateTime(start time.Time) OneTimeJobStartAtOption {
	return func(_ *internalJob) []time.Time {
		return []time.Time{start}
	}
}

// OneTimeJobStartDateTimes sets the date & times at which the job should run.
// At least one of the date/times must be in the future (according to the scheduler clock).
func OneTimeJobStartDateTimes(times ...time.Time) OneTimeJobStartAtOption {
	return func(_ *internalJob) []time.Time {
		return times
	}
}

// OneTimeJob is to run a job once at a specified time and not on
// any regular schedule.
func OneTimeJob(startAt OneTimeJobStartAtOption) JobDefinition {
	return oneTimeJobDefinition{
		startAt: startAt,
	}
}

// -----------------------------------------------
// -----------------------------------------------
// ----------------- Job Options -----------------
// -----------------------------------------------
// -----------------------------------------------

// JobOption defines the constructor for job options.
type JobOption func(*internalJob, time.Time) error

// WithDistributedJobLocker sets the locker to be used by multiple
// Scheduler instances to ensure that only one instance of each
// job is run.
func WithDistributedJobLocker(locker Locker) JobOption {
	return func(j *internalJob, _ time.Time) error {
		if locker == nil {
			return ErrWithDistributedJobLockerNil
		}
		j.locker = locker
		return nil
	}
}

// WithDisabledDistributedJobLocker disables the distributed job locker.
// This is useful when a global distributed locker has been set on the scheduler
// level using WithDistributedLocker and need to be disabled for specific jobs.
func WithDisabledDistributedJobLocker(disabled bool) JobOption {
	return func(j *internalJob, _ time.Time) error {
		j.disabledLocker = disabled
		return nil
	}
}

// WithEventListeners sets the event listeners that should be
// run for the job.
func WithEventListeners(eventListeners ...EventListener) JobOption {
	return func(j *internalJob, _ time.Time) error {
		for _, eventListener := range eventListeners {
			if err := eventListener(j); err != nil {
				return err
			}
		}
		return nil
	}
}

// WithLimitedRuns limits the number of executions of this job to n.
// Upon reaching the limit, the job is removed from the scheduler.
func WithLimitedRuns(limit uint) JobOption {
	return func(j *internalJob, _ time.Time) error {
		if limit == 0 {
			return ErrWithLimitedRunsZero
		}
		j.limitRunsTo = &limitRunsTo{
			limit:    limit,
			runCount: 0,
		}
		return nil
	}
}

// WithName sets the name of the job. Name provides
// a human-readable identifier for the job.
func WithName(name string) JobOption {
	return func(j *internalJob, _ time.Time) error {
		if name == "" {
			return ErrWithNameEmpty
		}
		j.name = name
		return nil
	}
}

// WithCronImplementation sets the custom Cron implementation for the job.
// This is only utilized for the CronJob type.
func WithCronImplementation(c Cron) JobOption {
	return func(j *internalJob, _ time.Time) error {
		j.cron = c
		return nil
	}
}

// WithSingletonMode keeps the job from running again if it is already running.
// This is useful for jobs that should not overlap, and that occasionally
// (but not consistently) run longer than the interval between job runs.
func WithSingletonMode(mode LimitMode) JobOption {
	return func(j *internalJob, _ time.Time) error {
		j.singletonMode = true
		j.singletonLimitMode = mode
		return nil
	}
}

// WithIntervalFromCompletion configures the job to calculate the next run time
// from the job's completion time rather than its scheduled start time.
// This ensures consistent rest periods between job executions regardless of
// how long each execution takes.
//
// By default (without this option), a job scheduled to run every N time units
// will start N time units after its previous scheduled start time. For example,
// if a job is scheduled to run every 5 minutes starting at 09:00 and takes 2 minutes
// to complete, the next run will start at 09:05 (5 minutes from 09:00), giving
// only 3 minutes of rest between completion and the next start.
//
// With this option enabled, the next run will start N time units after the job
// completes. Using the same example, if the job completes at 09:02, the next run
// will start at 09:07 (5 minutes from 09:02), ensuring a full 5 minutes of rest.
//
// Note: This option only makes sense with interval-based jobs (DurationJob, DurationRandomJob).
// For time-based jobs (CronJob, DailyJob, etc.) that run at specific times, this option
// will be ignored as those jobs are inherently scheduled at fixed times.
//
// Example:
//
//	s.NewJob(
//	    gocron.DurationJob(5*time.Minute),
//	    gocron.NewTask(func() {
//	        // Job that takes variable time to complete
//	        doWork()
//	    }),
//	    gocron.WithIntervalFromCompletion(),
//	)
//
// In this example, no matter how long doWork() takes, there will always be
// exactly 5 minutes between when it completes and when it starts again.
func WithIntervalFromCompletion() JobOption {
	return func(j *internalJob, _ time.Time) error {
		j.intervalFromCompletion = true
		return nil
	}
}

// WithStartAt sets the option for starting the job at
// a specific datetime.
func WithStartAt(option StartAtOption) JobOption {
	return func(j *internalJob, now time.Time) error {
		return option(j, now)
	}
}

// StartAtOption defines options for starting the job
type StartAtOption func(*internalJob, time.Time) error

// WithStartImmediately tells the scheduler to run the job immediately
// regardless of the type or schedule of job. After this immediate run
// the job is scheduled from this time based on the job definition.
func WithStartImmediately() StartAtOption {
	return func(j *internalJob, _ time.Time) error {
		j.startImmediately = true
		return nil
	}
}

// WithStartDateTime sets the first date & time at which the job should run.
// This datetime must be in the future.
func WithStartDateTime(start time.Time) StartAtOption {
	return func(j *internalJob, now time.Time) error {
		if start.IsZero() || start.Before(now) {
			return ErrWithStartDateTimePast
		}
		if !j.stopTime.IsZero() && j.stopTime.Before(start) {
			return ErrStartTimeLaterThanEndTime
		}
		j.startTime = start
		return nil
	}
}

// WithStartDateTimePast sets the first date & time at which the job should run
// from a time in the past. This is useful when you want to backdate
// the start time of a job to a time in the past, for example
// if you want to start a job from a specific date in the past
// and have it run on its schedule from then.
// The start time can be in the past, but not zero.
// If the start time is in the future, it behaves the same as WithStartDateTime.
func WithStartDateTimePast(start time.Time) StartAtOption {
	return func(j *internalJob, _ time.Time) error {
		if start.IsZero() {
			return ErrWithStartDateTimePastZero
		}
		if !j.stopTime.IsZero() && j.stopTime.Before(start) {
			return ErrStartTimeLaterThanEndTime
		}
		j.startTime = start
		return nil
	}
}

// WithStopAt sets the option for stopping the job from running
// after the specified time.
func WithStopAt(option StopAtOption) JobOption {
	return func(j *internalJob, now time.Time) error {
		return option(j, now)
	}
}

// StopAtOption defines options for stopping the job
type StopAtOption func(*internalJob, time.Time) error

// WithStopDateTime sets the final date & time after which the job should stop.
// This must be in the future and should be after the startTime (if specified).
// The job's final run may be at the stop time, but not after.
func WithStopDateTime(end time.Time) StopAtOption {
	return func(j *internalJob, now time.Time) error {
		if end.IsZero() || end.Before(now) {
			return ErrWithStopDateTimePast
		}
		if end.Before(j.startTime) {
			return ErrStopTimeEarlierThanStartTime
		}
		j.stopTime = end
		return nil
	}
}

// WithTags sets the tags for the job. Tags provide
// a way to identify jobs by a set of tags and remove
// multiple jobs by tag.
func WithTags(tags ...string) JobOption {
	return func(j *internalJob, _ time.Time) error {
		j.tags = tags
		return nil
	}
}

// WithIdentifier sets the identifier for the job. The identifier
// is used to uniquely identify the job and is used for logging
// and metrics.
func WithIdentifier(id uuid.UUID) JobOption {
	return func(j *internalJob, _ time.Time) error {
		if id == uuid.Nil {
			return ErrWithIdentifierNil
		}

		j.id = id
		return nil
	}
}

// WithContext sets the parent context for the job.
// If you set the first argument of your Task func to be a context.Context,
// gocron will pass in the provided context to the job and will cancel the
// context on shutdown. If you cancel the context the job will no longer be
// scheduled as well. This allows you to both control the job via a context
// and listen for and handle cancellation within your job.
func WithContext(ctx context.Context) JobOption {
	return func(j *internalJob, _ time.Time) error {
		if ctx == nil {
			return ErrWithContextNil
		}
		j.parentCtx = ctx
		return nil
	}
}

// -----------------------------------------------
// -----------------------------------------------
// ------------- Job Event Listeners -------------
// -----------------------------------------------
// -----------------------------------------------

// EventListener defines the constructor for event
// listeners that can be used to listen for job events.
type EventListener func(*internalJob) error

// BeforeJobRuns is used to listen for when a job is about to run and
// then run the provided function.
func BeforeJobRuns(eventListenerFunc func(jobID uuid.UUID, jobName string)) EventListener {
	return func(j *internalJob) error {
		if eventListenerFunc == nil {
			return ErrEventListenerFuncNil
		}
		j.beforeJobRuns = eventListenerFunc
		return nil
	}
}

// BeforeJobRunsSkipIfBeforeFuncErrors is used to listen for when a job is about to run and
// then runs the provided function. If the provided function returns an error, the job will be
// rescheduled and the current run will be skipped.
func BeforeJobRunsSkipIfBeforeFuncErrors(eventListenerFunc func(jobID uuid.UUID, jobName string) error) EventListener {
	return func(j *internalJob) error {
		if eventListenerFunc == nil {
			return ErrEventListenerFuncNil
		}
		j.beforeJobRunsSkipIfBeforeFuncErrors = eventListenerFunc
		return nil
	}
}

// AfterJobRuns is used to listen for when a job has run
// without an error, and then run the provided function.
func AfterJobRuns(eventListenerFunc func(jobID uuid.UUID, jobName string)) EventListener {
	return func(j *internalJob) error {
		if eventListenerFunc == nil {
			return ErrEventListenerFuncNil
		}
		j.afterJobRuns = eventListenerFunc
		return nil
	}
}

// AfterJobRunsWithError is used to listen for when a job has run and
// returned an error, and then run the provided function.
func AfterJobRunsWithError(eventListenerFunc func(jobID uuid.UUID, jobName string, err error)) EventListener {
	return func(j *internalJob) error {
		if eventListenerFunc == nil {
			return ErrEventListenerFuncNil
		}
		j.afterJobRunsWithError = eventListenerFunc
		return nil
	}
}

// AfterJobRunsWithPanic is used to listen for when a job has run and
// returned panicked recover data, and then run the provided function.
func AfterJobRunsWithPanic(eventListenerFunc func(jobID uuid.UUID, jobName string, recoverData any)) EventListener {
	return func(j *internalJob) error {
		if eventListenerFunc == nil {
			return ErrEventListenerFuncNil
		}
		j.afterJobRunsWithPanic = eventListenerFunc
		return nil
	}
}

// AfterLockError is used to when the distributed locker returns an error and
// then run the provided function.
func AfterLockError(eventListenerFunc func(jobID uuid.UUID, jobName string, err error)) EventListener {
	return func(j *internalJob) error {
		if eventListenerFunc == nil {
			return ErrEventListenerFuncNil
		}
		j.afterLockError = eventListenerFunc
		return nil
	}
}

// -----------------------------------------------
// -----------------------------------------------
// ---------------- Job Schedules ----------------
// -----------------------------------------------
// -----------------------------------------------

type jobSchedule interface {
	next(lastRun time.Time) time.Time
}

var _ jobSchedule = (*cronJob)(nil)

type cronJob struct {
	crontab      string
	cronSchedule Cron
}

func (j *cronJob) next(lastRun time.Time) time.Time {
	return j.cronSchedule.Next(lastRun)
}

var _ jobSchedule = (*durationJob)(nil)

type durationJob struct {
	duration time.Duration
}

func (j *durationJob) next(lastRun time.Time) time.Time {
	return lastRun.Add(j.duration)
}

var _ jobSchedule = (*durationRandomJob)(nil)

type durationRandomJob struct {
	min, max time.Duration
	rand     *rand.Rand
}

func (j *durationRandomJob) next(lastRun time.Time) time.Time {
	r := j.rand.Int63n(int64(j.max - j.min))
	return lastRun.Add(j.min + time.Duration(r))
}

var _ jobSchedule = (*dailyJob)(nil)

type dailyJob struct {
	interval uint
	atTimes  []time.Time
}

func (d dailyJob) next(lastRun time.Time) time.Time {
	firstPass := true
	next := d.nextDay(lastRun, firstPass)
	if !next.IsZero() {
		return next
	}
	firstPass = false

	startNextDay := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day()+int(d.interval), 0, 0, 0, 0, lastRun.Location())
	return d.nextDay(startNextDay, firstPass)
}

func (d dailyJob) nextDay(lastRun time.Time, firstPass bool) time.Time {
	for _, at := range d.atTimes {
		// sub the at time hour/min/sec onto the lastScheduledRun's values
		// to use in checks to see if we've got our next run time
		atDate := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), at.Hour(), at.Minute(), at.Second(), 0, lastRun.Location())

		if firstPass && atDate.After(lastRun) {
			// checking to see if it is after i.e. greater than,
			// and not greater or equal as our lastScheduledRun day/time
			// will be in the loop, and we don't want to select it again
			return atDate
		} else if !firstPass && !atDate.Before(lastRun) {
			// now that we're looking at the next day, it's ok to consider
			// the same at time that was last run (as lastScheduledRun has been incremented)
			return atDate
		}
	}
	return time.Time{}
}

var _ jobSchedule = (*weeklyJob)(nil)

type weeklyJob struct {
	interval   uint
	daysOfWeek []time.Weekday
	atTimes    []time.Time
}

func (w weeklyJob) next(lastRun time.Time) time.Time {
	next := w.nextWeekDayAtTime(lastRun, true)
	if !next.IsZero() {
		return next
	}

	startOfTheNextIntervalWeek := (lastRun.Day() - int(lastRun.Weekday())) + int(w.interval*7)
	from := time.Date(lastRun.Year(), lastRun.Month(), startOfTheNextIntervalWeek, 0, 0, 0, 0, lastRun.Location())
	return w.nextWeekDayAtTime(from, false)
}

func (w weeklyJob) nextWeekDayAtTime(lastRun time.Time, firstPass bool) time.Time {
	for _, wd := range w.daysOfWeek {
		// checking if we're on the same day or later in the same week
		if wd >= lastRun.Weekday() {
			// weekDayDiff is used to add the correct amount to the atDate day below
			weekDayDiff := wd - lastRun.Weekday()
			for _, at := range w.atTimes {
				// sub the at time hour/min/sec onto the lastScheduledRun's values
				// to use in checks to see if we've got our next run time
				atDate := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day()+int(weekDayDiff), at.Hour(), at.Minute(), at.Second(), 0, lastRun.Location())

				if firstPass && atDate.After(lastRun) {
					// checking to see if it is after i.e. greater than,
					// and not greater or equal as our lastScheduledRun day/time
					// will be in the loop, and we don't want to select it again
					return atDate
				} else if !firstPass && !atDate.Before(lastRun) {
					// now that we're looking at the next week, it's ok to consider
					// the same at time that was last run (as lastScheduledRun has been incremented)
					return atDate
				}
			}
		}
	}
	return time.Time{}
}

var _ jobSchedule = (*monthlyJob)(nil)

type monthlyJob struct {
	interval    uint
	days        []int
	daysFromEnd []int
	atTimes     []time.Time
}

func (m monthlyJob) next(lastRun time.Time) time.Time {
	daysList := make([]int, len(m.days))
	copy(daysList, m.days)

	daysFromEnd := m.handleNegativeDays(lastRun, daysList, m.daysFromEnd)
	next := m.nextMonthDayAtTime(lastRun, daysFromEnd, true)
	if !next.IsZero() {
		return next
	}

	from := time.Date(lastRun.Year(), lastRun.Month()+time.Month(m.interval), 1, 0, 0, 0, 0, lastRun.Location())
	for next.IsZero() {
		daysFromEnd = m.handleNegativeDays(from, daysList, m.daysFromEnd)
		next = m.nextMonthDayAtTime(from, daysFromEnd, false)
		from = from.AddDate(0, int(m.interval), 0)
	}

	return next
}

func (m monthlyJob) handleNegativeDays(from time.Time, days, negativeDays []int) []int {
	var out []int
	// getting a list of the days from the end of the following month
	// -1 == the last day of the month
	firstDayNextMonth := time.Date(from.Year(), from.Month()+1, 1, 0, 0, 0, 0, from.Location())
	for _, daySub := range negativeDays {
		day := firstDayNextMonth.AddDate(0, 0, daySub).Day()
		out = append(out, day)
	}
	out = append(out, days...)
	slices.Sort(out)
	return out
}

func (m monthlyJob) nextMonthDayAtTime(lastRun time.Time, days []int, firstPass bool) time.Time {
	// find the next day in the month that should run and then check for an at time
	for _, day := range days {
		if day >= lastRun.Day() {
			for _, at := range m.atTimes {
				// sub the day, and the at time hour/min/sec onto the lastScheduledRun's values
				// to use in checks to see if we've got our next run time
				atDate := time.Date(lastRun.Year(), lastRun.Month(), day, at.Hour(), at.Minute(), at.Second(), 0, lastRun.Location())

				if atDate.Month() != lastRun.Month() {
					// this check handles if we're setting a day not in the current month
					// e.g. setting day 31 in Feb results in March 2nd
					continue
				}

				if firstPass && atDate.After(lastRun) {
					// checking to see if it is after i.e. greater than,
					// and not greater or equal as our lastScheduledRun day/time
					// will be in the loop, and we don't want to select it again
					return atDate
				} else if !firstPass && !atDate.Before(lastRun) {
					// now that we're looking at the next month, it's ok to consider
					// the same at time that was  lastScheduledRun (as lastScheduledRun has been incremented)
					return atDate
				}
			}
			continue
		}
	}
	return time.Time{}
}

var _ jobSchedule = (*oneTimeJob)(nil)

type oneTimeJob struct {
	sortedTimes []time.Time
}

// next finds the next item in a sorted list of times using binary-search.
//
// example: sortedTimes: [2, 4, 6, 8]
//
// lastRun: 1 => [idx=0,found=false] => next is 2 - sorted[idx] idx=0
// lastRun: 2 => [idx=0,found=true] => next is 4 - sorted[idx+1] idx=1
// lastRun: 3 => [idx=1,found=false] => next is 4 - sorted[idx] idx=1
// lastRun: 4 => [idx=1,found=true] => next is 6 - sorted[idx+1] idx=2
// lastRun: 7 => [idx=3,found=false] => next is 8 - sorted[idx] idx=3
// lastRun: 8 => [idx=3,found=found] => next is none
// lastRun: 9 => [idx=3,found=found] => next is none
func (o oneTimeJob) next(lastRun time.Time) time.Time {
	idx, found := slices.BinarySearchFunc(o.sortedTimes, lastRun, ascendingTime)
	// if found, the next run is the following index
	if found {
		idx++
	}
	// exhausted runs
	if idx >= len(o.sortedTimes) {
		return time.Time{}
	}

	return o.sortedTimes[idx]
}

// -----------------------------------------------
// -----------------------------------------------
// ---------------- Job Interface ----------------
// -----------------------------------------------
// -----------------------------------------------

// Job provides the available methods on the job
// available to the caller.
type Job interface {
	// ID returns the job's unique identifier.
	ID() uuid.UUID
	// LastRun returns the time of the job's last run
	LastRun() (time.Time, error)
	// Name returns the name defined on the job.
	Name() string
	// NextRun returns the time of the job's next scheduled run.
	// This value is only available once the scheduler has been started
	// with Scheduler.Start(). Before that, it returns the zero time value.
	NextRun() (time.Time, error)
	// NextRuns returns the requested number of calculated next run values.
	// These values are only available once the scheduler has been started
	// with Scheduler.Start(). Before that, it returns nil.
	NextRuns(int) ([]time.Time, error)
	// RunNow runs the job once, now. This does not alter
	// the existing run schedule, and will respect all job
	// and scheduler limits. This means that running a job now may
	// cause the job's regular interval to be rescheduled due to
	// the instance being run by RunNow blocking your run limit.
	RunNow() error
	// Tags returns the job's string tags.
	Tags() []string
}

var _ Job = (*job)(nil)

// job is the internal struct that implements
// the public interface. This is used to avoid
// leaking information the caller never needs
// to have or tinker with.
type job struct {
	id            uuid.UUID
	name          string
	tags          []string
	jobOutRequest chan *jobOutRequest
	runJobRequest chan runJobRequest
}

func (j job) ID() uuid.UUID {
	return j.id
}

func (j job) LastRun() (time.Time, error) {
	ij := requestJob(j.id, j.jobOutRequest)
	if ij == nil || ij.id == uuid.Nil {
		return time.Time{}, ErrJobNotFound
	}
	return ij.lastRun, nil
}

func (j job) Name() string {
	return j.name
}

func (j job) NextRun() (time.Time, error) {
	ij := requestJob(j.id, j.jobOutRequest)
	if ij == nil || ij.id == uuid.Nil {
		return time.Time{}, ErrJobNotFound
	}
	if len(ij.nextScheduled) == 0 {
		return time.Time{}, nil
	}
	// the first element is the next scheduled run with subsequent
	// runs following after in the slice
	return ij.nextScheduled[0], nil
}

func (j job) NextRuns(count int) ([]time.Time, error) {
	ij := requestJob(j.id, j.jobOutRequest)
	if ij == nil || ij.id == uuid.Nil {
		return nil, ErrJobNotFound
	}

	lengthNextScheduled := len(ij.nextScheduled)
	if lengthNextScheduled == 0 {
		return nil, nil
	} else if count <= lengthNextScheduled {
		return ij.nextScheduled[:count], nil
	}

	out := make([]time.Time, count)
	for i := 0; i < count; i++ {
		if i < lengthNextScheduled {
			out[i] = ij.nextScheduled[i]
			continue
		}

		from := out[i-1]
		out[i] = ij.next(from)
	}

	return out, nil
}

func (j job) Tags() []string {
	return j.tags
}

func (j job) RunNow() error {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	resp := make(chan error, 1)

	t := time.NewTimer(100 * time.Millisecond)
	select {
	case j.runJobRequest <- runJobRequest{
		id:      j.id,
		outChan: resp,
	}:
		t.Stop()
	case <-t.C:
		return ErrJobRunNowFailed
	}
	var err error
	select {
	case <-ctx.Done():
		return ErrJobRunNowFailed
	case errReceived := <-resp:
		err = errReceived
	}
	return err
}


================================================
FILE: job_test.go
================================================
package gocron

import (
	"math/rand"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/jonboulle/clockwork"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestDurationJob_next(t *testing.T) {
	tests := []time.Duration{
		time.Millisecond,
		time.Second,
		100 * time.Second,
		1000 * time.Second,
		5 * time.Second,
		50 * time.Second,
		time.Minute,
		5 * time.Minute,
		100 * time.Minute,
		time.Hour,
		2 * time.Hour,
		100 * time.Hour,
		1000 * time.Hour,
	}

	lastRun := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)

	for _, duration := range tests {
		t.Run(duration.String(), func(t *testing.T) {
			d := durationJob{duration: duration}
			next := d.next(lastRun)
			expected := lastRun.Add(duration)

			assert.Equal(t, expected, next)
		})
	}
}

func TestDailyJob_next(t *testing.T) {
	americaChicago, err := time.LoadLocation("America/Chicago")
	require.NoError(t, err)

	tests := []struct {
		name                      string
		interval                  uint
		atTimes                   []time.Time
		lastRun                   time.Time
		expectedNextRun           time.Time
		expectedDurationToNextRun time.Duration
	}{
		{
			"daily at midnight",
			1,
			[]time.Time{
				time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC),
			},
			time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
			time.Date(2000, 1, 2, 0, 0, 0, 0, time.UTC),
			24 * time.Hour,
		},
		{
			"daily multiple at times",
			1,
			[]time.Time{
				time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
				time.Date(0, 0, 0, 12, 30, 0, 0, time.UTC),
			},
			time.Date(2000, 1, 1, 5, 30, 0, 0, time.UTC),
			time.Date(2000, 1, 1, 12, 30, 0, 0, time.UTC),
			7 * time.Hour,
		},
		{
			"every 2 days multiple at times",
			2,
			[]time.Time{
				time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
				time.Date(0, 0, 0, 12, 30, 0, 0, time.UTC),
			},
			time.Date(2000, 1, 1, 12, 30, 0, 0, time.UTC),
			time.Date(2000, 1, 3, 5, 30, 0, 0, time.UTC),
			41 * time.Hour,
		},
		{
			"daily at time with daylight savings time",
			1,
			[]time.Time{
				time.Date(0, 0, 0, 5, 30, 0, 0, americaChicago),
			},
			time.Date(2023, 3, 11, 5, 30, 0, 0, americaChicago),
			time.Date(2023, 3, 12, 5, 30, 0, 0, americaChicago),
			23 * time.Hour,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			d := dailyJob{
				interval: tt.interval,
				atTimes:  tt.atTimes,
			}

			next := d.next(tt.lastRun)
			assert.Equal(t, tt.expectedNextRun, next)
			assert.Equal(t, tt.expectedDurationToNextRun, next.Sub(tt.lastRun))
		})
	}
}

func TestWeeklyJob_next(t *testing.T) {
	americaChicago, err := time.LoadLocation("America/Chicago")
	require.NoError(t, err)

	tests := []struct {
		name                      string
		interval                  uint
		daysOfWeek                []time.Weekday
		atTimes                   []time.Time
		lastRun                   time.Time
		expectedNextRun           time.Time
		expectedDurationToNextRun time.Duration
	}{
		{
			"last run Monday, next run is Thursday",
			1,
			[]time.Weekday{time.Monday, time.Thursday},
			[]time.Time{
				time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC),
			},
			time.Date(2000, 1, 3, 0, 0, 0, 0, time.UTC),
			time.Date(2000, 1, 6, 0, 0, 0, 0, time.UTC),
			3 * 24 * time.Hour,
		},
		{
			"last run Thursday, next run is Monday",
			1,
			[]time.Weekday{time.Monday, time.Thursday},
			[]time.Time{
				time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
			},
			time.Date(2000, 1, 6, 5, 30, 0, 0, time.UTC),
			time.Date(2000, 1, 10, 5, 30, 0, 0, time.UTC),
			4 * 24 * time.Hour,
		},
		{
			"last run before daylight savings time, next run after",
			1,
			[]time.Weekday{time.Saturday},
			[]time.Time{
				time.Date(0, 0, 0, 5, 30, 0, 0, americaChicago),
			},
			time.Date(2023, 3, 11, 5, 30, 0, 0, americaChicago),
			time.Date(2023, 3, 18, 5, 30, 0, 0, americaChicago),
			7*24*time.Hour - time.Hour,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			w := weeklyJob{
				interval:   tt.interval,
				daysOfWeek: tt.daysOfWeek,
				atTimes:    tt.atTimes,
			}

			next := w.next(tt.lastRun)
			assert.Equal(t, tt.expectedNextRun, next)
			assert.Equal(t, tt.expectedDurationToNextRun, next.Sub(tt.lastRun))
		})
	}
}

func TestMonthlyJob_next(t *testing.T) {
	americaChicago, err := time.LoadLocation("America/Chicago")
	require.NoError(t, err)

	tests := []struct {
		name                      string
		interval                  uint
		days                      []int
		daysFromEnd               []int
		atTimes                   []time.Time
		lastRun                   time.Time
		expectedNextRun           time.Time
		expectedDurationToNextRun time.Duration
	}{
		{
			"same day - before at time",
			1,
			[]int{1},
			nil,
			[]time.Time{
				time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
			},
			time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
			time.Date(2000, 1, 1, 5, 30, 0, 0, time.UTC),
			5*time.Hour + 30*time.Minute,
		},
		{
			"same day - after at time, runs next available date",
			1,
			[]int{1, 10},
			nil,
			[]time.Time{
				time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC),
			},
			time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
			time.Date(2000, 1, 10, 0, 0, 0, 0, time.UTC),
			9 * 24 * time.Hour,
		},
		{
			"same day - after at time, runs next available date, following interval month",
			2,
			[]int{1},
			nil,
			[]time.Time{
				time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
			},
			time.Date(2000, 1, 1, 5, 30, 0, 0, time.UTC),
			time.Date(2000, 3, 1, 5, 30, 0, 0, time.UTC),
			60 * 24 * time.Hour,
		},
		{
			"daylight savings time",
			1,
			[]int{5},
			nil,
			[]time.Time{
				time.Date(0, 0, 0, 5, 30, 0, 0, americaChicago),
			},
			time.Date(2023, 11, 1, 0, 0, 0, 0, americaChicago),
			time.Date(2023, 11, 5, 5, 30, 0, 0, americaChicago),
			4*24*time.Hour + 6*time.Hour + 30*time.Minute,
		},
		{
			"negative days",
			1,
			nil,
			[]int{-1, -3, -5},
			[]time.Time{
				time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
			},
			time.Date(2000, 1, 29, 5, 30, 0, 0, time.UTC),
			time.Date(2000, 1, 31, 5, 30, 0, 0, time.UTC),
			2 * 24 * time.Hour,
		},
		{
			"day not in current month, runs next month (leap year)",
			1,
			[]int{31},
			nil,
			[]time.Time{
				time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
			},
			time.Date(2000, 1, 31, 5, 30, 0, 0, time.UTC),
			time.Date(2000, 3, 31, 5, 30, 0, 0, time.UTC),
			29*24*time.Hour + 31*24*time.Hour,
		},
		{
			"multiple days not in order",
			1,
			[]int{10, 7, 19, 2},
			nil,
			[]time.Time{
				time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
			},
			time.Date(2000, 1, 2, 5, 30, 0, 0, time.UTC),
			time.Date(2000, 1, 7, 5, 30, 0, 0, time.UTC),
			5 * 24 * time.Hour,
		},
		{
			"day not in next interval month, selects next available option, skips Feb, April & June",
			2,
			[]int{31},
			nil,
			[]time.Time{
				time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
			},
			time.Date(1999, 12, 31, 5, 30, 0, 0, time.UTC),
			time.Date(2000, 8, 31, 5, 30, 0, 0, time.UTC),
			244 * 24 * time.Hour,
		},
		{
			"handle -1 with differing month's day count",
			1,
			nil,
			[]int{-1},
			[]time.Time{
				time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
			},
			time.Date(2024, 1, 31, 5, 30, 0, 0, time.UTC),
			time.Date(2024, 2, 29, 5, 30, 0, 0, time.UTC),
			29 * 24 * time.Hour,
		},
		{
			"handle -1 with another differing month's day count",
			1,
			nil,
			[]int{-1},
			[]time.Time{
				time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
			},
			time.Date(2024, 2, 29, 5, 30, 0, 0, time.UTC),
			time.Date(2024, 3, 31, 5, 30, 0, 0, time.UTC),
			31 * 24 * time.Hour,
		},
		{
			"handle -1 every 3 months next run in February",
			3,
			nil,
			[]int{-1},
			[]time.Time{
				time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
			},
			time.Date(2023, 11, 30, 5, 30, 0, 0, time.UTC),
			time.Date(2024, 2, 29, 5, 30, 0, 0, time.UTC),
			91 * 24 * time.Hour,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			m := monthlyJob{
				interval:    tt.interval,
				days:        tt.days,
				daysFromEnd: tt.daysFromEnd,
				atTimes:     tt.atTimes,
			}

			next := m.next(tt.lastRun)
			assert.Equal(t, tt.expectedNextRun, next)
			assert.Equal(t, tt.expectedDurationToNextRun, next.Sub(tt.lastRun))
		})
	}
}

func TestDurationRandomJob_next(t *testing.T) {
	tests := []struct {
		name        string
		min         time.Duration
		max         time.Duration
		lastRun     time.Time
		expectedMin time.Time
		expectedMax time.Time
	}{
		{
			"min 1s, max 5s",
			time.Second,
			5 * time.Second,
			time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
			time.Date(2000, 1, 1, 0, 0, 1, 0, time.UTC),
			time.Date(2000, 1, 1, 0, 0, 5, 0, time.UTC),
		},
		{
			"min 100ms, max 1s",
			100 * time.Millisecond,
			1 * time.Second,
			time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
			time.Date(2000, 1, 1, 0, 0, 0, 100000000, time.UTC),
			time.Date(2000, 1, 1, 0, 0, 1, 0, time.UTC),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			rj := durationRandomJob{
				min:  tt.min,
				max:  tt.max,
				rand: rand.New(rand.NewSource(time.Now().UnixNano())), // nolint:gosec
			}

			for i := 0; i < 100; i++ {
				next := rj.next(tt.lastRun)
				assert.GreaterOrEqual(t, next, tt.expectedMin)
				assert.LessOrEqual(t, next, tt.expectedMax)
			}
		})
	}
}

func TestOneTimeJob_next(t *testing.T) {
	otj := oneTimeJob{}
	assert.Zero(t, otj.next(time.Time{}))
}

func TestJob_RunNow_Error(t *testing.T) {
	s := newTestScheduler(t)

	j, err := s.NewJob(
		DurationJob(time.Second),
		NewTask(func() {}),
	)
	require.NoError(t, err)

	require.NoError(t, s.Shutdown())

	assert.EqualError(t, j.RunNow(), ErrJobRunNowFailed.Error())
}

func TestJob_LastRun(t *testing.T) {
	testTime := time.Date(2000, 1, 1, 0, 0, 0, 0, time.Local)
	fakeClock := clockwork.NewFakeClockAt(testTime)

	s := newTestScheduler(t,
		WithClock(fakeClock),
	)

	j, err := s.NewJob(
		DurationJob(
			time.Second,
		),
		NewTask(
			func() {},
		),
		WithStartAt(WithStartImmediately()),
	)
	require.NoError(t, err)

	s.Start()
	time.Sleep(10 * time.Millisecond)

	lastRun, err := j.LastRun()
	assert.NoError(t, err)

	err = s.Shutdown()
	require.NoError(t, err)

	assert.Equal(t, testTime, lastRun)
}

func TestWithEventListeners(t *testing.T) {
	tests := []struct {
		name           string
		eventListeners []EventListener
		err            error
	}{
		{
			"no event listeners",
			nil,
			nil,
		},
		{
			"beforeJobRuns",
			[]EventListener{
				BeforeJobRuns(func(_ uuid.UUID, _ string) {}),
			},
			nil,
		},
		{
			"afterJobRuns",
			[]EventListener{
				AfterJobRuns(func(_ uuid.UUID, _ string) {}),
			},
			nil,
		},
		{
			"afterJobRunsWithError",
			[]EventListener{
				AfterJobRunsWithError(func(_ uuid.UUID, _ string, _ error) {}),
			},
			nil,
		},
		{
			"afterJobRunsWithPanic",
			[]EventListener{
				AfterJobRunsWithPanic(func(_ uuid.UUID, _ string, _ any) {}),
			},
			nil,
		},
		{
			"afterLockError",
			[]EventListener{
				AfterLockError(func(_ uuid.UUID, _ string, _ error) {}),
			},
			nil,
		},
		{
			"multiple event listeners",
			[]EventListener{
				AfterJobRuns(func(_ uuid.UUID, _ string) {}),
				AfterJobRunsWithError(func(_ uuid.UUID, _ string, _ error) {}),
				BeforeJobRuns(func(_ uuid.UUID, _ string) {}),
				AfterLockError(func(_ uuid.UUID, _ string, _ error) {}),
			},
			nil,
		},
		{
			"nil after job runs listener",
			[]EventListener{
				AfterJobRuns(nil),
			},
			ErrEventListenerFuncNil,
		},
		{
			"nil after job runs with error listener",
			[]EventListener{
				AfterJobRunsWithError(nil),
			},
			ErrEventListenerFuncNil,
		},
		{
			"nil before job runs listener",
			[]EventListener{
				BeforeJobRuns(nil),
			},
			ErrEventListenerFuncNil,
		},
		{
			"nil before job runs error listener",
			[]EventListener{
				BeforeJobRunsSkipIfBeforeFuncErrors(nil),
			},
			ErrEventListenerFuncNil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var ij internalJob
			err := WithEventListeners(tt.eventListeners...)(&ij, time.Now())
			assert.Equal(t, tt.err, err)

			if err != nil {
				return
			}
			var count int
			if ij.beforeJobRuns != nil {
				count++
			}
			if ij.afterJobRuns != nil {
				count++
			}
			if ij.afterJobRunsWithError != nil {
				count++
			}
			if ij.afterJobRunsWithPanic != nil {
				count++
			}
			if ij.afterLockError != nil {
				count++
			}
			assert.Equal(t, len(tt.eventListeners), count)
		})
	}
}

func TestJob_NextRun(t *testing.T) {
	tests := []struct {
		name string
		f    func()
	}{
		{
			"simple",
			func() {},
		},
		{
			"sleep 3 seconds",
			func() {
				time.Sleep(300 * time.Millisecond)
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			testTime := time.Now()

			s := newTestScheduler(t)

			j, err := s.NewJob(
				DurationJob(
					100*time.Millisecond,
				),
				NewTask(
					func() {},
				),
				WithStartAt(WithStartDateTime(testTime.Add(100*time.Millisecond))),
				WithSingletonMode(LimitModeReschedule),
			)
			require.NoError(t, err)

			s.Start()
			nextRun, err := j.NextRun()
			require.NoError(t, err)

			assert.Equal(t, testTime.Add(100*time.Millisecond), nextRun)

			time.Sleep(150 * time.Millisecond)

			nextRun, err = j.NextRun()
			assert.NoError(t, err)

			assert.Equal(t, testTime.Add(200*time.Millisecond), nextRun)
			assert.Equal(t, 200*time.Millisecond, nextRun.Sub(testTime))

			err = s.Shutdown()
			require.NoError(t, err)
		})
	}
}

func TestJob_NextRuns(t *testing.T) {
	tests := []struct {
		name      string
		jd        JobDefinition
		assertion func(t *testing.T, previousRun, nextRun time.Time)
	}{
		{
			"simple - milliseconds",
			DurationJob(
				100 * time.Millisecond,
			),
			func(t *testing.T, previousRun, nextRun time.Time) {
				assert.Equal(t, previousRun.UnixMilli()+100, nextRun.UnixMilli())
			},
		},
		{
			"weekly",
			WeeklyJob(
				2,
				NewWeekdays(time.Tuesday),
				NewAtTimes(
					NewAtTime(0, 0, 0),
				),
			),
			func(t *testing.T, previousRun, nextRun time.Time) {
				// With the fix for NextRun accuracy, the immediate run (Jan 1) is removed
				// from nextScheduled after it completes. So all intervals should be 14 days
				// (2 weeks as configured).
				diff := time.Hour * 14 * 24
				assert.Equal(t, previousRun.Add(diff).Day(), nextRun.Day())
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			testTime := time.Date(2000, 1, 1, 0, 0, 0, 0, time.Local)
			fakeClock := clockwork.NewFakeClockAt(testTime)

			s := newTestScheduler(t,
				WithClock(fakeClock),
			)

			j, err := s.NewJob(
				tt.jd,
				NewTask(
					func() {},
				),
				WithStartAt(WithStartImmediately()),
			)
			require.NoError(t, err)

			s.Start()
			time.Sleep(10 * time.Millisecond)

			nextRuns, err := j.NextRuns(5)
			require.NoError(t, err)

			assert.Len(t, nextRuns, 5)

			for i := range nextRuns {
				if i == 0 {
					// skipping because there is no previous run
					continue
				}
				tt.assertion(t, nextRuns[i-1], nextRuns[i])
			}

			assert.NoError(t, s.Shutdown())
		})
	}
}

func TestJob_PanicOccurred(t *testing.T) {
	gotCh := make(chan any)
	errCh := make(chan error)
	s := newTestScheduler(t)
	_, err := s.NewJob(
		DurationJob(10*time.Millisecond),
		NewTask(func() {
			a := 0
			_ = 1 / a
		}),
		WithEventListeners(
			AfterJobRunsWithPanic(func(_ uuid.UUID, _ string, recoverData any) {
				gotCh <- recoverData
			}), AfterJobRunsWithError(func(_ uuid.UUID, _ string, err error) {
				errCh <- err
			}),
		),
	)
	require.NoError(t, err)

	s.Start()
	got := <-gotCh
	require.EqualError(t, got.(error), "runtime error: integer divide by zero")

	err = <-errCh
	require.ErrorIs(t, err, ErrPanicRecovered)
	require.EqualError(t, err, "gocron: panic recovered from runtime error: integer divide by zero")

	require.NoError(t, s.Shutdown())
	close(gotCh)
	close(errCh)
}

func TestTimeFromAtTime(t *testing.T) {
	testTimeUTC := time.Date(0, 0, 0, 1, 1, 1, 0, time.UTC)
	cst, err := time.LoadLocation("America/Chicago")
	require.NoError(t, err)
	testTimeCST := time.Date(0, 0, 0, 1, 1, 1, 0, cst)

	tests := []struct {
		name         string
		at           AtTime
		loc          *time.Location
		expectedTime time.Time
		expectedStr  string
	}{
		{
			"UTC",
			NewAtTime(
				uint(testTimeUTC.Hour()),
				uint(testTimeUTC.Minute()),
				uint(testTimeUTC.Second()),
			),
			time.UTC,
			testTimeUTC,
			"01:01:01",
		},
		{
			"CST",
			NewAtTime(
				uint(testTimeCST.Hour()),
				uint(testTimeCST.Minute()),
				uint(testTimeCST.Second()),
			),
			cst,
			testTimeCST,
			"01:01:01",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := TimeFromAtTime(tt.at, tt.loc)
			assert.Equal(t, tt.expectedTime, result)

			resultFmt := result.Format("15:04:05")
			assert.Equal(t, tt.expectedStr, resultFmt)
		})
	}
}

func TestNewAtTimes(t *testing.T) {
	at := NewAtTimes(
		NewAtTime(1, 1, 1),
		NewAtTime(2, 2, 2),
	)

	var times []string
	for _, att := range at() {
		timeStr := TimeFromAtTime(att, time.UTC).Format("15:04")
		times = append(times, timeStr)
	}

	var timesAgain []string
	for _, att := range at() {
		timeStr := TimeFromAtTime(att, time.UTC).Format("15:04")
		timesAgain = append(timesAgain, timeStr)
	}

	assert.Equal(t, times, timesAgain)
}

func TestNewWeekdays(t *testing.T) {
	wd := NewWeekdays(
		time.Monday,
		time.Tuesday,
	)

	var dayStrings []string
	for _, w := range wd() {
		dayStrings = append(dayStrings, w.String())
	}

	var dayStringsAgain []string
	for _, w := range wd() {
		dayStringsAgain = append(dayStringsAgain, w.String())
	}

	assert.Equal(t, dayStrings, dayStringsAgain)
}

func TestNewDaysOfTheMonth(t *testing.T) {
	dom := NewDaysOfTheMonth(1, 2, 3)

	var domInts []int
	for _, d := range dom() {
		domInts = append(domInts, d)
	}

	var domIntsAgain []int
	for _, d := range dom() {
		domIntsAgain = append(domIntsAgain, d)
	}

	assert.Equal(t, domInts, domIntsAgain)
}

func TestWithIntervalFromCompletion_BasicFunctionality(t *testing.T) {
	t.Run("interval calculated from completion time", func(t *testing.T) {
		s, err := NewScheduler()
		require.NoError(t, err)
		defer func() { _ = s.Shutdown() }()

		var mu sync.Mutex
		executions := []struct {
			startTime    time.Time
			completeTime time.Time
		}{}

		jobExecutionTime := 2 * time.Second
		scheduledInterval := 5 * time.Second

		_, err = s.NewJob(
			DurationJob(scheduledInterval),
			NewTask(func() {
				start := time.Now()
				time.Sleep(jobExecutionTime)
				complete := time.Now()

				mu.Lock()
				executions = append(executions, struct {
					startTime    time.Time
					completeTime time.Time
				}{start, complete})
				mu.Unlock()
			}),
			WithIntervalFromCompletion(),
		)
		require.NoError(t, err)

		s.Start()

		// Wait for at least 3 executions
		// With intervalFromCompletion:
		// Execution 1: 0s-2s
		// Wait: 5s (from 2s to 7s)
		// Execution 2: 7s-9s
		// Wait: 5s (from 9s to 14s)
		// Execution 3: 14s-16s
		time.Sleep(18 * time.Second)

		mu.Lock()
		executionCount := len(executions)
		mu.Unlock()

		require.GreaterOrEqual(t, executionCount, 2,
			"Expected at least 2 executions")

		mu.Lock()
		defer mu.Unlock()

		for i := 1; i < len(executions); i++ {
			prev := executions[i-1]
			curr := executions[i]

			completionToStartGap := curr.startTime.Sub(prev.completeTime)

			assert.InDelta(t, scheduledInterval.Seconds(), completionToStartGap.Seconds(), 0.5,
				"Gap from completion to start should match the interval")
		}
	})
}

func TestWithIntervalFromCompletion_VariableExecutionTime(t *testing.T) {
	s, err := NewScheduler()
	require.NoError(t, err)
	defer func() { _ = s.Shutdown() }()

	var mu sync.Mutex
	executions := []struct {
		startTime    time.Time
		completeTime time.Time
		executionDur time.Duration
	}{}

	executionTimes := []time.Duration{
		1 * time.Second,
		3 * time.Second,
		500 * time.Millisecond,
	}
	currentExecution := atomic.Int32{}
	scheduledInterval := 4 * time.Second

	_, err = s.NewJob(
		DurationJob(scheduledInterval),
		NewTask(func() {
			idx := int(currentExecution.Add(1)) - 1
			if idx >= len(executionTimes) {
				return
			}

			start := time.Now()
			executionTime := executionTimes[idx]
			time.Sleep(executionTime)
			complete := time.Now()

			mu.Lock()
			executions = append(executions, struct {
				startTime    time.Time
				completeTime time.Time
				executionDur time.Duration
			}{start, complete, executionTime})
			mu.Unlock()
		}),
		WithIntervalFromCompletion(),
	)
	require.NoError(t, err)

	s.Start()

	// Wait for all 3 executions
	// Execution 1: 0s-1s, wait 4s → next at 5s
	// Execution 2: 5s-8s, wait 4s → next at 12s
	// Execution 3: 12s-12.5s
	time.Sleep(15 * time.Second)

	mu.Lock()
	defer mu.Unlock()

	require.GreaterOrEqual(t, len(executions), 2, "Expected at least 2 executions")

	for i := 1; i < len(executions); i++ {
		prev := executions[i-1]
		curr := executions[i]

		restPeriod := curr.startTime.Sub(prev.completeTime)

		assert.InDelta(t, scheduledInterval.Seconds(), restPeriod.Seconds(), 0.5,
			"Rest period should be consistent regardless of execution time")
	}
}

func TestWithIntervalFromCompletion_LongRunningJob(t *testing.T) {
	s, err := NewScheduler()
	require.NoError(t, err)
	defer func() { _ = s.Shutdown() }()

	var mu sync.Mutex
	executions := []struct {
		startTime    time.Time
		completeTime time.Time
	}{}

	jobExecutionTime := 6 * time.Second
	scheduledInterval := 3 * time.Second

	_, err = s.NewJob(
		DurationJob(scheduledInterval),
		NewTask(func() {
			start := time.Now()
			time.Sleep(jobExecutionTime)
			complete := time.Now()

			mu.Lock()
			executions = append(executions, struct {
				startTime    time.Time
				completeTime time.Time
			}{start, complete})
			mu.Unlock()
		}),
		WithIntervalFromCompletion(),
		WithSingletonMode(LimitModeReschedule),
	)
	require.NoError(t, err)

	s.Start()

	// Wait for 2 executions
	// Execution 1: 0s-6s, wait 3s → next at 9s
	// Execution 2: 9s-15s, wait 3s → next at 18s
	// Need to wait at least 16 seconds for 2 executions + buffer
	time.Sleep(22 * time.Second)

	mu.Lock()
	defer mu.Unlock()

	require.GreaterOrEqual(t, len(executions), 2, "Expected at least 2 executions")

	if len(executions) < 2 {
		t.Logf("Only got %d execution(s), skipping gap assertion", len(executions))
		return
	}

	prev := executions[0]
	curr := executions[1]

	completionGap := curr.startTime.Sub(prev.completeTime)

	assert.InDelta(t, scheduledInterval.Seconds(), completionGap.Seconds(), 0.5,
		"Gap should be the full interval even when execution time exceeds interval")
}

func TestWithIntervalFromCompletion_ComparedToDefault(t *testing.T) {
	jobExecutionTime := 2 * time.Second
	scheduledInterval := 5 * time.Second

	t.Run("default behavior - interval from scheduled time", func(t *testing.T) {
		s, err := NewScheduler()
		require.NoError(t, err)
		defer func() { _ = s.Shutdown() }()

		var mu sync.Mutex
		executions := []struct {
			startTime    time.Time
			completeTime time.Time
		}{}

		_, err = s.NewJob(
			DurationJob(scheduledInterval),
			NewTask(func() {
				start := time.Now()
				time.Sleep(jobExecutionTime)
				complete := time.Now()

				mu.Lock()
				executions = append(executions, struct {
					startTime    time.Time
					completeTime time.Time
				}{start, complete})
				mu.Unlock()
			}),
		)
		require.NoError(t, err)

		s.Start()
		time.Sleep(13 * time.Second)

		mu.Lock()
		defer mu.Unlock()

		require.GreaterOrEqual(t, len(executions), 2, "Expected at least 2 executions")

		prev := executions[0]
		curr := executions[1]
		completionGap := curr.startTime.Sub(prev.completeTime)

		expectedGap := scheduledInterval - jobExecutionTime
		assert.InDelta(t, expectedGap.Seconds(), completionGap.Seconds(), 0.5,
			"Default behavior: gap should be interval minus execution time")
	})

	t.Run("with intervalFromCompletion - interval from completion time", func(t *testing.T) {
		s, err := NewScheduler()
		require.NoError(t, err)
		defer func() { _ = s.Shutdown() }()

		var mu sync.Mutex
		executions := []struct {
			startTime    time.Time
			completeTime time.Time
		}{}

		_, err = s.NewJob(
			DurationJob(scheduledInterval),
			NewTask(func() {
				start := time.Now()
				time.Sleep(jobExecutionTime)
				complete := time.Now()

				mu.Lock()
				executions = append(executions, struct {
					startTime    time.Time
					completeTime time.Time
				}{start, complete})
				mu.Unlock()
			}),
			WithIntervalFromCompletion(),
		)
		require.NoError(t, err)

		s.Start()
		time.Sleep(15 * time.Second)

		mu.Lock()
		defer mu.Unlock()

		require.GreaterOrEqual(t, len(executions), 2, "Expected at least 2 executions")

		prev := executions[0]
		curr := executions[1]
		completionGap := curr.startTime.Sub(prev.completeTime)

		assert.InDelta(t, scheduledInterval.Seconds(), completionGap.Seconds(), 0.5,
			"With intervalFromCompletion: gap should be the full interval")
	})
}

func TestWithIntervalFromCompletion_DurationRandomJob(t *testing.T) {
	s, err := NewScheduler()
	require.NoError(t, err)
	defer func() { _ = s.Shutdown() }()

	var mu sync.Mutex
	executions := []struct {
		startTime    time.Time
		completeTime time.Time
	}{}

	jobExecutionTime := 1 * time.Second
	minInterval := 3 * time.Second
	maxInterval := 4 * time.Second

	_, err = s.NewJob(
		DurationRandomJob(minInterval, maxInterval),
		NewTask(func() {
			start := time.Now()
			time.Sleep(jobExecutionTime)
			complete := time.Now()

			mu.Lock()
			executions = append(executions, struct {
				startTime    time.Time
				completeTime time.Time
			}{start, complete})
			mu.Unlock()
		}),
		WithIntervalFromCompletion(),
	)
	require.NoError(t, err)

	s.Start()

	time.Sleep(15 * time.Second)

	mu.Lock()
	defer mu.Unlock()

	require.GreaterOrEqual(t, len(executions), 2, "Expected at least 2 executions")

	for i := 1; i < len(executions); i++ {
		prev := executions[i-1]
		curr := executions[i]

		restPeriod := curr.startTime.Sub(prev.completeTime)
		assert.GreaterOrEqual(t, restPeriod.Seconds(), minInterval.Seconds()-0.5,
			"Rest period should be at least minInterval")
		assert.LessOrEqual(t, restPeriod.Seconds(), maxInterval.Seconds()+0.5,
			"Rest period should be at most maxInterval")
	}
}

func TestWithIntervalFromCompletion_FirstRun(t *testing.T) {
	s, err := NewScheduler()
	require.NoError(t, err)
	defer func() { _ = s.Shutdown() }()

	var mu sync.Mutex
	var firstRunTime time.Time

	_, err = s.NewJob(
		DurationJob(5*time.Second),
		NewTask(func() {
			mu.Lock()
			if firstRunTime.IsZero() {
				firstRunTime = time.Now()
			}
			mu.Unlock()
		}),
		WithIntervalFromCompletion(),
		WithStartAt(WithStartImmediately()),
	)
	require.NoError(t, err)

	startTime := time.Now()
	s.Start()

	time.Sleep(1 * time.Second)

	mu.Lock()
	defer mu.Unlock()

	require.False(t, firstRunTime.IsZero(), "Job should have run at least once")

	timeSinceStart := firstRunTime.Sub(startTime)
	assert.Less(t, timeSinceStart.Seconds(), 1.0,
		"First run should happen quickly with WithStartImmediately")
}

func TestJob_NextRun_MultipleJobsSimultaneously(t *testing.T) {
	// This test reproduces the bug where multiple jobs completing simultaneously
	// would cause NextRun() to return stale values due to race conditions in
	// nextScheduled cleanup.

	testTime := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
	fakeClock := clockwork.NewFakeClockAt(testTime)

	s := newTestScheduler(t,
		WithClock(fakeClock),
		WithLocation(time.UTC),
	)

	jobsCompleted := make(chan struct{}, 4)

	// Create multiple jobs with different intervals that will complete around the same time
	job1, err := s.NewJob(
		DurationJob(1*time.Minute),
		NewTask(func() {
			jobsCompleted <- struct{}{}
		}),
		WithName("job1"),
		WithStartAt(WithStartImmediately()),
	)
	require.NoError(t, err)

	job2, err := s.NewJob(
		DurationJob(2*time.Minute),
		NewTask(func() {
			jobsCompleted <- struct{}{}
		}),
		WithName("job2"),
		WithStartAt(WithStartImmediately()),
	)
	require.NoError(t, err)

	job3, err := s.NewJob(
		DurationJob(3*time.Minute),
		NewTask(func() {
			jobsCompleted <- struct{}{}
		}),
		WithName("job3"),
		WithStartAt(WithStartImmediately()),
	)
	require.NoError(t, err)

	job4, err := s.NewJob(
		DurationJob(4*time.Minute),
		NewTask(func() {
			jobsCompleted <- struct{}{}
		}),
		WithName("job4"),
		WithStartAt(WithStartImmediately()),
	)
	require.NoError(t, err)

	s.Start()

	// Wait for all 4 jobs to complete their immediate run
	for i := 0; i < 4; i++ {
		<-jobsCompleted
	}

	// Give the scheduler time to process the completions and reschedule
	time.Sleep(50 * time.Millisecond)

	// Verify that NextRun() returns the correct next scheduled time for each job
	// and not a stale value from the just-completed run

	nextRun1, err := job1.NextRun()
	require.NoError(t, err)
	assert.Equal(t, testTime.Add(1*time.Minute), nextRun1, "job1 NextRun should be 1 minute from start")

	nextRun2, err := job2.NextRun()
	require.NoError(t, err)
	assert.Equal(t, testTime.Add(2*time.Minute), nextRun2, "job2 NextRun should be 2 minutes from start")

	nextRun3, err := job3.NextRun()
	require.NoError(t, err)
	assert.Equal(t, testTime.Add(3*time.Minute), nextRun3, "job3 NextRun should be 3 minutes from start")

	nextRun4, err := job4.NextRun()
	require.NoError(t, err)
	assert.Equal(t, testTime.Add(4*time.Minute), nextRun4, "job4 NextRun should be 4 minutes from start")

	// Advance time to trigger job1's next run
	fakeClock.Advance(1 * time.Minute)

	// Wait for job1 to complete
	<-jobsCompleted
	time.Sleep(50 * time.Millisecond)

	// After job1's second run, it should be scheduled for +2 minutes from start
	nextRun1, err = job1.NextRun()
	require.NoError(t, err)
	assert.Equal(t, testTime.Add(2*time.Minute), nextRun1, "job1 NextRun should be 2 minutes from start after first interval")

	require.NoError(t, s.Shutdown())
}

func TestJob_NextRun_ConcurrentCompletions(t *testing.T) {
	// This test verifies that when multiple jobs complete at exactly the same time,
	// their NextRun() values are correctly updated without race conditions.

	testTime := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
	fakeClock := clockwork.NewFakeClockAt(testTime)

	s := newTestScheduler(t,
		WithClock(fakeClock),
		WithLocation(time.UTC), // Set scheduler to use UTC to match our test time
	)

	var wg sync.WaitGroup
	jobCompletionBarrier := make(chan struct{})

	// Create jobs that will all complete at the same instant
	createJob := func(name string, interval time.Duration) Job {
		job, err := s.NewJob(
			DurationJob(interval),
			NewTask(func() {
				wg.Done()
				<-jobCompletionBarrier // Wait until all jobs are ready to complete
			}),
			WithName(name),
			WithStartAt(WithStartImmediately()),
		)
		require.NoError(t, err)
		return job
	}

	wg.Add(4)
	job1 := createJob("concurrent-job1", 1*time.Minute)
	job2 := createJob("concurrent-job2", 2*time.Minute)
	job3 := createJob("concurrent-job3", 3*time.Minute)
	job4 := createJob("concurrent-job4", 4*time.Minute)

	s.Start()

	wg.Wait()
	close(jobCompletionBarrier)

	// Give the scheduler time to process all completions
	time.Sleep(100 * time.Millisecond)

	// Verify NextRun() for all jobs concurrently to stress test the race condition
	var testWg sync.WaitGroup
	testWg.Add(4)

	go func() {
		defer testWg.Done()
		for i := 0; i < 10; i++ {
			nextRun, err := job1.NextRun()
			require.NoError(t, err)
			assert.Equal(t, testTime.Add(1*time.Minute), nextRun)
		}
	}()

	go func() {
		defer testWg.Done()
		for i := 0; i < 10; i++ {
			nextRun, err := job2.NextRun()
			require.NoError(t, err)
			assert.Equal(t, testTime.Add(2*time.Minute), nextRun)
		}
	}()

	go func() {
		defer testWg.Done()
		for i := 0; i < 10; i++ {
			nextRun, err := job3.NextRun()
			require.NoError(t, err)
			assert.Equal(t, testTime.Add(3*time.Minute), nextRun)
		}
	}()

	go func() {
		defer testWg.Done()
		for i := 0; i < 10; i++ {
			nextRun, err := job4.NextRun()
			require.NoError(t, err)
			assert.Equal(t, testTime.Add(4*time.Minute), nextRun)
		}
	}()

	testWg.Wait()
	require.NoError(t, s.Shutdown())
}


================================================
FILE: logger.go
================================================
//go:generate mockgen -destination=mocks/logger.go -package=gocronmocks . Logger
package gocron

import (
	"fmt"
	"log"
	"os"
	"strings"
)

// Logger is the interface that wraps the basic logging methods
// used by gocron. The methods are modeled after the standard
// library slog package. The default logger is a no-op logger.
// To enable logging, use one of the provided New*Logger functions
// or implement your own Logger. The actual level of Log that is logged
// is handled by the implementation.
type Logger interface {
	Debug(msg string, args ...any)
	Error(msg string, args ...any)
	Info(msg string, args ...any)
	Warn(msg string, args ...any)
}

var _ Logger = (*noOpLogger)(nil)

type noOpLogger struct{}

func (l noOpLogger) Debug(_ string, _ ...any) {}
func (l noOpLogger) Error(_ string, _ ...any) {}
func (l noOpLogger) Info(_ string, _ ...any)  {}
func (l noOpLogger) Warn(_ string, _ ...any)  {}

var _ Logger = (*logger)(nil)

// LogLevel is the level of logging that should be logged
// when using the basic NewLogger.
type LogLevel int

// The different log levels that can be used.
const (
	LogLevelError LogLevel = iota
	LogLevelWarn
	LogLevelInfo
	LogLevelDebug
)

type logger struct {
	log   *log.Logger
	level LogLevel
}

// NewLogger returns a new Logger that logs at the given level.
func NewLogger(level LogLevel) Logger {
	l := log.New(os.Stdout, "", log.LstdFlags)
	return &logger{
		log:   l,
		level: level,
	}
}

func (l *logger) Debug(msg string, args ...any) {
	if l.level < LogLevelDebug {
		return
	}
	l.log.Printf("DEBUG: %s%s\n", msg, logFormatArgs(args...))
}

func (l *logger) Error(msg string, args ...any) {
	if l.level < LogLevelError {
		return
	}
	l.log.Printf("ERROR: %s%s\n", msg, logFormatArgs(args...))
}

func (l *logger) Info(msg string, args ...any) {
	if l.level < LogLevelInfo {
		return
	}
	l.log.Printf("INFO: %s%s\n", msg, logFormatArgs(args...))
}

func (l *logger) Warn(msg string, args ...any) {
	if l.level < LogLevelWarn {
		return
	}
	l.log.Printf("WARN: %s%s\n", msg, logFormatArgs(args...))
}

func logFormatArgs(args ...any) string {
	if len(args) == 0 {
		return ""
	}
	if len(args)%2 != 0 {
		return ", " + fmt.Sprint(args...)
	}
	var pairs []string
	for i := 0; i < len(args); i += 2 {
		pairs = append(pairs, fmt.Sprintf("%s=%v", args[i], args[i+1]))
	}
	return ", " + strings.Join(pairs, ", ")
}


================================================
FILE: logger_test.go
================================================
package gocron

import (
	"bytes"
	"log"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestNoOpLogger(_ *testing.T) {
	noOp := noOpLogger{}
	noOp.Debug("debug", "arg1", "arg2")
	noOp.Error("error", "arg1", "arg2")
	noOp.Info("info", "arg1", "arg2")
	noOp.Warn("warn", "arg1", "arg2")
}

func TestNewLogger(t *testing.T) {
	tests := []struct {
		name  string
		level LogLevel
	}{
		{
			"debug",
			LogLevelDebug,
		},
		{
			"info",
			LogLevelInfo,
		},
		{
			"warn",
			LogLevelWarn,
		},
		{
			"error",
			LogLevelError,
		},
		{
			"Less than error",
			-1,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var results bytes.Buffer
			l := &logger{
				level: tt.level,
				log:   log.New(&results, "", log.LstdFlags),
			}

			var noArgs []any
			oneArg := []any{"arg1"}
			twoArgs := []any{"arg1", "arg2"}
			var noArgsStr []string
			oneArgStr := []string{"arg1"}
			twoArgsStr := []string{"arg1", "arg2"}

			for _, args := range []struct {
				argsAny []any
				argsStr []string
			}{
				{noArgs, noArgsStr},
				{oneArg, oneArgStr},
				{twoArgs, twoArgsStr},
			} {
				l.Debug("debug", args.argsAny...)
				if tt.level >= LogLevelDebug {
					r := results.String()
					assert.Contains(t, r, "DEBUG: debug")
					assert.Contains(t, r, strings.Join(args.argsStr, "="))
				} else {
					assert.Empty(t, results.String())
				}
				results.Reset()

				l.Info("info", args.argsAny...)
				if tt.level >= LogLevelInfo {
					r := results.String()
					assert.Contains(t, r, "INFO: info")
					assert.Contains(t, r, strings.Join(args.argsStr, "="))
				} else {
					assert.Empty(t, results.String())
				}
				results.Reset()

				l.Warn("warn", args.argsAny...)
				if tt.level >= LogLevelWarn {
					r := results.String()
					assert.Contains(t, r, "WARN: warn")
					assert.Contains(t, r, strings.Join(args.argsStr, "="))
				} else {
					assert.Empty(t, results.String())
				}
				results.Reset()

				l.Error("error", args.argsAny...)
				if tt.level >= LogLevelError {
					r := results.String()
					assert.Contains(t, r, "ERROR: error")
					assert.Contains(t, r, strings.Join(args.argsStr, "="))
				} else {
					assert.Empty(t, results.String())
				}
				results.Reset()
			}
		})
	}
}


================================================
FILE: migration_v1_to_v2.md
================================================
# Migration Guide: `gocron` v1 → v2

This guide helps you migrate your code from the `v1` branch to the `v2` branch of [go-co-op/gocron](https://github.com/go-co-op/gocron).
Version 2 is a major rewrite focusing on improving the internals of gocron, while also enhancing the user interfaces and error handling.
All major functionality has been ported over.

---

## Table of Contents

- [Overview of Major Changes](#overview-of-major-changes)
- [Installation](#installation)
- [API Changes](#api-changes)
- [Scheduler Creation](#scheduler-creation)
- [Job Definition](#job-definition)
- [Starting and Stopping the Scheduler](#starting-and-stopping-the-scheduler)
- [Error Handling](#error-handling)
- [Distributed Scheduling](#distributed-scheduling)
- [Examples Migration](#examples-migration)
- [Testing and Validation](#testing-and-validation)
- [Troubleshooting](#troubleshooting)
- [References](#references)

---

## Overview of Major Changes

- **Breaking API changes**: All major interfaces and types have changed.
- **Improved error reporting**: Most functions now return errors.
- **Job IDs and cancellation**: Jobs have unique IDs and can be cancelled.
- **Distributed and monitored scheduling**: Built-in support for distributed schedulers and job monitors.
- **Context and logging enhancements**: Improved support for cancellation, context, and custom logging interfaces.

---

## Installation

Update your dependency to v2:

```sh
go get github.com/go-co-op/gocron/v2
```

**Note:** The import path is `github.com/go-co-op/gocron/v2`.

---

## API Changes

### 1. Scheduler Creation

**v1:**
```go
import "github.com/go-co-op/gocron"

s := gocron.NewScheduler(time.UTC)
```

**v2:**
```go
import "github.com/go-co-op/gocron/v2"

s, err := gocron.NewScheduler()
if err != nil { panic(err) }
```
- **v2** returns an error on creation.
- **v2** does not require a location/timezone argument. Use `WithLocation()` if needed.

---

### 2. Job Creation

**v1:**
```go
s.Every(1).Second().Do(taskFunc)
```

**v2:**
```go
j, err := s.NewJob(
    gocron.DurationJob(1*time.Second),
    gocron.NewTask(taskFunc),
)
if err != nil { panic(err) }
```
- **v2** uses explicit job types (`DurationJob`, `CronJob`, etc).
- **v2** jobs have unique IDs: `j.ID()`.
- **v2** returns an error on job creation.

#### Cron Expressions

**v1:**
```go
s.Cron("*/5 * * * *").Do(taskFunc)
```

**v2:**
```go
j, err := s.NewJob(
    gocron.CronJob("*/5 * * * *"),
    gocron.NewTask(taskFunc),
)
```

#### Arguments

**v1:**
```go
s.Every(1).Second().Do(taskFunc, arg1, arg2)
```

**v2:**
```go
j, err := s.NewJob(
    gocron.DurationJob(1*time.Second),
    gocron.NewTask(taskFunc, arg1, arg2),
)
```

---

### 3. Starting and Stopping the Scheduler

**v1:**
```go
s.StartAsync()
s.Stop()
```

**v2:**
```go
s.Start()
s.Shutdown()
```

- Always call `Shutdown()` for graceful cleanup.

---

### 4. Error Handling

- Most v2 methods return errors. Always check `err`.
- Use `errors.go` for error definitions.

---

## References

- [v2 API Documentation](https://pkg.go.dev/github.com/go-co-op/gocron/v2)
- [Examples](https://pkg.go.dev/github.com/go-co-op/gocron/v2#pkg-examples)
- [Release Notes](https://github.com/go-co-op/gocron/releases)

---

**If you encounter issues, open a GitHub Issue or consider contributing a fix by checking out the [CONTRIBUTING.md](CONTRIBUTING.md) guide.**


================================================
FILE: mocks/README.md
================================================
# gocron mocks

## Quick Start

```
go get github.com/go-co-op/gocron/mocks/v2
```

write a test

```golang
package main

import (
	"testing"

	"github.com/go-co-op/gocron/mocks/v2"
	"github.com/go-co-op/gocron/v2"
	"go.uber.org/mock/gomock"
)

func myFunc(s gocron.Scheduler) {
	s.Start()
	_ = s.Shutdown()
}

func TestMyFunc(t *testing.T) {
	ctrl := gomock.NewController(t)
	s := gocronmocks.NewMockScheduler(ctrl)
	s.EXPECT().Start().Times(1)
	s.EXPECT().Shutdown().Times(1).Return(nil)

	myFunc(s)
}

```


================================================
FILE: mocks/distributed.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/go-co-op/gocron/v2 (interfaces: Elector,Locker,Lock)
//
// Generated by this command:
//
//	mockgen -destination=mocks/distributed.go -package=gocronmocks . Elector,Locker,Lock
//

// Package gocronmocks is a generated GoMock package.
package gocronmocks

import (
	context "context"
	reflect "reflect"

	v2 "github.com/go-co-op/gocron/v2"
	gomock "go.uber.org/mock/gomock"
)

// MockElector is a mock of Elector interface.
type MockElector struct {
	ctrl     *gomock.Controller
	recorder *MockElectorMockRecorder
	isgomock struct{}
}

// MockElectorMockRecorder is the mock recorder for MockElector.
type MockElectorMockRecorder struct {
	mock *MockElector
}

// NewMockElector creates a new mock instance.
func NewMockElector(ctrl *gomock.Controller) *MockElector {
	mock := &MockElector{ctrl: ctrl}
	mock.recorder = &MockElectorMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockElector) EXPECT() *MockElectorMockRecorder {
	return m.recorder
}

// IsLeader mocks base method.
func (m *MockElector) IsLeader(arg0 context.Context) error {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "IsLeader", arg0)
	ret0, _ := ret[0].(error)
	return ret0
}

// IsLeader indicates an expected call of IsLeader.
func (mr *MockElectorMockRecorder) IsLeader(arg0 any) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsLeader", reflect.TypeOf((*MockElector)(nil).IsLeader), arg0)
}

// MockLocker is a mock of Locker interface.
type MockLocker struct {
	ctrl     *gomock.Controller
	recorder *MockLockerMockRecorder
	isgomock struct{}
}

// MockLockerMockRecorder is the mock recorder for MockLocker.
type MockLockerMockRecorder struct {
	mock *MockLocker
}

// NewMockLocker creates a new mock instance.
func NewMockLocker(ctrl *gomock.Controller) *MockLocker {
	mock := &MockLocker{ctrl: ctrl}
	mock.recorder = &MockLockerMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLocker) EXPECT() *MockLockerMockRecorder {
	return m.recorder
}

// Lock mocks base method.
func (m *MockLocker) Lock(ctx context.Context, key string) (v2.Lock, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Lock", ctx, key)
	ret0, _ := ret[0].(v2.Lock)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Lock indicates an expected call of Lock.
func (mr *MockLockerMockRecorder) Lock(ctx, key any) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Lock", reflect.TypeOf((*MockLocker)(nil).Lock), ctx, key)
}

// MockLock is a mock of Lock interface.
type MockLock struct {
	ctrl     *gomock.Controller
	recorder *MockLockMockRecorder
	isgomock struct{}
}

// MockLockMockRecorder is the mock recorder for MockLock.
type MockLockMockRecorder struct {
	mock *MockLock
}

// NewMockLock creates a new mock instance.
func NewMockLock(ctrl *gomock.Controller) *MockLock {
	mock := &MockLock{ctrl: ctrl}
	mock.recorder = &MockLockMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLock) EXPECT() *MockLockMockRecorder {
	return m.recorder
}

// Unlock mocks base method.
func (m *MockLock) Unlock(ctx context.Context) error {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Unlock", ctx)
	ret0, _ := ret[0].(error)
	return ret0
}

// Unlock indicates an expected call of Unlock.
func (mr *MockLockMockRecorder) Unlock(ctx any) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unlock", reflect.TypeOf((*MockLock)(nil).Unlock), ctx)
}


================================================
FILE: mocks/go.mod
================================================
module github.com/go-co-op/gocron/mocks/v2

go 1.20

require (
	github.com/go-co-op/gocron/v2 v2.2.10
	github.com/google/uuid v1.6.0
	go.uber.org/mock v0.4.0
)

require (
	github.com/jonboulle/clockwork v0.4.0 // indirect
	github.com/robfig/cron/v3 v3.0.1 // indirect
	golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
)


================================================
FILE: mocks/go.sum
================================================
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/go-co-op/gocron/v2 v2.2.10 h1:o6u+RfvT5rBa39gmsA5cqPPLXTa+Ai70m7EGgHQoXyg=
github.com/go-co-op/gocron/v2 v2.2.10/go.mod h1:mZx3gMSlFnb97k3hRqX3+GdlG3+DUwTh6B8fnsTScXg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=


================================================
FILE: mocks/job.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/go-co-op/gocron/v2 (interfaces: Job)
//
// Generated by this command:
//
//	mockgen -destination=mocks/job.go -package=gocronmocks . Job
//

// Package gocronmocks is a generated GoMock package.
package gocronmocks

import (
	reflect "reflect"
	time "time"

	uuid "github.com/google/uuid"
	gomock "go.uber.org/mock/gomock"
)

// MockJob is a mock of Job interface.
type MockJob struct {
	ctrl     *gomock.Controller
	recorder *MockJobMockRecorder
	isgomock struct{}
}

// MockJobMockRecorder is the mock recorder for MockJob.
type MockJobMockRecorder struct {
	mock *MockJob
}

// NewMockJob creates a new mock instance.
func NewMockJob(ctrl *gomock.Controller) *MockJob {
	mock := &MockJob{ctrl: ctrl}
	mock.recorder = &MockJobMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockJob) EXPECT() *MockJobMockRecorder {
	return m.recorder
}

// ID mocks base method.
func (m *MockJob) ID() uuid.UUID {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "ID")
	ret0, _ := ret[0].(uuid.UUID)
	return ret0
}

// ID indicates an expected call of ID.
func (mr *MockJobMockRecorder) ID() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ID", reflect.TypeOf((*MockJob)(nil).ID))
}

// LastRun mocks base method.
func (m *MockJob) LastRun() (time.Time, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "LastRun")
	ret0, _ := ret[0].(time.Time)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// LastRun indicates an expected call of LastRun.
func (mr *MockJobMockRecorder) LastRun() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LastRun", reflect.TypeOf((*MockJob)(nil).LastRun))
}

// Name mocks base method.
func (m *MockJob) Name() string {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Name")
	ret0, _ := ret[0].(string)
	return ret0
}

// Name indicates an expected call of Name.
func (mr *MockJobMockRecorder) Name() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockJob)(nil).Name))
}

// NextRun mocks base method.
func (m *MockJob) NextRun() (time.Time, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "NextRun")
	ret0, _ := ret[0].(time.Time)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// NextRun indicates an expected call of NextRun.
func (mr *MockJobMockRecorder) NextRun() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextRun", reflect.TypeOf((*MockJob)(nil).NextRun))
}

// NextRuns mocks base method.
func (m *MockJob) NextRuns(arg0 int) ([]time.Time, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "NextRuns", arg0)
	ret0, _ := ret[0].([]time.Time)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// NextRuns indicates an expected call of NextRuns.
func (mr *MockJobMockRecorder) NextRuns(arg0 any) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextRuns", reflect.TypeOf((*MockJob)(nil).NextRuns), arg0)
}

// RunNow mocks base method.
func (m *MockJob) RunNow() error {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "RunNow")
	ret0, _ := ret[0].(error)
	return ret0
}

// RunNow indicates an expected call of RunNow.
func (mr *MockJobMockRecorder) RunNow() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunNow", reflect.TypeOf((*MockJob)(nil).RunNow))
}

// Tags mocks base method.
func (m *MockJob) Tags() []string {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Tags")
	ret0, _ := ret[0].([]string)
	return ret0
}

// Tags indicates an expected call of Tags.
func (mr *MockJobMockRecorder) Tags() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Tags", reflect.TypeOf((*MockJob)(nil).Tags))
}


================================================
FILE: mocks/logger.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/go-co-op/gocron/v2 (interfaces: Logger)
//
// Generated by this command:
//
//	mockgen -destination=mocks/logger.go -package=gocronmocks . Logger
//

// Package gocronmocks is a generated GoMock package.
package gocronmocks

import (
	reflect "reflect"

	gomock "go.uber.org/mock/gomock"
)

// MockLogger is a mock of Logger interface.
type MockLogger struct {
	ctrl     *gomock.Controller
	recorder *MockLoggerMockRecorder
	isgomock struct{}
}

// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
	mock *MockLogger
}

// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
	mock := &MockLogger{ctrl: ctrl}
	mock.recorder = &MockLoggerMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
	return m.recorder
}

// Debug mocks base method.
func (m *MockLogger) Debug(msg string, args ...any) {
	m.ctrl.T.Helper()
	varargs := []any{msg}
	for _, a := range args {
		varargs = append(varargs, a)
	}
	m.ctrl.Call(m, "Debug", varargs...)
}

// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(msg any, args ...any) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	varargs := append([]any{msg}, args...)
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), varargs...)
}

// Error mocks base method.
func (m *MockLogger) Error(msg string, args ...any) {
	m.ctrl.T.Helper()
	varargs := []any{msg}
	for _, a := range args {
		varargs = append(varargs, a)
	}
	m.ctrl.Call(m, "Error", varargs...)
}

// Error indicates an expected call of Error.
func (mr *MockLoggerMockRecorder) Error(msg any, args ...any) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	varargs := append([]any{msg}, args...)
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), varargs...)
}

// Info mocks base method.
func (m *MockLogger) Info(msg string, args ...any) {
	m.ctrl.T.Helper()
	varargs := []any{msg}
	for _, a := range args {
		varargs = append(varargs, a)
	}
	m.ctrl.Call(m, "Info", varargs...)
}

// Info indicates an expected call of Info.
func (mr *MockLoggerMockRecorder) Info(msg any, args ...any) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	varargs := append([]any{msg}, args...)
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), varargs...)
}

// Warn mocks base method.
func (m *MockLogger) Warn(msg string, args ...any) {
	m.ctrl.T.Helper()
	varargs := []any{msg}
	for _, a := range args {
		varargs = append(varargs, a)
	}
	m.ctrl.Call(m, "Warn", varargs...)
}

// Warn indicates an expected call of Warn.
func (mr *MockLoggerMockRecorder) Warn(msg any, args ...any) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	varargs := append([]any{msg}, args...)
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockLogger)(nil).Warn), varargs...)
}


================================================
FILE: mocks/scheduler.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/go-co-op/gocron/v2 (interfaces: Scheduler)
//
// Generated by this command:
//
//	mockgen -destination=mocks/scheduler.go -package=gocronmocks . Scheduler
//

// Package gocronmocks is a generated GoMock package.
package gocronmocks

import (
	reflect "reflect"

	v2 "github.com/go-co-op/gocron/v2"
	uuid "github.com/google/uuid"
	gomock "go.uber.org/mock/gomock"
)

// MockScheduler is a mock of Scheduler interface.
type MockScheduler struct {
	ctrl     *gomock.Controller
	recorder *MockSchedulerMockRecorder
	isgomock struct{}
}

// MockSchedulerMockRecorder is the mock recorder for MockScheduler.
type MockSchedulerMockRecorder struct {
	mock *MockScheduler
}

// NewMockScheduler creates a new mock instance.
func NewMockScheduler(ctrl *gomock.Controller) *MockScheduler {
	mock := &MockScheduler{ctrl: ctrl}
	mock.recorder = &MockSchedulerMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockScheduler) EXPECT() *MockSchedulerMockRecorder {
	return m.recorder
}

// Jobs mocks base method.
func (m *MockScheduler) Jobs() []v2.Job {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Jobs")
	ret0, _ := ret[0].([]v2.Job)
	return ret0
}

// Jobs indicates an expected call of Jobs.
func (mr *MockSchedulerMockRecorder) Jobs() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Jobs", reflect.TypeOf((*MockScheduler)(nil).Jobs))
}

// JobsWaitingInQueue mocks base method.
func (m *MockScheduler) JobsWaitingInQueue() int {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "JobsWaitingInQueue")
	ret0, _ := ret[0].(int)
	return ret0
}

// JobsWaitingInQueue indicates an expected call of JobsWaitingInQueue.
func (mr *MockSchedulerMockRecorder) JobsWaitingInQueue() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JobsWaitingInQueue", reflect.TypeOf((*MockScheduler)(nil).JobsWaitingInQueue))
}

// NewJob mocks base method.
func (m *MockScheduler) NewJob(arg0 v2.JobDefinition, arg1 v2.Task, arg2 ...v2.JobOption) (v2.Job, error) {
	m.ctrl.T.Helper()
	varargs := []any{arg0, arg1}
	for _, a := range arg2 {
		varargs = append(varargs, a)
	}
	ret := m.ctrl.Call(m, "NewJob", varargs...)
	ret0, _ := ret[0].(v2.Job)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// NewJob indicates an expected call of NewJob.
func (mr *MockSchedulerMockRecorder) NewJob(arg0, arg1 any, arg2 ...any) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	varargs := append([]any{arg0, arg1}, arg2...)
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewJob", reflect.TypeOf((*MockScheduler)(nil).NewJob), varargs...)
}

// RemoveByTags mocks base method.
func (m *MockScheduler) RemoveByTags(arg0 ...string) {
	m.ctrl.T.Helper()
	varargs := []any{}
	for _, a := range arg0 {
		varargs = append(varargs, a)
	}
	m.ctrl.Call(m, "RemoveByTags", varargs...)
}

// RemoveByTags indicates an expected call of RemoveByTags.
func (mr *MockSchedulerMockRecorder) RemoveByTags(arg0 ...any) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveByTags", reflect.TypeOf((*MockScheduler)(nil).RemoveByTags), arg0...)
}

// RemoveJob mocks base method.
func (m *MockScheduler) RemoveJob(arg0 uuid.UUID) error {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "RemoveJob", arg0)
	ret0, _ := ret[0].(error)
	return ret0
}

// RemoveJob indicates an expected call of RemoveJob.
func (mr *MockSchedulerMockRecorder) RemoveJob(arg0 any) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveJob", reflect.TypeOf((*MockScheduler)(nil).RemoveJob), arg0)
}

// Shutdown mocks base method.
func (m *MockScheduler) Shutdown() error {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Shutdown")
	ret0, _ := ret[0].(error)
	return ret0
}

// Shutdown indicates an expected call of Shutdown.
func (mr *MockSchedulerMockRecorder) Shutdown() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockScheduler)(nil).Shutdown))
}

// Start mocks base method.
func (m *MockScheduler) Start() {
	m.ctrl.T.Helper()
	m.ctrl.Call(m, "Start")
}

// Start indicates an expected call of Start.
func (mr *MockSchedulerMockRecorder) Start() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockScheduler)(nil).Start))
}

// StopJobs mocks base method.
func (m *MockScheduler) StopJobs() error {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "StopJobs")
	ret0, _ := ret[0].(error)
	return ret0
}

// StopJobs indicates an expected call of StopJobs.
func (mr *MockSchedulerMockRecorder) StopJobs() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopJobs", reflect.TypeOf((*MockScheduler)(nil).StopJobs))
}

// Update mocks base method.
func (m *MockScheduler) Update(arg0 uuid.UUID, arg1 v2.JobDefinition, arg2 v2.Task, arg3 ...v2.JobOption) (v2.Job, error) {
	m.ctrl.T.Helper()
	varargs := []any{arg0, arg1, arg2}
	for _, a := range arg3 {
		varargs = append(varargs, a)
	}
	ret := m.ctrl.Call(m, "Update", varargs...)
	ret0, _ := ret[0].(v2.Job)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Update indicates an expected call of Update.
func (mr *MockSchedulerMockRecorder) Update(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	varargs := append([]any{arg0, arg1, arg2}, arg3...)
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockScheduler)(nil).Update), varargs...)
}


================================================
FILE: monitor.go
================================================
package gocron

import (
	"time"

	"github.com/google/uuid"
)

// JobStatus is the status of job run that should be collected with the metric.
type JobStatus string

// The different statuses of job that can be used.
const (
	Fail                 JobStatus = "fail"
	Success              JobStatus = "success"
	Skip                 JobStatus = "skip"
	SingletonRescheduled JobStatus = "singleton_rescheduled"
)

// Monitor represents the interface to collect jobs metrics.
type Monitor interface {
	// IncrementJob will provide details about the job and expects the underlying implementation
	// to handle instantiating and incrementing a value
	IncrementJob(id uuid.UUID, name string, tags []string, status JobStatus)
	// RecordJobTiming will provide details about the job and the timing and expects the underlying implementation
	// to handle instantiating and recording the value
	RecordJobTiming(startTime, endTime time.Time, id uuid.UUID, name string, tags []string)
}

// MonitorStatus extends RecordJobTiming with the job status.
type MonitorStatus interface {
	Monitor
	// RecordJobTimingWithStatus will provide details about the job, its status, error and the timing and expects the underlying implementation
	// to handle instantiating and recording the value
	RecordJobTimingWithStatus(startTime, endTime time.Time, id uuid.UUID, name string, tags []string, status JobStatus, err error)
}


================================================
FILE: scheduler.go
================================================
//go:generate mockgen -destination=mocks/scheduler.go -package=gocronmocks . Scheduler
package gocron

import (
	"context"
	"reflect"
	"runtime"
	"slices"
	"strings"
	"sync/atomic"
	"time"

	"github.com/google/uuid"
	"github.com/jonboulle/clockwork"
)

var _ Scheduler = (*scheduler)(nil)

// Scheduler defines the interface for the Scheduler.
type Scheduler interface {
	// Jobs returns all the jobs currently in the scheduler.
	Jobs() []Job
	// NewJob creates a new job in the Scheduler. The job is scheduled per the provided
	// definition when the Scheduler is started. If the Scheduler is already running
	// the job will be scheduled when the Scheduler is started.
	// If you set the first argument of your Task func to be a context.Context,
	// gocron will pass in a context (either the default Job context, or one
	// provided via WithContext) to the job and will cancel the context on shutdown.
	// This allows you to listen for and handle cancellation within your job.
	NewJob(JobDefinition, Task, ...JobOption) (Job, error)
	// RemoveByTags removes all jobs that have at least one of the provided tags.
	RemoveByTags(...string)
	// RemoveJob removes the job with the provided id.
	RemoveJob(uuid.UUID) error
	// Shutdown should be called when you no longer need
	// the Scheduler or Job's as the Scheduler cannot
	// be restarted after calling Shutdown. This is similar
	// to a Close or Cleanup method and is often deferred after
	// starting the scheduler.
	Shutdown() error
	// Start begins scheduling jobs for execution based
	// on each job's definition. Job's added to an already
	// running scheduler will be scheduled immediately based
	// on definition. Start is non-blocking.
	Start()
	// StopJobs stops the execution of all jobs in the scheduler.
	// This can be useful in situations where jobs need to be
	// paused globally and then restarted with Start().
	StopJobs() error
	// Update replaces the existing Job's JobDefinition with the provided
	// JobDefinition. The Job's Job.ID() remains the same.
	Update(uuid.UUID, JobDefinition, Task, ...JobOption) (Job, error)
	// JobsWaitingInQueue number of jobs waiting in Queue in case of LimitModeWait
	// In case of LimitModeReschedule or no limit it will be always zero
	JobsWaitingInQueue() int
}

// -----------------------------------------------
// -----------------------------------------------
// ----------------- Scheduler -------------------
// -----------------------------------------------
// -----------------------------------------------

type scheduler struct {
	// context used for shutting down
	shutdownCtx context.Context
	// cancel used to signal scheduler should shut down
	shutdownCancel context.CancelFunc
	// the executor, which actually runs the jobs sent to it via the scheduler
	exec executor
	// the map of jobs registered in the scheduler
	jobs map[uuid.UUID]internalJob
	// the location used by the scheduler for scheduling when relevant
	location *time.Location
	// whether the scheduler has been started or not
	started atomic.Bool
	// globally applied JobOption's set on all jobs added to the scheduler
	// note: individually set JobOption's take precedence.
	globalJobOptions []JobOption
	// the scheduler's logger
	logger Logger

	// used to tell the scheduler to start
	startCh chan struct{}
	// used to report that the scheduler has started
	startedCh chan struct{}
	// used to tell the scheduler to stop
	stopCh chan struct{}
	// used to report that the scheduler has stopped
	stopErrCh chan error
	// used to send all the jobs out when a request is made by the client
	allJobsOutRequest chan allJobsOutRequest
	// used to send a jobs out when a request is 
Download .txt
gitextract_1w8mopnk/

├── .github/
│   ├── FUNDING.yml
│   ├── copilot-instructions.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── codeql-analysis.yml
│       ├── file_formatting.yml
│       └── go_test.yml
├── .gitignore
├── .golangci.yaml
├── .pre-commit-config.yaml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── SECURITY.md
├── distributed.go
├── errors.go
├── example_test.go
├── examples/
│   └── elector/
│       └── main.go
├── executor.go
├── go.mod
├── go.sum
├── gocron-monitor-test/
│   ├── debug_restart.go
│   ├── go.mod
│   └── go.sum
├── job.go
├── job_test.go
├── logger.go
├── logger_test.go
├── migration_v1_to_v2.md
├── mocks/
│   ├── README.md
│   ├── distributed.go
│   ├── go.mod
│   ├── go.sum
│   ├── job.go
│   ├── logger.go
│   └── scheduler.go
├── monitor.go
├── scheduler.go
├── scheduler_monitor.go
├── scheduler_monitor_test.go
├── scheduler_test.go
├── util.go
└── util_test.go
Download .txt
SYMBOL INDEX (479 symbols across 20 files)

FILE: distributed.go
  type Elector (line 10) | type Elector interface
  type Locker (line 36) | type Locker interface
  type Lock (line 43) | type Lock interface

FILE: example_test.go
  function ExampleAfterJobRuns (line 15) | func ExampleAfterJobRuns() {
  function ExampleAfterJobRunsWithError (line 36) | func ExampleAfterJobRunsWithError() {
  type errorLocker (line 59) | type errorLocker struct
    method Lock (line 61) | func (e errorLocker) Lock(_ context.Context, _ string) (gocron.Lock, e...
  function ExampleAfterLockError (line 65) | func ExampleAfterLockError() {
  function ExampleBeforeJobRuns (line 87) | func ExampleBeforeJobRuns() {
  function ExampleBeforeJobRunsSkipIfBeforeFuncErrors (line 108) | func ExampleBeforeJobRunsSkipIfBeforeFuncErrors() {
  function ExampleCronJob (line 131) | func ExampleCronJob() {
  function ExampleDailyJob (line 157) | func ExampleDailyJob() {
  function ExampleDurationJob (line 177) | func ExampleDurationJob() {
  function ExampleDurationRandomJob (line 191) | func ExampleDurationRandomJob() {
  function ExampleJob_id (line 206) | func ExampleJob_id() {
  function ExampleJob_lastRun (line 222) | func ExampleJob_lastRun() {
  function ExampleJob_name (line 238) | func ExampleJob_name() {
  function ExampleJob_nextRun (line 257) | func ExampleJob_nextRun() {
  function ExampleJob_nextRuns (line 277) | func ExampleJob_nextRuns() {
  function ExampleJob_runNow (line 297) | func ExampleJob_runNow() {
  function ExampleJob_tags (line 319) | func ExampleJob_tags() {
  function ExampleMonthlyJob (line 338) | func ExampleMonthlyJob() {
  function ExampleNewDefaultCron (line 357) | func ExampleNewDefaultCron() {
  function ExampleNewScheduler (line 365) | func ExampleNewScheduler() {
  function ExampleNewTask (line 372) | func ExampleNewTask() {
  function ExampleOneTimeJob (line 388) | func ExampleOneTimeJob() {
  function ExampleScheduler_jobs (line 425) | func ExampleScheduler_jobs() {
  function ExampleScheduler_newJob (line 442) | func ExampleScheduler_newJob() {
  function ExampleScheduler_removeByTags (line 460) | func ExampleScheduler_removeByTags() {
  function ExampleScheduler_removeJob (line 492) | func ExampleScheduler_removeJob() {
  function ExampleScheduler_shutdown (line 515) | func ExampleScheduler_shutdown() {
  function ExampleScheduler_start (line 520) | func ExampleScheduler_start() {
  function ExampleScheduler_stopJobs (line 537) | func ExampleScheduler_stopJobs() {
  function ExampleScheduler_update (line 556) | func ExampleScheduler_update() {
  function ExampleWeeklyJob (line 585) | func ExampleWeeklyJob() {
  function ExampleWithClock (line 604) | func ExampleWithClock() {
  function ExampleWithContext (line 632) | func ExampleWithContext() {
  type customCron (line 656) | type customCron struct
    method IsValid (line 658) | func (c customCron) IsValid(crontab string, location *time.Location, n...
    method Next (line 662) | func (c customCron) Next(lastRun time.Time) time.Time {
  function ExampleWithCronImplementation (line 666) | func ExampleWithCronImplementation() {
  function ExampleWithDisabledDistributedJobLocker (line 683) | func ExampleWithDisabledDistributedJobLocker() {
  type myElector (line 719) | type myElector struct
    method IsLeader (line 721) | func (m myElector) IsLeader(_ context.Context) error {
  function ExampleWithDistributedElector (line 725) | func ExampleWithDistributedElector() {
  type myLocker (line 743) | type myLocker struct
    method Lock (line 745) | func (m myLocker) Lock(ctx context.Context, key string) (gocron.Lock, ...
  type testLock (line 751) | type testLock struct
    method Unlock (line 753) | func (t testLock) Unlock(_ context.Context) error {
  function ExampleWithDistributedLocker (line 757) | func ExampleWithDistributedLocker() {
  function ExampleWithEventListeners (line 781) | func ExampleWithEventListeners() {
  function ExampleWithGlobalJobOptions (line 812) | func ExampleWithGlobalJobOptions() {
  function ExampleWithIdentifier (line 858) | func ExampleWithIdentifier() {
  function ExampleWithIntervalFromCompletion (line 879) | func ExampleWithIntervalFromCompletion() {
  function ExampleWithLimitConcurrentJobs (line 904) | func ExampleWithLimitConcurrentJobs() {
  function ExampleWithLimitedRuns (line 913) | func ExampleWithLimitedRuns() {
  function ExampleWithLocation (line 939) | func ExampleWithLocation() {
  function ExampleWithLogger (line 947) | func ExampleWithLogger() {
  function ExampleWithMonitor (line 955) | func ExampleWithMonitor() {
  function ExampleWithName (line 1018) | func ExampleWithName() {
  function ExampleWithSingletonMode (line 1039) | func ExampleWithSingletonMode() {
  function ExampleWithStartAt (line 1058) | func ExampleWithStartAt() {
  function ExampleWithStartDateTime (line 1088) | func ExampleWithStartDateTime() {
  function ExampleWithStartDateTimePast (line 1118) | func ExampleWithStartDateTimePast() {
  function ExampleWithStartImmediately (line 1147) | func ExampleWithStartImmediately() {
  function ExampleWithStopTimeout (line 1172) | func ExampleWithStopTimeout() {
  function ExampleWithTags (line 1178) | func ExampleWithTags() {

FILE: examples/elector/main.go
  type myElector (line 14) | type myElector struct
    method IsLeader (line 19) | func (m myElector) IsLeader(_ context.Context) error {
  function main (line 28) | func main() {

FILE: executor.go
  type executor (line 15) | type executor struct
    method start (line 87) | func (e *executor) start() {
    method sendOutForRescheduling (line 262) | func (e *executor) sendOutForRescheduling(jIn *jobIn) {
    method sendOutForNextRunUpdate (line 276) | func (e *executor) sendOutForNextRunUpdate(jIn *jobIn) {
    method limitModeRunner (line 284) | func (e *executor) limitModeRunner(name string, in chan jobIn, wg *wai...
    method singletonModeRunner (line 349) | func (e *executor) singletonModeRunner(name string, in chan jobIn, wg ...
    method runJob (line 388) | func (e *executor) runJob(j internalJob, jIn jobIn) {
    method callJobWithRecover (line 515) | func (e *executor) callJobWithRecover(j internalJob) (err error) {
    method recordJobTiming (line 528) | func (e *executor) recordJobTiming(start time.Time, end time.Time, j i...
    method recordJobTimingWithStatus (line 534) | func (e *executor) recordJobTimingWithStatus(start time.Time, end time...
    method incrementJobCounter (line 540) | func (e *executor) incrementJobCounter(j internalJob, status JobStatus) {
    method stop (line 546) | func (e *executor) stop(standardJobsWg, singletonJobsWg, limitModeJobs...
  type jobIn (line 63) | type jobIn struct
  type singletonRunner (line 68) | type singletonRunner struct
  type limitModeConfig (line 73) | type limitModeConfig struct

FILE: gocron-monitor-test/debug_restart.go
  type DebugMonitor (line 10) | type DebugMonitor struct
    method SchedulerStarted (line 21) | func (m *DebugMonitor) SchedulerStarted() {
    method SchedulerShutdown (line 26) | func (m *DebugMonitor) SchedulerShutdown() {
    method JobRegistered (line 31) | func (m *DebugMonitor) JobRegistered(job *gocron.Job) {
    method JobUnregistered (line 36) | func (m *DebugMonitor) JobUnregistered(job *gocron.Job) {
    method JobStarted (line 41) | func (m *DebugMonitor) JobStarted(job *gocron.Job) {
    method JobRunning (line 46) | func (m *DebugMonitor) JobRunning(job *gocron.Job) {
    method JobCompleted (line 51) | func (m *DebugMonitor) JobCompleted(job *gocron.Job) {
    method JobFailed (line 56) | func (m *DebugMonitor) JobFailed(job *gocron.Job, err error) {
  function main (line 61) | func main() {

FILE: job.go
  type internalJob (line 20) | type internalJob struct
    method stop (line 62) | func (j *internalJob) stop() {
    method stopTimeReached (line 69) | func (j *internalJob) stopTimeReached(now time.Time) bool {
  type task (line 78) | type task struct
  type Task (line 85) | type Task
  function NewTask (line 92) | func NewTask(function any, parameters ...any) Task {
  type limitRunsTo (line 104) | type limitRunsTo struct
  type Cron (line 118) | type Cron interface
  type JobDefinition (line 131) | type JobDefinition interface
  function newDefaultCronImplementation (line 137) | func newDefaultCronImplementation(withSeconds bool) Cron {
  function NewDefaultCron (line 146) | func NewDefaultCron(cronStatementsIncludeSeconds bool) Cron {
  type defaultCron (line 154) | type defaultCron struct
    method IsValid (line 159) | func (c *defaultCron) IsValid(crontab string, location *time.Location,...
    method Next (line 190) | func (c *defaultCron) Next(lastRun time.Time) time.Time {
  type cronJobDefinition (line 197) | type cronJobDefinition struct
    method setup (line 202) | func (c cronJobDefinition) setup(j *internalJob, location *time.Locati...
  function CronJob (line 221) | func CronJob(crontab string, withSeconds bool) JobDefinition {
  type durationJobDefinition (line 230) | type durationJobDefinition struct
    method setup (line 234) | func (d durationJobDefinition) setup(j *internalJob, _ *time.Location,...
  function DurationJob (line 247) | func DurationJob(duration time.Duration) JobDefinition {
  type durationRandomJobDefinition (line 255) | type durationRandomJobDefinition struct
    method setup (line 259) | func (d durationRandomJobDefinition) setup(j *internalJob, _ *time.Loc...
  function DurationRandomJob (line 286) | func DurationRandomJob(minDuration, maxDuration time.Duration) JobDefini...
  function DailyJob (line 299) | func DailyJob(interval uint, atTimes AtTimes) JobDefinition {
  type dailyJobDefinition (line 308) | type dailyJobDefinition struct
    method setup (line 313) | func (d dailyJobDefinition) setup(j *internalJob, location *time.Locat...
  type weeklyJobDefinition (line 340) | type weeklyJobDefinition struct
    method setup (line 346) | func (w weeklyJobDefinition) setup(j *internalJob, location *time.Loca...
  type Weekdays (line 380) | type Weekdays
  function NewWeekdays (line 383) | func NewWeekdays(weekday time.Weekday, weekdays ...time.Weekday) Weekdays {
  function WeeklyJob (line 397) | func WeeklyJob(interval uint, daysOfTheWeek Weekdays, atTimes AtTimes) J...
  type monthlyJobDefinition (line 407) | type monthlyJobDefinition struct
    method setup (line 413) | func (m monthlyJobDefinition) setup(j *internalJob, location *time.Loc...
  type days (line 458) | type days
  type DaysOfTheMonth (line 461) | type DaysOfTheMonth
  function NewDaysOfTheMonth (line 469) | func NewDaysOfTheMonth(day int, moreDays ...int) DaysOfTheMonth {
  type atTime (line 475) | type atTime struct
    method time (line 479) | func (a atTime) time(location *time.Location) time.Time {
  function TimeFromAtTime (line 487) | func TimeFromAtTime(at AtTime, loc *time.Location) time.Time {
  type AtTime (line 492) | type AtTime
  function NewAtTime (line 496) | func NewAtTime(hours, minutes, seconds uint) AtTime {
  type AtTimes (line 503) | type AtTimes
  function NewAtTimes (line 507) | func NewAtTimes(atTime AtTime, atTimes ...AtTime) AtTimes {
  function MonthlyJob (line 529) | func MonthlyJob(interval uint, daysOfTheMonth DaysOfTheMonth, atTimes At...
  type oneTimeJobDefinition (line 539) | type oneTimeJobDefinition struct
    method setup (line 543) | func (o oneTimeJobDefinition) setup(j *internalJob, _ *time.Location, ...
  function removeSliceDuplicatesTimeOnSortedSlice (line 561) | func removeSliceDuplicatesTimeOnSortedSlice(times []time.Time) []time.Ti...
  type OneTimeJobStartAtOption (line 572) | type OneTimeJobStartAtOption
  function OneTimeJobStartImmediately (line 575) | func OneTimeJobStartImmediately() OneTimeJobStartAtOption {
  function OneTimeJobStartDateTime (line 584) | func OneTimeJobStartDateTime(start time.Time) OneTimeJobStartAtOption {
  function OneTimeJobStartDateTimes (line 592) | func OneTimeJobStartDateTimes(times ...time.Time) OneTimeJobStartAtOption {
  function OneTimeJob (line 600) | func OneTimeJob(startAt OneTimeJobStartAtOption) JobDefinition {
  type JobOption (line 613) | type JobOption
  function WithDistributedJobLocker (line 618) | func WithDistributedJobLocker(locker Locker) JobOption {
  function WithDisabledDistributedJobLocker (line 631) | func WithDisabledDistributedJobLocker(disabled bool) JobOption {
  function WithEventListeners (line 640) | func WithEventListeners(eventListeners ...EventListener) JobOption {
  function WithLimitedRuns (line 653) | func WithLimitedRuns(limit uint) JobOption {
  function WithName (line 668) | func WithName(name string) JobOption {
  function WithCronImplementation (line 680) | func WithCronImplementation(c Cron) JobOption {
  function WithSingletonMode (line 690) | func WithSingletonMode(mode LimitMode) JobOption {
  function WithIntervalFromCompletion (line 730) | func WithIntervalFromCompletion() JobOption {
  function WithStartAt (line 739) | func WithStartAt(option StartAtOption) JobOption {
  type StartAtOption (line 746) | type StartAtOption
  function WithStartImmediately (line 751) | func WithStartImmediately() StartAtOption {
  function WithStartDateTime (line 760) | func WithStartDateTime(start time.Time) StartAtOption {
  function WithStartDateTimePast (line 780) | func WithStartDateTimePast(start time.Time) StartAtOption {
  function WithStopAt (line 795) | func WithStopAt(option StopAtOption) JobOption {
  type StopAtOption (line 802) | type StopAtOption
  function WithStopDateTime (line 807) | func WithStopDateTime(end time.Time) StopAtOption {
  function WithTags (line 823) | func WithTags(tags ...string) JobOption {
  function WithIdentifier (line 833) | func WithIdentifier(id uuid.UUID) JobOption {
  function WithContext (line 850) | func WithContext(ctx context.Context) JobOption {
  type EventListener (line 868) | type EventListener
  function BeforeJobRuns (line 872) | func BeforeJobRuns(eventListenerFunc func(jobID uuid.UUID, jobName strin...
  function BeforeJobRunsSkipIfBeforeFuncErrors (line 885) | func BeforeJobRunsSkipIfBeforeFuncErrors(eventListenerFunc func(jobID uu...
  function AfterJobRuns (line 897) | func AfterJobRuns(eventListenerFunc func(jobID uuid.UUID, jobName string...
  function AfterJobRunsWithError (line 909) | func AfterJobRunsWithError(eventListenerFunc func(jobID uuid.UUID, jobNa...
  function AfterJobRunsWithPanic (line 921) | func AfterJobRunsWithPanic(eventListenerFunc func(jobID uuid.UUID, jobNa...
  function AfterLockError (line 933) | func AfterLockError(eventListenerFunc func(jobID uuid.UUID, jobName stri...
  type jobSchedule (line 949) | type jobSchedule interface
  type cronJob (line 955) | type cronJob struct
    method next (line 960) | func (j *cronJob) next(lastRun time.Time) time.Time {
  type durationJob (line 966) | type durationJob struct
    method next (line 970) | func (j *durationJob) next(lastRun time.Time) time.Time {
  type durationRandomJob (line 976) | type durationRandomJob struct
    method next (line 981) | func (j *durationRandomJob) next(lastRun time.Time) time.Time {
  type dailyJob (line 988) | type dailyJob struct
    method next (line 993) | func (d dailyJob) next(lastRun time.Time) time.Time {
    method nextDay (line 1005) | func (d dailyJob) nextDay(lastRun time.Time, firstPass bool) time.Time {
  type weeklyJob (line 1027) | type weeklyJob struct
    method next (line 1033) | func (w weeklyJob) next(lastRun time.Time) time.Time {
    method nextWeekDayAtTime (line 1044) | func (w weeklyJob) nextWeekDayAtTime(lastRun time.Time, firstPass bool...
  type monthlyJob (line 1073) | type monthlyJob struct
    method next (line 1080) | func (m monthlyJob) next(lastRun time.Time) time.Time {
    method handleNegativeDays (line 1100) | func (m monthlyJob) handleNegativeDays(from time.Time, days, negativeD...
    method nextMonthDayAtTime (line 1114) | func (m monthlyJob) nextMonthDayAtTime(lastRun time.Time, days []int, ...
  type oneTimeJob (line 1148) | type oneTimeJob struct
    method next (line 1163) | func (o oneTimeJob) next(lastRun time.Time) time.Time {
  type Job (line 1185) | type Job interface
  type job (line 1216) | type job struct
    method ID (line 1224) | func (j job) ID() uuid.UUID {
    method LastRun (line 1228) | func (j job) LastRun() (time.Time, error) {
    method Name (line 1236) | func (j job) Name() string {
    method NextRun (line 1240) | func (j job) NextRun() (time.Time, error) {
    method NextRuns (line 1253) | func (j job) NextRuns(count int) ([]time.Time, error) {
    method Tags (line 1280) | func (j job) Tags() []string {
    method RunNow (line 1284) | func (j job) RunNow() error {

FILE: job_test.go
  function TestDurationJob_next (line 16) | func TestDurationJob_next(t *testing.T) {
  function TestDailyJob_next (line 46) | func TestDailyJob_next(t *testing.T) {
  function TestWeeklyJob_next (line 116) | func TestWeeklyJob_next(t *testing.T) {
  function TestMonthlyJob_next (line 179) | func TestMonthlyJob_next(t *testing.T) {
  function TestDurationRandomJob_next (line 343) | func TestDurationRandomJob_next(t *testing.T) {
  function TestOneTimeJob_next (line 387) | func TestOneTimeJob_next(t *testing.T) {
  function TestJob_RunNow_Error (line 392) | func TestJob_RunNow_Error(t *testing.T) {
  function TestJob_LastRun (line 406) | func TestJob_LastRun(t *testing.T) {
  function TestWithEventListeners (line 437) | func TestWithEventListeners(t *testing.T) {
  function TestJob_NextRun (line 553) | func TestJob_NextRun(t *testing.T) {
  function TestJob_NextRuns (line 608) | func TestJob_NextRuns(t *testing.T) {
  function TestJob_PanicOccurred (line 681) | func TestJob_PanicOccurred(t *testing.T) {
  function TestTimeFromAtTime (line 714) | func TestTimeFromAtTime(t *testing.T) {
  function TestNewAtTimes (line 762) | func TestNewAtTimes(t *testing.T) {
  function TestNewWeekdays (line 783) | func TestNewWeekdays(t *testing.T) {
  function TestNewDaysOfTheMonth (line 802) | func TestNewDaysOfTheMonth(t *testing.T) {
  function TestWithIntervalFromCompletion_BasicFunctionality (line 818) | func TestWithIntervalFromCompletion_BasicFunctionality(t *testing.T) {
  function TestWithIntervalFromCompletion_VariableExecutionTime (line 884) | func TestWithIntervalFromCompletion_VariableExecutionTime(t *testing.T) {
  function TestWithIntervalFromCompletion_LongRunningJob (line 953) | func TestWithIntervalFromCompletion_LongRunningJob(t *testing.T) {
  function TestWithIntervalFromCompletion_ComparedToDefault (line 1013) | func TestWithIntervalFromCompletion_ComparedToDefault(t *testing.T) {
  function TestWithIntervalFromCompletion_DurationRandomJob (line 1108) | func TestWithIntervalFromCompletion_DurationRandomJob(t *testing.T) {
  function TestWithIntervalFromCompletion_FirstRun (line 1162) | func TestWithIntervalFromCompletion_FirstRun(t *testing.T) {
  function TestJob_NextRun_MultipleJobsSimultaneously (line 1199) | func TestJob_NextRun_MultipleJobsSimultaneously(t *testing.T) {
  function TestJob_NextRun_ConcurrentCompletions (line 1299) | func TestJob_NextRun_ConcurrentCompletions(t *testing.T) {

FILE: logger.go
  type Logger (line 17) | type Logger interface
  type noOpLogger (line 26) | type noOpLogger struct
    method Debug (line 28) | func (l noOpLogger) Debug(_ string, _ ...any) {}
    method Error (line 29) | func (l noOpLogger) Error(_ string, _ ...any) {}
    method Info (line 30) | func (l noOpLogger) Info(_ string, _ ...any)  {}
    method Warn (line 31) | func (l noOpLogger) Warn(_ string, _ ...any)  {}
  type LogLevel (line 37) | type LogLevel
  constant LogLevelError (line 41) | LogLevelError LogLevel = iota
  constant LogLevelWarn (line 42) | LogLevelWarn
  constant LogLevelInfo (line 43) | LogLevelInfo
  constant LogLevelDebug (line 44) | LogLevelDebug
  type logger (line 47) | type logger struct
    method Debug (line 61) | func (l *logger) Debug(msg string, args ...any) {
    method Error (line 68) | func (l *logger) Error(msg string, args ...any) {
    method Info (line 75) | func (l *logger) Info(msg string, args ...any) {
    method Warn (line 82) | func (l *logger) Warn(msg string, args ...any) {
  function NewLogger (line 53) | func NewLogger(level LogLevel) Logger {
  function logFormatArgs (line 89) | func logFormatArgs(args ...any) string {

FILE: logger_test.go
  function TestNoOpLogger (line 12) | func TestNoOpLogger(_ *testing.T) {
  function TestNewLogger (line 20) | func TestNewLogger(t *testing.T) {

FILE: mocks/distributed.go
  type MockElector (line 21) | type MockElector struct
    method EXPECT (line 40) | func (m *MockElector) EXPECT() *MockElectorMockRecorder {
    method IsLeader (line 45) | func (m *MockElector) IsLeader(arg0 context.Context) error {
  type MockElectorMockRecorder (line 28) | type MockElectorMockRecorder struct
    method IsLeader (line 53) | func (mr *MockElectorMockRecorder) IsLeader(arg0 any) *gomock.Call {
  function NewMockElector (line 33) | func NewMockElector(ctrl *gomock.Controller) *MockElector {
  type MockLocker (line 59) | type MockLocker struct
    method EXPECT (line 78) | func (m *MockLocker) EXPECT() *MockLockerMockRecorder {
    method Lock (line 83) | func (m *MockLocker) Lock(ctx context.Context, key string) (v2.Lock, e...
  type MockLockerMockRecorder (line 66) | type MockLockerMockRecorder struct
    method Lock (line 92) | func (mr *MockLockerMockRecorder) Lock(ctx, key any) *gomock.Call {
  function NewMockLocker (line 71) | func NewMockLocker(ctrl *gomock.Controller) *MockLocker {
  type MockLock (line 98) | type MockLock struct
    method EXPECT (line 117) | func (m *MockLock) EXPECT() *MockLockMockRecorder {
    method Unlock (line 122) | func (m *MockLock) Unlock(ctx context.Context) error {
  type MockLockMockRecorder (line 105) | type MockLockMockRecorder struct
    method Unlock (line 130) | func (mr *MockLockMockRecorder) Unlock(ctx any) *gomock.Call {
  function NewMockLock (line 110) | func NewMockLock(ctrl *gomock.Controller) *MockLock {

FILE: mocks/job.go
  type MockJob (line 21) | type MockJob struct
    method EXPECT (line 40) | func (m *MockJob) EXPECT() *MockJobMockRecorder {
    method ID (line 45) | func (m *MockJob) ID() uuid.UUID {
    method LastRun (line 59) | func (m *MockJob) LastRun() (time.Time, error) {
    method Name (line 74) | func (m *MockJob) Name() string {
    method NextRun (line 88) | func (m *MockJob) NextRun() (time.Time, error) {
    method NextRuns (line 103) | func (m *MockJob) NextRuns(arg0 int) ([]time.Time, error) {
    method RunNow (line 118) | func (m *MockJob) RunNow() error {
    method Tags (line 132) | func (m *MockJob) Tags() []string {
  type MockJobMockRecorder (line 28) | type MockJobMockRecorder struct
    method ID (line 53) | func (mr *MockJobMockRecorder) ID() *gomock.Call {
    method LastRun (line 68) | func (mr *MockJobMockRecorder) LastRun() *gomock.Call {
    method Name (line 82) | func (mr *MockJobMockRecorder) Name() *gomock.Call {
    method NextRun (line 97) | func (mr *MockJobMockRecorder) NextRun() *gomock.Call {
    method NextRuns (line 112) | func (mr *MockJobMockRecorder) NextRuns(arg0 any) *gomock.Call {
    method RunNow (line 126) | func (mr *MockJobMockRecorder) RunNow() *gomock.Call {
    method Tags (line 140) | func (mr *MockJobMockRecorder) Tags() *gomock.Call {
  function NewMockJob (line 33) | func NewMockJob(ctrl *gomock.Controller) *MockJob {

FILE: mocks/logger.go
  type MockLogger (line 19) | type MockLogger struct
    method EXPECT (line 38) | func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
    method Debug (line 43) | func (m *MockLogger) Debug(msg string, args ...any) {
    method Error (line 60) | func (m *MockLogger) Error(msg string, args ...any) {
    method Info (line 77) | func (m *MockLogger) Info(msg string, args ...any) {
    method Warn (line 94) | func (m *MockLogger) Warn(msg string, args ...any) {
  type MockLoggerMockRecorder (line 26) | type MockLoggerMockRecorder struct
    method Debug (line 53) | func (mr *MockLoggerMockRecorder) Debug(msg any, args ...any) *gomock....
    method Error (line 70) | func (mr *MockLoggerMockRecorder) Error(msg any, args ...any) *gomock....
    method Info (line 87) | func (mr *MockLoggerMockRecorder) Info(msg any, args ...any) *gomock.C...
    method Warn (line 104) | func (mr *MockLoggerMockRecorder) Warn(msg any, args ...any) *gomock.C...
  function NewMockLogger (line 31) | func NewMockLogger(ctrl *gomock.Controller) *MockLogger {

FILE: mocks/scheduler.go
  type MockScheduler (line 21) | type MockScheduler struct
    method EXPECT (line 40) | func (m *MockScheduler) EXPECT() *MockSchedulerMockRecorder {
    method Jobs (line 45) | func (m *MockScheduler) Jobs() []v2.Job {
    method JobsWaitingInQueue (line 59) | func (m *MockScheduler) JobsWaitingInQueue() int {
    method NewJob (line 73) | func (m *MockScheduler) NewJob(arg0 v2.JobDefinition, arg1 v2.Task, ar...
    method RemoveByTags (line 93) | func (m *MockScheduler) RemoveByTags(arg0 ...string) {
    method RemoveJob (line 109) | func (m *MockScheduler) RemoveJob(arg0 uuid.UUID) error {
    method Shutdown (line 123) | func (m *MockScheduler) Shutdown() error {
    method Start (line 137) | func (m *MockScheduler) Start() {
    method StopJobs (line 149) | func (m *MockScheduler) StopJobs() error {
    method Update (line 163) | func (m *MockScheduler) Update(arg0 uuid.UUID, arg1 v2.JobDefinition, ...
  type MockSchedulerMockRecorder (line 28) | type MockSchedulerMockRecorder struct
    method Jobs (line 53) | func (mr *MockSchedulerMockRecorder) Jobs() *gomock.Call {
    method JobsWaitingInQueue (line 67) | func (mr *MockSchedulerMockRecorder) JobsWaitingInQueue() *gomock.Call {
    method NewJob (line 86) | func (mr *MockSchedulerMockRecorder) NewJob(arg0, arg1 any, arg2 ...an...
    method RemoveByTags (line 103) | func (mr *MockSchedulerMockRecorder) RemoveByTags(arg0 ...any) *gomock...
    method RemoveJob (line 117) | func (mr *MockSchedulerMockRecorder) RemoveJob(arg0 any) *gomock.Call {
    method Shutdown (line 131) | func (mr *MockSchedulerMockRecorder) Shutdown() *gomock.Call {
    method Start (line 143) | func (mr *MockSchedulerMockRecorder) Start() *gomock.Call {
    method StopJobs (line 157) | func (mr *MockSchedulerMockRecorder) StopJobs() *gomock.Call {
    method Update (line 176) | func (mr *MockSchedulerMockRecorder) Update(arg0, arg1, arg2 any, arg3...
  function NewMockScheduler (line 33) | func NewMockScheduler(ctrl *gomock.Controller) *MockScheduler {

FILE: monitor.go
  type JobStatus (line 10) | type JobStatus
  constant Fail (line 14) | Fail                 JobStatus = "fail"
  constant Success (line 15) | Success              JobStatus = "success"
  constant Skip (line 16) | Skip                 JobStatus = "skip"
  constant SingletonRescheduled (line 17) | SingletonRescheduled JobStatus = "singleton_rescheduled"
  type Monitor (line 21) | type Monitor interface
  type MonitorStatus (line 31) | type MonitorStatus interface

FILE: scheduler.go
  type Scheduler (line 20) | type Scheduler interface
  type scheduler (line 64) | type scheduler struct
    method stopScheduler (line 239) | func (s *scheduler) stopScheduler() {
    method selectAllJobsOutRequest (line 285) | func (s *scheduler) selectAllJobsOutRequest(out allJobsOutRequest) {
    method selectRunJobRequest (line 302) | func (s *scheduler) selectRunJobRequest(run runJobRequest) {
    method selectRemoveJob (line 328) | func (s *scheduler) selectRemoveJob(id uuid.UUID) {
    method selectExecJobsOutForRescheduling (line 343) | func (s *scheduler) selectExecJobsOutForRescheduling(id uuid.UUID) {
    method updateNextScheduled (line 440) | func (s *scheduler) updateNextScheduled(id uuid.UUID) {
    method selectExecJobsOutCompleted (line 456) | func (s *scheduler) selectExecJobsOutCompleted(id uuid.UUID) {
    method selectJobOutRequest (line 494) | func (s *scheduler) selectJobOutRequest(out *jobOutRequest) {
    method selectNewJob (line 504) | func (s *scheduler) selectNewJob(in newJobIn) {
    method selectRemoveJobsByTags (line 547) | func (s *scheduler) selectRemoveJobsByTags(tags []string) {
    method selectStart (line 563) | func (s *scheduler) selectStart() {
    method now (line 617) | func (s *scheduler) now() time.Time {
    method jobFromInternalJob (line 621) | func (s *scheduler) jobFromInternalJob(in internalJob) job {
    method Jobs (line 631) | func (s *scheduler) Jobs() []Job {
    method NewJob (line 647) | func (s *scheduler) NewJob(jobDefinition JobDefinition, task Task, opt...
    method verifyInterfaceVariadic (line 651) | func (s *scheduler) verifyInterfaceVariadic(taskFunc reflect.Value, ts...
    method verifyVariadic (line 661) | func (s *scheduler) verifyVariadic(taskFunc reflect.Value, tsk task, v...
    method verifyNonVariadic (line 687) | func (s *scheduler) verifyNonVariadic(taskFunc reflect.Value, tsk task...
    method verifyParameterType (line 706) | func (s *scheduler) verifyParameterType(taskFunc reflect.Value, tsk ta...
    method addOrUpdateJob (line 722) | func (s *scheduler) addOrUpdateJob(id uuid.UUID, definition JobDefinit...
    method RemoveByTags (line 824) | func (s *scheduler) RemoveByTags(tags ...string) {
    method RemoveJob (line 831) | func (s *scheduler) RemoveJob(id uuid.UUID) error {
    method Start (line 844) | func (s *scheduler) Start() {
    method StopJobs (line 862) | func (s *scheduler) StopJobs() error {
    method Shutdown (line 879) | func (s *scheduler) Shutdown() error {
    method Update (line 900) | func (s *scheduler) Update(id uuid.UUID, jobDefinition JobDefinition, ...
    method JobsWaitingInQueue (line 904) | func (s *scheduler) JobsWaitingInQueue() int {
    method notifySchedulerStarted (line 1112) | func (s *scheduler) notifySchedulerStarted() {
    method notifySchedulerShutdown (line 1119) | func (s *scheduler) notifySchedulerShutdown() {
    method notifyJobRegistered (line 1126) | func (s *scheduler) notifyJobRegistered(job Job) {
    method notifyJobUnregistered (line 1133) | func (s *scheduler) notifyJobUnregistered(job Job) {
    method notifyJobStarted (line 1140) | func (s *scheduler) notifyJobStarted(job Job) {
    method notifyJobRunning (line 1147) | func (s *scheduler) notifyJobRunning(job Job) {
    method notifyJobCompleted (line 1154) | func (s *scheduler) notifyJobCompleted(job Job) {
    method notifyJobFailed (line 1161) | func (s *scheduler) notifyJobFailed(job Job, err error) {
    method notifySchedulerStopped (line 1168) | func (s *scheduler) notifySchedulerStopped() {
    method notifyJobExecutionTime (line 1175) | func (s *scheduler) notifyJobExecutionTime(job Job, duration time.Dura...
    method notifyJobSchedulingDelay (line 1182) | func (s *scheduler) notifyJobSchedulingDelay(job Job, scheduledTime ti...
    method notifyConcurrencyLimitReached (line 1189) | func (s *scheduler) notifyConcurrencyLimitReached(limitType string, jo...
  type newJobIn (line 108) | type newJobIn struct
  type jobOutRequest (line 114) | type jobOutRequest struct
  type runJobRequest (line 119) | type runJobRequest struct
  type allJobsOutRequest (line 124) | type allJobsOutRequest struct
  function NewScheduler (line 133) | func NewScheduler(options ...SchedulerOption) (Scheduler, error) {
  type SchedulerOption (line 919) | type SchedulerOption
  function WithClock (line 923) | func WithClock(clock clockwork.Clock) SchedulerOption {
  function WithDistributedElector (line 937) | func WithDistributedElector(elector Elector) SchedulerOption {
  function WithDistributedLocker (line 952) | func WithDistributedLocker(locker Locker) SchedulerOption {
  function WithGlobalJobOptions (line 965) | func WithGlobalJobOptions(jobOptions ...JobOption) SchedulerOption {
  type LimitMode (line 974) | type LimitMode
  constant LimitModeReschedule (line 981) | LimitModeReschedule = 1
  constant LimitModeWait (line 1007) | LimitModeWait = 2
  function WithLimitConcurrentJobs (line 1021) | func WithLimitConcurrentJobs(limit uint, mode LimitMode) SchedulerOption {
  function WithLocation (line 1042) | func WithLocation(location *time.Location) SchedulerOption {
  function WithLogger (line 1053) | func WithLogger(logger Logger) SchedulerOption {
  function WithStopTimeout (line 1068) | func WithStopTimeout(timeout time.Duration) SchedulerOption {
  function WithMonitor (line 1079) | func WithMonitor(monitor Monitor) SchedulerOption {
  function WithMonitorStatus (line 1090) | func WithMonitorStatus(monitor MonitorStatus) SchedulerOption {
  function WithSchedulerMonitor (line 1101) | func WithSchedulerMonitor(monitor SchedulerMonitor) SchedulerOption {

FILE: scheduler_monitor.go
  type SchedulerMonitor (line 7) | type SchedulerMonitor interface

FILE: scheduler_monitor_test.go
  type testSchedulerMonitor (line 16) | type testSchedulerMonitor struct
    method SchedulerStarted (line 68) | func (t *testSchedulerMonitor) SchedulerStarted() {
    method SchedulerStopped (line 75) | func (t *testSchedulerMonitor) SchedulerStopped() {
    method SchedulerShutdown (line 82) | func (t *testSchedulerMonitor) SchedulerShutdown() {
    method getStartedCount (line 89) | func (t *testSchedulerMonitor) getStartedCount() int64 {
    method getShutdownCount (line 93) | func (t *testSchedulerMonitor) getShutdownCount() int64 {
    method getStartedCalls (line 97) | func (t *testSchedulerMonitor) getStartedCalls() []time.Time {
    method getShutdownCalls (line 103) | func (t *testSchedulerMonitor) getShutdownCalls() []time.Time {
    method JobRegistered (line 109) | func (t *testSchedulerMonitor) JobRegistered(job Job) {
    method JobUnregistered (line 116) | func (t *testSchedulerMonitor) JobUnregistered(job Job) {
    method JobStarted (line 123) | func (t *testSchedulerMonitor) JobStarted(job Job) {
    method JobRunning (line 130) | func (t *testSchedulerMonitor) JobRunning(job Job) {
    method JobCompleted (line 137) | func (t *testSchedulerMonitor) JobCompleted(job Job) {
    method JobFailed (line 144) | func (t *testSchedulerMonitor) JobFailed(job Job, err error) {
    method JobExecutionTime (line 152) | func (t *testSchedulerMonitor) JobExecutionTime(_ Job, duration time.D...
    method JobSchedulingDelay (line 158) | func (t *testSchedulerMonitor) JobSchedulingDelay(_ Job, scheduledTime...
    method ConcurrencyLimitReached (line 167) | func (t *testSchedulerMonitor) ConcurrencyLimitReached(limitType strin...
    method getJobRegCount (line 174) | func (t *testSchedulerMonitor) getJobRegCount() int64 {
    method getJobUnregCount (line 178) | func (t *testSchedulerMonitor) getJobUnregCount() int64 {
    method getJobStartCount (line 182) | func (t *testSchedulerMonitor) getJobStartCount() int64 {
    method getJobRunningCount (line 186) | func (t *testSchedulerMonitor) getJobRunningCount() int64 {
    method getJobCompletedCount (line 190) | func (t *testSchedulerMonitor) getJobCompletedCount() int64 {
    method getJobFailedCount (line 194) | func (t *testSchedulerMonitor) getJobFailedCount() int64 {
    method getJobFailedCalls (line 228) | func (t *testSchedulerMonitor) getJobFailedCalls() ([]Job, []error) {
  function newTestSchedulerMonitor (line 45) | func newTestSchedulerMonitor() *testSchedulerMonitor {
  function TestSchedulerMonitor_Basic (line 236) | func TestSchedulerMonitor_Basic(t *testing.T) {
  function TestSchedulerMonitor_MultipleStartStop (line 280) | func TestSchedulerMonitor_MultipleStartStop(t *testing.T) {
  function TestSchedulerMonitor_WithoutMonitor (line 313) | func TestSchedulerMonitor_WithoutMonitor(t *testing.T) {
  function TestSchedulerMonitor_NilMonitor (line 332) | func TestSchedulerMonitor_NilMonitor(t *testing.T) {
  function TestSchedulerMonitor_ConcurrentAccess (line 339) | func TestSchedulerMonitor_ConcurrentAccess(t *testing.T) {
  function TestSchedulerMonitor_StartWithoutJobs (line 368) | func TestSchedulerMonitor_StartWithoutJobs(t *testing.T) {
  function TestSchedulerMonitor_ShutdownWithoutStart (line 386) | func TestSchedulerMonitor_ShutdownWithoutStart(t *testing.T) {
  function TestSchedulerMonitor_ThreadSafety (line 408) | func TestSchedulerMonitor_ThreadSafety(t *testing.T) {
  function TestSchedulerMonitor_IntegrationWithJobs (line 438) | func TestSchedulerMonitor_IntegrationWithJobs(t *testing.T) {

FILE: scheduler_test.go
  constant testEnvLocal (line 23) | testEnvLocal = "local"
  function init (line 27) | func init() {
  function newTestScheduler (line 41) | func newTestScheduler(t *testing.T, options ...SchedulerOption) Scheduler {
  type errorLocker (line 57) | type errorLocker struct
    method Lock (line 59) | func (e errorLocker) Lock(_ context.Context, _ string) (Lock, error) {
  function TestScheduler_OneSecond_NoOptions (line 63) | func TestScheduler_OneSecond_NoOptions(t *testing.T) {
  function TestScheduler_LongRunningJobs (line 132) | func TestScheduler_LongRunningJobs(t *testing.T) {
  function TestScheduler_Update (line 233) | func TestScheduler_Update(t *testing.T) {
  function TestScheduler_StopTimeout (line 308) | func TestScheduler_StopTimeout(t *testing.T) {
  function TestScheduler_StopLongRunningJobs (line 363) | func TestScheduler_StopLongRunningJobs(t *testing.T) {
  function TestScheduler_StopAndStartLongRunningJobs (line 476) | func TestScheduler_StopAndStartLongRunningJobs(t *testing.T) {
  function TestScheduler_Shutdown (line 515) | func TestScheduler_Shutdown(t *testing.T) {
  function TestScheduler_Start (line 581) | func TestScheduler_Start(t *testing.T) {
  function TestScheduler_NewJob (line 616) | func TestScheduler_NewJob(t *testing.T) {
  function TestScheduler_NewJobErrors (line 713) | func TestScheduler_NewJobErrors(t *testing.T) {
  function TestScheduler_NewJobTask (line 1127) | func TestScheduler_NewJobTask(t *testing.T) {
  function TestScheduler_WithOptionsErrors (line 1267) | func TestScheduler_WithOptionsErrors(t *testing.T) {
  function TestScheduler_Singleton (line 1329) | func TestScheduler_Singleton(t *testing.T) {
  function TestScheduler_LimitMode (line 1392) | func TestScheduler_LimitMode(t *testing.T) {
  function TestScheduler_LimitModeAndSingleton (line 1464) | func TestScheduler_LimitModeAndSingleton(t *testing.T) {
  function TestScheduler_OneTimeJob_DoesNotCleanupNext (line 1550) | func TestScheduler_OneTimeJob_DoesNotCleanupNext(t *testing.T) {
  type testElector (line 1638) | type testElector struct
    method IsLeader (line 1644) | func (t *testElector) IsLeader(ctx context.Context) error {
  type testLocker (line 1663) | type testLocker struct
    method Lock (line 1669) | func (t *testLocker) Lock(_ context.Context, _ string) (Lock, error) {
  type testLock (line 1682) | type testLock struct
    method Unlock (line 1684) | func (t testLock) Unlock(_ context.Context) error {
  function TestScheduler_WithDistributed (line 1688) | func TestScheduler_WithDistributed(t *testing.T) {
  function TestScheduler_RemoveJob (line 1877) | func TestScheduler_RemoveJob(t *testing.T) {
  function TestScheduler_JobsWaitingInQueue (line 1916) | func TestScheduler_JobsWaitingInQueue(t *testing.T) {
  function TestScheduler_RemoveLotsOfJobs (line 1969) | func TestScheduler_RemoveLotsOfJobs(t *testing.T) {
  function TestScheduler_RemoveJob_RemoveSelf (line 2011) | func TestScheduler_RemoveJob_RemoveSelf(t *testing.T) {
  function TestScheduler_WithEventListeners (line 2034) | func TestScheduler_WithEventListeners(t *testing.T) {
  function TestScheduler_WithLocker_WithEventListeners (line 2128) | func TestScheduler_WithLocker_WithEventListeners(t *testing.T) {
  function TestScheduler_ManyJobs (line 2188) | func TestScheduler_ManyJobs(t *testing.T) {
  function TestScheduler_RunJobNow (line 2223) | func TestScheduler_RunJobNow(t *testing.T) {
  function TestScheduler_LastRunSingleton (line 2373) | func TestScheduler_LastRunSingleton(t *testing.T) {
  function TestScheduler_OneTimeJob (line 2441) | func TestScheduler_OneTimeJob(t *testing.T) {
  function TestScheduler_AtTimesJob (line 2489) | func TestScheduler_AtTimesJob(t *testing.T) {
  function TestScheduler_WithLimitedRuns (line 2724) | func TestScheduler_WithLimitedRuns(t *testing.T) {
  function TestScheduler_Jobs (line 2799) | func TestScheduler_Jobs(t *testing.T) {
  type testMonitor (line 2831) | type testMonitor struct
    method IncrementJob (line 2844) | func (t *testMonitor) IncrementJob(_ uuid.UUID, name string, _ []strin...
    method RecordJobTiming (line 2854) | func (t *testMonitor) RecordJobTiming(startTime, endTime time.Time, _ ...
  function newTestMonitor (line 2837) | func newTestMonitor() *testMonitor {
  function TestScheduler_WithMonitor (line 2864) | func TestScheduler_WithMonitor(t *testing.T) {
  function TestScheduler_WithStartAtDateTimePast (line 2915) | func TestScheduler_WithStartAtDateTimePast(t *testing.T) {

FILE: util.go
  function callJobFuncWithParams (line 13) | func callJobFuncWithParams(jobFunc any, params ...any) error {
  function requestJob (line 38) | func requestJob(id uuid.UUID, ch chan *jobOutRequest) *internalJob {
  function requestJobCtx (line 44) | func requestJobCtx(ctx context.Context, id uuid.UUID, ch chan *jobOutReq...
  function removeSliceDuplicatesInt (line 64) | func removeSliceDuplicatesInt(in []int) []int {
  function convertAtTimesToDateTime (line 69) | func convertAtTimesToDateTime(atTimes AtTimes, location *time.Location) ...
  function ascendingTime (line 90) | func ascendingTime(a, b time.Time) int {
  type waitGroupWithMutex (line 94) | type waitGroupWithMutex struct
    method Add (line 99) | func (w *waitGroupWithMutex) Add(delta int) {
    method Done (line 105) | func (w *waitGroupWithMutex) Done() {
    method Wait (line 109) | func (w *waitGroupWithMutex) Wait() {

FILE: util_test.go
  function TestRemoveSliceDuplicatesInt (line 11) | func TestRemoveSliceDuplicatesInt(t *testing.T) {
  function TestCallJobFuncWithParams (line 38) | func TestCallJobFuncWithParams(t *testing.T) {
  function TestConvertAtTimesToDateTime (line 90) | func TestConvertAtTimesToDateTime(t *testing.T) {
Condensed preview — 44 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (339K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 678,
    "preview": "# These are supported funding model platforms\n\ngithub: go-co-op # Replace with up to 4 GitHub Sponsors-enabled usernames"
  },
  {
    "path": ".github/copilot-instructions.md",
    "chars": 5305,
    "preview": "# gocron: Go Job Scheduling Library\n\nAlways reference these instructions first and fallback to search or bash commands o"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 605,
    "preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "chars": 2416,
    "preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
  },
  {
    "path": ".github/workflows/file_formatting.yml",
    "chars": 352,
    "preview": "on:\n  push:\n    branches:\n      - v2\n  pull_request:\n    branches:\n      - v2\n\nname: formatting\njobs:\n  check-sorted:\n  "
  },
  {
    "path": ".github/workflows/go_test.yml",
    "chars": 611,
    "preview": "on:\n  push:\n    branches:\n      - v2\n  pull_request:\n    branches:\n      - v2\n\nname: lint and test\njobs:\n  golangci:\n   "
  },
  {
    "path": ".gitignore",
    "chars": 321,
    "preview": "# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with `go test -c`\n*.test\nlocal"
  },
  {
    "path": ".golangci.yaml",
    "chars": 865,
    "preview": "version: \"2\"\nrun:\n  issues-exit-code: 1\n  tests: true\noutput:\n  formats:\n    text:\n      path: stdout\n      print-linter"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 681,
    "preview": "# See https://pre-commit.com for more information\n# See https://pre-commit.com/hooks.html for more hooks\nrepos:\n  - repo"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 3153,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1674,
    "preview": "# Contributing to gocron\n\nThank you for coming to contribute to gocron! We welcome new ideas, PRs and general feedback.\n"
  },
  {
    "path": "LICENSE",
    "chars": 1061,
    "preview": "MIT License\n\nCopyright (c) 2014, 辣椒面\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof th"
  },
  {
    "path": "Makefile",
    "chars": 497,
    "preview": ".PHONY: fmt lint test mocks test_coverage test_ci\n\nGO_PKGS   := $(shell go list -f {{.Dir}} ./...)\n\nfmt:\n\t@go list -f {{"
  },
  {
    "path": "README.md",
    "chars": 13083,
    "preview": "# gocron: A Golang Job Scheduling Package\n\n[![CI State](https://github.com/go-co-op/gocron/actions/workflows/go_test.yml"
  },
  {
    "path": "SECURITY.md",
    "chars": 714,
    "preview": "# Security Policy\n\n## Supported Versions\n\nThe current plan is to maintain version 2 as long as possible incorporating an"
  },
  {
    "path": "distributed.go",
    "chars": 1968,
    "preview": "//go:generate mockgen -destination=mocks/distributed.go -package=gocronmocks . Elector,Locker,Lock\npackage gocron\n\nimpor"
  },
  {
    "path": "errors.go",
    "chars": 6112,
    "preview": "package gocron\n\nimport (\n\t\"errors\"\n)\n\n// Public error definitions\nvar (\n\tErrCronJobInvalid                = errors.New(\""
  },
  {
    "path": "example_test.go",
    "chars": 21635,
    "preview": "package gocron_test\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-co-op/gocron/v2\"\n\t\"github.com"
  },
  {
    "path": "examples/elector/main.go",
    "chars": 1155,
    "preview": "package main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/go-co-op/gocron/v2\"\n)\n\nvar _ gocron.Elector = ("
  },
  {
    "path": "executor.go",
    "chars": 19821,
    "preview": "package gocron\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/jonboulle/clockwork\"\n\n\t\"github.com/g"
  },
  {
    "path": "go.mod",
    "chars": 481,
    "preview": "module github.com/go-co-op/gocron/v2\n\ngo 1.21.4\n\nrequire (\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/jonboulle/clockwor"
  },
  {
    "path": "go.sum",
    "chars": 2216,
    "preview": "github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.1 h1"
  },
  {
    "path": "gocron-monitor-test/debug_restart.go",
    "chars": 3872,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-co-op/gocron/v2\"\n)\n\ntype DebugMonitor struct {\n\tstartCount      i"
  },
  {
    "path": "gocron-monitor-test/go.mod",
    "chars": 269,
    "preview": "module test\n\ngo 1.21.4\n\nrequire github.com/go-co-op/gocron/v2 v2.17.0\n\nrequire (\n\tgithub.com/google/uuid v1.6.0 // indir"
  },
  {
    "path": "gocron-monitor-test/go.sum",
    "chars": 1340,
    "preview": "github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.m"
  },
  {
    "path": "job.go",
    "chars": 40474,
    "preview": "//go:generate mockgen -destination=mocks/job.go -package=gocronmocks . Job\npackage gocron\n\nimport (\n\t\"context\"\n\t\"errors\""
  },
  {
    "path": "job_test.go",
    "chars": 32062,
    "preview": "package gocron\n\nimport (\n\t\"math/rand\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/"
  },
  {
    "path": "logger.go",
    "chars": 2370,
    "preview": "//go:generate mockgen -destination=mocks/logger.go -package=gocronmocks . Logger\npackage gocron\n\nimport (\n\t\"fmt\"\n\t\"log\"\n"
  },
  {
    "path": "logger_test.go",
    "chars": 2264,
    "preview": "package gocron\n\nimport (\n\t\"bytes\"\n\t\"log\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNoOpLo"
  },
  {
    "path": "migration_v1_to_v2.md",
    "chars": 3376,
    "preview": "# Migration Guide: `gocron` v1 → v2\n\nThis guide helps you migrate your code from the `v1` branch to the `v2` branch of ["
  },
  {
    "path": "mocks/README.md",
    "chars": 509,
    "preview": "# gocron mocks\n\n## Quick Start\n\n```\ngo get github.com/go-co-op/gocron/mocks/v2\n```\n\nwrite a test\n\n```golang\npackage main"
  },
  {
    "path": "mocks/distributed.go",
    "chars": 3713,
    "preview": "// Code generated by MockGen. DO NOT EDIT.\n// Source: github.com/go-co-op/gocron/v2 (interfaces: Elector,Locker,Lock)\n//"
  },
  {
    "path": "mocks/go.mod",
    "chars": 335,
    "preview": "module github.com/go-co-op/gocron/mocks/v2\n\ngo 1.20\n\nrequire (\n\tgithub.com/go-co-op/gocron/v2 v2.2.10\n\tgithub.com/google"
  },
  {
    "path": "mocks/go.sum",
    "chars": 1444,
    "preview": "github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/go-co-op/gocron/v2 v2.2.10 "
  },
  {
    "path": "mocks/job.go",
    "chars": 3907,
    "preview": "// Code generated by MockGen. DO NOT EDIT.\n// Source: github.com/go-co-op/gocron/v2 (interfaces: Job)\n//\n// Generated by"
  },
  {
    "path": "mocks/logger.go",
    "chars": 3102,
    "preview": "// Code generated by MockGen. DO NOT EDIT.\n// Source: github.com/go-co-op/gocron/v2 (interfaces: Logger)\n//\n// Generated"
  },
  {
    "path": "mocks/scheduler.go",
    "chars": 5668,
    "preview": "// Code generated by MockGen. DO NOT EDIT.\n// Source: github.com/go-co-op/gocron/v2 (interfaces: Scheduler)\n//\n// Genera"
  },
  {
    "path": "monitor.go",
    "chars": 1400,
    "preview": "package gocron\n\nimport (\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n)\n\n// JobStatus is the status of job run that should be coll"
  },
  {
    "path": "scheduler.go",
    "chars": 32284,
    "preview": "//go:generate mockgen -destination=mocks/scheduler.go -package=gocronmocks . Scheduler\npackage gocron\n\nimport (\n\t\"contex"
  },
  {
    "path": "scheduler_monitor.go",
    "chars": 1782,
    "preview": "package gocron\n\nimport \"time\"\n\n// SchedulerMonitor is called by the Scheduler to provide scheduler-level\n// metrics and "
  },
  {
    "path": "scheduler_monitor_test.go",
    "chars": 13516,
    "preview": "package gocron\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"githu"
  },
  {
    "path": "scheduler_test.go",
    "chars": 61127,
    "preview": "package gocron\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"os\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google"
  },
  {
    "path": "util.go",
    "chars": 2154,
    "preview": "package gocron\n\nimport (\n\t\"context\"\n\t\"reflect\"\n\t\"slices\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n)\n\nfunc callJobFuncW"
  },
  {
    "path": "util_test.go",
    "chars": 2830,
    "preview": "package gocron\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestRemoveSliceDupl"
  }
]

About this extraction

This page contains the full source code of the go-co-op/gocron GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 44 files (298.0 KB), approximately 87.9k tokens, and a symbol index with 479 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!