[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: go-co-op # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: gocron\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/copilot-instructions.md",
    "content": "# gocron: Go Job Scheduling Library\n\nAlways reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.\n\n## Working Effectively\n\n### Bootstrap and Build Commands\n- Install dependencies: `go mod tidy`\n- Build the library: `go build -v ./...`\n- Install required tools:\n  - `go install go.uber.org/mock/mockgen@latest` \n  - `export PATH=$PATH:$(go env GOPATH)/bin` (add to shell profile)\n- Generate mocks: `make mocks`\n- Format code: `make fmt`\n\n### Testing Commands  \n- Run all tests: `make test` -- takes 50 seconds. NEVER CANCEL. Set timeout to 90+ seconds.\n- Run CI tests: `make test_ci` -- takes 50 seconds. NEVER CANCEL. Set timeout to 90+ seconds.\n- Run with coverage: `make test_coverage` -- takes 50 seconds. NEVER CANCEL. Set timeout to 90+ seconds.\n- Run specific tests: `go test -v -race -count=1 ./...`\n\n### Linting Commands\n- Format verification: `grep \"^func [a-zA-Z]\" example_test.go | sort -c`\n- Full linting: `make lint` -- MAY FAIL due to golangci-lint config compatibility issues. This is a known issue.\n- Alternative basic linting: `go vet ./...` and `gofmt -d .`\n\n## Validation\n\n### Required Validation Steps\n- ALWAYS run `make test` before submitting changes. Tests must pass.\n- ALWAYS run `make fmt` to ensure proper formatting.\n- ALWAYS run `make mocks` if you change interface definitions.\n- ALWAYS verify examples still work by running them: `cd examples/elector && go run main.go`\n\n### Manual Testing Scenarios\nSince this is a library, not an application, testing involves:\n1. **Basic Scheduler Creation**: Verify you can create a scheduler with `gocron.NewScheduler()`\n2. **Job Creation**: Verify you can create jobs with various `JobDefinition` types\n3. **Scheduler Lifecycle**: Verify Start() and Shutdown() work correctly\n4. **Example Validation**: Run examples in `examples/` directory to ensure functionality\n\nExample validation script:\n```go\npackage main\nimport (\n    \"fmt\"\n    \"time\"\n    \"github.com/go-co-op/gocron/v2\"\n)\nfunc main() {\n    s, err := gocron.NewScheduler()\n    if err != nil { panic(err) }\n    j, err := s.NewJob(\n        gocron.DurationJob(2*time.Second),\n        gocron.NewTask(func() { fmt.Println(\"Working!\") }),\n    )\n    if err != nil { panic(err) }\n    fmt.Printf(\"Job ID: %s\\n\", j.ID())\n    s.Start()\n    time.Sleep(6 * time.Second)\n    s.Shutdown()\n}\n```\n\n### CI Requirements\nThe CI will fail if:\n- Tests don't pass (`make test_ci`)\n- Function order in `example_test.go` is incorrect\n- golangci-lint finds issues (though config compatibility varies)\n\n## Common Tasks\n\n### Repository Structure\n```\n.\n├── README.md              # Main documentation\n├── CONTRIBUTING.md        # Contribution guidelines  \n├── SECURITY.md           # Security policy\n├── Makefile              # Build automation\n├── go.mod               # Go module definition\n├── .github/             # GitHub workflows and configs\n├── .golangci.yaml       # Linting configuration\n├── examples/            # Usage examples\n│   └── elector/         # Distributed elector example\n├── mocks/               # Generated mock files\n├── *.go                # Library source files\n└── *_test.go           # Test files\n```\n\n### Key Source Files\n- `scheduler.go` - Main scheduler implementation\n- `job.go` - Job definitions and scheduling logic\n- `executor.go` - Job execution engine\n- `logger.go` - Logging interfaces and implementations  \n- `distributed.go` - Distributed scheduling support\n- `monitor.go` - Job monitoring interfaces\n- `util.go` - Utility functions\n- `errors.go` - Error definitions\n\n### Dependencies and Versions\n- Requires Go 1.23.0+\n- Key dependencies automatically managed via `go mod`:\n  - `github.com/google/uuid` - UUID generation\n  - `github.com/jonboulle/clockwork` - Time mocking for tests\n  - `github.com/robfig/cron/v3` - Cron expression parsing\n  - `github.com/stretchr/testify` - Testing utilities\n  - `go.uber.org/goleak` - Goroutine leak detection\n\n### Testing Patterns\n- Uses table-driven tests following Go best practices\n- Extensive use of goroutine leak detection (may be skipped in CI via TEST_ENV)\n- Mock-based testing for interfaces\n- Race condition detection enabled (`-race` flag)\n- 93.8% test coverage expected\n\n### Build and Release\n- No application to build - this is a library\n- Version managed via Git tags (v2.x.x)\n- Distribution via Go module system\n- CI tests on Go 1.23 and 1.24\n\n## Troubleshooting\n\n### Common Issues\n1. **mockgen not found**: Install with `go install go.uber.org/mock/mockgen@latest`\n2. **golangci-lint config errors**: Known compatibility issue - use `go vet` instead\n3. **Test timeouts**: Tests can take 50+ seconds, always set adequate timeouts\n4. **PATH issues**: Ensure `$(go env GOPATH)/bin` is in PATH\n5. **Import errors in examples**: Run `go mod tidy` to resolve dependencies\n\n### Expected Timings\n- `make test`: ~50 seconds\n- `make test_coverage`: ~50 seconds  \n- `make test_ci`: ~50 seconds\n- `go build`: ~5 seconds\n- `make mocks`: ~2 seconds\n- `make fmt`: <1 second\n\n### Known Limitations\n- golangci-lint configuration may have compatibility issues with certain versions\n- Some tests are skipped in CI environments (controlled by TEST_ENV variable)\n- Examples directory has no tests but should be manually validated"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n\nversion: 2\nupdates:\n  # Maintain dependencies for GitHub Actions\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n\n  # Maintain Go dependencies\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\n#\n# ******** NOTE ********\n# We have attempted to detect the languages in your repository. Please check\n# the `language` matrix defined below to confirm you have the correct set of\n# supported CodeQL languages.\n#\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [ v2 ]\n    branches-ignore:\n      - \"dependabot/**\"\n  pull_request:\n    paths-ignore:\n      - '**.md'\n    # The branches below must be a subset of the branches above\n    branches: [ v2 ]\n  schedule:\n    - cron: '34 7 * * 1'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'go' ]\n        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]\n        # Learn more:\n        # 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\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v6\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v4\n      with:\n        languages: ${{ matrix.language }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file.\n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n        # queries: ./path/to/local/query, your-org/your-repo/queries@main\n\n    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).\n    # If this step fails, then you should remove it and run the build manually (see below)\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v4\n\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 https://git.io/JvXDl\n\n    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines\n    #    and modify them (or add more) to build your code if your project\n    #    uses a compiled language\n\n    #- run: |\n    #   make bootstrap\n    #   make release\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v4\n"
  },
  {
    "path": ".github/workflows/file_formatting.yml",
    "content": "on:\n  push:\n    branches:\n      - v2\n  pull_request:\n    branches:\n      - v2\n\nname: formatting\njobs:\n  check-sorted:\n    name: check sorted\n    runs-on: ubuntu-latest\n    steps:\n      - name: checkout code\n        uses: actions/checkout@v6\n      - name: verify example_test.go\n        run: |\n          grep \"^func [a-z-A-Z]\" example_test.go | sort -c\n"
  },
  {
    "path": ".github/workflows/go_test.yml",
    "content": "on:\n  push:\n    branches:\n      - v2\n  pull_request:\n    branches:\n      - v2\n\nname: lint and test\njobs:\n  golangci:\n    strategy:\n      matrix:\n        go-version:\n          - \"1.24\"\n          - \"1.25\"\n    name: lint and test\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: ${{ matrix.go-version }}\n      - name: golangci-lint\n        uses: golangci/golangci-lint-action@v9.2.0\n        with:\n          version: v2.4.0\n      - name: test\n        run: make test_ci\n"
  },
  {
    "path": ".gitignore",
    "content": "# 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_testing\ncoverage.out\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Dependency directories (remove the comment below to include it)\nvendor/\n\n# IDE project files\n.idea\n"
  },
  {
    "path": ".golangci.yaml",
    "content": "version: \"2\"\nrun:\n  issues-exit-code: 1\n  tests: true\noutput:\n  formats:\n    text:\n      path: stdout\n      print-linter-name: true\n      print-issued-lines: true\n  path-prefix: \"\"\nlinters:\n  enable:\n    - bodyclose\n    - copyloopvar\n    - misspell\n    - revive\n    - whitespace\n  exclusions:\n    generated: lax\n    presets:\n      - common-false-positives\n      - legacy\n      - std-error-handling\n    rules:\n      - linters:\n          - revive\n        path: example_test.go\n        text: seems to be unused\n      - linters:\n          - revive\n        text: package-comments\n    paths:\n      - local\n      - third_party$\n      - builtin$\n      - examples$\nissues:\n  max-same-issues: 100\n  fix: true\nformatters:\n  enable:\n    - gofumpt\n    - goimports\n  exclusions:\n    generated: lax\n    paths:\n      - local\n      - third_party$\n      - builtin$\n      - examples$\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "# See https://pre-commit.com for more information\n# See https://pre-commit.com/hooks.html for more hooks\nrepos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v5.0.0\n    hooks:\n      - id: check-added-large-files\n      - id: check-case-conflict\n      - id: check-merge-conflict\n      - id: check-yaml\n      - id: detect-private-key\n      - id: end-of-file-fixer\n      - id: trailing-whitespace\n  - repo: https://github.com/golangci/golangci-lint\n    rev: v2.4.0\n    hooks:\n      - id: golangci-lint\n  - repo: https://github.com/TekWizely/pre-commit-golang\n    rev: v1.0.0-rc.1\n    hooks:\n      - id: go-fumpt\n        args:\n          - -w\n      - id: go-mod-tidy\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone. And we mean everyone!\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and kind language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team initially on Slack to coordinate private communication. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to gocron\n\nThank you for coming to contribute to gocron! We welcome new ideas, PRs and general feedback.\n\n## Reporting Bugs\n\nIf you find a bug then please let the project know by opening an issue after doing the following:\n\n- Do a quick search of the existing issues to make sure the bug isn't already reported\n- Try and make a minimal list of steps that can reliably reproduce the bug you are experiencing\n- Collect as much information as you can to help identify what the issue is (project version, configuration files, etc)\n\n## Suggesting Enhancements\n\nIf 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:\n\n- Is this a common use case?\n- Is it simple to understand?\n\nYou can help us out by doing the following before raising a new issue:\n\n- Check that the feature hasn't been requested already by searching existing issues\n- Try and reduce your enhancement into a single, concise and deliverable request, rather than a general idea\n- Explain your own use cases as the basis of the request\n\n## Adding Features\n\nPull requests are always welcome. However, before going through the trouble of implementing a change it's worth creating a bug or feature request issue.\nThis allows us to discuss the changes and make sure they are a good fit for the project.\n\nPlease always make sure a pull request has been:\n\n- Unit tested with `make test`\n- Linted with `make lint`\n\n## Writing Tests\n\nTests 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.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2014, 辣椒面\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: fmt lint test mocks test_coverage test_ci\n\nGO_PKGS   := $(shell go list -f {{.Dir}} ./...)\n\nfmt:\n\t@go list -f {{.Dir}} ./... | xargs -I{} gofmt -w -s {}\n\nlint:\n\t@grep \"^func [a-zA-Z]\" example_test.go | sort -c\n\t@golangci-lint run\n\ntest:\n\t@go test -race -v $(GO_FLAGS) -count=1 $(GO_PKGS)\n\ntest_coverage:\n\t@go test -race -v $(GO_FLAGS) -count=1 -coverprofile=coverage.out -covermode=atomic $(GO_PKGS)\n\ntest_ci:\n\t@go test -race -v $(GO_FLAGS) -count=1 $(GO_PKGS)\n\nmocks:\n\t@go generate ./...\n"
  },
  {
    "path": "README.md",
    "content": "# gocron: A Golang Job Scheduling Package\n\n[![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)\n![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)\n\ngocron is a job scheduling package which lets you run Go functions at pre-determined intervals.\n\n> Looking for a visual interface?  \n> 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.\n\nIf you want to chat, you can find us on Slack at\n[<img src=\"https://img.shields.io/badge/gophers-gocron-brightgreen?logo=slack\">](https://gophers.slack.com/archives/CQ7T0T1FW)\n\n## Quick Start\n\n```\ngo get github.com/go-co-op/gocron/v2\n```\n\n```golang\npackage main\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-co-op/gocron/v2\"\n)\n\nfunc main() {\n\t// create a scheduler\n\ts, err := gocron.NewScheduler()\n\tif err != nil {\n\t\t// handle error\n\t}\n\n\t// add a job to the scheduler\n\tj, err := s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\t10*time.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc(a string, b int) {\n\t\t\t\t// do things\n\t\t\t},\n\t\t\t\"hello\",\n\t\t\t1,\n\t\t),\n\t)\n\tif err != nil {\n\t\t// handle error\n\t}\n\t// each job has a unique id\n\tfmt.Println(j.ID())\n\n\t// start the scheduler\n\ts.Start()\n\n\t// block until you are ready to shut down\n\tselect {\n\tcase <-time.After(time.Minute):\n\t}\n\n\t// when you're done, shut it down\n\terr = s.Shutdown()\n\tif err != nil {\n\t\t// handle error\n\t}\n}\n```\n\n## Examples\n\n- [Go doc examples](https://pkg.go.dev/github.com/go-co-op/gocron/v2#pkg-examples)\n- [Examples directory](examples)\n\n## Articles & Blog Posts\n\nCommunity articles and tutorials about using gocron:\n\n- [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.\n\n## Concepts\n\n- **Job**: The job encapsulates a \"task\", which is made up of a go function and any function parameters. The Job then\n  provides the scheduler with the time the job should next be scheduled to run.\n- **Scheduler**: The scheduler keeps track of all the jobs and sends each job to the executor when\n  it is ready to be run.\n- **Executor**: The executor calls the job's task and manages the complexities of different job\n  execution timing requirements (e.g. singletons that shouldn't overrun each other, limiting the max number of jobs running)\n\n\n## Features\n\n### Job types\nJobs can be run at various intervals.\n- [**Duration**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#DurationJob):\nJobs can be run at a fixed `time.Duration`.\n- [**Random duration**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#DurationRandomJob):\nJobs can be run at a random `time.Duration` between a min and max.\n- [**Cron**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#CronJob):\nJobs can be run using a crontab.\n- [**Daily**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#DailyJob):\nJobs can be run every x days at specific times.\n- [**Weekly**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WeeklyJob):\nJobs can be run every x weeks on specific days of the week and at specific times.\n- [**Monthly**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#MonthlyJob):\nJobs can be run every x months on specific days of the month and at specific times.\n- [**One time**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#OneTimeJob):\nJobs can be run at specific time(s) (either once or many times).\n\n### Interval Timing\nJobs can be scheduled with different interval timing modes.\n- [**Interval from scheduled time (default)**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#DurationJob):\nBy default, jobs calculate their next run time from when they were scheduled to start, resulting in fixed intervals \nregardless of execution time. Good for cron-like scheduling at predictable times.\n- [**Interval from completion time**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithIntervalFromCompletion):\nJobs can calculate their next run time from when they complete, ensuring consistent rest periods between executions.\nIdeal for rate-limited APIs, resource-intensive jobs, and scenarios where execution time varies.\n\n### Concurrency Limits\nJobs can be limited individually or across the entire scheduler.\n- [**Per job limiting with singleton mode**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithSingletonMode):\nJobs can be limited to a single concurrent execution that either reschedules (skips overlapping executions)\nor queues (waits for the previous execution to finish).\n- [**Per scheduler limiting with limit mode**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithLimitConcurrentJobs):\nJobs can be limited to a certain number of concurrent executions across the entire scheduler\nusing either reschedule (skip when the limit is met) or queue (jobs are added to a queue to\nwait for the limit to be available).\n- **Note:** A scheduler limit and a job limit can both be enabled.\n\n### Distributed instances of gocron\nMultiple instances of gocron can be run.\n- [**Elector**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithDistributedElector):\nAn elector can be used to elect a single instance of gocron to run as the primary with the\nother instances checking to see if a new leader needs to be elected.\n  - Implementations: [go-co-op electors](https://github.com/go-co-op?q=-elector&type=all&language=&sort=)\n    (don't see what you need? request on slack to get a repo created to contribute it!)\n- [**Locker**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithDistributedLocker):\nA locker can be used to lock each run of a job to a single instance of gocron.\nLocker can be at job or scheduler, if it is defined both at job and scheduler then locker of job will take precedence.\n  - See Notes in the doc for [Locker](https://pkg.go.dev/github.com/go-co-op/gocron/v2#Locker) for\n    details and limitations of the locker design.\n  - Implementations: [go-co-op lockers](https://github.com/go-co-op?q=-lock&type=all&language=&sort=)\n    (don't see what you need? request on slack to get a repo created to contribute it!)\n\n### Events\nJob events can trigger actions.\n- [**Listeners**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithEventListeners):\nCan be added to a job, with [event listeners](https://pkg.go.dev/github.com/go-co-op/gocron/v2#EventListener),\nor all jobs across the\n[scheduler](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithGlobalJobOptions)\nto listen for job events and trigger actions.\n\n### Options\nMany job and scheduler options are available.\n- [**Job options**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#JobOption):\nJob options can be set when creating a job using `NewJob`.\n- [**Global job options**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithGlobalJobOptions):\nGlobal job options can be set when creating a scheduler using `NewScheduler`\nand the `WithGlobalJobOptions` option.\n- [**Scheduler options**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#SchedulerOption):\nScheduler options can be set when creating a scheduler using `NewScheduler`.\n\n### Logging\nLogs can be enabled.\n- [Logger](https://pkg.go.dev/github.com/go-co-op/gocron/v2#Logger):\nThe Logger interface can be implemented with your desired logging library.\nThe provided NewLogger uses the standard library's log package.\n\n### Metrics\nMetrics may be collected from the execution of each job and scheduler lifecycle events.\n- [**Monitor**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#Monitor):\nA monitor can be used to collect metrics for each job from a scheduler.\n  - Implementations: There are currently no open source implementations of the Monitor interface available.\n    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)\n    to get started, or reach out on [Slack](https://gophers.slack.com/archives/CQ7T0T1FW) if you'd like to discuss your implementation.\n- [**MonitorStatus**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#MonitorStatus):\nExtends Monitor with status and error tracking for each job.\n  - Implementations: There are currently no open source implementations of the MonitorStatus interface available.\n    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)\n    to get started, or reach out on [Slack](https://gophers.slack.com/archives/CQ7T0T1FW) if you'd like to discuss your implementation.\n- [**SchedulerMonitor**](https://pkg.go.dev/github.com/go-co-op/gocron/v2#SchedulerMonitor):\nA scheduler monitor provides comprehensive observability into scheduler and job lifecycle events.\n\n  **Available Metrics:**\n  - **Scheduler Lifecycle**: `SchedulerStarted`, `SchedulerStopped`, `SchedulerShutdown`\n  - **Job Management**: `JobRegistered`, `JobUnregistered` - track jobs added/removed from scheduler\n  - **Job Execution**: `JobStarted`, `JobRunning`, `JobCompleted`, `JobFailed` - monitor job execution flow\n  - **Performance**: `JobExecutionTime`, `JobSchedulingDelay` - measure job duration and scheduling lag\n  - **Concurrency**: `ConcurrencyLimitReached` - detect when singleton or limit mode constraints are hit\n\n  **Derived Metrics** (calculable from events):\n  - Error rate: `JobFailed / (JobCompleted + JobFailed)`\n  - Average execution time: from `JobExecutionTime` events\n  - Active jobs: `JobRegistered - JobUnregistered`\n  - Current queue depth: `JobStarted - (JobCompleted + JobFailed)`\n\n  **Example - Prometheus Integration:**\n  ```go\n  type PrometheusMonitor struct {\n      jobsCompleted   prometheus.Counter\n      jobsFailed      prometheus.Counter\n      executionTime   prometheus.Histogram\n      schedulingDelay prometheus.Histogram\n  }\n\n  func (p *PrometheusMonitor) JobExecutionTime(job gocron.Job, duration time.Duration) {\n      p.executionTime.Observe(duration.Seconds())\n  }\n\n  func (p *PrometheusMonitor) JobSchedulingDelay(job gocron.Job, scheduled, actual time.Time) {\n      if delay := actual.Sub(scheduled); delay > 0 {\n          p.schedulingDelay.Observe(delay.Seconds())\n      }\n  }\n\n  // Initialize scheduler with monitor\n  s, _ := gocron.NewScheduler(gocron.WithSchedulerMonitor(monitor))\n  ```\n\n  **Use Cases:** Prometheus metrics, custom dashboards, alerting systems, performance monitoring\n\n  - Implementations: There are currently no open source implementations of the SchedulerMonitor interface available.\n    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)\n    to get started, or reach out on [Slack](https://gophers.slack.com/archives/CQ7T0T1FW) if you'd like to discuss your implementation.\n\n### Testing\nThe gocron library is set up to enable testing.\n- Mocks are provided in [the mock package](mocks) using [gomock](https://github.com/uber-go/mock).\n- Time can be mocked by passing in a [FakeClock](https://pkg.go.dev/github.com/jonboulle/clockwork#FakeClock)\nto [WithClock](https://pkg.go.dev/github.com/go-co-op/gocron/v2#WithClock) -\nsee the [example on WithClock](https://pkg.go.dev/github.com/go-co-op/gocron/v2#example-WithClock).\n\n## Supporters\n\nWe appreciate the support for free and open source software!\n\nThis project is supported by:\n\n[JetBrains](https://www.jetbrains.com/?from=gocron)\n\n<a href=\"https://www.jetbrains.com/?from=gocron\">\n <picture>\n   <source media=\"(prefers-color-scheme: dark)\" srcset=\"assets/jetbrains-mono-white.png\" />\n   <source media=\"(prefers-color-scheme: light)\" srcset=\"https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.png\" />\n   <img alt=\"JetBrains logo\" src=\"https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.png\" />\n </picture>\n</a>\n\n[Sentry](https://sentry.io/welcome/)\n\n<a href=\"https://sentry.io/?utm_source=github&utm_medium=logo\">\n <picture>\n   <source media=\"(prefers-color-scheme: dark)\" srcset=\"assets/sentry-wordmark-light-280x84.png\" />\n   <source media=\"(prefers-color-scheme: light)\" srcset=\"https://sentry-brand.storage.googleapis.com/sentry-wordmark-dark-280x84.png\" />\n   <img alt=\"Sentry logo\" src=\"https://sentry-brand.storage.googleapis.com/sentry-wordmark-dark-280x84.png\" />\n </picture>\n</a>\n\n## Star History\n\n<a href=\"https://www.star-history.com/#go-co-op/gocron&Date\">\n <picture>\n   <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/svg?repos=go-co-op/gocron&type=Date&theme=dark\" />\n   <source media=\"(prefers-color-scheme: light)\" srcset=\"https://api.star-history.com/svg?repos=go-co-op/gocron&type=Date\" />\n   <img alt=\"Star History Chart\" src=\"https://api.star-history.com/svg?repos=go-co-op/gocron&type=Date\" />\n </picture>\n</a>\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nThe 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.\n\n| Version | Supported          |\n| ------- | ------------------ |\n| 1.x.x   | :heavy_multiplication_x: |\n| 2.x.x   | :white_check_mark: |\n\n## Reporting a Vulnerability\n\nVulnerabilities 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)\n\nWe will do our best to address any vulnerabilities in an expeditious manner.\n"
  },
  {
    "path": "distributed.go",
    "content": "//go:generate mockgen -destination=mocks/distributed.go -package=gocronmocks . Elector,Locker,Lock\npackage gocron\n\nimport (\n\t\"context\"\n)\n\n// Elector determines the leader from instances asking to be the leader. Only\n// the leader runs jobs. If the leader goes down, a new leader will be elected.\ntype Elector interface {\n\t// IsLeader should return  nil if the job should be scheduled by the instance\n\t// making the request and an error if the job should not be scheduled.\n\tIsLeader(context.Context) error\n}\n\n// Locker represents the required interface to lock jobs when running multiple schedulers.\n// The lock is held for the duration of the job's run, and it is expected that the\n// locker implementation handles time splay between schedulers.\n// The lock key passed is the job's name - which, if not set, defaults to the\n// go function's name, e.g. \"pkg.myJob\" for func myJob() {} in pkg\n//\n// Notes: The locker and scheduler do not handle synchronization of run times across\n// schedulers.\n//\n//  1. If you are using duration based jobs (DurationJob), you can utilize the JobOption\n//     WithStartAt to set a start time for the job to the nearest time rounded to your\n//     duration. For example, if you have a job that runs every 5 minutes, you can set\n//     the start time to the nearest 5 minute e.g. 12:05, 12:10.\n//\n//  2. For all jobs, the implementation is still vulnerable to clockskew between scheduler\n//     instances. This may result in a single scheduler instance running the majority of the\n//     jobs.\n//\n// For distributed jobs, consider utilizing the Elector option if these notes are not acceptable\n// to your use case.\ntype Locker interface {\n\t// Lock if an error is returned by lock, the job will not be scheduled.\n\tLock(ctx context.Context, key string) (Lock, error)\n}\n\n// Lock represents an obtained lock. The lock is released after the execution of the job\n// by the scheduler.\ntype Lock interface {\n\tUnlock(ctx context.Context) error\n}\n"
  },
  {
    "path": "errors.go",
    "content": "package gocron\n\nimport (\n\t\"errors\"\n)\n\n// Public error definitions\nvar (\n\tErrCronJobInvalid                = errors.New(\"gocron: CronJob: invalid crontab\")\n\tErrCronJobParse                  = errors.New(\"gocron: CronJob: crontab parse failure\")\n\tErrDailyJobAtTimeNil             = errors.New(\"gocron: DailyJob: atTime within atTimes must not be nil\")\n\tErrDailyJobAtTimesNil            = errors.New(\"gocron: DailyJob: atTimes must not be nil\")\n\tErrDailyJobHours                 = errors.New(\"gocron: DailyJob: atTimes hours must be between 0 and 23 inclusive\")\n\tErrDailyJobZeroInterval          = errors.New(\"gocron: DailyJob: interval must be greater than 0\")\n\tErrDailyJobMinutesSeconds        = errors.New(\"gocron: DailyJob: atTimes minutes and seconds must be between 0 and 59 inclusive\")\n\tErrDurationJobIntervalZero       = errors.New(\"gocron: DurationJob: time interval is 0\")\n\tErrDurationJobIntervalNegative   = errors.New(\"gocron: DurationJob: time interval must be greater than 0\")\n\tErrDurationRandomJobPositive     = errors.New(\"gocron: DurationRandomJob: minimum and maximum durations must be greater than 0\")\n\tErrDurationRandomJobMinMax       = errors.New(\"gocron: DurationRandomJob: minimum duration must be less than maximum duration\")\n\tErrEventListenerFuncNil          = errors.New(\"gocron: eventListenerFunc must not be nil\")\n\tErrJobNotFound                   = errors.New(\"gocron: job not found\")\n\tErrJobRunNowFailed               = errors.New(\"gocron: Job: RunNow: scheduler unreachable\")\n\tErrMonthlyJobDays                = errors.New(\"gocron: MonthlyJob: daysOfTheMonth must be between 31 and -31 inclusive, and not 0\")\n\tErrMonthlyJobAtTimeNil           = errors.New(\"gocron: MonthlyJob: atTime within atTimes must not be nil\")\n\tErrMonthlyJobAtTimesNil          = errors.New(\"gocron: MonthlyJob: atTimes must not be nil\")\n\tErrMonthlyJobDaysNil             = errors.New(\"gocron: MonthlyJob: daysOfTheMonth must not be nil\")\n\tErrMonthlyJobHours               = errors.New(\"gocron: MonthlyJob: atTimes hours must be between 0 and 23 inclusive\")\n\tErrMonthlyJobZeroInterval        = errors.New(\"gocron: MonthlyJob: interval must be greater than 0\")\n\tErrMonthlyJobMinutesSeconds      = errors.New(\"gocron: MonthlyJob: atTimes minutes and seconds must be between 0 and 59 inclusive\")\n\tErrNewJobTaskNil                 = errors.New(\"gocron: NewJob: Task must not be nil\")\n\tErrNewJobTaskNotFunc             = errors.New(\"gocron: NewJob: Task.Function must be of kind reflect.Func\")\n\tErrNewJobWrongNumberOfParameters = errors.New(\"gocron: NewJob: Number of provided parameters does not match expected\")\n\tErrNewJobWrongTypeOfParameters   = errors.New(\"gocron: NewJob: Type of provided parameters does not match expected\")\n\tErrOneTimeJobStartDateTimePast   = errors.New(\"gocron: OneTimeJob: start must not be in the past\")\n\tErrStopExecutorTimedOut          = errors.New(\"gocron: timed out waiting for executor to stop\")\n\tErrStopJobsTimedOut              = errors.New(\"gocron: timed out waiting for jobs to finish\")\n\tErrStopSchedulerTimedOut         = errors.New(\"gocron: timed out waiting for scheduler to stop\")\n\tErrWeeklyJobAtTimeNil            = errors.New(\"gocron: WeeklyJob: atTime within atTimes must not be nil\")\n\tErrWeeklyJobAtTimesNil           = errors.New(\"gocron: WeeklyJob: atTimes must not be nil\")\n\tErrWeeklyJobDaysOfTheWeekNil     = errors.New(\"gocron: WeeklyJob: daysOfTheWeek must not be nil\")\n\tErrWeeklyJobHours                = errors.New(\"gocron: WeeklyJob: atTimes hours must be between 0 and 23 inclusive\")\n\tErrWeeklyJobZeroInterval         = errors.New(\"gocron: WeeklyJob: interval must be greater than 0\")\n\tErrWeeklyJobMinutesSeconds       = errors.New(\"gocron: WeeklyJob: atTimes minutes and seconds must be between 0 and 59 inclusive\")\n\tErrPanicRecovered                = errors.New(\"gocron: panic recovered\")\n\tErrWithClockNil                  = errors.New(\"gocron: WithClock: clock must not be nil\")\n\tErrWithContextNil                = errors.New(\"gocron: WithContext: context must not be nil\")\n\tErrWithDistributedElectorNil     = errors.New(\"gocron: WithDistributedElector: elector must not be nil\")\n\tErrWithDistributedLockerNil      = errors.New(\"gocron: WithDistributedLocker: locker must not be nil\")\n\tErrWithDistributedJobLockerNil   = errors.New(\"gocron: WithDistributedJobLocker: locker must not be nil\")\n\tErrWithIdentifierNil             = errors.New(\"gocron: WithIdentifier: identifier must not be nil\")\n\tErrSchedulerMonitorNil           = errors.New(\"gocron: WithSchedulerMonitor: monitor must not be nil\")\n\tErrWithLimitConcurrentJobsZero   = errors.New(\"gocron: WithLimitConcurrentJobs: limit must be greater than 0\")\n\tErrWithLocationNil               = errors.New(\"gocron: WithLocation: location must not be nil\")\n\tErrWithLoggerNil                 = errors.New(\"gocron: WithLogger: logger must not be nil\")\n\tErrWithMonitorNil                = errors.New(\"gocron: WithMonitor: monitor must not be nil\")\n\tErrWithNameEmpty                 = errors.New(\"gocron: WithName: name must not be empty\")\n\tErrWithStartDateTimePast         = errors.New(\"gocron: WithStartDateTime: start must not be in the past\")\n\tErrWithStartDateTimePastZero     = errors.New(\"gocron: WithStartDateTime: start must not be zero\")\n\tErrWithStopDateTimePast          = errors.New(\"gocron: WithStopDateTime: end must not be in the past\")\n\tErrStartTimeLaterThanEndTime     = errors.New(\"gocron: WithStartDateTime: start must not be later than end\")\n\tErrStopTimeEarlierThanStartTime  = errors.New(\"gocron: WithStopDateTime: end must not be earlier than start\")\n\tErrWithStopTimeoutZeroOrNegative = errors.New(\"gocron: WithStopTimeout: timeout must be greater than 0\")\n\tErrWithSchedulerMonitorNil       = errors.New(\"gocron: WithSchedulerMonitor: scheduler monitor cannot be nil\")\n\tErrWithLimitedRunsZero           = errors.New(\"gocron: WithLimitedRuns: limit must be greater than 0\")\n)\n\n// internal errors\nvar (\n\terrAtTimeNil    = errors.New(\"errAtTimeNil\")\n\terrAtTimesNil   = errors.New(\"errAtTimesNil\")\n\terrAtTimeHours  = errors.New(\"errAtTimeHours\")\n\terrAtTimeMinSec = errors.New(\"errAtTimeMinSec\")\n)\n"
  },
  {
    "path": "example_test.go",
    "content": "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/google/uuid\"\n\t\"github.com/jonboulle/clockwork\"\n)\n\nfunc ExampleAfterJobRuns() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\t_, _ = s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t\tgocron.WithEventListeners(\n\t\t\tgocron.AfterJobRuns(\n\t\t\t\tfunc(jobID uuid.UUID, jobName string) {\n\t\t\t\t\t// do something after the job completes\n\t\t\t\t},\n\t\t\t),\n\t\t),\n\t)\n}\n\nfunc ExampleAfterJobRunsWithError() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\t_, _ = s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t\tgocron.WithEventListeners(\n\t\t\tgocron.AfterJobRunsWithError(\n\t\t\t\tfunc(jobID uuid.UUID, jobName string, err error) {\n\t\t\t\t\t// do something when the job returns an error\n\t\t\t\t},\n\t\t\t),\n\t\t),\n\t)\n}\n\nvar _ gocron.Locker = new(errorLocker)\n\ntype errorLocker struct{}\n\nfunc (e errorLocker) Lock(_ context.Context, _ string) (gocron.Lock, error) {\n\treturn nil, errors.New(\"locked\")\n}\n\nfunc ExampleAfterLockError() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\t_, _ = s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t\tgocron.WithDistributedJobLocker(&errorLocker{}),\n\t\tgocron.WithEventListeners(\n\t\t\tgocron.AfterLockError(\n\t\t\t\tfunc(jobID uuid.UUID, jobName string, err error) {\n\t\t\t\t\t// do something immediately before the job is run\n\t\t\t\t},\n\t\t\t),\n\t\t),\n\t)\n}\n\nfunc ExampleBeforeJobRuns() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\t_, _ = s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t\tgocron.WithEventListeners(\n\t\t\tgocron.BeforeJobRuns(\n\t\t\t\tfunc(jobID uuid.UUID, jobName string) {\n\t\t\t\t\t// do something immediately before the job is run\n\t\t\t\t},\n\t\t\t),\n\t\t),\n\t)\n}\n\nfunc ExampleBeforeJobRunsSkipIfBeforeFuncErrors() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\t_, _ = s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {\n\t\t\t\tfmt.Println(\"Will never run, because before job func errors\")\n\t\t\t},\n\t\t),\n\t\tgocron.WithEventListeners(\n\t\t\tgocron.BeforeJobRunsSkipIfBeforeFuncErrors(\n\t\t\t\tfunc(jobID uuid.UUID, jobName string) error {\n\t\t\t\t\treturn errors.New(\"error\")\n\t\t\t\t},\n\t\t\t),\n\t\t),\n\t)\n}\n\nfunc ExampleCronJob() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\t_, _ = s.NewJob(\n\t\tgocron.CronJob(\n\t\t\t// standard cron tab parsing\n\t\t\t\"1 * * * *\",\n\t\t\tfalse,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t)\n\t_, _ = s.NewJob(\n\t\tgocron.CronJob(\n\t\t\t// optionally include seconds as the first field\n\t\t\t\"* 1 * * * *\",\n\t\t\ttrue,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t)\n}\n\nfunc ExampleDailyJob() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\t_, _ = s.NewJob(\n\t\tgocron.DailyJob(\n\t\t\t1,\n\t\t\tgocron.NewAtTimes(\n\t\t\t\tgocron.NewAtTime(10, 30, 0),\n\t\t\t\tgocron.NewAtTime(14, 0, 0),\n\t\t\t),\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc(a, b string) {},\n\t\t\t\"a\",\n\t\t\t\"b\",\n\t\t),\n\t)\n}\n\nfunc ExampleDurationJob() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\t_, _ = s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second*5,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t)\n}\n\nfunc ExampleDurationRandomJob() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\t_, _ = s.NewJob(\n\t\tgocron.DurationRandomJob(\n\t\t\ttime.Second,\n\t\t\t5*time.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t)\n}\n\nfunc ExampleJob_id() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\tj, _ := s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t)\n\n\tfmt.Println(j.ID())\n}\n\nfunc ExampleJob_lastRun() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\tj, _ := s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t)\n\n\tfmt.Println(j.LastRun())\n}\n\nfunc ExampleJob_name() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\tj, _ := s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t\tgocron.WithName(\"foobar\"),\n\t)\n\n\tfmt.Println(j.Name())\n\t// Output:\n\t// foobar\n}\n\nfunc ExampleJob_nextRun() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\tj, _ := s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t)\n\n\t// NextRun is only available after the scheduler has been started.\n\ts.Start()\n\n\tnextRun, _ := j.NextRun()\n\tfmt.Println(nextRun)\n}\n\nfunc ExampleJob_nextRuns() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\tj, _ := s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t)\n\n\t// NextRuns is only available after the scheduler has been started.\n\ts.Start()\n\n\tnextRuns, _ := j.NextRuns(5)\n\tfmt.Println(nextRuns)\n}\n\nfunc ExampleJob_runNow() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\tj, _ := s.NewJob(\n\t\tgocron.MonthlyJob(\n\t\t\t1,\n\t\t\tgocron.NewDaysOfTheMonth(3, -5, -1),\n\t\t\tgocron.NewAtTimes(\n\t\t\t\tgocron.NewAtTime(10, 30, 0),\n\t\t\t\tgocron.NewAtTime(11, 15, 0),\n\t\t\t),\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t)\n\ts.Start()\n\t// Runs the job one time now, without impacting the schedule\n\t_ = j.RunNow()\n}\n\nfunc ExampleJob_tags() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\tj, _ := s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t\tgocron.WithTags(\"foo\", \"bar\"),\n\t)\n\n\tfmt.Println(j.Tags())\n\t// Output:\n\t// [foo bar]\n}\n\nfunc ExampleMonthlyJob() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\t_, _ = s.NewJob(\n\t\tgocron.MonthlyJob(\n\t\t\t1,\n\t\t\tgocron.NewDaysOfTheMonth(3, -5, -1),\n\t\t\tgocron.NewAtTimes(\n\t\t\t\tgocron.NewAtTime(10, 30, 0),\n\t\t\t\tgocron.NewAtTime(11, 15, 0),\n\t\t\t),\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t)\n}\n\nfunc ExampleNewDefaultCron() {\n\tc := gocron.NewDefaultCron(true)\n\terr := c.IsValid(\"* * * * * *\", time.Local, time.Now())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc ExampleNewScheduler() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\tfmt.Println(s.Jobs())\n}\n\nfunc ExampleNewTask() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\t_, _ = s.NewJob(\n\t\tgocron.DurationJob(time.Second),\n\t\tgocron.NewTask(\n\t\t\tfunc(ctx context.Context) {\n\t\t\t\t// gocron will pass in a context (either the default Job context, or one\n\t\t\t\t// provided via WithContext) to the job and will cancel the context on shutdown.\n\t\t\t\t// This allows you to listen for and handle cancellation within your job.\n\t\t\t},\n\t\t),\n\t)\n}\n\nfunc ExampleOneTimeJob() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\t// run a job once, immediately\n\t_, _ = s.NewJob(\n\t\tgocron.OneTimeJob(\n\t\t\tgocron.OneTimeJobStartImmediately(),\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t)\n\t// run a job once in 10 seconds\n\t_, _ = s.NewJob(\n\t\tgocron.OneTimeJob(\n\t\t\tgocron.OneTimeJobStartDateTime(time.Now().Add(10*time.Second)),\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t)\n\t// run job twice - once in 10 seconds and once in 55 minutes\n\tn := time.Now()\n\t_, _ = s.NewJob(\n\t\tgocron.OneTimeJob(\n\t\t\tgocron.OneTimeJobStartDateTimes(\n\t\t\t\tn.Add(10*time.Second),\n\t\t\t\tn.Add(55*time.Minute),\n\t\t\t),\n\t\t),\n\t\tgocron.NewTask(func() {}),\n\t)\n\n\ts.Start()\n}\n\nfunc ExampleScheduler_jobs() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\t_, _ = s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\t10*time.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t)\n\tfmt.Println(len(s.Jobs()))\n\t// Output:\n\t// 1\n}\n\nfunc ExampleScheduler_newJob() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\tj, err := s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\t10*time.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Println(j.ID())\n}\n\nfunc ExampleScheduler_removeByTags() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\t_, _ = s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t\tgocron.WithTags(\"tag1\"),\n\t)\n\t_, _ = s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t\tgocron.WithTags(\"tag2\"),\n\t)\n\tfmt.Println(len(s.Jobs()))\n\n\ts.RemoveByTags(\"tag1\", \"tag2\")\n\n\tfmt.Println(len(s.Jobs()))\n\t// Output:\n\t// 2\n\t// 0\n}\n\nfunc ExampleScheduler_removeJob() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\tj, _ := s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t)\n\n\tfmt.Println(len(s.Jobs()))\n\n\t_ = s.RemoveJob(j.ID())\n\n\tfmt.Println(len(s.Jobs()))\n\t// Output:\n\t// 1\n\t// 0\n}\n\nfunc ExampleScheduler_shutdown() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n}\n\nfunc ExampleScheduler_start() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\t_, _ = s.NewJob(\n\t\tgocron.CronJob(\n\t\t\t\"* * * * *\",\n\t\t\tfalse,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t)\n\n\ts.Start()\n}\n\nfunc ExampleScheduler_stopJobs() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\t_, _ = s.NewJob(\n\t\tgocron.CronJob(\n\t\t\t\"* * * * *\",\n\t\t\tfalse,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t)\n\n\ts.Start()\n\n\t_ = s.StopJobs()\n}\n\nfunc ExampleScheduler_update() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\tj, _ := s.NewJob(\n\t\tgocron.CronJob(\n\t\t\t\"* * * * *\",\n\t\t\tfalse,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t)\n\n\ts.Start()\n\n\t// after some time, need to change the job\n\n\tj, _ = s.Update(\n\t\tj.ID(),\n\t\tgocron.DurationJob(\n\t\t\t5*time.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t)\n}\n\nfunc ExampleWeeklyJob() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\t_, _ = s.NewJob(\n\t\tgocron.WeeklyJob(\n\t\t\t2,\n\t\t\tgocron.NewWeekdays(time.Tuesday, time.Wednesday, time.Saturday),\n\t\t\tgocron.NewAtTimes(\n\t\t\t\tgocron.NewAtTime(1, 30, 0),\n\t\t\t\tgocron.NewAtTime(12, 0, 30),\n\t\t\t),\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t)\n}\n\nfunc ExampleWithClock() {\n\tfakeClock := clockwork.NewFakeClock()\n\ts, _ := gocron.NewScheduler(\n\t\tgocron.WithClock(fakeClock),\n\t)\n\tvar wg sync.WaitGroup\n\twg.Add(1)\n\t_, _ = s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second*5,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc(one string, two int) {\n\t\t\t\tfmt.Printf(\"%s, %d\\n\", one, two)\n\t\t\t\twg.Done()\n\t\t\t},\n\t\t\t\"one\", 2,\n\t\t),\n\t)\n\ts.Start()\n\t_ = fakeClock.BlockUntilContext(context.Background(), 1)\n\tfakeClock.Advance(time.Second * 5)\n\twg.Wait()\n\t_ = s.StopJobs()\n\t// Output:\n\t// one, 2\n}\n\nfunc ExampleWithContext() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\t_, _ = s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc(ctx context.Context) {\n\t\t\t\t// gocron will pass in the context provided via WithContext\n\t\t\t\t// to the job and will cancel the context on shutdown.\n\t\t\t\t// This allows you to listen for and handle cancellation within your job.\n\t\t\t},\n\t\t),\n\t\tgocron.WithContext(ctx),\n\t)\n}\n\nvar _ gocron.Cron = (*customCron)(nil)\n\ntype customCron struct{}\n\nfunc (c customCron) IsValid(crontab string, location *time.Location, now time.Time) error {\n\treturn nil\n}\n\nfunc (c customCron) Next(lastRun time.Time) time.Time {\n\treturn time.Now().Add(time.Second)\n}\n\nfunc ExampleWithCronImplementation() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\t_, _ = s.NewJob(\n\t\tgocron.CronJob(\n\t\t\t\"* * * * *\",\n\t\t\tfalse,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t\tgocron.WithCronImplementation(\n\t\t\t&customCron{},\n\t\t),\n\t)\n}\n\nfunc ExampleWithDisabledDistributedJobLocker() {\n\t// var _ gocron.Locker = (*myLocker)(nil)\n\t//\n\t// type myLocker struct{}\n\t//\n\t// func (m myLocker) Lock(ctx context.Context, key string) (Lock, error) {\n\t//     return &testLock{}, nil\n\t// }\n\t//\n\t// var _ gocron.Lock = (*testLock)(nil)\n\t//\n\t// type testLock struct{}\n\t//\n\t// func (t testLock) Unlock(_ context.Context) error {\n\t//     return nil\n\t// }\n\n\tlocker := &myLocker{}\n\n\ts, _ := gocron.NewScheduler(\n\t\tgocron.WithDistributedLocker(locker),\n\t)\n\n\t_, _ = s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t\tgocron.WithDisabledDistributedJobLocker(true),\n\t)\n}\n\nvar _ gocron.Elector = (*myElector)(nil)\n\ntype myElector struct{}\n\nfunc (m myElector) IsLeader(_ context.Context) error {\n\treturn nil\n}\n\nfunc ExampleWithDistributedElector() {\n\t// var _ gocron.Elector = (*myElector)(nil)\n\t//\n\t// type myElector struct{}\n\t//\n\t// func (m myElector) IsLeader(_ context.Context) error {\n\t//     return nil\n\t// }\n\t//\n\telector := &myElector{}\n\n\t_, _ = gocron.NewScheduler(\n\t\tgocron.WithDistributedElector(elector),\n\t)\n}\n\nvar _ gocron.Locker = (*myLocker)(nil)\n\ntype myLocker struct{}\n\nfunc (m myLocker) Lock(ctx context.Context, key string) (gocron.Lock, error) {\n\treturn &testLock{}, nil\n}\n\nvar _ gocron.Lock = (*testLock)(nil)\n\ntype testLock struct{}\n\nfunc (t testLock) Unlock(_ context.Context) error {\n\treturn nil\n}\n\nfunc ExampleWithDistributedLocker() {\n\t// var _ gocron.Locker = (*myLocker)(nil)\n\t//\n\t// type myLocker struct{}\n\t//\n\t// func (m myLocker) Lock(ctx context.Context, key string) (Lock, error) {\n\t//     return &testLock{}, nil\n\t// }\n\t//\n\t// var _ gocron.Lock = (*testLock)(nil)\n\t//\n\t// type testLock struct{}\n\t//\n\t// func (t testLock) Unlock(_ context.Context) error {\n\t//     return nil\n\t// }\n\n\tlocker := &myLocker{}\n\n\t_, _ = gocron.NewScheduler(\n\t\tgocron.WithDistributedLocker(locker),\n\t)\n}\n\nfunc ExampleWithEventListeners() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\t_, _ = s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {},\n\t\t),\n\t\tgocron.WithEventListeners(\n\t\t\tgocron.AfterJobRuns(\n\t\t\t\tfunc(jobID uuid.UUID, jobName string) {\n\t\t\t\t\t// do something after the job completes\n\t\t\t\t},\n\t\t\t),\n\t\t\tgocron.AfterJobRunsWithError(\n\t\t\t\tfunc(jobID uuid.UUID, jobName string, err error) {\n\t\t\t\t\t// do something when the job returns an error\n\t\t\t\t},\n\t\t\t),\n\t\t\tgocron.BeforeJobRuns(\n\t\t\t\tfunc(jobID uuid.UUID, jobName string) {\n\t\t\t\t\t// do something immediately before the job is run\n\t\t\t\t},\n\t\t\t),\n\t\t),\n\t)\n}\n\nfunc ExampleWithGlobalJobOptions() {\n\ts, _ := gocron.NewScheduler(\n\t\tgocron.WithGlobalJobOptions(\n\t\t\tgocron.WithTags(\"tag1\", \"tag2\", \"tag3\"),\n\t\t),\n\t)\n\n\tj, _ := s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc(one string, two int) {\n\t\t\t\tfmt.Printf(\"%s, %d\", one, two)\n\t\t\t},\n\t\t\t\"one\", 2,\n\t\t),\n\t)\n\t// The job will have the globally applied tags\n\tfmt.Println(j.Tags())\n\n\ts2, _ := gocron.NewScheduler(\n\t\tgocron.WithGlobalJobOptions(\n\t\t\tgocron.WithTags(\"tag1\", \"tag2\", \"tag3\"),\n\t\t),\n\t)\n\tj2, _ := s2.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc(one string, two int) {\n\t\t\t\tfmt.Printf(\"%s, %d\", one, two)\n\t\t\t},\n\t\t\t\"one\", 2,\n\t\t),\n\t\tgocron.WithTags(\"tag4\", \"tag5\", \"tag6\"),\n\t)\n\t// The job will have the tags set specifically on the job\n\t// overriding those set globally by the scheduler\n\tfmt.Println(j2.Tags())\n\t// Output:\n\t// [tag1 tag2 tag3]\n\t// [tag4 tag5 tag6]\n}\n\nfunc ExampleWithIdentifier() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\tj, _ := s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc(one string, two int) {\n\t\t\t\tfmt.Printf(\"%s, %d\", one, two)\n\t\t\t},\n\t\t\t\"one\", 2,\n\t\t),\n\t\tgocron.WithIdentifier(uuid.MustParse(\"87b95dfc-3e71-11ef-9454-0242ac120002\")),\n\t)\n\tfmt.Println(j.ID())\n\t// Output:\n\t// 87b95dfc-3e71-11ef-9454-0242ac120002\n}\n\nfunc ExampleWithIntervalFromCompletion() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\t_, _ = s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\t5*time.Minute,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {\n\t\t\t\ttime.Sleep(30 * time.Second)\n\t\t\t},\n\t\t),\n\t\tgocron.WithIntervalFromCompletion(),\n\t)\n\n\t// Without WithIntervalFromCompletion (default behavior):\n\t// If the job starts at 00:00 and completes at 00:00:30,\n\t// the next job starts at 00:05:00 (only 4m30s rest).\n\n\t// With WithIntervalFromCompletion:\n\t// If the job starts at 00:00 and completes at 00:00:30,\n\t// the next job starts at 00:05:30 (full 5m rest).\n}\n\nfunc ExampleWithLimitConcurrentJobs() {\n\t_, _ = gocron.NewScheduler(\n\t\tgocron.WithLimitConcurrentJobs(\n\t\t\t1,\n\t\t\tgocron.LimitModeReschedule,\n\t\t),\n\t)\n}\n\nfunc ExampleWithLimitedRuns() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\t_, _ = s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Millisecond,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc(one string, two int) {\n\t\t\t\tfmt.Printf(\"%s, %d\\n\", one, two)\n\t\t\t},\n\t\t\t\"one\", 2,\n\t\t),\n\t\tgocron.WithLimitedRuns(1),\n\t)\n\ts.Start()\n\n\ttime.Sleep(100 * time.Millisecond)\n\t_ = s.StopJobs()\n\tfmt.Printf(\"no jobs in scheduler: %v\\n\", s.Jobs())\n\t// Output:\n\t// one, 2\n\t// no jobs in scheduler: []\n}\n\nfunc ExampleWithLocation() {\n\tlocation, _ := time.LoadLocation(\"Asia/Kolkata\")\n\n\t_, _ = gocron.NewScheduler(\n\t\tgocron.WithLocation(location),\n\t)\n}\n\nfunc ExampleWithLogger() {\n\t_, _ = gocron.NewScheduler(\n\t\tgocron.WithLogger(\n\t\t\tgocron.NewLogger(gocron.LogLevelDebug),\n\t\t),\n\t)\n}\n\nfunc ExampleWithMonitor() {\n\t//type exampleMonitor struct {\n\t//\tmu      sync.Mutex\n\t//\tcounter map[string]int\n\t//\ttime    map[string][]time.Duration\n\t//}\n\t//\n\t//func newExampleMonitor() *exampleMonitor {\n\t//\treturn &exampleMonitor{\n\t//\tcounter: make(map[string]int),\n\t//\ttime:    make(map[string][]time.Duration),\n\t//}\n\t//}\n\t//\n\t//func (t *exampleMonitor) IncrementJob(_ uuid.UUID, name string, _ []string, _ JobStatus) {\n\t//\tt.mu.Lock()\n\t//\tdefer t.mu.Unlock()\n\t//\t_, ok := t.counter[name]\n\t//\tif !ok {\n\t//\t\tt.counter[name] = 0\n\t//\t}\n\t//\tt.counter[name]++\n\t//}\n\t//\n\t//func (t *exampleMonitor) RecordJobTiming(startTime, endTime time.Time, _ uuid.UUID, name string, _ []string) {\n\t//\tt.mu.Lock()\n\t//\tdefer t.mu.Unlock()\n\t//\t_, ok := t.time[name]\n\t//\tif !ok {\n\t//\t\tt.time[name] = make([]time.Duration, 0)\n\t//\t}\n\t//\tt.time[name] = append(t.time[name], endTime.Sub(startTime))\n\t//}\n\t//\n\t//monitor := newExampleMonitor()\n\t//s, _ := NewScheduler(\n\t//\tWithMonitor(monitor),\n\t//)\n\t//name := \"example\"\n\t//_, _ = s.NewJob(\n\t//\tDurationJob(\n\t//\t\ttime.Second,\n\t//\t),\n\t//\tNewTask(\n\t//\t\tfunc() {\n\t//\t\t\ttime.Sleep(1 * time.Second)\n\t//\t\t},\n\t//\t),\n\t//\tWithName(name),\n\t//\tWithStartAt(\n\t//\t\tWithStartImmediately(),\n\t//\t),\n\t//)\n\t//s.Start()\n\t//time.Sleep(5 * time.Second)\n\t//_ = s.Shutdown()\n\t//\n\t//fmt.Printf(\"Job %q total execute count: %d\\n\", name, monitor.counter[name])\n\t//for i, val := range monitor.time[name] {\n\t//\tfmt.Printf(\"Job %q execute #%d elapsed %.4f seconds\\n\", name, i+1, val.Seconds())\n\t//}\n}\n\nfunc ExampleWithName() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\tj, _ := s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc(one string, two int) {\n\t\t\t\tfmt.Printf(\"%s, %d\", one, two)\n\t\t\t},\n\t\t\t\"one\", 2,\n\t\t),\n\t\tgocron.WithName(\"job 1\"),\n\t)\n\tfmt.Println(j.Name())\n\t// Output:\n\t// job 1\n}\n\nfunc ExampleWithSingletonMode() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\t_, _ = s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc() {\n\t\t\t\t// this job will skip half it's executions\n\t\t\t\t// and effectively run every 2 seconds\n\t\t\t\ttime.Sleep(1500 * time.Second)\n\t\t\t},\n\t\t),\n\t\tgocron.WithSingletonMode(gocron.LimitModeReschedule),\n\t)\n}\n\nfunc ExampleWithStartAt() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\tstart := time.Date(9999, 9, 9, 9, 9, 9, 9, time.UTC)\n\n\tj, _ := s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc(one string, two int) {\n\t\t\t\tfmt.Printf(\"%s, %d\", one, two)\n\t\t\t},\n\t\t\t\"one\", 2,\n\t\t),\n\t\tgocron.WithStartAt(\n\t\t\tgocron.WithStartDateTime(start),\n\t\t),\n\t)\n\ts.Start()\n\n\tnext, _ := j.NextRun()\n\tfmt.Println(next)\n\n\t_ = s.StopJobs()\n\t// Output:\n\t// 9999-09-09 09:09:09.000000009 +0000 UTC\n}\n\nfunc ExampleWithStartDateTime() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\tstart := time.Date(9999, 9, 9, 9, 9, 9, 9, time.UTC)\n\n\tj, _ := s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc(one string, two int) {\n\t\t\t\tfmt.Printf(\"%s, %d\", one, two)\n\t\t\t},\n\t\t\t\"one\", 2,\n\t\t),\n\t\tgocron.WithStartAt(\n\t\t\tgocron.WithStartDateTime(start),\n\t\t),\n\t)\n\ts.Start()\n\n\tnext, _ := j.NextRun()\n\tfmt.Println(next)\n\n\t_ = s.StopJobs()\n\t// Output:\n\t// 9999-09-09 09:09:09.000000009 +0000 UTC\n}\n\nfunc ExampleWithStartDateTimePast() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\tstart := time.Now().Add(-time.Minute)\n\n\tj, _ := s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc(one string, two int) {\n\t\t\t\tfmt.Printf(\"%s, %d\", one, two)\n\t\t\t},\n\t\t\t\"one\", 2,\n\t\t),\n\t\tgocron.WithStartAt(\n\t\t\tgocron.WithStartDateTimePast(start),\n\t\t),\n\t)\n\ts.Start()\n\n\ttime.Sleep(100 * time.Millisecond)\n\n\t_, _ = j.NextRun()\n\n\t_ = s.StopJobs()\n}\n\nfunc ExampleWithStartImmediately() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\tj, _ := s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc(one string, two int) {\n\t\t\t\tfmt.Printf(\"%s, %d\", one, two)\n\t\t\t},\n\t\t\t\"one\", 2,\n\t\t),\n\t\tgocron.WithStartAt(\n\t\t\tgocron.WithStartImmediately(),\n\t\t),\n\t)\n\ts.Start()\n\n\t_, _ = j.NextRun()\n\n\t_ = s.StopJobs()\n}\n\nfunc ExampleWithStopTimeout() {\n\t_, _ = gocron.NewScheduler(\n\t\tgocron.WithStopTimeout(time.Second * 5),\n\t)\n}\n\nfunc ExampleWithTags() {\n\ts, _ := gocron.NewScheduler()\n\tdefer func() { _ = s.Shutdown() }()\n\n\tj, _ := s.NewJob(\n\t\tgocron.DurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tgocron.NewTask(\n\t\t\tfunc(one string, two int) {\n\t\t\t\tfmt.Printf(\"%s, %d\", one, two)\n\t\t\t},\n\t\t\t\"one\", 2,\n\t\t),\n\t\tgocron.WithTags(\"tag1\", \"tag2\", \"tag3\"),\n\t)\n\tfmt.Println(j.Tags())\n\t// Output:\n\t// [tag1 tag2 tag3]\n}\n"
  },
  {
    "path": "examples/elector/main.go",
    "content": "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 = (*myElector)(nil)\n\ntype myElector struct {\n\tnum    int\n\tleader bool\n}\n\nfunc (m myElector) IsLeader(_ context.Context) error {\n\tif m.leader {\n\t\tlog.Printf(\"node %d is leader\", m.num)\n\t\treturn nil\n\t}\n\tlog.Printf(\"node %d is not leader\", m.num)\n\treturn errors.New(\"not leader\")\n}\n\nfunc main() {\n\tlog.SetFlags(log.LstdFlags | log.Lmicroseconds)\n\n\tfor i := 0; i < 3; i++ {\n\t\tgo func(i int) {\n\t\t\telector := &myElector{\n\t\t\t\tnum: i,\n\t\t\t}\n\t\t\tif i == 0 {\n\t\t\t\telector.leader = true\n\t\t\t}\n\n\t\t\tscheduler, err := gocron.NewScheduler(\n\t\t\t\tgocron.WithDistributedElector(elector),\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\tlog.Println(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t_, err = scheduler.NewJob(\n\t\t\t\tgocron.DurationJob(time.Second),\n\t\t\t\tgocron.NewTask(func() {\n\t\t\t\t\tlog.Println(\"run job\")\n\t\t\t\t}),\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\tlog.Println(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tscheduler.Start()\n\n\t\t\tif i == 0 {\n\t\t\t\ttime.Sleep(5 * time.Second)\n\t\t\t\telector.leader = false\n\t\t\t}\n\t\t\tif i == 1 {\n\t\t\t\ttime.Sleep(5 * time.Second)\n\t\t\t\telector.leader = true\n\t\t\t}\n\t\t}(i)\n\t}\n\n\tselect {} // wait forever\n}\n"
  },
  {
    "path": "executor.go",
    "content": "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/google/uuid\"\n)\n\ntype executor struct {\n\t// context used for shutting down\n\tctx context.Context\n\t// cancel used by the executor to signal a stop of it's functions\n\tcancel context.CancelFunc\n\t// clock used for regular time or mocking time\n\tclock clockwork.Clock\n\t// the executor's logger\n\tlogger Logger\n\n\t// receives jobs scheduled to execute\n\tjobsIn chan jobIn\n\t// sends out jobs for rescheduling\n\tjobsOutForRescheduling chan uuid.UUID\n\t// sends out jobs once completed\n\tjobsOutCompleted chan uuid.UUID\n\t// used to request jobs from the scheduler\n\tjobOutRequest chan *jobOutRequest\n\n\t// sends out job needs to update the next runs\n\tjobUpdateNextRuns chan uuid.UUID\n\n\t// used by the executor to receive a stop signal from the scheduler\n\tstopCh chan struct{}\n\t// ensure that stop runs before the next call to start and only runs once\n\tstopOnce *sync.Once\n\t// the timeout value when stopping\n\tstopTimeout time.Duration\n\t// used to signal that the executor has completed shutdown\n\tdone chan error\n\n\t// runners for any singleton type jobs\n\t// map[uuid.UUID]singletonRunner\n\tsingletonRunners *sync.Map\n\t// config for limit mode\n\tlimitMode *limitModeConfig\n\t// the elector when running distributed instances\n\telector Elector\n\t// the locker when running distributed instances\n\tlocker Locker\n\t// monitor for reporting metrics\n\tmonitor Monitor\n\t// monitorStatus for reporting metrics\n\tmonitorStatus MonitorStatus\n\t// reference to parent scheduler for lifecycle notifications\n\tscheduler *scheduler\n}\n\ntype jobIn struct {\n\tid            uuid.UUID\n\tshouldSendOut bool\n}\n\ntype singletonRunner struct {\n\tin                chan jobIn\n\trescheduleLimiter chan struct{}\n}\n\ntype limitModeConfig struct {\n\tstarted           bool\n\tmode              LimitMode\n\tlimit             uint\n\trescheduleLimiter chan struct{}\n\tin                chan jobIn\n\t// singletonJobs is used to track singleton jobs that are running\n\t// in the limit mode runner. This is used to prevent the same job\n\t// from running multiple times across limit mode runners when both\n\t// a limit mode and singleton mode are enabled.\n\tsingletonJobs   map[uuid.UUID]struct{}\n\tsingletonJobsMu sync.Mutex\n}\n\nfunc (e *executor) start() {\n\te.logger.Debug(\"gocron: executor started\")\n\n\t// creating the executor's context here as the executor\n\t// is the only goroutine that should access this context\n\t// any other uses within the executor should create a context\n\t// using the executor context as parent.\n\te.ctx, e.cancel = context.WithCancel(context.Background())\n\te.stopOnce = &sync.Once{}\n\n\t// the standardJobsWg tracks\n\tstandardJobsWg := &waitGroupWithMutex{}\n\n\tsingletonJobsWg := &waitGroupWithMutex{}\n\n\tlimitModeJobsWg := &waitGroupWithMutex{}\n\n\t// create a fresh map for tracking singleton runners\n\te.singletonRunners = &sync.Map{}\n\n\t// start the for leap that is the executor\n\t// selecting on channels for work to do\n\tfor {\n\t\tselect {\n\t\t// job ids in are sent from 1 of 2 places:\n\t\t// 1. the scheduler sends directly when jobs\n\t\t//    are run immediately.\n\t\t// 2. sent from time.AfterFuncs in which job schedules\n\t\t// \t  are spun up by the scheduler\n\t\tcase jIn := <-e.jobsIn:\n\t\t\tselect {\n\t\t\tcase <-e.stopCh:\n\t\t\t\te.stop(standardJobsWg, singletonJobsWg, limitModeJobsWg)\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t}\n\t\t\t// this context is used to handle cancellation of the executor\n\t\t\t// on requests for a job to the scheduler via requestJobCtx\n\t\t\tctx, cancel := context.WithCancel(e.ctx)\n\n\t\t\tif e.limitMode != nil && !e.limitMode.started {\n\t\t\t\t// check if we are already running the limit mode runners\n\t\t\t\t// if not, spin up the required number i.e. limit!\n\t\t\t\te.limitMode.started = true\n\t\t\t\tfor i := e.limitMode.limit; i > 0; i-- {\n\t\t\t\t\tlimitModeJobsWg.Add(1)\n\t\t\t\t\tgo e.limitModeRunner(\"limitMode-\"+strconv.Itoa(int(i)), e.limitMode.in, limitModeJobsWg, e.limitMode.mode, e.limitMode.rescheduleLimiter)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// spin off into a goroutine to unblock the executor and\n\t\t\t// allow for processing for more work\n\t\t\tgo func(executorCtx context.Context) {\n\t\t\t\t// make sure to cancel the above context per the docs\n\t\t\t\t// // Canceling this context releases resources associated with it, so code should\n\t\t\t\t// // call cancel as soon as the operations running in this Context complete.\n\t\t\t\tdefer cancel()\n\n\t\t\t\t// check for limit mode - this spins up a separate runner which handles\n\t\t\t\t// limiting the total number of concurrently running jobs\n\t\t\t\tif e.limitMode != nil {\n\t\t\t\t\tif e.limitMode.mode == LimitModeReschedule {\n\t\t\t\t\t\tselect {\n\t\t\t\t\t\t// rescheduleLimiter is a channel the size of the limit\n\t\t\t\t\t\t// this blocks publishing to the channel and keeps\n\t\t\t\t\t\t// the executor from building up a waiting queue\n\t\t\t\t\t\t// and forces rescheduling\n\t\t\t\t\t\tcase e.limitMode.rescheduleLimiter <- struct{}{}:\n\t\t\t\t\t\t\te.limitMode.in <- jIn\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t// all runners are busy, reschedule the work for later\n\t\t\t\t\t\t\t// which means we just skip it here and do nothing\n\t\t\t\t\t\t\t// TODO when metrics are added, this should increment a rescheduled metric\n\t\t\t\t\t\t\t// Notify concurrency limit reached if monitor is configured\n\t\t\t\t\t\t\tif e.scheduler != nil && e.scheduler.schedulerMonitor != nil {\n\t\t\t\t\t\t\t\tctx2, cancel2 := context.WithCancel(executorCtx)\n\t\t\t\t\t\t\t\tjob := requestJobCtx(ctx2, jIn.id, e.jobOutRequest)\n\t\t\t\t\t\t\t\tcancel2()\n\t\t\t\t\t\t\t\tif job != nil {\n\t\t\t\t\t\t\t\t\te.scheduler.notifyConcurrencyLimitReached(\"limit\", e.scheduler.jobFromInternalJob(*job))\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\te.sendOutForRescheduling(&jIn)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// since we're not using LimitModeReschedule, but instead using LimitModeWait\n\t\t\t\t\t\t// we do want to queue up the work to the limit mode runners and allow them\n\t\t\t\t\t\t// to work through the channel backlog. A hard limit of 1000 is in place\n\t\t\t\t\t\t// at which point this call would block.\n\t\t\t\t\t\t// TODO when metrics are added, this should increment a wait metric\n\t\t\t\t\t\te.sendOutForRescheduling(&jIn)\n\t\t\t\t\t\te.limitMode.in <- jIn\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// no limit mode, so we're either running a regular job or\n\t\t\t\t\t// a job with a singleton mode\n\t\t\t\t\t//\n\t\t\t\t\t// get the job, so we can figure out what kind it is and how\n\t\t\t\t\t// to execute it\n\t\t\t\t\tj := requestJobCtx(ctx, jIn.id, e.jobOutRequest)\n\t\t\t\t\tif j == nil {\n\t\t\t\t\t\t// safety check as it'd be strange bug if this occurred\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif j.singletonMode {\n\t\t\t\t\t\t// for singleton mode, get the existing runner for the job\n\t\t\t\t\t\t// or spin up a new one\n\t\t\t\t\t\trunner := &singletonRunner{}\n\t\t\t\t\t\trunnerSrc, ok := e.singletonRunners.Load(jIn.id)\n\t\t\t\t\t\tif !ok {\n\t\t\t\t\t\t\trunner.in = make(chan jobIn, 1000)\n\t\t\t\t\t\t\tif j.singletonLimitMode == LimitModeReschedule {\n\t\t\t\t\t\t\t\trunner.rescheduleLimiter = make(chan struct{}, 1)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\te.singletonRunners.Store(jIn.id, runner)\n\t\t\t\t\t\t\tsingletonJobsWg.Add(1)\n\t\t\t\t\t\t\tgo e.singletonModeRunner(\"singleton-\"+jIn.id.String(), runner.in, singletonJobsWg, j.singletonLimitMode, runner.rescheduleLimiter)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trunner = runnerSrc.(*singletonRunner)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif j.singletonLimitMode == LimitModeReschedule {\n\t\t\t\t\t\t\t// reschedule mode uses the limiter channel to check\n\t\t\t\t\t\t\t// for a running job and reschedules if the channel is full.\n\t\t\t\t\t\t\tselect {\n\t\t\t\t\t\t\tcase runner.rescheduleLimiter <- struct{}{}:\n\t\t\t\t\t\t\t\trunner.in <- jIn\n\t\t\t\t\t\t\t\t// For intervalFromCompletion, skip rescheduling here - it will happen after job completes\n\t\t\t\t\t\t\t\tif !j.intervalFromCompletion {\n\t\t\t\t\t\t\t\t\te.sendOutForRescheduling(&jIn)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\t// runner is busy, reschedule the work for later\n\t\t\t\t\t\t\t\t// which means we just skip it here and do nothing\n\t\t\t\t\t\t\t\te.incrementJobCounter(*j, SingletonRescheduled)\n\t\t\t\t\t\t\t\te.sendOutForRescheduling(&jIn)\n\t\t\t\t\t\t\t\t// Notify concurrency limit reached if monitor is configured\n\t\t\t\t\t\t\t\tif e.scheduler != nil && e.scheduler.schedulerMonitor != nil {\n\t\t\t\t\t\t\t\t\te.scheduler.notifyConcurrencyLimitReached(\"singleton\", e.scheduler.jobFromInternalJob(*j))\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// wait mode, fill up that queue (buffered channel, so it's ok)\n\t\t\t\t\t\t\trunner.in <- jIn\n\t\t\t\t\t\t\t// For intervalFromCompletion, skip rescheduling here - it will happen after job completes\n\t\t\t\t\t\t\tif !j.intervalFromCompletion {\n\t\t\t\t\t\t\t\te.sendOutForRescheduling(&jIn)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tselect {\n\t\t\t\t\t\tcase <-executorCtx.Done():\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// we've gotten to the basic / standard jobs --\n\t\t\t\t\t\t// the ones without anything special that just want\n\t\t\t\t\t\t// to be run. Add to the WaitGroup so that\n\t\t\t\t\t\t// stopping or shutting down can wait for the jobs to\n\t\t\t\t\t\t// complete.\n\t\t\t\t\t\tstandardJobsWg.Add(1)\n\t\t\t\t\t\tgo func(j internalJob) {\n\t\t\t\t\t\t\te.runJob(j, jIn)\n\t\t\t\t\t\t\tstandardJobsWg.Done()\n\t\t\t\t\t\t}(*j)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}(e.ctx)\n\t\tcase <-e.stopCh:\n\t\t\te.stop(standardJobsWg, singletonJobsWg, limitModeJobsWg)\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (e *executor) sendOutForRescheduling(jIn *jobIn) {\n\tif jIn.shouldSendOut {\n\t\tselect {\n\t\tcase e.jobsOutForRescheduling <- jIn.id:\n\t\tcase <-e.ctx.Done():\n\t\t\treturn\n\t\t}\n\t}\n\t// we need to set this to false now, because to handle\n\t// non-limit jobs, we send out from the e.runJob function\n\t// and in this case we don't want to send out twice.\n\tjIn.shouldSendOut = false\n}\n\nfunc (e *executor) sendOutForNextRunUpdate(jIn *jobIn) {\n\tselect {\n\tcase e.jobUpdateNextRuns <- jIn.id:\n\tcase <-e.ctx.Done():\n\t\treturn\n\t}\n}\n\nfunc (e *executor) limitModeRunner(name string, in chan jobIn, wg *waitGroupWithMutex, limitMode LimitMode, rescheduleLimiter chan struct{}) {\n\te.logger.Debug(\"gocron: limitModeRunner starting\", \"name\", name)\n\tfor {\n\t\tselect {\n\t\tcase jIn := <-in:\n\t\t\tselect {\n\t\t\tcase <-e.ctx.Done():\n\t\t\t\te.logger.Debug(\"gocron: limitModeRunner shutting down\", \"name\", name)\n\t\t\t\twg.Done()\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t}\n\n\t\t\tctx, cancel := context.WithCancel(e.ctx)\n\t\t\tj := requestJobCtx(ctx, jIn.id, e.jobOutRequest)\n\t\t\tcancel()\n\t\t\tif j != nil {\n\t\t\t\tif j.singletonMode {\n\t\t\t\t\te.limitMode.singletonJobsMu.Lock()\n\t\t\t\t\t_, ok := e.limitMode.singletonJobs[jIn.id]\n\t\t\t\t\tif ok {\n\t\t\t\t\t\t// this job is already running, so don't run it\n\t\t\t\t\t\t// but instead reschedule it\n\t\t\t\t\t\te.limitMode.singletonJobsMu.Unlock()\n\t\t\t\t\t\tif jIn.shouldSendOut {\n\t\t\t\t\t\t\tselect {\n\t\t\t\t\t\t\tcase <-e.ctx.Done():\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\tcase <-j.ctx.Done():\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\tcase e.jobsOutForRescheduling <- j.id:\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// remove the limiter block, as this particular job\n\t\t\t\t\t\t// was a singleton already running, and we want to\n\t\t\t\t\t\t// allow another job to be scheduled\n\t\t\t\t\t\tif limitMode == LimitModeReschedule {\n\t\t\t\t\t\t\t<-rescheduleLimiter\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\te.limitMode.singletonJobs[jIn.id] = struct{}{}\n\t\t\t\t\te.limitMode.singletonJobsMu.Unlock()\n\t\t\t\t}\n\t\t\t\te.runJob(*j, jIn)\n\n\t\t\t\tif j.singletonMode {\n\t\t\t\t\te.limitMode.singletonJobsMu.Lock()\n\t\t\t\t\tdelete(e.limitMode.singletonJobs, jIn.id)\n\t\t\t\t\te.limitMode.singletonJobsMu.Unlock()\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// remove the limiter block to allow another job to be scheduled\n\t\t\tif limitMode == LimitModeReschedule {\n\t\t\t\t<-rescheduleLimiter\n\t\t\t}\n\t\tcase <-e.ctx.Done():\n\t\t\te.logger.Debug(\"limitModeRunner shutting down\", \"name\", name)\n\t\t\twg.Done()\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (e *executor) singletonModeRunner(name string, in chan jobIn, wg *waitGroupWithMutex, limitMode LimitMode, rescheduleLimiter chan struct{}) {\n\te.logger.Debug(\"gocron: singletonModeRunner starting\", \"name\", name)\n\tfor {\n\t\tselect {\n\t\tcase jIn := <-in:\n\t\t\tselect {\n\t\t\tcase <-e.ctx.Done():\n\t\t\t\te.logger.Debug(\"gocron: singletonModeRunner shutting down\", \"name\", name)\n\t\t\t\twg.Done()\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t}\n\n\t\t\tctx, cancel := context.WithCancel(e.ctx)\n\t\t\tj := requestJobCtx(ctx, jIn.id, e.jobOutRequest)\n\t\t\tcancel()\n\t\t\tif j != nil {\n\t\t\t\t// need to set shouldSendOut = false here, as there is a duplicative call to sendOutForRescheduling\n\t\t\t\t// inside the runJob function that needs to be skipped. sendOutForRescheduling is previously called\n\t\t\t\t// when the job is sent to the singleton mode runner.\n\t\t\t\t// Exception: for intervalFromCompletion, we want rescheduling to happen AFTER job completion\n\t\t\t\tif !j.intervalFromCompletion {\n\t\t\t\t\tjIn.shouldSendOut = false\n\t\t\t\t}\n\t\t\t\te.runJob(*j, jIn)\n\t\t\t}\n\n\t\t\t// remove the limiter block to allow another job to be scheduled\n\t\t\tif limitMode == LimitModeReschedule {\n\t\t\t\t<-rescheduleLimiter\n\t\t\t}\n\t\tcase <-e.ctx.Done():\n\t\t\te.logger.Debug(\"singletonModeRunner shutting down\", \"name\", name)\n\t\t\twg.Done()\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (e *executor) runJob(j internalJob, jIn jobIn) {\n\tif j.ctx == nil {\n\t\treturn\n\t}\n\tselect {\n\tcase <-e.ctx.Done():\n\t\treturn\n\tcase <-j.ctx.Done():\n\t\treturn\n\tdefault:\n\t}\n\n\tif j.stopTimeReached(e.clock.Now()) {\n\t\treturn\n\t}\n\n\tif e.elector != nil {\n\t\tif err := e.elector.IsLeader(j.ctx); err != nil {\n\t\t\te.sendOutForRescheduling(&jIn)\n\t\t\te.incrementJobCounter(j, Skip)\n\t\t\treturn\n\t\t}\n\t} else if !j.disabledLocker && j.locker != nil {\n\t\tlock, err := j.locker.Lock(j.ctx, j.name)\n\t\tif err != nil {\n\t\t\t_ = callJobFuncWithParams(j.afterLockError, j.id, j.name, err)\n\t\t\te.sendOutForRescheduling(&jIn)\n\t\t\te.incrementJobCounter(j, Skip)\n\t\t\te.sendOutForNextRunUpdate(&jIn)\n\t\t\treturn\n\t\t}\n\t\tdefer func() { _ = lock.Unlock(j.ctx) }()\n\t} else if !j.disabledLocker && e.locker != nil {\n\t\tlock, err := e.locker.Lock(j.ctx, j.name)\n\t\tif err != nil {\n\t\t\t_ = callJobFuncWithParams(j.afterLockError, j.id, j.name, err)\n\t\t\te.sendOutForRescheduling(&jIn)\n\t\t\te.incrementJobCounter(j, Skip)\n\t\t\te.sendOutForNextRunUpdate(&jIn)\n\t\t\treturn\n\t\t}\n\t\tdefer func() { _ = lock.Unlock(j.ctx) }()\n\t}\n\n\t_ = callJobFuncWithParams(j.beforeJobRuns, j.id, j.name)\n\n\t//  Notify job started\n\tactualStartTime := time.Now()\n\tif e.scheduler != nil && e.scheduler.schedulerMonitor != nil {\n\t\tjobObj := e.scheduler.jobFromInternalJob(j)\n\t\te.scheduler.notifyJobStarted(jobObj)\n\t\t// Notify scheduling delay if job had a scheduled time\n\t\tif len(j.nextScheduled) > 0 {\n\t\t\te.scheduler.notifyJobSchedulingDelay(jobObj, j.nextScheduled[0], actualStartTime)\n\t\t}\n\t}\n\n\terr := callJobFuncWithParams(j.beforeJobRunsSkipIfBeforeFuncErrors, j.id, j.name)\n\tif err != nil {\n\t\te.sendOutForRescheduling(&jIn)\n\t\tselect {\n\t\tcase e.jobsOutCompleted <- j.id:\n\t\tcase <-e.ctx.Done():\n\t\t}\n\t\t// Notify job failed (before actual run)\n\t\tif e.scheduler != nil && e.scheduler.schedulerMonitor != nil {\n\t\t\te.scheduler.notifyJobFailed(e.scheduler.jobFromInternalJob(j), err)\n\t\t}\n\t\treturn\n\t}\n\n\t// Notify job running\n\tif e.scheduler != nil && e.scheduler.schedulerMonitor != nil {\n\t\te.scheduler.notifyJobRunning(e.scheduler.jobFromInternalJob(j))\n\t}\n\n\t// For intervalFromCompletion, we need to reschedule AFTER the job completes,\n\t// not before. For regular jobs, we reschedule before execution (existing behavior).\n\tif !j.intervalFromCompletion {\n\t\te.sendOutForRescheduling(&jIn)\n\t\tselect {\n\t\tcase e.jobsOutCompleted <- j.id:\n\t\tcase <-e.ctx.Done():\n\t\t}\n\t}\n\n\tstartTime := time.Now()\n\tif j.afterJobRunsWithPanic != nil {\n\t\terr = e.callJobWithRecover(j)\n\t} else {\n\t\terr = callJobFuncWithParams(j.function, j.parameters...)\n\t}\n\te.recordJobTiming(startTime, time.Now(), j)\n\tif err != nil {\n\t\t_ = callJobFuncWithParams(j.afterJobRunsWithError, j.id, j.name, err)\n\t\te.incrementJobCounter(j, Fail)\n\t\tendTime := time.Now()\n\t\te.recordJobTimingWithStatus(startTime, endTime, j, Fail, err)\n\t\t// Notify job failed\n\t\tif e.scheduler != nil && e.scheduler.schedulerMonitor != nil {\n\t\t\tjobObj := e.scheduler.jobFromInternalJob(j)\n\t\t\te.scheduler.notifyJobFailed(jobObj, err)\n\t\t\te.scheduler.notifyJobExecutionTime(jobObj, endTime.Sub(startTime))\n\t\t}\n\t} else {\n\t\t_ = callJobFuncWithParams(j.afterJobRuns, j.id, j.name)\n\t\te.incrementJobCounter(j, Success)\n\t\tendTime := time.Now()\n\t\te.recordJobTimingWithStatus(startTime, endTime, j, Success, nil)\n\t\t// Notify job completed\n\t\tif e.scheduler != nil && e.scheduler.schedulerMonitor != nil {\n\t\t\tjobObj := e.scheduler.jobFromInternalJob(j)\n\t\t\te.scheduler.notifyJobCompleted(jobObj)\n\t\t\te.scheduler.notifyJobExecutionTime(jobObj, endTime.Sub(startTime))\n\t\t}\n\t}\n\n\t// For intervalFromCompletion, reschedule AFTER the job completes\n\tif j.intervalFromCompletion {\n\t\tselect {\n\t\tcase e.jobsOutCompleted <- j.id:\n\t\tcase <-e.ctx.Done():\n\t\t}\n\t\te.sendOutForRescheduling(&jIn)\n\t}\n}\n\nfunc (e *executor) callJobWithRecover(j internalJob) (err error) {\n\tdefer func() {\n\t\tif recoverData := recover(); recoverData != nil {\n\t\t\t_ = callJobFuncWithParams(j.afterJobRunsWithPanic, j.id, j.name, recoverData)\n\n\t\t\t// if panic is occurred, we should return an error\n\t\t\terr = fmt.Errorf(\"%w from %v\", ErrPanicRecovered, recoverData)\n\t\t}\n\t}()\n\n\treturn callJobFuncWithParams(j.function, j.parameters...)\n}\n\nfunc (e *executor) recordJobTiming(start time.Time, end time.Time, j internalJob) {\n\tif e.monitor != nil {\n\t\te.monitor.RecordJobTiming(start, end, j.id, j.name, j.tags)\n\t}\n}\n\nfunc (e *executor) recordJobTimingWithStatus(start time.Time, end time.Time, j internalJob, status JobStatus, err error) {\n\tif e.monitorStatus != nil {\n\t\te.monitorStatus.RecordJobTimingWithStatus(start, end, j.id, j.name, j.tags, status, err)\n\t}\n}\n\nfunc (e *executor) incrementJobCounter(j internalJob, status JobStatus) {\n\tif e.monitor != nil {\n\t\te.monitor.IncrementJob(j.id, j.name, j.tags, status)\n\t}\n}\n\nfunc (e *executor) stop(standardJobsWg, singletonJobsWg, limitModeJobsWg *waitGroupWithMutex) {\n\te.stopOnce.Do(func() {\n\t\te.logger.Debug(\"gocron: stopping executor\")\n\t\t// we've been asked to stop. This is either because the scheduler has been told\n\t\t// to stop all jobs or the scheduler has been asked to completely shutdown.\n\t\t//\n\t\t// cancel tells all the functions to stop their work and send in a done response\n\t\te.cancel()\n\n\t\t// the wait for job channels are used to report back whether we successfully waited\n\t\t// for all jobs to complete or if we hit the configured timeout.\n\t\twaitForJobs := make(chan struct{}, 1)\n\t\twaitForSingletons := make(chan struct{}, 1)\n\t\twaitForLimitMode := make(chan struct{}, 1)\n\n\t\t// the waiter context is used to cancel the functions waiting on jobs.\n\t\t// this is done to avoid goroutine leaks.\n\t\twaiterCtx, waiterCancel := context.WithCancel(context.Background())\n\n\t\t// wait for standard jobs to complete\n\t\tgo func() {\n\t\t\te.logger.Debug(\"gocron: waiting for standard jobs to complete\")\n\t\t\tgo func() {\n\t\t\t\t// this is done in a separate goroutine, so we aren't\n\t\t\t\t// blocked by the WaitGroup's Wait call in the event\n\t\t\t\t// that the waiter context is cancelled.\n\t\t\t\t// This particular goroutine could leak in the event that\n\t\t\t\t// some long-running standard job doesn't complete.\n\t\t\t\tstandardJobsWg.Wait()\n\t\t\t\te.logger.Debug(\"gocron: standard jobs completed\")\n\t\t\t\twaitForJobs <- struct{}{}\n\t\t\t}()\n\t\t\t<-waiterCtx.Done()\n\t\t}()\n\n\t\t// wait for per job singleton limit mode runner jobs to complete\n\t\tgo func() {\n\t\t\te.logger.Debug(\"gocron: waiting for singleton jobs to complete\")\n\t\t\tgo func() {\n\t\t\t\tsingletonJobsWg.Wait()\n\t\t\t\te.logger.Debug(\"gocron: singleton jobs completed\")\n\t\t\t\twaitForSingletons <- struct{}{}\n\t\t\t}()\n\t\t\t<-waiterCtx.Done()\n\t\t}()\n\n\t\t// wait for limit mode runners to complete\n\t\tgo func() {\n\t\t\te.logger.Debug(\"gocron: waiting for limit mode jobs to complete\")\n\t\t\tgo func() {\n\t\t\t\tlimitModeJobsWg.Wait()\n\t\t\t\te.logger.Debug(\"gocron: limitMode jobs completed\")\n\t\t\t\twaitForLimitMode <- struct{}{}\n\t\t\t}()\n\t\t\t<-waiterCtx.Done()\n\t\t}()\n\n\t\t// now either wait for all the jobs to complete,\n\t\t// or hit the timeout.\n\t\tvar count int\n\t\ttimeout := time.Now().Add(e.stopTimeout)\n\t\tfor time.Now().Before(timeout) && count < 3 {\n\t\t\tselect {\n\t\t\tcase <-waitForJobs:\n\t\t\t\tcount++\n\t\t\tcase <-waitForSingletons:\n\t\t\t\tcount++\n\t\t\tcase <-waitForLimitMode:\n\t\t\t\tcount++\n\t\t\tdefault:\n\t\t\t}\n\t\t}\n\t\tif count < 3 {\n\t\t\te.done <- ErrStopJobsTimedOut\n\t\t\te.logger.Debug(\"gocron: executor stopped - timed out\")\n\t\t} else {\n\t\t\te.done <- nil\n\t\t\te.logger.Debug(\"gocron: executor stopped\")\n\t\t}\n\t\twaiterCancel()\n\n\t\tif e.limitMode != nil {\n\t\t\te.limitMode.started = false\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "go.mod",
    "content": "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/clockwork v0.5.0\n\tgithub.com/robfig/cron/v3 v3.0.1\n\tgithub.com/stretchr/testify v1.11.1\n\tgo.uber.org/goleak v1.3.0\n)\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/kr/text v0.2.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=\ngithub.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=\ngithub.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=\ngithub.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "gocron-monitor-test/debug_restart.go",
    "content": "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      int\n\tstopCount       int\n\tjobRegCount     int\n\tjobUnregCount   int\n\tjobStartCount   int\n\tjobRunningCount int\n\tjobCompletCount int\n\tjobFailCount    int\n}\n\nfunc (m *DebugMonitor) SchedulerStarted() {\n\tm.startCount++\n\tfmt.Printf(\"✓ SchedulerStarted() called (total: %d)\\n\", m.startCount)\n}\n\nfunc (m *DebugMonitor) SchedulerShutdown() {\n\tm.stopCount++\n\tfmt.Printf(\"✓ SchedulerShutdown() called (total: %d)\\n\", m.stopCount)\n}\n\nfunc (m *DebugMonitor) JobRegistered(job *gocron.Job) {\n\tm.jobRegCount++\n\tfmt.Printf(\"✓ JobRegistered() called (total: %d) - Job ID: %s\\n\", m.jobRegCount, (*job).ID())\n}\n\nfunc (m *DebugMonitor) JobUnregistered(job *gocron.Job) {\n\tm.jobUnregCount++\n\tfmt.Printf(\"✓ JobUnregistered() called (total: %d) - Job ID: %s\\n\", m.jobUnregCount, (*job).ID())\n}\n\nfunc (m *DebugMonitor) JobStarted(job *gocron.Job) {\n\tm.jobStartCount++\n\tfmt.Printf(\"✓ JobStarted() called (total: %d) - Job ID: %s\\n\", m.jobStartCount, (*job).ID())\n}\n\nfunc (m *DebugMonitor) JobRunning(job *gocron.Job) {\n\tm.jobRunningCount++\n\tfmt.Printf(\"✓ JobRunning() called (total: %d) - Job ID: %s\\n\", m.jobRunningCount, (*job).ID())\n}\n\nfunc (m *DebugMonitor) JobCompleted(job *gocron.Job) {\n\tm.jobCompletCount++\n\tfmt.Printf(\"✓ JobCompleted() called (total: %d) - Job ID: %s\\n\", m.jobCompletCount, (*job).ID())\n}\n\nfunc (m *DebugMonitor) JobFailed(job *gocron.Job, err error) {\n\tm.jobFailCount++\n\tfmt.Printf(\"✓ JobFailed() called (total: %d) - Job ID: %s, Error: %v\\n\", m.jobFailCount, (*job).ID(), err)\n}\n\nfunc main() {\n\t// ONE monitor, multiple scheduler instances\n\tmonitor := &DebugMonitor{}\n\n\tfmt.Println(\"=== Cycle 1 (Scheduler Instance 1) ===\")\n\ts1, err := gocron.NewScheduler(\n\t\tgocron.WithSchedulerMonitor(monitor),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Create and register some test jobs\n\tfmt.Println(\"Creating jobs...\")\n\t_, err = s1.NewJob(\n\t\tgocron.DurationJob(1*time.Second),\n\t\tgocron.NewTask(func() { fmt.Println(\"Job 1 running\") }),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = s1.NewJob(\n\t\tgocron.DurationJob(2*time.Second),\n\t\tgocron.NewTask(func() error {\n\t\t\tfmt.Println(\"Job 2 executing and returning error\")\n\t\t\treturn fmt.Errorf(\"simulated job failure\")\n\t\t}), // This job will fail with error\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(\"Calling Start()...\")\n\ts1.Start()\n\ttime.Sleep(3 * time.Second) // Wait for jobs to execute\n\n\tfmt.Println(\"Calling Shutdown()...\")\n\terr = s1.Shutdown()\n\tif err != nil {\n\t\tfmt.Printf(\"Shutdown error: %v\\n\", err)\n\t}\n\n\tfmt.Println(\"\\n=== Cycle 2 (Job Updates) ===\")\n\ts2, err := gocron.NewScheduler(\n\t\tgocron.WithSchedulerMonitor(monitor),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(\"Creating and updating jobs...\")\n\tjob3, err := s2.NewJob(\n\t\tgocron.DurationJob(1*time.Second),\n\t\tgocron.NewTask(func() { fmt.Println(\"Job 3 running\") }),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Update the job\n\t_, err = s2.Update(\n\t\tjob3.ID(),\n\t\tgocron.DurationJob(2*time.Second),\n\t\tgocron.NewTask(func() { fmt.Println(\"Job 3 updated\") }),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(\"Calling Start()...\")\n\ts2.Start()\n\ttime.Sleep(3 * time.Second)\n\n\tfmt.Println(\"Calling Shutdown()...\")\n\terr = s2.Shutdown()\n\tif err != nil {\n\t\tfmt.Printf(\"Shutdown error: %v\\n\", err)\n\t}\n\n\tfmt.Println(\"\\n=== Summary ===\")\n\tfmt.Printf(\"Total Scheduler Starts: %d\\n\", monitor.startCount)\n\tfmt.Printf(\"Total Scheduler Stops: %d\\n\", monitor.stopCount)\n\tfmt.Printf(\"Total Jobs Registered: %d\\n\", monitor.jobRegCount)\n\tfmt.Printf(\"Total Jobs Unregistered: %d\\n\", monitor.jobUnregCount)\n\tfmt.Printf(\"Total Jobs Started: %d\\n\", monitor.jobStartCount)\n\tfmt.Printf(\"Total Jobs Running: %d\\n\", monitor.jobRunningCount)\n\tfmt.Printf(\"Total Jobs Completed: %d\\n\", monitor.jobCompletCount)\n\tfmt.Printf(\"Total Jobs Failed: %d\\n\", monitor.jobFailCount)\n}\n"
  },
  {
    "path": "gocron-monitor-test/go.mod",
    "content": "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 // indirect\n\tgithub.com/jonboulle/clockwork v0.5.0 // indirect\n\tgithub.com/robfig/cron/v3 v3.0.1 // indirect\n)\n\nreplace github.com/go-co-op/gocron/v2 => ../\n"
  },
  {
    "path": "gocron-monitor-test/go.sum",
    "content": "github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=\ngithub.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=\ngithub.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "job.go",
    "content": "//go:generate mockgen -destination=mocks/job.go -package=gocronmocks . Job\npackage gocron\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/jonboulle/clockwork\"\n\t\"github.com/robfig/cron/v3\"\n)\n\n// internalJob stores the information needed by the scheduler\n// to manage scheduling, starting and stopping the job\ntype internalJob struct {\n\tctx       context.Context\n\tparentCtx context.Context\n\tcancel    context.CancelFunc\n\tid        uuid.UUID\n\tname      string\n\ttags      []string\n\tcron      Cron\n\tjobSchedule\n\n\t// as some jobs may queue up, it's possible to\n\t// have multiple nextScheduled times\n\tnextScheduled []time.Time\n\n\tlastRun                time.Time\n\tfunction               any\n\tparameters             []any\n\ttimer                  clockwork.Timer\n\tsingletonMode          bool\n\tsingletonLimitMode     LimitMode\n\tlimitRunsTo            *limitRunsTo\n\tstartTime              time.Time\n\tstartImmediately       bool\n\tstopTime               time.Time\n\tintervalFromCompletion bool\n\t// event listeners\n\tafterJobRuns                        func(jobID uuid.UUID, jobName string)\n\tbeforeJobRuns                       func(jobID uuid.UUID, jobName string)\n\tbeforeJobRunsSkipIfBeforeFuncErrors func(jobID uuid.UUID, jobName string) error\n\tafterJobRunsWithError               func(jobID uuid.UUID, jobName string, err error)\n\tafterJobRunsWithPanic               func(jobID uuid.UUID, jobName string, recoverData any)\n\tafterLockError                      func(jobID uuid.UUID, jobName string, err error)\n\tdisabledLocker                      bool\n\n\tlocker Locker\n}\n\n// stop is used to stop the job's timer and cancel the context\n// stopping the timer is critical for cleaning up jobs that are\n// sleeping in a time.AfterFunc timer when the job is being stopped.\n// cancelling the context keeps the executor from continuing to try\n// and run the job.\nfunc (j *internalJob) stop() {\n\tif j.timer != nil {\n\t\tj.timer.Stop()\n\t}\n\tj.cancel()\n}\n\nfunc (j *internalJob) stopTimeReached(now time.Time) bool {\n\tif j.stopTime.IsZero() {\n\t\treturn false\n\t}\n\treturn j.stopTime.Before(now)\n}\n\n// task stores the function and parameters\n// that are actually run when the job is executed.\ntype task struct {\n\tfunction   any\n\tparameters []any\n}\n\n// Task defines a function that returns the task\n// function and parameters.\ntype Task func() task\n\n// NewTask provides the job's task function and parameters.\n// If you set the first argument of your Task func to be a context.Context,\n// gocron will pass in a context (either the default Job context, or one\n// provided via WithContext) to the job and will cancel the context on shutdown.\n// This allows you to listen for and handle cancellation within your job.\nfunc NewTask(function any, parameters ...any) Task {\n\treturn func() task {\n\t\treturn task{\n\t\t\tfunction:   function,\n\t\t\tparameters: parameters,\n\t\t}\n\t}\n}\n\n// limitRunsTo is used for managing the number of runs\n// when the user only wants the job to run a certain\n// number of times and then be removed from the scheduler.\ntype limitRunsTo struct {\n\tlimit    uint\n\trunCount uint\n}\n\n// -----------------------------------------------\n// -----------------------------------------------\n// --------------- Custom Cron -------------------\n// -----------------------------------------------\n// -----------------------------------------------\n\n// Cron defines the interface that must be\n// implemented to provide a custom cron implementation for\n// the job. Pass in the implementation using the JobOption WithCronImplementation.\ntype Cron interface {\n\tIsValid(crontab string, location *time.Location, now time.Time) error\n\tNext(lastRun time.Time) time.Time\n}\n\n// -----------------------------------------------\n// -----------------------------------------------\n// --------------- Job Variants ------------------\n// -----------------------------------------------\n// -----------------------------------------------\n\n// JobDefinition defines the interface that must be\n// implemented to create a job from the definition.\ntype JobDefinition interface {\n\tsetup(j *internalJob, l *time.Location, now time.Time) error\n}\n\n// Default cron implementation\n\nfunc newDefaultCronImplementation(withSeconds bool) Cron {\n\treturn &defaultCron{\n\t\twithSeconds: withSeconds,\n\t}\n}\n\n// NewDefaultCron returns the default cron implementation for use outside the\n// scheduling of a job. For example, validating crontab syntax before passing to the\n// NewJob function.\nfunc NewDefaultCron(cronStatementsIncludeSeconds bool) Cron {\n\treturn &defaultCron{\n\t\twithSeconds: cronStatementsIncludeSeconds,\n\t}\n}\n\nvar _ Cron = (*defaultCron)(nil)\n\ntype defaultCron struct {\n\tcronSchedule cron.Schedule\n\twithSeconds  bool\n}\n\nfunc (c *defaultCron) IsValid(crontab string, location *time.Location, now time.Time) error {\n\tvar withLocation string\n\tif strings.HasPrefix(crontab, \"TZ=\") || strings.HasPrefix(crontab, \"CRON_TZ=\") {\n\t\twithLocation = crontab\n\t} else {\n\t\t// since the user didn't provide a timezone default to the location\n\t\t// passed in by the scheduler. Default: time.Local\n\t\twithLocation = fmt.Sprintf(\"CRON_TZ=%s %s\", location.String(), crontab)\n\t}\n\n\tvar (\n\t\tcronSchedule cron.Schedule\n\t\terr          error\n\t)\n\n\tif c.withSeconds {\n\t\tp := cron.NewParser(cron.SecondOptional | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)\n\t\tcronSchedule, err = p.Parse(withLocation)\n\t} else {\n\t\tcronSchedule, err = cron.ParseStandard(withLocation)\n\t}\n\tif err != nil {\n\t\treturn errors.Join(ErrCronJobParse, err)\n\t}\n\tif cronSchedule.Next(now).IsZero() {\n\t\treturn ErrCronJobInvalid\n\t}\n\tc.cronSchedule = cronSchedule\n\treturn nil\n}\n\nfunc (c *defaultCron) Next(lastRun time.Time) time.Time {\n\treturn c.cronSchedule.Next(lastRun)\n}\n\n// default cron job implementation\nvar _ JobDefinition = (*cronJobDefinition)(nil)\n\ntype cronJobDefinition struct {\n\tcrontab string\n\tcron    Cron\n}\n\nfunc (c cronJobDefinition) setup(j *internalJob, location *time.Location, now time.Time) error {\n\tif j.cron != nil {\n\t\tc.cron = j.cron\n\t}\n\n\tif err := c.cron.IsValid(c.crontab, location, now); err != nil {\n\t\treturn err\n\t}\n\n\tj.jobSchedule = &cronJob{crontab: c.crontab, cronSchedule: c.cron}\n\treturn nil\n}\n\n// CronJob defines a new job using the crontab syntax: `* * * * *`.\n// An optional 6th field can be used at the beginning if withSeconds\n// is set to true: `* * * * * *`.\n// The timezone can be set on the Scheduler using WithLocation, or in the\n// crontab in the form `TZ=America/Chicago * * * * *` or\n// `CRON_TZ=America/Chicago * * * * *`\nfunc CronJob(crontab string, withSeconds bool) JobDefinition {\n\treturn cronJobDefinition{\n\t\tcrontab: crontab,\n\t\tcron:    newDefaultCronImplementation(withSeconds),\n\t}\n}\n\nvar _ JobDefinition = (*durationJobDefinition)(nil)\n\ntype durationJobDefinition struct {\n\tduration time.Duration\n}\n\nfunc (d durationJobDefinition) setup(j *internalJob, _ *time.Location, _ time.Time) error {\n\tif d.duration == 0 {\n\t\treturn ErrDurationJobIntervalZero\n\t}\n\tif d.duration < 0 {\n\t\treturn ErrDurationJobIntervalNegative\n\t}\n\tj.jobSchedule = &durationJob{duration: d.duration}\n\treturn nil\n}\n\n// DurationJob defines a new job using time.Duration\n// for the interval.\nfunc DurationJob(duration time.Duration) JobDefinition {\n\treturn durationJobDefinition{\n\t\tduration: duration,\n\t}\n}\n\nvar _ JobDefinition = (*durationRandomJobDefinition)(nil)\n\ntype durationRandomJobDefinition struct {\n\tmin, max time.Duration\n}\n\nfunc (d durationRandomJobDefinition) setup(j *internalJob, _ *time.Location, _ time.Time) error {\n\tif d.min >= d.max {\n\t\treturn ErrDurationRandomJobMinMax\n\t}\n\n\tif d.min <= 0 || d.max <= 0 {\n\t\treturn ErrDurationRandomJobPositive\n\t}\n\n\tj.jobSchedule = &durationRandomJob{\n\t\tmin:  d.min,\n\t\tmax:  d.max,\n\t\trand: rand.New(rand.NewSource(time.Now().UnixNano())), // nolint:gosec\n\t}\n\treturn nil\n}\n\n// DurationRandomJob defines a new job that runs on a random interval\n// between the min and max duration values provided.\n//\n// To achieve a similar behavior as tools that use a splay/jitter technique\n// consider the median value as the baseline and the difference between the\n// max-median or median-min as the splay/jitter.\n//\n// For example, if you want a job to run every 5 minutes, but want to add\n// up to 1 min of jitter to the interval, you could use\n// DurationRandomJob(4*time.Minute, 6*time.Minute)\nfunc DurationRandomJob(minDuration, maxDuration time.Duration) JobDefinition {\n\treturn durationRandomJobDefinition{\n\t\tmin: minDuration,\n\t\tmax: maxDuration,\n\t}\n}\n\n// DailyJob runs the job on the interval of days, and at the set times.\n// By default, the job will start the next available day, considering the last run to be now,\n// and the time and day based on the interval and times you input. This means, if you\n// select an interval greater than 1, your job by default will run X (interval) days from now\n// if there are no atTimes left in the current day. You can use WithStartAt to tell the\n// scheduler to start the job sooner.\nfunc DailyJob(interval uint, atTimes AtTimes) JobDefinition {\n\treturn dailyJobDefinition{\n\t\tinterval: interval,\n\t\tatTimes:  atTimes,\n\t}\n}\n\nvar _ JobDefinition = (*dailyJobDefinition)(nil)\n\ntype dailyJobDefinition struct {\n\tinterval uint\n\tatTimes  AtTimes\n}\n\nfunc (d dailyJobDefinition) setup(j *internalJob, location *time.Location, _ time.Time) error {\n\tatTimesDate, err := convertAtTimesToDateTime(d.atTimes, location)\n\tswitch {\n\tcase errors.Is(err, errAtTimesNil):\n\t\treturn ErrDailyJobAtTimesNil\n\tcase errors.Is(err, errAtTimeNil):\n\t\treturn ErrDailyJobAtTimeNil\n\tcase errors.Is(err, errAtTimeHours):\n\t\treturn ErrDailyJobHours\n\tcase errors.Is(err, errAtTimeMinSec):\n\t\treturn ErrDailyJobMinutesSeconds\n\t}\n\n\tif d.interval == 0 {\n\t\treturn ErrDailyJobZeroInterval\n\t}\n\n\tds := dailyJob{\n\t\tinterval: d.interval,\n\t\tatTimes:  atTimesDate,\n\t}\n\tj.jobSchedule = ds\n\treturn nil\n}\n\nvar _ JobDefinition = (*weeklyJobDefinition)(nil)\n\ntype weeklyJobDefinition struct {\n\tinterval      uint\n\tdaysOfTheWeek Weekdays\n\tatTimes       AtTimes\n}\n\nfunc (w weeklyJobDefinition) setup(j *internalJob, location *time.Location, _ time.Time) error {\n\tvar ws weeklyJob\n\tif w.interval == 0 {\n\t\treturn ErrWeeklyJobZeroInterval\n\t}\n\tws.interval = w.interval\n\n\tif w.daysOfTheWeek == nil {\n\t\treturn ErrWeeklyJobDaysOfTheWeekNil\n\t}\n\n\tdaysOfTheWeek := w.daysOfTheWeek()\n\n\tslices.Sort(daysOfTheWeek)\n\tws.daysOfWeek = daysOfTheWeek\n\n\tatTimesDate, err := convertAtTimesToDateTime(w.atTimes, location)\n\tswitch {\n\tcase errors.Is(err, errAtTimesNil):\n\t\treturn ErrWeeklyJobAtTimesNil\n\tcase errors.Is(err, errAtTimeNil):\n\t\treturn ErrWeeklyJobAtTimeNil\n\tcase errors.Is(err, errAtTimeHours):\n\t\treturn ErrWeeklyJobHours\n\tcase errors.Is(err, errAtTimeMinSec):\n\t\treturn ErrWeeklyJobMinutesSeconds\n\t}\n\tws.atTimes = atTimesDate\n\n\tj.jobSchedule = ws\n\treturn nil\n}\n\n// Weekdays defines a function that returns a list of week days.\ntype Weekdays func() []time.Weekday\n\n// NewWeekdays provide the days of the week the job should run.\nfunc NewWeekdays(weekday time.Weekday, weekdays ...time.Weekday) Weekdays {\n\treturn func() []time.Weekday {\n\t\treturn append([]time.Weekday{weekday}, weekdays...)\n\t}\n}\n\n// WeeklyJob runs the job on the interval of weeks, on the specific days of the week\n// specified, and at the set times.\n//\n// By default, the job will start the next available day, considering the last run to be now,\n// and the time and day based on the interval, days and times you input. This means, if you\n// select an interval greater than 1, your job by default will run X (interval) weeks from now\n// if there are no daysOfTheWeek left in the current week. You can use WithStartAt to tell the\n// scheduler to start the job sooner.\nfunc WeeklyJob(interval uint, daysOfTheWeek Weekdays, atTimes AtTimes) JobDefinition {\n\treturn weeklyJobDefinition{\n\t\tinterval:      interval,\n\t\tdaysOfTheWeek: daysOfTheWeek,\n\t\tatTimes:       atTimes,\n\t}\n}\n\nvar _ JobDefinition = (*monthlyJobDefinition)(nil)\n\ntype monthlyJobDefinition struct {\n\tinterval       uint\n\tdaysOfTheMonth DaysOfTheMonth\n\tatTimes        AtTimes\n}\n\nfunc (m monthlyJobDefinition) setup(j *internalJob, location *time.Location, _ time.Time) error {\n\tvar ms monthlyJob\n\tif m.interval == 0 {\n\t\treturn ErrMonthlyJobZeroInterval\n\t}\n\tms.interval = m.interval\n\n\tif m.daysOfTheMonth == nil {\n\t\treturn ErrMonthlyJobDaysNil\n\t}\n\n\tvar daysStart, daysEnd []int\n\tfor _, day := range m.daysOfTheMonth() {\n\t\tif day > 31 || day == 0 || day < -31 {\n\t\t\treturn ErrMonthlyJobDays\n\t\t}\n\t\tif day > 0 {\n\t\t\tdaysStart = append(daysStart, day)\n\t\t} else {\n\t\t\tdaysEnd = append(daysEnd, day)\n\t\t}\n\t}\n\tdaysStart = removeSliceDuplicatesInt(daysStart)\n\tms.days = daysStart\n\n\tdaysEnd = removeSliceDuplicatesInt(daysEnd)\n\tms.daysFromEnd = daysEnd\n\n\tatTimesDate, err := convertAtTimesToDateTime(m.atTimes, location)\n\tswitch {\n\tcase errors.Is(err, errAtTimesNil):\n\t\treturn ErrMonthlyJobAtTimesNil\n\tcase errors.Is(err, errAtTimeNil):\n\t\treturn ErrMonthlyJobAtTimeNil\n\tcase errors.Is(err, errAtTimeHours):\n\t\treturn ErrMonthlyJobHours\n\tcase errors.Is(err, errAtTimeMinSec):\n\t\treturn ErrMonthlyJobMinutesSeconds\n\t}\n\tms.atTimes = atTimesDate\n\n\tj.jobSchedule = ms\n\treturn nil\n}\n\ntype days []int\n\n// DaysOfTheMonth defines a function that returns a list of days.\ntype DaysOfTheMonth func() days\n\n// NewDaysOfTheMonth provide the days of the month the job should\n// run. The days can be positive 1 to 31 and/or negative -31 to -1.\n// Negative values count backwards from the end of the month.\n// For example: -1 == the last day of the month.\n//\n//\t-5 == 5 days before the end of the month.\nfunc NewDaysOfTheMonth(day int, moreDays ...int) DaysOfTheMonth {\n\treturn func() days {\n\t\treturn append([]int{day}, moreDays...)\n\t}\n}\n\ntype atTime struct {\n\thours, minutes, seconds uint\n}\n\nfunc (a atTime) time(location *time.Location) time.Time {\n\treturn time.Date(0, 0, 0, int(a.hours), int(a.minutes), int(a.seconds), 0, location)\n}\n\n// TimeFromAtTime is a helper function to allow converting AtTime into a time.Time value\n// Note: the time.Time value will have zero values for all Time fields except Hours, Minutes, Seconds.\n//\n//\tFor example: time.Date(0, 0, 0, 1, 1, 1, 0, time.UTC)\nfunc TimeFromAtTime(at AtTime, loc *time.Location) time.Time {\n\treturn at().time(loc)\n}\n\n// AtTime defines a function that returns the internal atTime\ntype AtTime func() atTime\n\n// NewAtTime provide the hours, minutes and seconds at which\n// the job should be run\nfunc NewAtTime(hours, minutes, seconds uint) AtTime {\n\treturn func() atTime {\n\t\treturn atTime{hours: hours, minutes: minutes, seconds: seconds}\n\t}\n}\n\n// AtTimes define a list of AtTime\ntype AtTimes func() []AtTime\n\n// NewAtTimes provide the hours, minutes and seconds at which\n// the job should be run\nfunc NewAtTimes(atTime AtTime, atTimes ...AtTime) AtTimes {\n\treturn func() []AtTime {\n\t\treturn append([]AtTime{atTime}, atTimes...)\n\t}\n}\n\n// MonthlyJob runs the job on the interval of months, on the specific days of the month\n// specified, and at the set times. Days of the month can be 1 to 31 or negative (-1 to -31), which\n// count backwards from the end of the month. E.g. -1 is the last day of the month.\n//\n// If a day of the month is selected that does not exist in all months (e.g. 31st)\n// any month that does not have that day will be skipped.\n//\n// By default, the job will start the next available day, considering the last run to be now,\n// and the time and month based on the interval, days and times you input.\n// This means, if you select an interval greater than 1, your job by default will run\n// X (interval) months from now if there are no daysOfTheMonth left in the current month.\n// You can use WithStartAt to tell the scheduler to start the job sooner.\n//\n// Carefully consider your configuration!\n//   - For example: an interval of 2 months on the 31st of each month, starting 12/31\n//     would skip Feb, April, June, and next run would be in August.\nfunc MonthlyJob(interval uint, daysOfTheMonth DaysOfTheMonth, atTimes AtTimes) JobDefinition {\n\treturn monthlyJobDefinition{\n\t\tinterval:       interval,\n\t\tdaysOfTheMonth: daysOfTheMonth,\n\t\tatTimes:        atTimes,\n\t}\n}\n\nvar _ JobDefinition = (*oneTimeJobDefinition)(nil)\n\ntype oneTimeJobDefinition struct {\n\tstartAt OneTimeJobStartAtOption\n}\n\nfunc (o oneTimeJobDefinition) setup(j *internalJob, _ *time.Location, now time.Time) error {\n\tsortedTimes := o.startAt(j)\n\tslices.SortStableFunc(sortedTimes, ascendingTime)\n\t// deduplicate the times\n\tsortedTimes = removeSliceDuplicatesTimeOnSortedSlice(sortedTimes)\n\t// keep only schedules that are in the future\n\tidx, found := slices.BinarySearchFunc(sortedTimes, now, ascendingTime)\n\tif found {\n\t\tidx++\n\t}\n\tsortedTimes = sortedTimes[idx:]\n\tif !j.startImmediately && len(sortedTimes) == 0 {\n\t\treturn ErrOneTimeJobStartDateTimePast\n\t}\n\tj.jobSchedule = oneTimeJob{sortedTimes: sortedTimes}\n\treturn nil\n}\n\nfunc removeSliceDuplicatesTimeOnSortedSlice(times []time.Time) []time.Time {\n\tret := make([]time.Time, 0, len(times))\n\tfor i, t := range times {\n\t\tif i == 0 || t != times[i-1] {\n\t\t\tret = append(ret, t)\n\t\t}\n\t}\n\treturn ret\n}\n\n// OneTimeJobStartAtOption defines when the one time job is run\ntype OneTimeJobStartAtOption func(*internalJob) []time.Time\n\n// OneTimeJobStartImmediately tells the scheduler to run the one time job immediately.\nfunc OneTimeJobStartImmediately() OneTimeJobStartAtOption {\n\treturn func(j *internalJob) []time.Time {\n\t\tj.startImmediately = true\n\t\treturn []time.Time{}\n\t}\n}\n\n// OneTimeJobStartDateTime sets the date & time at which the job should run.\n// This datetime must be in the future (according to the scheduler clock).\nfunc OneTimeJobStartDateTime(start time.Time) OneTimeJobStartAtOption {\n\treturn func(_ *internalJob) []time.Time {\n\t\treturn []time.Time{start}\n\t}\n}\n\n// OneTimeJobStartDateTimes sets the date & times at which the job should run.\n// At least one of the date/times must be in the future (according to the scheduler clock).\nfunc OneTimeJobStartDateTimes(times ...time.Time) OneTimeJobStartAtOption {\n\treturn func(_ *internalJob) []time.Time {\n\t\treturn times\n\t}\n}\n\n// OneTimeJob is to run a job once at a specified time and not on\n// any regular schedule.\nfunc OneTimeJob(startAt OneTimeJobStartAtOption) JobDefinition {\n\treturn oneTimeJobDefinition{\n\t\tstartAt: startAt,\n\t}\n}\n\n// -----------------------------------------------\n// -----------------------------------------------\n// ----------------- Job Options -----------------\n// -----------------------------------------------\n// -----------------------------------------------\n\n// JobOption defines the constructor for job options.\ntype JobOption func(*internalJob, time.Time) error\n\n// WithDistributedJobLocker sets the locker to be used by multiple\n// Scheduler instances to ensure that only one instance of each\n// job is run.\nfunc WithDistributedJobLocker(locker Locker) JobOption {\n\treturn func(j *internalJob, _ time.Time) error {\n\t\tif locker == nil {\n\t\t\treturn ErrWithDistributedJobLockerNil\n\t\t}\n\t\tj.locker = locker\n\t\treturn nil\n\t}\n}\n\n// WithDisabledDistributedJobLocker disables the distributed job locker.\n// This is useful when a global distributed locker has been set on the scheduler\n// level using WithDistributedLocker and need to be disabled for specific jobs.\nfunc WithDisabledDistributedJobLocker(disabled bool) JobOption {\n\treturn func(j *internalJob, _ time.Time) error {\n\t\tj.disabledLocker = disabled\n\t\treturn nil\n\t}\n}\n\n// WithEventListeners sets the event listeners that should be\n// run for the job.\nfunc WithEventListeners(eventListeners ...EventListener) JobOption {\n\treturn func(j *internalJob, _ time.Time) error {\n\t\tfor _, eventListener := range eventListeners {\n\t\t\tif err := eventListener(j); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// WithLimitedRuns limits the number of executions of this job to n.\n// Upon reaching the limit, the job is removed from the scheduler.\nfunc WithLimitedRuns(limit uint) JobOption {\n\treturn func(j *internalJob, _ time.Time) error {\n\t\tif limit == 0 {\n\t\t\treturn ErrWithLimitedRunsZero\n\t\t}\n\t\tj.limitRunsTo = &limitRunsTo{\n\t\t\tlimit:    limit,\n\t\t\trunCount: 0,\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// WithName sets the name of the job. Name provides\n// a human-readable identifier for the job.\nfunc WithName(name string) JobOption {\n\treturn func(j *internalJob, _ time.Time) error {\n\t\tif name == \"\" {\n\t\t\treturn ErrWithNameEmpty\n\t\t}\n\t\tj.name = name\n\t\treturn nil\n\t}\n}\n\n// WithCronImplementation sets the custom Cron implementation for the job.\n// This is only utilized for the CronJob type.\nfunc WithCronImplementation(c Cron) JobOption {\n\treturn func(j *internalJob, _ time.Time) error {\n\t\tj.cron = c\n\t\treturn nil\n\t}\n}\n\n// WithSingletonMode keeps the job from running again if it is already running.\n// This is useful for jobs that should not overlap, and that occasionally\n// (but not consistently) run longer than the interval between job runs.\nfunc WithSingletonMode(mode LimitMode) JobOption {\n\treturn func(j *internalJob, _ time.Time) error {\n\t\tj.singletonMode = true\n\t\tj.singletonLimitMode = mode\n\t\treturn nil\n\t}\n}\n\n// WithIntervalFromCompletion configures the job to calculate the next run time\n// from the job's completion time rather than its scheduled start time.\n// This ensures consistent rest periods between job executions regardless of\n// how long each execution takes.\n//\n// By default (without this option), a job scheduled to run every N time units\n// will start N time units after its previous scheduled start time. For example,\n// if a job is scheduled to run every 5 minutes starting at 09:00 and takes 2 minutes\n// to complete, the next run will start at 09:05 (5 minutes from 09:00), giving\n// only 3 minutes of rest between completion and the next start.\n//\n// With this option enabled, the next run will start N time units after the job\n// completes. Using the same example, if the job completes at 09:02, the next run\n// will start at 09:07 (5 minutes from 09:02), ensuring a full 5 minutes of rest.\n//\n// Note: This option only makes sense with interval-based jobs (DurationJob, DurationRandomJob).\n// For time-based jobs (CronJob, DailyJob, etc.) that run at specific times, this option\n// will be ignored as those jobs are inherently scheduled at fixed times.\n//\n// Example:\n//\n//\ts.NewJob(\n//\t    gocron.DurationJob(5*time.Minute),\n//\t    gocron.NewTask(func() {\n//\t        // Job that takes variable time to complete\n//\t        doWork()\n//\t    }),\n//\t    gocron.WithIntervalFromCompletion(),\n//\t)\n//\n// In this example, no matter how long doWork() takes, there will always be\n// exactly 5 minutes between when it completes and when it starts again.\nfunc WithIntervalFromCompletion() JobOption {\n\treturn func(j *internalJob, _ time.Time) error {\n\t\tj.intervalFromCompletion = true\n\t\treturn nil\n\t}\n}\n\n// WithStartAt sets the option for starting the job at\n// a specific datetime.\nfunc WithStartAt(option StartAtOption) JobOption {\n\treturn func(j *internalJob, now time.Time) error {\n\t\treturn option(j, now)\n\t}\n}\n\n// StartAtOption defines options for starting the job\ntype StartAtOption func(*internalJob, time.Time) error\n\n// WithStartImmediately tells the scheduler to run the job immediately\n// regardless of the type or schedule of job. After this immediate run\n// the job is scheduled from this time based on the job definition.\nfunc WithStartImmediately() StartAtOption {\n\treturn func(j *internalJob, _ time.Time) error {\n\t\tj.startImmediately = true\n\t\treturn nil\n\t}\n}\n\n// WithStartDateTime sets the first date & time at which the job should run.\n// This datetime must be in the future.\nfunc WithStartDateTime(start time.Time) StartAtOption {\n\treturn func(j *internalJob, now time.Time) error {\n\t\tif start.IsZero() || start.Before(now) {\n\t\t\treturn ErrWithStartDateTimePast\n\t\t}\n\t\tif !j.stopTime.IsZero() && j.stopTime.Before(start) {\n\t\t\treturn ErrStartTimeLaterThanEndTime\n\t\t}\n\t\tj.startTime = start\n\t\treturn nil\n\t}\n}\n\n// WithStartDateTimePast sets the first date & time at which the job should run\n// from a time in the past. This is useful when you want to backdate\n// the start time of a job to a time in the past, for example\n// if you want to start a job from a specific date in the past\n// and have it run on its schedule from then.\n// The start time can be in the past, but not zero.\n// If the start time is in the future, it behaves the same as WithStartDateTime.\nfunc WithStartDateTimePast(start time.Time) StartAtOption {\n\treturn func(j *internalJob, _ time.Time) error {\n\t\tif start.IsZero() {\n\t\t\treturn ErrWithStartDateTimePastZero\n\t\t}\n\t\tif !j.stopTime.IsZero() && j.stopTime.Before(start) {\n\t\t\treturn ErrStartTimeLaterThanEndTime\n\t\t}\n\t\tj.startTime = start\n\t\treturn nil\n\t}\n}\n\n// WithStopAt sets the option for stopping the job from running\n// after the specified time.\nfunc WithStopAt(option StopAtOption) JobOption {\n\treturn func(j *internalJob, now time.Time) error {\n\t\treturn option(j, now)\n\t}\n}\n\n// StopAtOption defines options for stopping the job\ntype StopAtOption func(*internalJob, time.Time) error\n\n// WithStopDateTime sets the final date & time after which the job should stop.\n// This must be in the future and should be after the startTime (if specified).\n// The job's final run may be at the stop time, but not after.\nfunc WithStopDateTime(end time.Time) StopAtOption {\n\treturn func(j *internalJob, now time.Time) error {\n\t\tif end.IsZero() || end.Before(now) {\n\t\t\treturn ErrWithStopDateTimePast\n\t\t}\n\t\tif end.Before(j.startTime) {\n\t\t\treturn ErrStopTimeEarlierThanStartTime\n\t\t}\n\t\tj.stopTime = end\n\t\treturn nil\n\t}\n}\n\n// WithTags sets the tags for the job. Tags provide\n// a way to identify jobs by a set of tags and remove\n// multiple jobs by tag.\nfunc WithTags(tags ...string) JobOption {\n\treturn func(j *internalJob, _ time.Time) error {\n\t\tj.tags = tags\n\t\treturn nil\n\t}\n}\n\n// WithIdentifier sets the identifier for the job. The identifier\n// is used to uniquely identify the job and is used for logging\n// and metrics.\nfunc WithIdentifier(id uuid.UUID) JobOption {\n\treturn func(j *internalJob, _ time.Time) error {\n\t\tif id == uuid.Nil {\n\t\t\treturn ErrWithIdentifierNil\n\t\t}\n\n\t\tj.id = id\n\t\treturn nil\n\t}\n}\n\n// WithContext sets the parent context for the job.\n// If you set the first argument of your Task func to be a context.Context,\n// gocron will pass in the provided context to the job and will cancel the\n// context on shutdown. If you cancel the context the job will no longer be\n// scheduled as well. This allows you to both control the job via a context\n// and listen for and handle cancellation within your job.\nfunc WithContext(ctx context.Context) JobOption {\n\treturn func(j *internalJob, _ time.Time) error {\n\t\tif ctx == nil {\n\t\t\treturn ErrWithContextNil\n\t\t}\n\t\tj.parentCtx = ctx\n\t\treturn nil\n\t}\n}\n\n// -----------------------------------------------\n// -----------------------------------------------\n// ------------- Job Event Listeners -------------\n// -----------------------------------------------\n// -----------------------------------------------\n\n// EventListener defines the constructor for event\n// listeners that can be used to listen for job events.\ntype EventListener func(*internalJob) error\n\n// BeforeJobRuns is used to listen for when a job is about to run and\n// then run the provided function.\nfunc BeforeJobRuns(eventListenerFunc func(jobID uuid.UUID, jobName string)) EventListener {\n\treturn func(j *internalJob) error {\n\t\tif eventListenerFunc == nil {\n\t\t\treturn ErrEventListenerFuncNil\n\t\t}\n\t\tj.beforeJobRuns = eventListenerFunc\n\t\treturn nil\n\t}\n}\n\n// BeforeJobRunsSkipIfBeforeFuncErrors is used to listen for when a job is about to run and\n// then runs the provided function. If the provided function returns an error, the job will be\n// rescheduled and the current run will be skipped.\nfunc BeforeJobRunsSkipIfBeforeFuncErrors(eventListenerFunc func(jobID uuid.UUID, jobName string) error) EventListener {\n\treturn func(j *internalJob) error {\n\t\tif eventListenerFunc == nil {\n\t\t\treturn ErrEventListenerFuncNil\n\t\t}\n\t\tj.beforeJobRunsSkipIfBeforeFuncErrors = eventListenerFunc\n\t\treturn nil\n\t}\n}\n\n// AfterJobRuns is used to listen for when a job has run\n// without an error, and then run the provided function.\nfunc AfterJobRuns(eventListenerFunc func(jobID uuid.UUID, jobName string)) EventListener {\n\treturn func(j *internalJob) error {\n\t\tif eventListenerFunc == nil {\n\t\t\treturn ErrEventListenerFuncNil\n\t\t}\n\t\tj.afterJobRuns = eventListenerFunc\n\t\treturn nil\n\t}\n}\n\n// AfterJobRunsWithError is used to listen for when a job has run and\n// returned an error, and then run the provided function.\nfunc AfterJobRunsWithError(eventListenerFunc func(jobID uuid.UUID, jobName string, err error)) EventListener {\n\treturn func(j *internalJob) error {\n\t\tif eventListenerFunc == nil {\n\t\t\treturn ErrEventListenerFuncNil\n\t\t}\n\t\tj.afterJobRunsWithError = eventListenerFunc\n\t\treturn nil\n\t}\n}\n\n// AfterJobRunsWithPanic is used to listen for when a job has run and\n// returned panicked recover data, and then run the provided function.\nfunc AfterJobRunsWithPanic(eventListenerFunc func(jobID uuid.UUID, jobName string, recoverData any)) EventListener {\n\treturn func(j *internalJob) error {\n\t\tif eventListenerFunc == nil {\n\t\t\treturn ErrEventListenerFuncNil\n\t\t}\n\t\tj.afterJobRunsWithPanic = eventListenerFunc\n\t\treturn nil\n\t}\n}\n\n// AfterLockError is used to when the distributed locker returns an error and\n// then run the provided function.\nfunc AfterLockError(eventListenerFunc func(jobID uuid.UUID, jobName string, err error)) EventListener {\n\treturn func(j *internalJob) error {\n\t\tif eventListenerFunc == nil {\n\t\t\treturn ErrEventListenerFuncNil\n\t\t}\n\t\tj.afterLockError = eventListenerFunc\n\t\treturn nil\n\t}\n}\n\n// -----------------------------------------------\n// -----------------------------------------------\n// ---------------- Job Schedules ----------------\n// -----------------------------------------------\n// -----------------------------------------------\n\ntype jobSchedule interface {\n\tnext(lastRun time.Time) time.Time\n}\n\nvar _ jobSchedule = (*cronJob)(nil)\n\ntype cronJob struct {\n\tcrontab      string\n\tcronSchedule Cron\n}\n\nfunc (j *cronJob) next(lastRun time.Time) time.Time {\n\treturn j.cronSchedule.Next(lastRun)\n}\n\nvar _ jobSchedule = (*durationJob)(nil)\n\ntype durationJob struct {\n\tduration time.Duration\n}\n\nfunc (j *durationJob) next(lastRun time.Time) time.Time {\n\treturn lastRun.Add(j.duration)\n}\n\nvar _ jobSchedule = (*durationRandomJob)(nil)\n\ntype durationRandomJob struct {\n\tmin, max time.Duration\n\trand     *rand.Rand\n}\n\nfunc (j *durationRandomJob) next(lastRun time.Time) time.Time {\n\tr := j.rand.Int63n(int64(j.max - j.min))\n\treturn lastRun.Add(j.min + time.Duration(r))\n}\n\nvar _ jobSchedule = (*dailyJob)(nil)\n\ntype dailyJob struct {\n\tinterval uint\n\tatTimes  []time.Time\n}\n\nfunc (d dailyJob) next(lastRun time.Time) time.Time {\n\tfirstPass := true\n\tnext := d.nextDay(lastRun, firstPass)\n\tif !next.IsZero() {\n\t\treturn next\n\t}\n\tfirstPass = false\n\n\tstartNextDay := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day()+int(d.interval), 0, 0, 0, 0, lastRun.Location())\n\treturn d.nextDay(startNextDay, firstPass)\n}\n\nfunc (d dailyJob) nextDay(lastRun time.Time, firstPass bool) time.Time {\n\tfor _, at := range d.atTimes {\n\t\t// sub the at time hour/min/sec onto the lastScheduledRun's values\n\t\t// to use in checks to see if we've got our next run time\n\t\tatDate := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), at.Hour(), at.Minute(), at.Second(), 0, lastRun.Location())\n\n\t\tif firstPass && atDate.After(lastRun) {\n\t\t\t// checking to see if it is after i.e. greater than,\n\t\t\t// and not greater or equal as our lastScheduledRun day/time\n\t\t\t// will be in the loop, and we don't want to select it again\n\t\t\treturn atDate\n\t\t} else if !firstPass && !atDate.Before(lastRun) {\n\t\t\t// now that we're looking at the next day, it's ok to consider\n\t\t\t// the same at time that was last run (as lastScheduledRun has been incremented)\n\t\t\treturn atDate\n\t\t}\n\t}\n\treturn time.Time{}\n}\n\nvar _ jobSchedule = (*weeklyJob)(nil)\n\ntype weeklyJob struct {\n\tinterval   uint\n\tdaysOfWeek []time.Weekday\n\tatTimes    []time.Time\n}\n\nfunc (w weeklyJob) next(lastRun time.Time) time.Time {\n\tnext := w.nextWeekDayAtTime(lastRun, true)\n\tif !next.IsZero() {\n\t\treturn next\n\t}\n\n\tstartOfTheNextIntervalWeek := (lastRun.Day() - int(lastRun.Weekday())) + int(w.interval*7)\n\tfrom := time.Date(lastRun.Year(), lastRun.Month(), startOfTheNextIntervalWeek, 0, 0, 0, 0, lastRun.Location())\n\treturn w.nextWeekDayAtTime(from, false)\n}\n\nfunc (w weeklyJob) nextWeekDayAtTime(lastRun time.Time, firstPass bool) time.Time {\n\tfor _, wd := range w.daysOfWeek {\n\t\t// checking if we're on the same day or later in the same week\n\t\tif wd >= lastRun.Weekday() {\n\t\t\t// weekDayDiff is used to add the correct amount to the atDate day below\n\t\t\tweekDayDiff := wd - lastRun.Weekday()\n\t\t\tfor _, at := range w.atTimes {\n\t\t\t\t// sub the at time hour/min/sec onto the lastScheduledRun's values\n\t\t\t\t// to use in checks to see if we've got our next run time\n\t\t\t\tatDate := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day()+int(weekDayDiff), at.Hour(), at.Minute(), at.Second(), 0, lastRun.Location())\n\n\t\t\t\tif firstPass && atDate.After(lastRun) {\n\t\t\t\t\t// checking to see if it is after i.e. greater than,\n\t\t\t\t\t// and not greater or equal as our lastScheduledRun day/time\n\t\t\t\t\t// will be in the loop, and we don't want to select it again\n\t\t\t\t\treturn atDate\n\t\t\t\t} else if !firstPass && !atDate.Before(lastRun) {\n\t\t\t\t\t// now that we're looking at the next week, it's ok to consider\n\t\t\t\t\t// the same at time that was last run (as lastScheduledRun has been incremented)\n\t\t\t\t\treturn atDate\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn time.Time{}\n}\n\nvar _ jobSchedule = (*monthlyJob)(nil)\n\ntype monthlyJob struct {\n\tinterval    uint\n\tdays        []int\n\tdaysFromEnd []int\n\tatTimes     []time.Time\n}\n\nfunc (m monthlyJob) next(lastRun time.Time) time.Time {\n\tdaysList := make([]int, len(m.days))\n\tcopy(daysList, m.days)\n\n\tdaysFromEnd := m.handleNegativeDays(lastRun, daysList, m.daysFromEnd)\n\tnext := m.nextMonthDayAtTime(lastRun, daysFromEnd, true)\n\tif !next.IsZero() {\n\t\treturn next\n\t}\n\n\tfrom := time.Date(lastRun.Year(), lastRun.Month()+time.Month(m.interval), 1, 0, 0, 0, 0, lastRun.Location())\n\tfor next.IsZero() {\n\t\tdaysFromEnd = m.handleNegativeDays(from, daysList, m.daysFromEnd)\n\t\tnext = m.nextMonthDayAtTime(from, daysFromEnd, false)\n\t\tfrom = from.AddDate(0, int(m.interval), 0)\n\t}\n\n\treturn next\n}\n\nfunc (m monthlyJob) handleNegativeDays(from time.Time, days, negativeDays []int) []int {\n\tvar out []int\n\t// getting a list of the days from the end of the following month\n\t// -1 == the last day of the month\n\tfirstDayNextMonth := time.Date(from.Year(), from.Month()+1, 1, 0, 0, 0, 0, from.Location())\n\tfor _, daySub := range negativeDays {\n\t\tday := firstDayNextMonth.AddDate(0, 0, daySub).Day()\n\t\tout = append(out, day)\n\t}\n\tout = append(out, days...)\n\tslices.Sort(out)\n\treturn out\n}\n\nfunc (m monthlyJob) nextMonthDayAtTime(lastRun time.Time, days []int, firstPass bool) time.Time {\n\t// find the next day in the month that should run and then check for an at time\n\tfor _, day := range days {\n\t\tif day >= lastRun.Day() {\n\t\t\tfor _, at := range m.atTimes {\n\t\t\t\t// sub the day, and the at time hour/min/sec onto the lastScheduledRun's values\n\t\t\t\t// to use in checks to see if we've got our next run time\n\t\t\t\tatDate := time.Date(lastRun.Year(), lastRun.Month(), day, at.Hour(), at.Minute(), at.Second(), 0, lastRun.Location())\n\n\t\t\t\tif atDate.Month() != lastRun.Month() {\n\t\t\t\t\t// this check handles if we're setting a day not in the current month\n\t\t\t\t\t// e.g. setting day 31 in Feb results in March 2nd\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tif firstPass && atDate.After(lastRun) {\n\t\t\t\t\t// checking to see if it is after i.e. greater than,\n\t\t\t\t\t// and not greater or equal as our lastScheduledRun day/time\n\t\t\t\t\t// will be in the loop, and we don't want to select it again\n\t\t\t\t\treturn atDate\n\t\t\t\t} else if !firstPass && !atDate.Before(lastRun) {\n\t\t\t\t\t// now that we're looking at the next month, it's ok to consider\n\t\t\t\t\t// the same at time that was  lastScheduledRun (as lastScheduledRun has been incremented)\n\t\t\t\t\treturn atDate\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t}\n\treturn time.Time{}\n}\n\nvar _ jobSchedule = (*oneTimeJob)(nil)\n\ntype oneTimeJob struct {\n\tsortedTimes []time.Time\n}\n\n// next finds the next item in a sorted list of times using binary-search.\n//\n// example: sortedTimes: [2, 4, 6, 8]\n//\n// lastRun: 1 => [idx=0,found=false] => next is 2 - sorted[idx] idx=0\n// lastRun: 2 => [idx=0,found=true] => next is 4 - sorted[idx+1] idx=1\n// lastRun: 3 => [idx=1,found=false] => next is 4 - sorted[idx] idx=1\n// lastRun: 4 => [idx=1,found=true] => next is 6 - sorted[idx+1] idx=2\n// lastRun: 7 => [idx=3,found=false] => next is 8 - sorted[idx] idx=3\n// lastRun: 8 => [idx=3,found=found] => next is none\n// lastRun: 9 => [idx=3,found=found] => next is none\nfunc (o oneTimeJob) next(lastRun time.Time) time.Time {\n\tidx, found := slices.BinarySearchFunc(o.sortedTimes, lastRun, ascendingTime)\n\t// if found, the next run is the following index\n\tif found {\n\t\tidx++\n\t}\n\t// exhausted runs\n\tif idx >= len(o.sortedTimes) {\n\t\treturn time.Time{}\n\t}\n\n\treturn o.sortedTimes[idx]\n}\n\n// -----------------------------------------------\n// -----------------------------------------------\n// ---------------- Job Interface ----------------\n// -----------------------------------------------\n// -----------------------------------------------\n\n// Job provides the available methods on the job\n// available to the caller.\ntype Job interface {\n\t// ID returns the job's unique identifier.\n\tID() uuid.UUID\n\t// LastRun returns the time of the job's last run\n\tLastRun() (time.Time, error)\n\t// Name returns the name defined on the job.\n\tName() string\n\t// NextRun returns the time of the job's next scheduled run.\n\t// This value is only available once the scheduler has been started\n\t// with Scheduler.Start(). Before that, it returns the zero time value.\n\tNextRun() (time.Time, error)\n\t// NextRuns returns the requested number of calculated next run values.\n\t// These values are only available once the scheduler has been started\n\t// with Scheduler.Start(). Before that, it returns nil.\n\tNextRuns(int) ([]time.Time, error)\n\t// RunNow runs the job once, now. This does not alter\n\t// the existing run schedule, and will respect all job\n\t// and scheduler limits. This means that running a job now may\n\t// cause the job's regular interval to be rescheduled due to\n\t// the instance being run by RunNow blocking your run limit.\n\tRunNow() error\n\t// Tags returns the job's string tags.\n\tTags() []string\n}\n\nvar _ Job = (*job)(nil)\n\n// job is the internal struct that implements\n// the public interface. This is used to avoid\n// leaking information the caller never needs\n// to have or tinker with.\ntype job struct {\n\tid            uuid.UUID\n\tname          string\n\ttags          []string\n\tjobOutRequest chan *jobOutRequest\n\trunJobRequest chan runJobRequest\n}\n\nfunc (j job) ID() uuid.UUID {\n\treturn j.id\n}\n\nfunc (j job) LastRun() (time.Time, error) {\n\tij := requestJob(j.id, j.jobOutRequest)\n\tif ij == nil || ij.id == uuid.Nil {\n\t\treturn time.Time{}, ErrJobNotFound\n\t}\n\treturn ij.lastRun, nil\n}\n\nfunc (j job) Name() string {\n\treturn j.name\n}\n\nfunc (j job) NextRun() (time.Time, error) {\n\tij := requestJob(j.id, j.jobOutRequest)\n\tif ij == nil || ij.id == uuid.Nil {\n\t\treturn time.Time{}, ErrJobNotFound\n\t}\n\tif len(ij.nextScheduled) == 0 {\n\t\treturn time.Time{}, nil\n\t}\n\t// the first element is the next scheduled run with subsequent\n\t// runs following after in the slice\n\treturn ij.nextScheduled[0], nil\n}\n\nfunc (j job) NextRuns(count int) ([]time.Time, error) {\n\tij := requestJob(j.id, j.jobOutRequest)\n\tif ij == nil || ij.id == uuid.Nil {\n\t\treturn nil, ErrJobNotFound\n\t}\n\n\tlengthNextScheduled := len(ij.nextScheduled)\n\tif lengthNextScheduled == 0 {\n\t\treturn nil, nil\n\t} else if count <= lengthNextScheduled {\n\t\treturn ij.nextScheduled[:count], nil\n\t}\n\n\tout := make([]time.Time, count)\n\tfor i := 0; i < count; i++ {\n\t\tif i < lengthNextScheduled {\n\t\t\tout[i] = ij.nextScheduled[i]\n\t\t\tcontinue\n\t\t}\n\n\t\tfrom := out[i-1]\n\t\tout[i] = ij.next(from)\n\t}\n\n\treturn out, nil\n}\n\nfunc (j job) Tags() []string {\n\treturn j.tags\n}\n\nfunc (j job) RunNow() error {\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second)\n\tdefer cancel()\n\tresp := make(chan error, 1)\n\n\tt := time.NewTimer(100 * time.Millisecond)\n\tselect {\n\tcase j.runJobRequest <- runJobRequest{\n\t\tid:      j.id,\n\t\toutChan: resp,\n\t}:\n\t\tt.Stop()\n\tcase <-t.C:\n\t\treturn ErrJobRunNowFailed\n\t}\n\tvar err error\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ErrJobRunNowFailed\n\tcase errReceived := <-resp:\n\t\terr = errReceived\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "job_test.go",
    "content": "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/jonboulle/clockwork\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDurationJob_next(t *testing.T) {\n\ttests := []time.Duration{\n\t\ttime.Millisecond,\n\t\ttime.Second,\n\t\t100 * time.Second,\n\t\t1000 * time.Second,\n\t\t5 * time.Second,\n\t\t50 * time.Second,\n\t\ttime.Minute,\n\t\t5 * time.Minute,\n\t\t100 * time.Minute,\n\t\ttime.Hour,\n\t\t2 * time.Hour,\n\t\t100 * time.Hour,\n\t\t1000 * time.Hour,\n\t}\n\n\tlastRun := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)\n\n\tfor _, duration := range tests {\n\t\tt.Run(duration.String(), func(t *testing.T) {\n\t\t\td := durationJob{duration: duration}\n\t\t\tnext := d.next(lastRun)\n\t\t\texpected := lastRun.Add(duration)\n\n\t\t\tassert.Equal(t, expected, next)\n\t\t})\n\t}\n}\n\nfunc TestDailyJob_next(t *testing.T) {\n\tamericaChicago, err := time.LoadLocation(\"America/Chicago\")\n\trequire.NoError(t, err)\n\n\ttests := []struct {\n\t\tname                      string\n\t\tinterval                  uint\n\t\tatTimes                   []time.Time\n\t\tlastRun                   time.Time\n\t\texpectedNextRun           time.Time\n\t\texpectedDurationToNextRun time.Duration\n\t}{\n\t\t{\n\t\t\t\"daily at midnight\",\n\t\t\t1,\n\t\t\t[]time.Time{\n\t\t\t\ttime.Date(0, 0, 0, 0, 0, 0, 0, time.UTC),\n\t\t\t},\n\t\t\ttime.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\t\ttime.Date(2000, 1, 2, 0, 0, 0, 0, time.UTC),\n\t\t\t24 * time.Hour,\n\t\t},\n\t\t{\n\t\t\t\"daily multiple at times\",\n\t\t\t1,\n\t\t\t[]time.Time{\n\t\t\t\ttime.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),\n\t\t\t\ttime.Date(0, 0, 0, 12, 30, 0, 0, time.UTC),\n\t\t\t},\n\t\t\ttime.Date(2000, 1, 1, 5, 30, 0, 0, time.UTC),\n\t\t\ttime.Date(2000, 1, 1, 12, 30, 0, 0, time.UTC),\n\t\t\t7 * time.Hour,\n\t\t},\n\t\t{\n\t\t\t\"every 2 days multiple at times\",\n\t\t\t2,\n\t\t\t[]time.Time{\n\t\t\t\ttime.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),\n\t\t\t\ttime.Date(0, 0, 0, 12, 30, 0, 0, time.UTC),\n\t\t\t},\n\t\t\ttime.Date(2000, 1, 1, 12, 30, 0, 0, time.UTC),\n\t\t\ttime.Date(2000, 1, 3, 5, 30, 0, 0, time.UTC),\n\t\t\t41 * time.Hour,\n\t\t},\n\t\t{\n\t\t\t\"daily at time with daylight savings time\",\n\t\t\t1,\n\t\t\t[]time.Time{\n\t\t\t\ttime.Date(0, 0, 0, 5, 30, 0, 0, americaChicago),\n\t\t\t},\n\t\t\ttime.Date(2023, 3, 11, 5, 30, 0, 0, americaChicago),\n\t\t\ttime.Date(2023, 3, 12, 5, 30, 0, 0, americaChicago),\n\t\t\t23 * time.Hour,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\td := dailyJob{\n\t\t\t\tinterval: tt.interval,\n\t\t\t\tatTimes:  tt.atTimes,\n\t\t\t}\n\n\t\t\tnext := d.next(tt.lastRun)\n\t\t\tassert.Equal(t, tt.expectedNextRun, next)\n\t\t\tassert.Equal(t, tt.expectedDurationToNextRun, next.Sub(tt.lastRun))\n\t\t})\n\t}\n}\n\nfunc TestWeeklyJob_next(t *testing.T) {\n\tamericaChicago, err := time.LoadLocation(\"America/Chicago\")\n\trequire.NoError(t, err)\n\n\ttests := []struct {\n\t\tname                      string\n\t\tinterval                  uint\n\t\tdaysOfWeek                []time.Weekday\n\t\tatTimes                   []time.Time\n\t\tlastRun                   time.Time\n\t\texpectedNextRun           time.Time\n\t\texpectedDurationToNextRun time.Duration\n\t}{\n\t\t{\n\t\t\t\"last run Monday, next run is Thursday\",\n\t\t\t1,\n\t\t\t[]time.Weekday{time.Monday, time.Thursday},\n\t\t\t[]time.Time{\n\t\t\t\ttime.Date(0, 0, 0, 0, 0, 0, 0, time.UTC),\n\t\t\t},\n\t\t\ttime.Date(2000, 1, 3, 0, 0, 0, 0, time.UTC),\n\t\t\ttime.Date(2000, 1, 6, 0, 0, 0, 0, time.UTC),\n\t\t\t3 * 24 * time.Hour,\n\t\t},\n\t\t{\n\t\t\t\"last run Thursday, next run is Monday\",\n\t\t\t1,\n\t\t\t[]time.Weekday{time.Monday, time.Thursday},\n\t\t\t[]time.Time{\n\t\t\t\ttime.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),\n\t\t\t},\n\t\t\ttime.Date(2000, 1, 6, 5, 30, 0, 0, time.UTC),\n\t\t\ttime.Date(2000, 1, 10, 5, 30, 0, 0, time.UTC),\n\t\t\t4 * 24 * time.Hour,\n\t\t},\n\t\t{\n\t\t\t\"last run before daylight savings time, next run after\",\n\t\t\t1,\n\t\t\t[]time.Weekday{time.Saturday},\n\t\t\t[]time.Time{\n\t\t\t\ttime.Date(0, 0, 0, 5, 30, 0, 0, americaChicago),\n\t\t\t},\n\t\t\ttime.Date(2023, 3, 11, 5, 30, 0, 0, americaChicago),\n\t\t\ttime.Date(2023, 3, 18, 5, 30, 0, 0, americaChicago),\n\t\t\t7*24*time.Hour - time.Hour,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tw := weeklyJob{\n\t\t\t\tinterval:   tt.interval,\n\t\t\t\tdaysOfWeek: tt.daysOfWeek,\n\t\t\t\tatTimes:    tt.atTimes,\n\t\t\t}\n\n\t\t\tnext := w.next(tt.lastRun)\n\t\t\tassert.Equal(t, tt.expectedNextRun, next)\n\t\t\tassert.Equal(t, tt.expectedDurationToNextRun, next.Sub(tt.lastRun))\n\t\t})\n\t}\n}\n\nfunc TestMonthlyJob_next(t *testing.T) {\n\tamericaChicago, err := time.LoadLocation(\"America/Chicago\")\n\trequire.NoError(t, err)\n\n\ttests := []struct {\n\t\tname                      string\n\t\tinterval                  uint\n\t\tdays                      []int\n\t\tdaysFromEnd               []int\n\t\tatTimes                   []time.Time\n\t\tlastRun                   time.Time\n\t\texpectedNextRun           time.Time\n\t\texpectedDurationToNextRun time.Duration\n\t}{\n\t\t{\n\t\t\t\"same day - before at time\",\n\t\t\t1,\n\t\t\t[]int{1},\n\t\t\tnil,\n\t\t\t[]time.Time{\n\t\t\t\ttime.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),\n\t\t\t},\n\t\t\ttime.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\t\ttime.Date(2000, 1, 1, 5, 30, 0, 0, time.UTC),\n\t\t\t5*time.Hour + 30*time.Minute,\n\t\t},\n\t\t{\n\t\t\t\"same day - after at time, runs next available date\",\n\t\t\t1,\n\t\t\t[]int{1, 10},\n\t\t\tnil,\n\t\t\t[]time.Time{\n\t\t\t\ttime.Date(0, 0, 0, 0, 0, 0, 0, time.UTC),\n\t\t\t},\n\t\t\ttime.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\t\ttime.Date(2000, 1, 10, 0, 0, 0, 0, time.UTC),\n\t\t\t9 * 24 * time.Hour,\n\t\t},\n\t\t{\n\t\t\t\"same day - after at time, runs next available date, following interval month\",\n\t\t\t2,\n\t\t\t[]int{1},\n\t\t\tnil,\n\t\t\t[]time.Time{\n\t\t\t\ttime.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),\n\t\t\t},\n\t\t\ttime.Date(2000, 1, 1, 5, 30, 0, 0, time.UTC),\n\t\t\ttime.Date(2000, 3, 1, 5, 30, 0, 0, time.UTC),\n\t\t\t60 * 24 * time.Hour,\n\t\t},\n\t\t{\n\t\t\t\"daylight savings time\",\n\t\t\t1,\n\t\t\t[]int{5},\n\t\t\tnil,\n\t\t\t[]time.Time{\n\t\t\t\ttime.Date(0, 0, 0, 5, 30, 0, 0, americaChicago),\n\t\t\t},\n\t\t\ttime.Date(2023, 11, 1, 0, 0, 0, 0, americaChicago),\n\t\t\ttime.Date(2023, 11, 5, 5, 30, 0, 0, americaChicago),\n\t\t\t4*24*time.Hour + 6*time.Hour + 30*time.Minute,\n\t\t},\n\t\t{\n\t\t\t\"negative days\",\n\t\t\t1,\n\t\t\tnil,\n\t\t\t[]int{-1, -3, -5},\n\t\t\t[]time.Time{\n\t\t\t\ttime.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),\n\t\t\t},\n\t\t\ttime.Date(2000, 1, 29, 5, 30, 0, 0, time.UTC),\n\t\t\ttime.Date(2000, 1, 31, 5, 30, 0, 0, time.UTC),\n\t\t\t2 * 24 * time.Hour,\n\t\t},\n\t\t{\n\t\t\t\"day not in current month, runs next month (leap year)\",\n\t\t\t1,\n\t\t\t[]int{31},\n\t\t\tnil,\n\t\t\t[]time.Time{\n\t\t\t\ttime.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),\n\t\t\t},\n\t\t\ttime.Date(2000, 1, 31, 5, 30, 0, 0, time.UTC),\n\t\t\ttime.Date(2000, 3, 31, 5, 30, 0, 0, time.UTC),\n\t\t\t29*24*time.Hour + 31*24*time.Hour,\n\t\t},\n\t\t{\n\t\t\t\"multiple days not in order\",\n\t\t\t1,\n\t\t\t[]int{10, 7, 19, 2},\n\t\t\tnil,\n\t\t\t[]time.Time{\n\t\t\t\ttime.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),\n\t\t\t},\n\t\t\ttime.Date(2000, 1, 2, 5, 30, 0, 0, time.UTC),\n\t\t\ttime.Date(2000, 1, 7, 5, 30, 0, 0, time.UTC),\n\t\t\t5 * 24 * time.Hour,\n\t\t},\n\t\t{\n\t\t\t\"day not in next interval month, selects next available option, skips Feb, April & June\",\n\t\t\t2,\n\t\t\t[]int{31},\n\t\t\tnil,\n\t\t\t[]time.Time{\n\t\t\t\ttime.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),\n\t\t\t},\n\t\t\ttime.Date(1999, 12, 31, 5, 30, 0, 0, time.UTC),\n\t\t\ttime.Date(2000, 8, 31, 5, 30, 0, 0, time.UTC),\n\t\t\t244 * 24 * time.Hour,\n\t\t},\n\t\t{\n\t\t\t\"handle -1 with differing month's day count\",\n\t\t\t1,\n\t\t\tnil,\n\t\t\t[]int{-1},\n\t\t\t[]time.Time{\n\t\t\t\ttime.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),\n\t\t\t},\n\t\t\ttime.Date(2024, 1, 31, 5, 30, 0, 0, time.UTC),\n\t\t\ttime.Date(2024, 2, 29, 5, 30, 0, 0, time.UTC),\n\t\t\t29 * 24 * time.Hour,\n\t\t},\n\t\t{\n\t\t\t\"handle -1 with another differing month's day count\",\n\t\t\t1,\n\t\t\tnil,\n\t\t\t[]int{-1},\n\t\t\t[]time.Time{\n\t\t\t\ttime.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),\n\t\t\t},\n\t\t\ttime.Date(2024, 2, 29, 5, 30, 0, 0, time.UTC),\n\t\t\ttime.Date(2024, 3, 31, 5, 30, 0, 0, time.UTC),\n\t\t\t31 * 24 * time.Hour,\n\t\t},\n\t\t{\n\t\t\t\"handle -1 every 3 months next run in February\",\n\t\t\t3,\n\t\t\tnil,\n\t\t\t[]int{-1},\n\t\t\t[]time.Time{\n\t\t\t\ttime.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),\n\t\t\t},\n\t\t\ttime.Date(2023, 11, 30, 5, 30, 0, 0, time.UTC),\n\t\t\ttime.Date(2024, 2, 29, 5, 30, 0, 0, time.UTC),\n\t\t\t91 * 24 * time.Hour,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := monthlyJob{\n\t\t\t\tinterval:    tt.interval,\n\t\t\t\tdays:        tt.days,\n\t\t\t\tdaysFromEnd: tt.daysFromEnd,\n\t\t\t\tatTimes:     tt.atTimes,\n\t\t\t}\n\n\t\t\tnext := m.next(tt.lastRun)\n\t\t\tassert.Equal(t, tt.expectedNextRun, next)\n\t\t\tassert.Equal(t, tt.expectedDurationToNextRun, next.Sub(tt.lastRun))\n\t\t})\n\t}\n}\n\nfunc TestDurationRandomJob_next(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tmin         time.Duration\n\t\tmax         time.Duration\n\t\tlastRun     time.Time\n\t\texpectedMin time.Time\n\t\texpectedMax time.Time\n\t}{\n\t\t{\n\t\t\t\"min 1s, max 5s\",\n\t\t\ttime.Second,\n\t\t\t5 * time.Second,\n\t\t\ttime.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\t\ttime.Date(2000, 1, 1, 0, 0, 1, 0, time.UTC),\n\t\t\ttime.Date(2000, 1, 1, 0, 0, 5, 0, time.UTC),\n\t\t},\n\t\t{\n\t\t\t\"min 100ms, max 1s\",\n\t\t\t100 * time.Millisecond,\n\t\t\t1 * time.Second,\n\t\t\ttime.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\t\ttime.Date(2000, 1, 1, 0, 0, 0, 100000000, time.UTC),\n\t\t\ttime.Date(2000, 1, 1, 0, 0, 1, 0, time.UTC),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\trj := durationRandomJob{\n\t\t\t\tmin:  tt.min,\n\t\t\t\tmax:  tt.max,\n\t\t\t\trand: rand.New(rand.NewSource(time.Now().UnixNano())), // nolint:gosec\n\t\t\t}\n\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\tnext := rj.next(tt.lastRun)\n\t\t\t\tassert.GreaterOrEqual(t, next, tt.expectedMin)\n\t\t\t\tassert.LessOrEqual(t, next, tt.expectedMax)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestOneTimeJob_next(t *testing.T) {\n\totj := oneTimeJob{}\n\tassert.Zero(t, otj.next(time.Time{}))\n}\n\nfunc TestJob_RunNow_Error(t *testing.T) {\n\ts := newTestScheduler(t)\n\n\tj, err := s.NewJob(\n\t\tDurationJob(time.Second),\n\t\tNewTask(func() {}),\n\t)\n\trequire.NoError(t, err)\n\n\trequire.NoError(t, s.Shutdown())\n\n\tassert.EqualError(t, j.RunNow(), ErrJobRunNowFailed.Error())\n}\n\nfunc TestJob_LastRun(t *testing.T) {\n\ttestTime := time.Date(2000, 1, 1, 0, 0, 0, 0, time.Local)\n\tfakeClock := clockwork.NewFakeClockAt(testTime)\n\n\ts := newTestScheduler(t,\n\t\tWithClock(fakeClock),\n\t)\n\n\tj, err := s.NewJob(\n\t\tDurationJob(\n\t\t\ttime.Second,\n\t\t),\n\t\tNewTask(\n\t\t\tfunc() {},\n\t\t),\n\t\tWithStartAt(WithStartImmediately()),\n\t)\n\trequire.NoError(t, err)\n\n\ts.Start()\n\ttime.Sleep(10 * time.Millisecond)\n\n\tlastRun, err := j.LastRun()\n\tassert.NoError(t, err)\n\n\terr = s.Shutdown()\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, testTime, lastRun)\n}\n\nfunc TestWithEventListeners(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\teventListeners []EventListener\n\t\terr            error\n\t}{\n\t\t{\n\t\t\t\"no event listeners\",\n\t\t\tnil,\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"beforeJobRuns\",\n\t\t\t[]EventListener{\n\t\t\t\tBeforeJobRuns(func(_ uuid.UUID, _ string) {}),\n\t\t\t},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"afterJobRuns\",\n\t\t\t[]EventListener{\n\t\t\t\tAfterJobRuns(func(_ uuid.UUID, _ string) {}),\n\t\t\t},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"afterJobRunsWithError\",\n\t\t\t[]EventListener{\n\t\t\t\tAfterJobRunsWithError(func(_ uuid.UUID, _ string, _ error) {}),\n\t\t\t},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"afterJobRunsWithPanic\",\n\t\t\t[]EventListener{\n\t\t\t\tAfterJobRunsWithPanic(func(_ uuid.UUID, _ string, _ any) {}),\n\t\t\t},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"afterLockError\",\n\t\t\t[]EventListener{\n\t\t\t\tAfterLockError(func(_ uuid.UUID, _ string, _ error) {}),\n\t\t\t},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"multiple event listeners\",\n\t\t\t[]EventListener{\n\t\t\t\tAfterJobRuns(func(_ uuid.UUID, _ string) {}),\n\t\t\t\tAfterJobRunsWithError(func(_ uuid.UUID, _ string, _ error) {}),\n\t\t\t\tBeforeJobRuns(func(_ uuid.UUID, _ string) {}),\n\t\t\t\tAfterLockError(func(_ uuid.UUID, _ string, _ error) {}),\n\t\t\t},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"nil after job runs listener\",\n\t\t\t[]EventListener{\n\t\t\t\tAfterJobRuns(nil),\n\t\t\t},\n\t\t\tErrEventListenerFuncNil,\n\t\t},\n\t\t{\n\t\t\t\"nil after job runs with error listener\",\n\t\t\t[]EventListener{\n\t\t\t\tAfterJobRunsWithError(nil),\n\t\t\t},\n\t\t\tErrEventListenerFuncNil,\n\t\t},\n\t\t{\n\t\t\t\"nil before job runs listener\",\n\t\t\t[]EventListener{\n\t\t\t\tBeforeJobRuns(nil),\n\t\t\t},\n\t\t\tErrEventListenerFuncNil,\n\t\t},\n\t\t{\n\t\t\t\"nil before job runs error listener\",\n\t\t\t[]EventListener{\n\t\t\t\tBeforeJobRunsSkipIfBeforeFuncErrors(nil),\n\t\t\t},\n\t\t\tErrEventListenerFuncNil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar ij internalJob\n\t\t\terr := WithEventListeners(tt.eventListeners...)(&ij, time.Now())\n\t\t\tassert.Equal(t, tt.err, err)\n\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tvar count int\n\t\t\tif ij.beforeJobRuns != nil {\n\t\t\t\tcount++\n\t\t\t}\n\t\t\tif ij.afterJobRuns != nil {\n\t\t\t\tcount++\n\t\t\t}\n\t\t\tif ij.afterJobRunsWithError != nil {\n\t\t\t\tcount++\n\t\t\t}\n\t\t\tif ij.afterJobRunsWithPanic != nil {\n\t\t\t\tcount++\n\t\t\t}\n\t\t\tif ij.afterLockError != nil {\n\t\t\t\tcount++\n\t\t\t}\n\t\t\tassert.Equal(t, len(tt.eventListeners), count)\n\t\t})\n\t}\n}\n\nfunc TestJob_NextRun(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tf    func()\n\t}{\n\t\t{\n\t\t\t\"simple\",\n\t\t\tfunc() {},\n\t\t},\n\t\t{\n\t\t\t\"sleep 3 seconds\",\n\t\t\tfunc() {\n\t\t\t\ttime.Sleep(300 * time.Millisecond)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttestTime := time.Now()\n\n\t\t\ts := newTestScheduler(t)\n\n\t\t\tj, err := s.NewJob(\n\t\t\t\tDurationJob(\n\t\t\t\t\t100*time.Millisecond,\n\t\t\t\t),\n\t\t\t\tNewTask(\n\t\t\t\t\tfunc() {},\n\t\t\t\t),\n\t\t\t\tWithStartAt(WithStartDateTime(testTime.Add(100*time.Millisecond))),\n\t\t\t\tWithSingletonMode(LimitModeReschedule),\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\ts.Start()\n\t\t\tnextRun, err := j.NextRun()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, testTime.Add(100*time.Millisecond), nextRun)\n\n\t\t\ttime.Sleep(150 * time.Millisecond)\n\n\t\t\tnextRun, err = j.NextRun()\n\t\t\tassert.NoError(t, err)\n\n\t\t\tassert.Equal(t, testTime.Add(200*time.Millisecond), nextRun)\n\t\t\tassert.Equal(t, 200*time.Millisecond, nextRun.Sub(testTime))\n\n\t\t\terr = s.Shutdown()\n\t\t\trequire.NoError(t, err)\n\t\t})\n\t}\n}\n\nfunc TestJob_NextRuns(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tjd        JobDefinition\n\t\tassertion func(t *testing.T, previousRun, nextRun time.Time)\n\t}{\n\t\t{\n\t\t\t\"simple - milliseconds\",\n\t\t\tDurationJob(\n\t\t\t\t100 * time.Millisecond,\n\t\t\t),\n\t\t\tfunc(t *testing.T, previousRun, nextRun time.Time) {\n\t\t\t\tassert.Equal(t, previousRun.UnixMilli()+100, nextRun.UnixMilli())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"weekly\",\n\t\t\tWeeklyJob(\n\t\t\t\t2,\n\t\t\t\tNewWeekdays(time.Tuesday),\n\t\t\t\tNewAtTimes(\n\t\t\t\t\tNewAtTime(0, 0, 0),\n\t\t\t\t),\n\t\t\t),\n\t\t\tfunc(t *testing.T, previousRun, nextRun time.Time) {\n\t\t\t\t// With the fix for NextRun accuracy, the immediate run (Jan 1) is removed\n\t\t\t\t// from nextScheduled after it completes. So all intervals should be 14 days\n\t\t\t\t// (2 weeks as configured).\n\t\t\t\tdiff := time.Hour * 14 * 24\n\t\t\t\tassert.Equal(t, previousRun.Add(diff).Day(), nextRun.Day())\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttestTime := time.Date(2000, 1, 1, 0, 0, 0, 0, time.Local)\n\t\t\tfakeClock := clockwork.NewFakeClockAt(testTime)\n\n\t\t\ts := newTestScheduler(t,\n\t\t\t\tWithClock(fakeClock),\n\t\t\t)\n\n\t\t\tj, err := s.NewJob(\n\t\t\t\ttt.jd,\n\t\t\t\tNewTask(\n\t\t\t\t\tfunc() {},\n\t\t\t\t),\n\t\t\t\tWithStartAt(WithStartImmediately()),\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\ts.Start()\n\t\t\ttime.Sleep(10 * time.Millisecond)\n\n\t\t\tnextRuns, err := j.NextRuns(5)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Len(t, nextRuns, 5)\n\n\t\t\tfor i := range nextRuns {\n\t\t\t\tif i == 0 {\n\t\t\t\t\t// skipping because there is no previous run\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\ttt.assertion(t, nextRuns[i-1], nextRuns[i])\n\t\t\t}\n\n\t\t\tassert.NoError(t, s.Shutdown())\n\t\t})\n\t}\n}\n\nfunc TestJob_PanicOccurred(t *testing.T) {\n\tgotCh := make(chan any)\n\terrCh := make(chan error)\n\ts := newTestScheduler(t)\n\t_, err := s.NewJob(\n\t\tDurationJob(10*time.Millisecond),\n\t\tNewTask(func() {\n\t\t\ta := 0\n\t\t\t_ = 1 / a\n\t\t}),\n\t\tWithEventListeners(\n\t\t\tAfterJobRunsWithPanic(func(_ uuid.UUID, _ string, recoverData any) {\n\t\t\t\tgotCh <- recoverData\n\t\t\t}), AfterJobRunsWithError(func(_ uuid.UUID, _ string, err error) {\n\t\t\t\terrCh <- err\n\t\t\t}),\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\ts.Start()\n\tgot := <-gotCh\n\trequire.EqualError(t, got.(error), \"runtime error: integer divide by zero\")\n\n\terr = <-errCh\n\trequire.ErrorIs(t, err, ErrPanicRecovered)\n\trequire.EqualError(t, err, \"gocron: panic recovered from runtime error: integer divide by zero\")\n\n\trequire.NoError(t, s.Shutdown())\n\tclose(gotCh)\n\tclose(errCh)\n}\n\nfunc TestTimeFromAtTime(t *testing.T) {\n\ttestTimeUTC := time.Date(0, 0, 0, 1, 1, 1, 0, time.UTC)\n\tcst, err := time.LoadLocation(\"America/Chicago\")\n\trequire.NoError(t, err)\n\ttestTimeCST := time.Date(0, 0, 0, 1, 1, 1, 0, cst)\n\n\ttests := []struct {\n\t\tname         string\n\t\tat           AtTime\n\t\tloc          *time.Location\n\t\texpectedTime time.Time\n\t\texpectedStr  string\n\t}{\n\t\t{\n\t\t\t\"UTC\",\n\t\t\tNewAtTime(\n\t\t\t\tuint(testTimeUTC.Hour()),\n\t\t\t\tuint(testTimeUTC.Minute()),\n\t\t\t\tuint(testTimeUTC.Second()),\n\t\t\t),\n\t\t\ttime.UTC,\n\t\t\ttestTimeUTC,\n\t\t\t\"01:01:01\",\n\t\t},\n\t\t{\n\t\t\t\"CST\",\n\t\t\tNewAtTime(\n\t\t\t\tuint(testTimeCST.Hour()),\n\t\t\t\tuint(testTimeCST.Minute()),\n\t\t\t\tuint(testTimeCST.Second()),\n\t\t\t),\n\t\t\tcst,\n\t\t\ttestTimeCST,\n\t\t\t\"01:01:01\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := TimeFromAtTime(tt.at, tt.loc)\n\t\t\tassert.Equal(t, tt.expectedTime, result)\n\n\t\t\tresultFmt := result.Format(\"15:04:05\")\n\t\t\tassert.Equal(t, tt.expectedStr, resultFmt)\n\t\t})\n\t}\n}\n\nfunc TestNewAtTimes(t *testing.T) {\n\tat := NewAtTimes(\n\t\tNewAtTime(1, 1, 1),\n\t\tNewAtTime(2, 2, 2),\n\t)\n\n\tvar times []string\n\tfor _, att := range at() {\n\t\ttimeStr := TimeFromAtTime(att, time.UTC).Format(\"15:04\")\n\t\ttimes = append(times, timeStr)\n\t}\n\n\tvar timesAgain []string\n\tfor _, att := range at() {\n\t\ttimeStr := TimeFromAtTime(att, time.UTC).Format(\"15:04\")\n\t\ttimesAgain = append(timesAgain, timeStr)\n\t}\n\n\tassert.Equal(t, times, timesAgain)\n}\n\nfunc TestNewWeekdays(t *testing.T) {\n\twd := NewWeekdays(\n\t\ttime.Monday,\n\t\ttime.Tuesday,\n\t)\n\n\tvar dayStrings []string\n\tfor _, w := range wd() {\n\t\tdayStrings = append(dayStrings, w.String())\n\t}\n\n\tvar dayStringsAgain []string\n\tfor _, w := range wd() {\n\t\tdayStringsAgain = append(dayStringsAgain, w.String())\n\t}\n\n\tassert.Equal(t, dayStrings, dayStringsAgain)\n}\n\nfunc TestNewDaysOfTheMonth(t *testing.T) {\n\tdom := NewDaysOfTheMonth(1, 2, 3)\n\n\tvar domInts []int\n\tfor _, d := range dom() {\n\t\tdomInts = append(domInts, d)\n\t}\n\n\tvar domIntsAgain []int\n\tfor _, d := range dom() {\n\t\tdomIntsAgain = append(domIntsAgain, d)\n\t}\n\n\tassert.Equal(t, domInts, domIntsAgain)\n}\n\nfunc TestWithIntervalFromCompletion_BasicFunctionality(t *testing.T) {\n\tt.Run(\"interval calculated from completion time\", func(t *testing.T) {\n\t\ts, err := NewScheduler()\n\t\trequire.NoError(t, err)\n\t\tdefer func() { _ = s.Shutdown() }()\n\n\t\tvar mu sync.Mutex\n\t\texecutions := []struct {\n\t\t\tstartTime    time.Time\n\t\t\tcompleteTime time.Time\n\t\t}{}\n\n\t\tjobExecutionTime := 2 * time.Second\n\t\tscheduledInterval := 5 * time.Second\n\n\t\t_, err = s.NewJob(\n\t\t\tDurationJob(scheduledInterval),\n\t\t\tNewTask(func() {\n\t\t\t\tstart := time.Now()\n\t\t\t\ttime.Sleep(jobExecutionTime)\n\t\t\t\tcomplete := time.Now()\n\n\t\t\t\tmu.Lock()\n\t\t\t\texecutions = append(executions, struct {\n\t\t\t\t\tstartTime    time.Time\n\t\t\t\t\tcompleteTime time.Time\n\t\t\t\t}{start, complete})\n\t\t\t\tmu.Unlock()\n\t\t\t}),\n\t\t\tWithIntervalFromCompletion(),\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\ts.Start()\n\n\t\t// Wait for at least 3 executions\n\t\t// With intervalFromCompletion:\n\t\t// Execution 1: 0s-2s\n\t\t// Wait: 5s (from 2s to 7s)\n\t\t// Execution 2: 7s-9s\n\t\t// Wait: 5s (from 9s to 14s)\n\t\t// Execution 3: 14s-16s\n\t\ttime.Sleep(18 * time.Second)\n\n\t\tmu.Lock()\n\t\texecutionCount := len(executions)\n\t\tmu.Unlock()\n\n\t\trequire.GreaterOrEqual(t, executionCount, 2,\n\t\t\t\"Expected at least 2 executions\")\n\n\t\tmu.Lock()\n\t\tdefer mu.Unlock()\n\n\t\tfor i := 1; i < len(executions); i++ {\n\t\t\tprev := executions[i-1]\n\t\t\tcurr := executions[i]\n\n\t\t\tcompletionToStartGap := curr.startTime.Sub(prev.completeTime)\n\n\t\t\tassert.InDelta(t, scheduledInterval.Seconds(), completionToStartGap.Seconds(), 0.5,\n\t\t\t\t\"Gap from completion to start should match the interval\")\n\t\t}\n\t})\n}\n\nfunc TestWithIntervalFromCompletion_VariableExecutionTime(t *testing.T) {\n\ts, err := NewScheduler()\n\trequire.NoError(t, err)\n\tdefer func() { _ = s.Shutdown() }()\n\n\tvar mu sync.Mutex\n\texecutions := []struct {\n\t\tstartTime    time.Time\n\t\tcompleteTime time.Time\n\t\texecutionDur time.Duration\n\t}{}\n\n\texecutionTimes := []time.Duration{\n\t\t1 * time.Second,\n\t\t3 * time.Second,\n\t\t500 * time.Millisecond,\n\t}\n\tcurrentExecution := atomic.Int32{}\n\tscheduledInterval := 4 * time.Second\n\n\t_, err = s.NewJob(\n\t\tDurationJob(scheduledInterval),\n\t\tNewTask(func() {\n\t\t\tidx := int(currentExecution.Add(1)) - 1\n\t\t\tif idx >= len(executionTimes) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tstart := time.Now()\n\t\t\texecutionTime := executionTimes[idx]\n\t\t\ttime.Sleep(executionTime)\n\t\t\tcomplete := time.Now()\n\n\t\t\tmu.Lock()\n\t\t\texecutions = append(executions, struct {\n\t\t\t\tstartTime    time.Time\n\t\t\t\tcompleteTime time.Time\n\t\t\t\texecutionDur time.Duration\n\t\t\t}{start, complete, executionTime})\n\t\t\tmu.Unlock()\n\t\t}),\n\t\tWithIntervalFromCompletion(),\n\t)\n\trequire.NoError(t, err)\n\n\ts.Start()\n\n\t// Wait for all 3 executions\n\t// Execution 1: 0s-1s, wait 4s → next at 5s\n\t// Execution 2: 5s-8s, wait 4s → next at 12s\n\t// Execution 3: 12s-12.5s\n\ttime.Sleep(15 * time.Second)\n\n\tmu.Lock()\n\tdefer mu.Unlock()\n\n\trequire.GreaterOrEqual(t, len(executions), 2, \"Expected at least 2 executions\")\n\n\tfor i := 1; i < len(executions); i++ {\n\t\tprev := executions[i-1]\n\t\tcurr := executions[i]\n\n\t\trestPeriod := curr.startTime.Sub(prev.completeTime)\n\n\t\tassert.InDelta(t, scheduledInterval.Seconds(), restPeriod.Seconds(), 0.5,\n\t\t\t\"Rest period should be consistent regardless of execution time\")\n\t}\n}\n\nfunc TestWithIntervalFromCompletion_LongRunningJob(t *testing.T) {\n\ts, err := NewScheduler()\n\trequire.NoError(t, err)\n\tdefer func() { _ = s.Shutdown() }()\n\n\tvar mu sync.Mutex\n\texecutions := []struct {\n\t\tstartTime    time.Time\n\t\tcompleteTime time.Time\n\t}{}\n\n\tjobExecutionTime := 6 * time.Second\n\tscheduledInterval := 3 * time.Second\n\n\t_, err = s.NewJob(\n\t\tDurationJob(scheduledInterval),\n\t\tNewTask(func() {\n\t\t\tstart := time.Now()\n\t\t\ttime.Sleep(jobExecutionTime)\n\t\t\tcomplete := time.Now()\n\n\t\t\tmu.Lock()\n\t\t\texecutions = append(executions, struct {\n\t\t\t\tstartTime    time.Time\n\t\t\t\tcompleteTime time.Time\n\t\t\t}{start, complete})\n\t\t\tmu.Unlock()\n\t\t}),\n\t\tWithIntervalFromCompletion(),\n\t\tWithSingletonMode(LimitModeReschedule),\n\t)\n\trequire.NoError(t, err)\n\n\ts.Start()\n\n\t// Wait for 2 executions\n\t// Execution 1: 0s-6s, wait 3s → next at 9s\n\t// Execution 2: 9s-15s, wait 3s → next at 18s\n\t// Need to wait at least 16 seconds for 2 executions + buffer\n\ttime.Sleep(22 * time.Second)\n\n\tmu.Lock()\n\tdefer mu.Unlock()\n\n\trequire.GreaterOrEqual(t, len(executions), 2, \"Expected at least 2 executions\")\n\n\tif len(executions) < 2 {\n\t\tt.Logf(\"Only got %d execution(s), skipping gap assertion\", len(executions))\n\t\treturn\n\t}\n\n\tprev := executions[0]\n\tcurr := executions[1]\n\n\tcompletionGap := curr.startTime.Sub(prev.completeTime)\n\n\tassert.InDelta(t, scheduledInterval.Seconds(), completionGap.Seconds(), 0.5,\n\t\t\"Gap should be the full interval even when execution time exceeds interval\")\n}\n\nfunc TestWithIntervalFromCompletion_ComparedToDefault(t *testing.T) {\n\tjobExecutionTime := 2 * time.Second\n\tscheduledInterval := 5 * time.Second\n\n\tt.Run(\"default behavior - interval from scheduled time\", func(t *testing.T) {\n\t\ts, err := NewScheduler()\n\t\trequire.NoError(t, err)\n\t\tdefer func() { _ = s.Shutdown() }()\n\n\t\tvar mu sync.Mutex\n\t\texecutions := []struct {\n\t\t\tstartTime    time.Time\n\t\t\tcompleteTime time.Time\n\t\t}{}\n\n\t\t_, err = s.NewJob(\n\t\t\tDurationJob(scheduledInterval),\n\t\t\tNewTask(func() {\n\t\t\t\tstart := time.Now()\n\t\t\t\ttime.Sleep(jobExecutionTime)\n\t\t\t\tcomplete := time.Now()\n\n\t\t\t\tmu.Lock()\n\t\t\t\texecutions = append(executions, struct {\n\t\t\t\t\tstartTime    time.Time\n\t\t\t\t\tcompleteTime time.Time\n\t\t\t\t}{start, complete})\n\t\t\t\tmu.Unlock()\n\t\t\t}),\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\ts.Start()\n\t\ttime.Sleep(13 * time.Second)\n\n\t\tmu.Lock()\n\t\tdefer mu.Unlock()\n\n\t\trequire.GreaterOrEqual(t, len(executions), 2, \"Expected at least 2 executions\")\n\n\t\tprev := executions[0]\n\t\tcurr := executions[1]\n\t\tcompletionGap := curr.startTime.Sub(prev.completeTime)\n\n\t\texpectedGap := scheduledInterval - jobExecutionTime\n\t\tassert.InDelta(t, expectedGap.Seconds(), completionGap.Seconds(), 0.5,\n\t\t\t\"Default behavior: gap should be interval minus execution time\")\n\t})\n\n\tt.Run(\"with intervalFromCompletion - interval from completion time\", func(t *testing.T) {\n\t\ts, err := NewScheduler()\n\t\trequire.NoError(t, err)\n\t\tdefer func() { _ = s.Shutdown() }()\n\n\t\tvar mu sync.Mutex\n\t\texecutions := []struct {\n\t\t\tstartTime    time.Time\n\t\t\tcompleteTime time.Time\n\t\t}{}\n\n\t\t_, err = s.NewJob(\n\t\t\tDurationJob(scheduledInterval),\n\t\t\tNewTask(func() {\n\t\t\t\tstart := time.Now()\n\t\t\t\ttime.Sleep(jobExecutionTime)\n\t\t\t\tcomplete := time.Now()\n\n\t\t\t\tmu.Lock()\n\t\t\t\texecutions = append(executions, struct {\n\t\t\t\t\tstartTime    time.Time\n\t\t\t\t\tcompleteTime time.Time\n\t\t\t\t}{start, complete})\n\t\t\t\tmu.Unlock()\n\t\t\t}),\n\t\t\tWithIntervalFromCompletion(),\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\ts.Start()\n\t\ttime.Sleep(15 * time.Second)\n\n\t\tmu.Lock()\n\t\tdefer mu.Unlock()\n\n\t\trequire.GreaterOrEqual(t, len(executions), 2, \"Expected at least 2 executions\")\n\n\t\tprev := executions[0]\n\t\tcurr := executions[1]\n\t\tcompletionGap := curr.startTime.Sub(prev.completeTime)\n\n\t\tassert.InDelta(t, scheduledInterval.Seconds(), completionGap.Seconds(), 0.5,\n\t\t\t\"With intervalFromCompletion: gap should be the full interval\")\n\t})\n}\n\nfunc TestWithIntervalFromCompletion_DurationRandomJob(t *testing.T) {\n\ts, err := NewScheduler()\n\trequire.NoError(t, err)\n\tdefer func() { _ = s.Shutdown() }()\n\n\tvar mu sync.Mutex\n\texecutions := []struct {\n\t\tstartTime    time.Time\n\t\tcompleteTime time.Time\n\t}{}\n\n\tjobExecutionTime := 1 * time.Second\n\tminInterval := 3 * time.Second\n\tmaxInterval := 4 * time.Second\n\n\t_, err = s.NewJob(\n\t\tDurationRandomJob(minInterval, maxInterval),\n\t\tNewTask(func() {\n\t\t\tstart := time.Now()\n\t\t\ttime.Sleep(jobExecutionTime)\n\t\t\tcomplete := time.Now()\n\n\t\t\tmu.Lock()\n\t\t\texecutions = append(executions, struct {\n\t\t\t\tstartTime    time.Time\n\t\t\t\tcompleteTime time.Time\n\t\t\t}{start, complete})\n\t\t\tmu.Unlock()\n\t\t}),\n\t\tWithIntervalFromCompletion(),\n\t)\n\trequire.NoError(t, err)\n\n\ts.Start()\n\n\ttime.Sleep(15 * time.Second)\n\n\tmu.Lock()\n\tdefer mu.Unlock()\n\n\trequire.GreaterOrEqual(t, len(executions), 2, \"Expected at least 2 executions\")\n\n\tfor i := 1; i < len(executions); i++ {\n\t\tprev := executions[i-1]\n\t\tcurr := executions[i]\n\n\t\trestPeriod := curr.startTime.Sub(prev.completeTime)\n\t\tassert.GreaterOrEqual(t, restPeriod.Seconds(), minInterval.Seconds()-0.5,\n\t\t\t\"Rest period should be at least minInterval\")\n\t\tassert.LessOrEqual(t, restPeriod.Seconds(), maxInterval.Seconds()+0.5,\n\t\t\t\"Rest period should be at most maxInterval\")\n\t}\n}\n\nfunc TestWithIntervalFromCompletion_FirstRun(t *testing.T) {\n\ts, err := NewScheduler()\n\trequire.NoError(t, err)\n\tdefer func() { _ = s.Shutdown() }()\n\n\tvar mu sync.Mutex\n\tvar firstRunTime time.Time\n\n\t_, err = s.NewJob(\n\t\tDurationJob(5*time.Second),\n\t\tNewTask(func() {\n\t\t\tmu.Lock()\n\t\t\tif firstRunTime.IsZero() {\n\t\t\t\tfirstRunTime = time.Now()\n\t\t\t}\n\t\t\tmu.Unlock()\n\t\t}),\n\t\tWithIntervalFromCompletion(),\n\t\tWithStartAt(WithStartImmediately()),\n\t)\n\trequire.NoError(t, err)\n\n\tstartTime := time.Now()\n\ts.Start()\n\n\ttime.Sleep(1 * time.Second)\n\n\tmu.Lock()\n\tdefer mu.Unlock()\n\n\trequire.False(t, firstRunTime.IsZero(), \"Job should have run at least once\")\n\n\ttimeSinceStart := firstRunTime.Sub(startTime)\n\tassert.Less(t, timeSinceStart.Seconds(), 1.0,\n\t\t\"First run should happen quickly with WithStartImmediately\")\n}\n\nfunc TestJob_NextRun_MultipleJobsSimultaneously(t *testing.T) {\n\t// This test reproduces the bug where multiple jobs completing simultaneously\n\t// would cause NextRun() to return stale values due to race conditions in\n\t// nextScheduled cleanup.\n\n\ttestTime := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)\n\tfakeClock := clockwork.NewFakeClockAt(testTime)\n\n\ts := newTestScheduler(t,\n\t\tWithClock(fakeClock),\n\t\tWithLocation(time.UTC),\n\t)\n\n\tjobsCompleted := make(chan struct{}, 4)\n\n\t// Create multiple jobs with different intervals that will complete around the same time\n\tjob1, err := s.NewJob(\n\t\tDurationJob(1*time.Minute),\n\t\tNewTask(func() {\n\t\t\tjobsCompleted <- struct{}{}\n\t\t}),\n\t\tWithName(\"job1\"),\n\t\tWithStartAt(WithStartImmediately()),\n\t)\n\trequire.NoError(t, err)\n\n\tjob2, err := s.NewJob(\n\t\tDurationJob(2*time.Minute),\n\t\tNewTask(func() {\n\t\t\tjobsCompleted <- struct{}{}\n\t\t}),\n\t\tWithName(\"job2\"),\n\t\tWithStartAt(WithStartImmediately()),\n\t)\n\trequire.NoError(t, err)\n\n\tjob3, err := s.NewJob(\n\t\tDurationJob(3*time.Minute),\n\t\tNewTask(func() {\n\t\t\tjobsCompleted <- struct{}{}\n\t\t}),\n\t\tWithName(\"job3\"),\n\t\tWithStartAt(WithStartImmediately()),\n\t)\n\trequire.NoError(t, err)\n\n\tjob4, err := s.NewJob(\n\t\tDurationJob(4*time.Minute),\n\t\tNewTask(func() {\n\t\t\tjobsCompleted <- struct{}{}\n\t\t}),\n\t\tWithName(\"job4\"),\n\t\tWithStartAt(WithStartImmediately()),\n\t)\n\trequire.NoError(t, err)\n\n\ts.Start()\n\n\t// Wait for all 4 jobs to complete their immediate run\n\tfor i := 0; i < 4; i++ {\n\t\t<-jobsCompleted\n\t}\n\n\t// Give the scheduler time to process the completions and reschedule\n\ttime.Sleep(50 * time.Millisecond)\n\n\t// Verify that NextRun() returns the correct next scheduled time for each job\n\t// and not a stale value from the just-completed run\n\n\tnextRun1, err := job1.NextRun()\n\trequire.NoError(t, err)\n\tassert.Equal(t, testTime.Add(1*time.Minute), nextRun1, \"job1 NextRun should be 1 minute from start\")\n\n\tnextRun2, err := job2.NextRun()\n\trequire.NoError(t, err)\n\tassert.Equal(t, testTime.Add(2*time.Minute), nextRun2, \"job2 NextRun should be 2 minutes from start\")\n\n\tnextRun3, err := job3.NextRun()\n\trequire.NoError(t, err)\n\tassert.Equal(t, testTime.Add(3*time.Minute), nextRun3, \"job3 NextRun should be 3 minutes from start\")\n\n\tnextRun4, err := job4.NextRun()\n\trequire.NoError(t, err)\n\tassert.Equal(t, testTime.Add(4*time.Minute), nextRun4, \"job4 NextRun should be 4 minutes from start\")\n\n\t// Advance time to trigger job1's next run\n\tfakeClock.Advance(1 * time.Minute)\n\n\t// Wait for job1 to complete\n\t<-jobsCompleted\n\ttime.Sleep(50 * time.Millisecond)\n\n\t// After job1's second run, it should be scheduled for +2 minutes from start\n\tnextRun1, err = job1.NextRun()\n\trequire.NoError(t, err)\n\tassert.Equal(t, testTime.Add(2*time.Minute), nextRun1, \"job1 NextRun should be 2 minutes from start after first interval\")\n\n\trequire.NoError(t, s.Shutdown())\n}\n\nfunc TestJob_NextRun_ConcurrentCompletions(t *testing.T) {\n\t// This test verifies that when multiple jobs complete at exactly the same time,\n\t// their NextRun() values are correctly updated without race conditions.\n\n\ttestTime := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)\n\tfakeClock := clockwork.NewFakeClockAt(testTime)\n\n\ts := newTestScheduler(t,\n\t\tWithClock(fakeClock),\n\t\tWithLocation(time.UTC), // Set scheduler to use UTC to match our test time\n\t)\n\n\tvar wg sync.WaitGroup\n\tjobCompletionBarrier := make(chan struct{})\n\n\t// Create jobs that will all complete at the same instant\n\tcreateJob := func(name string, interval time.Duration) Job {\n\t\tjob, err := s.NewJob(\n\t\t\tDurationJob(interval),\n\t\t\tNewTask(func() {\n\t\t\t\twg.Done()\n\t\t\t\t<-jobCompletionBarrier // Wait until all jobs are ready to complete\n\t\t\t}),\n\t\t\tWithName(name),\n\t\t\tWithStartAt(WithStartImmediately()),\n\t\t)\n\t\trequire.NoError(t, err)\n\t\treturn job\n\t}\n\n\twg.Add(4)\n\tjob1 := createJob(\"concurrent-job1\", 1*time.Minute)\n\tjob2 := createJob(\"concurrent-job2\", 2*time.Minute)\n\tjob3 := createJob(\"concurrent-job3\", 3*time.Minute)\n\tjob4 := createJob(\"concurrent-job4\", 4*time.Minute)\n\n\ts.Start()\n\n\twg.Wait()\n\tclose(jobCompletionBarrier)\n\n\t// Give the scheduler time to process all completions\n\ttime.Sleep(100 * time.Millisecond)\n\n\t// Verify NextRun() for all jobs concurrently to stress test the race condition\n\tvar testWg sync.WaitGroup\n\ttestWg.Add(4)\n\n\tgo func() {\n\t\tdefer testWg.Done()\n\t\tfor i := 0; i < 10; i++ {\n\t\t\tnextRun, err := job1.NextRun()\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, testTime.Add(1*time.Minute), nextRun)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tdefer testWg.Done()\n\t\tfor i := 0; i < 10; i++ {\n\t\t\tnextRun, err := job2.NextRun()\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, testTime.Add(2*time.Minute), nextRun)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tdefer testWg.Done()\n\t\tfor i := 0; i < 10; i++ {\n\t\t\tnextRun, err := job3.NextRun()\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, testTime.Add(3*time.Minute), nextRun)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tdefer testWg.Done()\n\t\tfor i := 0; i < 10; i++ {\n\t\t\tnextRun, err := job4.NextRun()\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, testTime.Add(4*time.Minute), nextRun)\n\t\t}\n\t}()\n\n\ttestWg.Wait()\n\trequire.NoError(t, s.Shutdown())\n}\n"
  },
  {
    "path": "logger.go",
    "content": "//go:generate mockgen -destination=mocks/logger.go -package=gocronmocks . Logger\npackage gocron\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n)\n\n// Logger is the interface that wraps the basic logging methods\n// used by gocron. The methods are modeled after the standard\n// library slog package. The default logger is a no-op logger.\n// To enable logging, use one of the provided New*Logger functions\n// or implement your own Logger. The actual level of Log that is logged\n// is handled by the implementation.\ntype Logger interface {\n\tDebug(msg string, args ...any)\n\tError(msg string, args ...any)\n\tInfo(msg string, args ...any)\n\tWarn(msg string, args ...any)\n}\n\nvar _ Logger = (*noOpLogger)(nil)\n\ntype noOpLogger struct{}\n\nfunc (l noOpLogger) Debug(_ string, _ ...any) {}\nfunc (l noOpLogger) Error(_ string, _ ...any) {}\nfunc (l noOpLogger) Info(_ string, _ ...any)  {}\nfunc (l noOpLogger) Warn(_ string, _ ...any)  {}\n\nvar _ Logger = (*logger)(nil)\n\n// LogLevel is the level of logging that should be logged\n// when using the basic NewLogger.\ntype LogLevel int\n\n// The different log levels that can be used.\nconst (\n\tLogLevelError LogLevel = iota\n\tLogLevelWarn\n\tLogLevelInfo\n\tLogLevelDebug\n)\n\ntype logger struct {\n\tlog   *log.Logger\n\tlevel LogLevel\n}\n\n// NewLogger returns a new Logger that logs at the given level.\nfunc NewLogger(level LogLevel) Logger {\n\tl := log.New(os.Stdout, \"\", log.LstdFlags)\n\treturn &logger{\n\t\tlog:   l,\n\t\tlevel: level,\n\t}\n}\n\nfunc (l *logger) Debug(msg string, args ...any) {\n\tif l.level < LogLevelDebug {\n\t\treturn\n\t}\n\tl.log.Printf(\"DEBUG: %s%s\\n\", msg, logFormatArgs(args...))\n}\n\nfunc (l *logger) Error(msg string, args ...any) {\n\tif l.level < LogLevelError {\n\t\treturn\n\t}\n\tl.log.Printf(\"ERROR: %s%s\\n\", msg, logFormatArgs(args...))\n}\n\nfunc (l *logger) Info(msg string, args ...any) {\n\tif l.level < LogLevelInfo {\n\t\treturn\n\t}\n\tl.log.Printf(\"INFO: %s%s\\n\", msg, logFormatArgs(args...))\n}\n\nfunc (l *logger) Warn(msg string, args ...any) {\n\tif l.level < LogLevelWarn {\n\t\treturn\n\t}\n\tl.log.Printf(\"WARN: %s%s\\n\", msg, logFormatArgs(args...))\n}\n\nfunc logFormatArgs(args ...any) string {\n\tif len(args) == 0 {\n\t\treturn \"\"\n\t}\n\tif len(args)%2 != 0 {\n\t\treturn \", \" + fmt.Sprint(args...)\n\t}\n\tvar pairs []string\n\tfor i := 0; i < len(args); i += 2 {\n\t\tpairs = append(pairs, fmt.Sprintf(\"%s=%v\", args[i], args[i+1]))\n\t}\n\treturn \", \" + strings.Join(pairs, \", \")\n}\n"
  },
  {
    "path": "logger_test.go",
    "content": "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 TestNoOpLogger(_ *testing.T) {\n\tnoOp := noOpLogger{}\n\tnoOp.Debug(\"debug\", \"arg1\", \"arg2\")\n\tnoOp.Error(\"error\", \"arg1\", \"arg2\")\n\tnoOp.Info(\"info\", \"arg1\", \"arg2\")\n\tnoOp.Warn(\"warn\", \"arg1\", \"arg2\")\n}\n\nfunc TestNewLogger(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tlevel LogLevel\n\t}{\n\t\t{\n\t\t\t\"debug\",\n\t\t\tLogLevelDebug,\n\t\t},\n\t\t{\n\t\t\t\"info\",\n\t\t\tLogLevelInfo,\n\t\t},\n\t\t{\n\t\t\t\"warn\",\n\t\t\tLogLevelWarn,\n\t\t},\n\t\t{\n\t\t\t\"error\",\n\t\t\tLogLevelError,\n\t\t},\n\t\t{\n\t\t\t\"Less than error\",\n\t\t\t-1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar results bytes.Buffer\n\t\t\tl := &logger{\n\t\t\t\tlevel: tt.level,\n\t\t\t\tlog:   log.New(&results, \"\", log.LstdFlags),\n\t\t\t}\n\n\t\t\tvar noArgs []any\n\t\t\toneArg := []any{\"arg1\"}\n\t\t\ttwoArgs := []any{\"arg1\", \"arg2\"}\n\t\t\tvar noArgsStr []string\n\t\t\toneArgStr := []string{\"arg1\"}\n\t\t\ttwoArgsStr := []string{\"arg1\", \"arg2\"}\n\n\t\t\tfor _, args := range []struct {\n\t\t\t\targsAny []any\n\t\t\t\targsStr []string\n\t\t\t}{\n\t\t\t\t{noArgs, noArgsStr},\n\t\t\t\t{oneArg, oneArgStr},\n\t\t\t\t{twoArgs, twoArgsStr},\n\t\t\t} {\n\t\t\t\tl.Debug(\"debug\", args.argsAny...)\n\t\t\t\tif tt.level >= LogLevelDebug {\n\t\t\t\t\tr := results.String()\n\t\t\t\t\tassert.Contains(t, r, \"DEBUG: debug\")\n\t\t\t\t\tassert.Contains(t, r, strings.Join(args.argsStr, \"=\"))\n\t\t\t\t} else {\n\t\t\t\t\tassert.Empty(t, results.String())\n\t\t\t\t}\n\t\t\t\tresults.Reset()\n\n\t\t\t\tl.Info(\"info\", args.argsAny...)\n\t\t\t\tif tt.level >= LogLevelInfo {\n\t\t\t\t\tr := results.String()\n\t\t\t\t\tassert.Contains(t, r, \"INFO: info\")\n\t\t\t\t\tassert.Contains(t, r, strings.Join(args.argsStr, \"=\"))\n\t\t\t\t} else {\n\t\t\t\t\tassert.Empty(t, results.String())\n\t\t\t\t}\n\t\t\t\tresults.Reset()\n\n\t\t\t\tl.Warn(\"warn\", args.argsAny...)\n\t\t\t\tif tt.level >= LogLevelWarn {\n\t\t\t\t\tr := results.String()\n\t\t\t\t\tassert.Contains(t, r, \"WARN: warn\")\n\t\t\t\t\tassert.Contains(t, r, strings.Join(args.argsStr, \"=\"))\n\t\t\t\t} else {\n\t\t\t\t\tassert.Empty(t, results.String())\n\t\t\t\t}\n\t\t\t\tresults.Reset()\n\n\t\t\t\tl.Error(\"error\", args.argsAny...)\n\t\t\t\tif tt.level >= LogLevelError {\n\t\t\t\t\tr := results.String()\n\t\t\t\t\tassert.Contains(t, r, \"ERROR: error\")\n\t\t\t\t\tassert.Contains(t, r, strings.Join(args.argsStr, \"=\"))\n\t\t\t\t} else {\n\t\t\t\t\tassert.Empty(t, results.String())\n\t\t\t\t}\n\t\t\t\tresults.Reset()\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "migration_v1_to_v2.md",
    "content": "# Migration Guide: `gocron` v1 → v2\n\nThis 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).\nVersion 2 is a major rewrite focusing on improving the internals of gocron, while also enhancing the user interfaces and error handling.\nAll major functionality has been ported over.\n\n---\n\n## Table of Contents\n\n- [Overview of Major Changes](#overview-of-major-changes)\n- [Installation](#installation)\n- [API Changes](#api-changes)\n- [Scheduler Creation](#scheduler-creation)\n- [Job Definition](#job-definition)\n- [Starting and Stopping the Scheduler](#starting-and-stopping-the-scheduler)\n- [Error Handling](#error-handling)\n- [Distributed Scheduling](#distributed-scheduling)\n- [Examples Migration](#examples-migration)\n- [Testing and Validation](#testing-and-validation)\n- [Troubleshooting](#troubleshooting)\n- [References](#references)\n\n---\n\n## Overview of Major Changes\n\n- **Breaking API changes**: All major interfaces and types have changed.\n- **Improved error reporting**: Most functions now return errors.\n- **Job IDs and cancellation**: Jobs have unique IDs and can be cancelled.\n- **Distributed and monitored scheduling**: Built-in support for distributed schedulers and job monitors.\n- **Context and logging enhancements**: Improved support for cancellation, context, and custom logging interfaces.\n\n---\n\n## Installation\n\nUpdate your dependency to v2:\n\n```sh\ngo get github.com/go-co-op/gocron/v2\n```\n\n**Note:** The import path is `github.com/go-co-op/gocron/v2`.\n\n---\n\n## API Changes\n\n### 1. Scheduler Creation\n\n**v1:**\n```go\nimport \"github.com/go-co-op/gocron\"\n\ns := gocron.NewScheduler(time.UTC)\n```\n\n**v2:**\n```go\nimport \"github.com/go-co-op/gocron/v2\"\n\ns, err := gocron.NewScheduler()\nif err != nil { panic(err) }\n```\n- **v2** returns an error on creation.\n- **v2** does not require a location/timezone argument. Use `WithLocation()` if needed.\n\n---\n\n### 2. Job Creation\n\n**v1:**\n```go\ns.Every(1).Second().Do(taskFunc)\n```\n\n**v2:**\n```go\nj, err := s.NewJob(\n    gocron.DurationJob(1*time.Second),\n    gocron.NewTask(taskFunc),\n)\nif err != nil { panic(err) }\n```\n- **v2** uses explicit job types (`DurationJob`, `CronJob`, etc).\n- **v2** jobs have unique IDs: `j.ID()`.\n- **v2** returns an error on job creation.\n\n#### Cron Expressions\n\n**v1:**\n```go\ns.Cron(\"*/5 * * * *\").Do(taskFunc)\n```\n\n**v2:**\n```go\nj, err := s.NewJob(\n    gocron.CronJob(\"*/5 * * * *\"),\n    gocron.NewTask(taskFunc),\n)\n```\n\n#### Arguments\n\n**v1:**\n```go\ns.Every(1).Second().Do(taskFunc, arg1, arg2)\n```\n\n**v2:**\n```go\nj, err := s.NewJob(\n    gocron.DurationJob(1*time.Second),\n    gocron.NewTask(taskFunc, arg1, arg2),\n)\n```\n\n---\n\n### 3. Starting and Stopping the Scheduler\n\n**v1:**\n```go\ns.StartAsync()\ns.Stop()\n```\n\n**v2:**\n```go\ns.Start()\ns.Shutdown()\n```\n\n- Always call `Shutdown()` for graceful cleanup.\n\n---\n\n### 4. Error Handling\n\n- Most v2 methods return errors. Always check `err`.\n- Use `errors.go` for error definitions.\n\n---\n\n## References\n\n- [v2 API Documentation](https://pkg.go.dev/github.com/go-co-op/gocron/v2)\n- [Examples](https://pkg.go.dev/github.com/go-co-op/gocron/v2#pkg-examples)\n- [Release Notes](https://github.com/go-co-op/gocron/releases)\n\n---\n\n**If you encounter issues, open a GitHub Issue or consider contributing a fix by checking out the [CONTRIBUTING.md](CONTRIBUTING.md) guide.**\n"
  },
  {
    "path": "mocks/README.md",
    "content": "# 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\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-co-op/gocron/mocks/v2\"\n\t\"github.com/go-co-op/gocron/v2\"\n\t\"go.uber.org/mock/gomock\"\n)\n\nfunc myFunc(s gocron.Scheduler) {\n\ts.Start()\n\t_ = s.Shutdown()\n}\n\nfunc TestMyFunc(t *testing.T) {\n\tctrl := gomock.NewController(t)\n\ts := gocronmocks.NewMockScheduler(ctrl)\n\ts.EXPECT().Start().Times(1)\n\ts.EXPECT().Shutdown().Times(1).Return(nil)\n\n\tmyFunc(s)\n}\n\n```\n"
  },
  {
    "path": "mocks/distributed.go",
    "content": "// Code generated by MockGen. DO NOT EDIT.\n// Source: github.com/go-co-op/gocron/v2 (interfaces: Elector,Locker,Lock)\n//\n// Generated by this command:\n//\n//\tmockgen -destination=mocks/distributed.go -package=gocronmocks . Elector,Locker,Lock\n//\n\n// Package gocronmocks is a generated GoMock package.\npackage gocronmocks\n\nimport (\n\tcontext \"context\"\n\treflect \"reflect\"\n\n\tv2 \"github.com/go-co-op/gocron/v2\"\n\tgomock \"go.uber.org/mock/gomock\"\n)\n\n// MockElector is a mock of Elector interface.\ntype MockElector struct {\n\tctrl     *gomock.Controller\n\trecorder *MockElectorMockRecorder\n\tisgomock struct{}\n}\n\n// MockElectorMockRecorder is the mock recorder for MockElector.\ntype MockElectorMockRecorder struct {\n\tmock *MockElector\n}\n\n// NewMockElector creates a new mock instance.\nfunc NewMockElector(ctrl *gomock.Controller) *MockElector {\n\tmock := &MockElector{ctrl: ctrl}\n\tmock.recorder = &MockElectorMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use.\nfunc (m *MockElector) EXPECT() *MockElectorMockRecorder {\n\treturn m.recorder\n}\n\n// IsLeader mocks base method.\nfunc (m *MockElector) IsLeader(arg0 context.Context) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"IsLeader\", arg0)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// IsLeader indicates an expected call of IsLeader.\nfunc (mr *MockElectorMockRecorder) IsLeader(arg0 any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"IsLeader\", reflect.TypeOf((*MockElector)(nil).IsLeader), arg0)\n}\n\n// MockLocker is a mock of Locker interface.\ntype MockLocker struct {\n\tctrl     *gomock.Controller\n\trecorder *MockLockerMockRecorder\n\tisgomock struct{}\n}\n\n// MockLockerMockRecorder is the mock recorder for MockLocker.\ntype MockLockerMockRecorder struct {\n\tmock *MockLocker\n}\n\n// NewMockLocker creates a new mock instance.\nfunc NewMockLocker(ctrl *gomock.Controller) *MockLocker {\n\tmock := &MockLocker{ctrl: ctrl}\n\tmock.recorder = &MockLockerMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use.\nfunc (m *MockLocker) EXPECT() *MockLockerMockRecorder {\n\treturn m.recorder\n}\n\n// Lock mocks base method.\nfunc (m *MockLocker) Lock(ctx context.Context, key string) (v2.Lock, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Lock\", ctx, key)\n\tret0, _ := ret[0].(v2.Lock)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// Lock indicates an expected call of Lock.\nfunc (mr *MockLockerMockRecorder) Lock(ctx, key any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Lock\", reflect.TypeOf((*MockLocker)(nil).Lock), ctx, key)\n}\n\n// MockLock is a mock of Lock interface.\ntype MockLock struct {\n\tctrl     *gomock.Controller\n\trecorder *MockLockMockRecorder\n\tisgomock struct{}\n}\n\n// MockLockMockRecorder is the mock recorder for MockLock.\ntype MockLockMockRecorder struct {\n\tmock *MockLock\n}\n\n// NewMockLock creates a new mock instance.\nfunc NewMockLock(ctrl *gomock.Controller) *MockLock {\n\tmock := &MockLock{ctrl: ctrl}\n\tmock.recorder = &MockLockMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use.\nfunc (m *MockLock) EXPECT() *MockLockMockRecorder {\n\treturn m.recorder\n}\n\n// Unlock mocks base method.\nfunc (m *MockLock) Unlock(ctx context.Context) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Unlock\", ctx)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// Unlock indicates an expected call of Unlock.\nfunc (mr *MockLockMockRecorder) Unlock(ctx any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Unlock\", reflect.TypeOf((*MockLock)(nil).Unlock), ctx)\n}\n"
  },
  {
    "path": "mocks/go.mod",
    "content": "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/uuid v1.6.0\n\tgo.uber.org/mock v0.4.0\n)\n\nrequire (\n\tgithub.com/jonboulle/clockwork v0.4.0 // indirect\n\tgithub.com/robfig/cron/v3 v3.0.1 // indirect\n\tgolang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect\n)\n"
  },
  {
    "path": "mocks/go.sum",
    "content": "github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/go-co-op/gocron/v2 v2.2.10 h1:o6u+RfvT5rBa39gmsA5cqPPLXTa+Ai70m7EGgHQoXyg=\ngithub.com/go-co-op/gocron/v2 v2.2.10/go.mod h1:mZx3gMSlFnb97k3hRqX3+GdlG3+DUwTh6B8fnsTScXg=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=\ngithub.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=\ngithub.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=\ngo.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=\ngolang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY=\ngolang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\n"
  },
  {
    "path": "mocks/job.go",
    "content": "// Code generated by MockGen. DO NOT EDIT.\n// Source: github.com/go-co-op/gocron/v2 (interfaces: Job)\n//\n// Generated by this command:\n//\n//\tmockgen -destination=mocks/job.go -package=gocronmocks . Job\n//\n\n// Package gocronmocks is a generated GoMock package.\npackage gocronmocks\n\nimport (\n\treflect \"reflect\"\n\ttime \"time\"\n\n\tuuid \"github.com/google/uuid\"\n\tgomock \"go.uber.org/mock/gomock\"\n)\n\n// MockJob is a mock of Job interface.\ntype MockJob struct {\n\tctrl     *gomock.Controller\n\trecorder *MockJobMockRecorder\n\tisgomock struct{}\n}\n\n// MockJobMockRecorder is the mock recorder for MockJob.\ntype MockJobMockRecorder struct {\n\tmock *MockJob\n}\n\n// NewMockJob creates a new mock instance.\nfunc NewMockJob(ctrl *gomock.Controller) *MockJob {\n\tmock := &MockJob{ctrl: ctrl}\n\tmock.recorder = &MockJobMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use.\nfunc (m *MockJob) EXPECT() *MockJobMockRecorder {\n\treturn m.recorder\n}\n\n// ID mocks base method.\nfunc (m *MockJob) ID() uuid.UUID {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ID\")\n\tret0, _ := ret[0].(uuid.UUID)\n\treturn ret0\n}\n\n// ID indicates an expected call of ID.\nfunc (mr *MockJobMockRecorder) ID() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ID\", reflect.TypeOf((*MockJob)(nil).ID))\n}\n\n// LastRun mocks base method.\nfunc (m *MockJob) LastRun() (time.Time, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"LastRun\")\n\tret0, _ := ret[0].(time.Time)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// LastRun indicates an expected call of LastRun.\nfunc (mr *MockJobMockRecorder) LastRun() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"LastRun\", reflect.TypeOf((*MockJob)(nil).LastRun))\n}\n\n// Name mocks base method.\nfunc (m *MockJob) Name() string {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Name\")\n\tret0, _ := ret[0].(string)\n\treturn ret0\n}\n\n// Name indicates an expected call of Name.\nfunc (mr *MockJobMockRecorder) Name() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Name\", reflect.TypeOf((*MockJob)(nil).Name))\n}\n\n// NextRun mocks base method.\nfunc (m *MockJob) NextRun() (time.Time, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"NextRun\")\n\tret0, _ := ret[0].(time.Time)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// NextRun indicates an expected call of NextRun.\nfunc (mr *MockJobMockRecorder) NextRun() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"NextRun\", reflect.TypeOf((*MockJob)(nil).NextRun))\n}\n\n// NextRuns mocks base method.\nfunc (m *MockJob) NextRuns(arg0 int) ([]time.Time, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"NextRuns\", arg0)\n\tret0, _ := ret[0].([]time.Time)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// NextRuns indicates an expected call of NextRuns.\nfunc (mr *MockJobMockRecorder) NextRuns(arg0 any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"NextRuns\", reflect.TypeOf((*MockJob)(nil).NextRuns), arg0)\n}\n\n// RunNow mocks base method.\nfunc (m *MockJob) RunNow() error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"RunNow\")\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// RunNow indicates an expected call of RunNow.\nfunc (mr *MockJobMockRecorder) RunNow() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"RunNow\", reflect.TypeOf((*MockJob)(nil).RunNow))\n}\n\n// Tags mocks base method.\nfunc (m *MockJob) Tags() []string {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Tags\")\n\tret0, _ := ret[0].([]string)\n\treturn ret0\n}\n\n// Tags indicates an expected call of Tags.\nfunc (mr *MockJobMockRecorder) Tags() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Tags\", reflect.TypeOf((*MockJob)(nil).Tags))\n}\n"
  },
  {
    "path": "mocks/logger.go",
    "content": "// Code generated by MockGen. DO NOT EDIT.\n// Source: github.com/go-co-op/gocron/v2 (interfaces: Logger)\n//\n// Generated by this command:\n//\n//\tmockgen -destination=mocks/logger.go -package=gocronmocks . Logger\n//\n\n// Package gocronmocks is a generated GoMock package.\npackage gocronmocks\n\nimport (\n\treflect \"reflect\"\n\n\tgomock \"go.uber.org/mock/gomock\"\n)\n\n// MockLogger is a mock of Logger interface.\ntype MockLogger struct {\n\tctrl     *gomock.Controller\n\trecorder *MockLoggerMockRecorder\n\tisgomock struct{}\n}\n\n// MockLoggerMockRecorder is the mock recorder for MockLogger.\ntype MockLoggerMockRecorder struct {\n\tmock *MockLogger\n}\n\n// NewMockLogger creates a new mock instance.\nfunc NewMockLogger(ctrl *gomock.Controller) *MockLogger {\n\tmock := &MockLogger{ctrl: ctrl}\n\tmock.recorder = &MockLoggerMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use.\nfunc (m *MockLogger) EXPECT() *MockLoggerMockRecorder {\n\treturn m.recorder\n}\n\n// Debug mocks base method.\nfunc (m *MockLogger) Debug(msg string, args ...any) {\n\tm.ctrl.T.Helper()\n\tvarargs := []any{msg}\n\tfor _, a := range args {\n\t\tvarargs = append(varargs, a)\n\t}\n\tm.ctrl.Call(m, \"Debug\", varargs...)\n}\n\n// Debug indicates an expected call of Debug.\nfunc (mr *MockLoggerMockRecorder) Debug(msg any, args ...any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]any{msg}, args...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Debug\", reflect.TypeOf((*MockLogger)(nil).Debug), varargs...)\n}\n\n// Error mocks base method.\nfunc (m *MockLogger) Error(msg string, args ...any) {\n\tm.ctrl.T.Helper()\n\tvarargs := []any{msg}\n\tfor _, a := range args {\n\t\tvarargs = append(varargs, a)\n\t}\n\tm.ctrl.Call(m, \"Error\", varargs...)\n}\n\n// Error indicates an expected call of Error.\nfunc (mr *MockLoggerMockRecorder) Error(msg any, args ...any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]any{msg}, args...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Error\", reflect.TypeOf((*MockLogger)(nil).Error), varargs...)\n}\n\n// Info mocks base method.\nfunc (m *MockLogger) Info(msg string, args ...any) {\n\tm.ctrl.T.Helper()\n\tvarargs := []any{msg}\n\tfor _, a := range args {\n\t\tvarargs = append(varargs, a)\n\t}\n\tm.ctrl.Call(m, \"Info\", varargs...)\n}\n\n// Info indicates an expected call of Info.\nfunc (mr *MockLoggerMockRecorder) Info(msg any, args ...any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]any{msg}, args...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Info\", reflect.TypeOf((*MockLogger)(nil).Info), varargs...)\n}\n\n// Warn mocks base method.\nfunc (m *MockLogger) Warn(msg string, args ...any) {\n\tm.ctrl.T.Helper()\n\tvarargs := []any{msg}\n\tfor _, a := range args {\n\t\tvarargs = append(varargs, a)\n\t}\n\tm.ctrl.Call(m, \"Warn\", varargs...)\n}\n\n// Warn indicates an expected call of Warn.\nfunc (mr *MockLoggerMockRecorder) Warn(msg any, args ...any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]any{msg}, args...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Warn\", reflect.TypeOf((*MockLogger)(nil).Warn), varargs...)\n}\n"
  },
  {
    "path": "mocks/scheduler.go",
    "content": "// Code generated by MockGen. DO NOT EDIT.\n// Source: github.com/go-co-op/gocron/v2 (interfaces: Scheduler)\n//\n// Generated by this command:\n//\n//\tmockgen -destination=mocks/scheduler.go -package=gocronmocks . Scheduler\n//\n\n// Package gocronmocks is a generated GoMock package.\npackage gocronmocks\n\nimport (\n\treflect \"reflect\"\n\n\tv2 \"github.com/go-co-op/gocron/v2\"\n\tuuid \"github.com/google/uuid\"\n\tgomock \"go.uber.org/mock/gomock\"\n)\n\n// MockScheduler is a mock of Scheduler interface.\ntype MockScheduler struct {\n\tctrl     *gomock.Controller\n\trecorder *MockSchedulerMockRecorder\n\tisgomock struct{}\n}\n\n// MockSchedulerMockRecorder is the mock recorder for MockScheduler.\ntype MockSchedulerMockRecorder struct {\n\tmock *MockScheduler\n}\n\n// NewMockScheduler creates a new mock instance.\nfunc NewMockScheduler(ctrl *gomock.Controller) *MockScheduler {\n\tmock := &MockScheduler{ctrl: ctrl}\n\tmock.recorder = &MockSchedulerMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use.\nfunc (m *MockScheduler) EXPECT() *MockSchedulerMockRecorder {\n\treturn m.recorder\n}\n\n// Jobs mocks base method.\nfunc (m *MockScheduler) Jobs() []v2.Job {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Jobs\")\n\tret0, _ := ret[0].([]v2.Job)\n\treturn ret0\n}\n\n// Jobs indicates an expected call of Jobs.\nfunc (mr *MockSchedulerMockRecorder) Jobs() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Jobs\", reflect.TypeOf((*MockScheduler)(nil).Jobs))\n}\n\n// JobsWaitingInQueue mocks base method.\nfunc (m *MockScheduler) JobsWaitingInQueue() int {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"JobsWaitingInQueue\")\n\tret0, _ := ret[0].(int)\n\treturn ret0\n}\n\n// JobsWaitingInQueue indicates an expected call of JobsWaitingInQueue.\nfunc (mr *MockSchedulerMockRecorder) JobsWaitingInQueue() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"JobsWaitingInQueue\", reflect.TypeOf((*MockScheduler)(nil).JobsWaitingInQueue))\n}\n\n// NewJob mocks base method.\nfunc (m *MockScheduler) NewJob(arg0 v2.JobDefinition, arg1 v2.Task, arg2 ...v2.JobOption) (v2.Job, error) {\n\tm.ctrl.T.Helper()\n\tvarargs := []any{arg0, arg1}\n\tfor _, a := range arg2 {\n\t\tvarargs = append(varargs, a)\n\t}\n\tret := m.ctrl.Call(m, \"NewJob\", varargs...)\n\tret0, _ := ret[0].(v2.Job)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// NewJob indicates an expected call of NewJob.\nfunc (mr *MockSchedulerMockRecorder) NewJob(arg0, arg1 any, arg2 ...any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]any{arg0, arg1}, arg2...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"NewJob\", reflect.TypeOf((*MockScheduler)(nil).NewJob), varargs...)\n}\n\n// RemoveByTags mocks base method.\nfunc (m *MockScheduler) RemoveByTags(arg0 ...string) {\n\tm.ctrl.T.Helper()\n\tvarargs := []any{}\n\tfor _, a := range arg0 {\n\t\tvarargs = append(varargs, a)\n\t}\n\tm.ctrl.Call(m, \"RemoveByTags\", varargs...)\n}\n\n// RemoveByTags indicates an expected call of RemoveByTags.\nfunc (mr *MockSchedulerMockRecorder) RemoveByTags(arg0 ...any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"RemoveByTags\", reflect.TypeOf((*MockScheduler)(nil).RemoveByTags), arg0...)\n}\n\n// RemoveJob mocks base method.\nfunc (m *MockScheduler) RemoveJob(arg0 uuid.UUID) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"RemoveJob\", arg0)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// RemoveJob indicates an expected call of RemoveJob.\nfunc (mr *MockSchedulerMockRecorder) RemoveJob(arg0 any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"RemoveJob\", reflect.TypeOf((*MockScheduler)(nil).RemoveJob), arg0)\n}\n\n// Shutdown mocks base method.\nfunc (m *MockScheduler) Shutdown() error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Shutdown\")\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// Shutdown indicates an expected call of Shutdown.\nfunc (mr *MockSchedulerMockRecorder) Shutdown() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Shutdown\", reflect.TypeOf((*MockScheduler)(nil).Shutdown))\n}\n\n// Start mocks base method.\nfunc (m *MockScheduler) Start() {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"Start\")\n}\n\n// Start indicates an expected call of Start.\nfunc (mr *MockSchedulerMockRecorder) Start() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Start\", reflect.TypeOf((*MockScheduler)(nil).Start))\n}\n\n// StopJobs mocks base method.\nfunc (m *MockScheduler) StopJobs() error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"StopJobs\")\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// StopJobs indicates an expected call of StopJobs.\nfunc (mr *MockSchedulerMockRecorder) StopJobs() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"StopJobs\", reflect.TypeOf((*MockScheduler)(nil).StopJobs))\n}\n\n// Update mocks base method.\nfunc (m *MockScheduler) Update(arg0 uuid.UUID, arg1 v2.JobDefinition, arg2 v2.Task, arg3 ...v2.JobOption) (v2.Job, error) {\n\tm.ctrl.T.Helper()\n\tvarargs := []any{arg0, arg1, arg2}\n\tfor _, a := range arg3 {\n\t\tvarargs = append(varargs, a)\n\t}\n\tret := m.ctrl.Call(m, \"Update\", varargs...)\n\tret0, _ := ret[0].(v2.Job)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// Update indicates an expected call of Update.\nfunc (mr *MockSchedulerMockRecorder) Update(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]any{arg0, arg1, arg2}, arg3...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Update\", reflect.TypeOf((*MockScheduler)(nil).Update), varargs...)\n}\n"
  },
  {
    "path": "monitor.go",
    "content": "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 collected with the metric.\ntype JobStatus string\n\n// The different statuses of job that can be used.\nconst (\n\tFail                 JobStatus = \"fail\"\n\tSuccess              JobStatus = \"success\"\n\tSkip                 JobStatus = \"skip\"\n\tSingletonRescheduled JobStatus = \"singleton_rescheduled\"\n)\n\n// Monitor represents the interface to collect jobs metrics.\ntype Monitor interface {\n\t// IncrementJob will provide details about the job and expects the underlying implementation\n\t// to handle instantiating and incrementing a value\n\tIncrementJob(id uuid.UUID, name string, tags []string, status JobStatus)\n\t// RecordJobTiming will provide details about the job and the timing and expects the underlying implementation\n\t// to handle instantiating and recording the value\n\tRecordJobTiming(startTime, endTime time.Time, id uuid.UUID, name string, tags []string)\n}\n\n// MonitorStatus extends RecordJobTiming with the job status.\ntype MonitorStatus interface {\n\tMonitor\n\t// RecordJobTimingWithStatus will provide details about the job, its status, error and the timing and expects the underlying implementation\n\t// to handle instantiating and recording the value\n\tRecordJobTimingWithStatus(startTime, endTime time.Time, id uuid.UUID, name string, tags []string, status JobStatus, err error)\n}\n"
  },
  {
    "path": "scheduler.go",
    "content": "//go:generate mockgen -destination=mocks/scheduler.go -package=gocronmocks . Scheduler\npackage gocron\n\nimport (\n\t\"context\"\n\t\"reflect\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/jonboulle/clockwork\"\n)\n\nvar _ Scheduler = (*scheduler)(nil)\n\n// Scheduler defines the interface for the Scheduler.\ntype Scheduler interface {\n\t// Jobs returns all the jobs currently in the scheduler.\n\tJobs() []Job\n\t// NewJob creates a new job in the Scheduler. The job is scheduled per the provided\n\t// definition when the Scheduler is started. If the Scheduler is already running\n\t// the job will be scheduled when the Scheduler is started.\n\t// If you set the first argument of your Task func to be a context.Context,\n\t// gocron will pass in a context (either the default Job context, or one\n\t// provided via WithContext) to the job and will cancel the context on shutdown.\n\t// This allows you to listen for and handle cancellation within your job.\n\tNewJob(JobDefinition, Task, ...JobOption) (Job, error)\n\t// RemoveByTags removes all jobs that have at least one of the provided tags.\n\tRemoveByTags(...string)\n\t// RemoveJob removes the job with the provided id.\n\tRemoveJob(uuid.UUID) error\n\t// Shutdown should be called when you no longer need\n\t// the Scheduler or Job's as the Scheduler cannot\n\t// be restarted after calling Shutdown. This is similar\n\t// to a Close or Cleanup method and is often deferred after\n\t// starting the scheduler.\n\tShutdown() error\n\t// Start begins scheduling jobs for execution based\n\t// on each job's definition. Job's added to an already\n\t// running scheduler will be scheduled immediately based\n\t// on definition. Start is non-blocking.\n\tStart()\n\t// StopJobs stops the execution of all jobs in the scheduler.\n\t// This can be useful in situations where jobs need to be\n\t// paused globally and then restarted with Start().\n\tStopJobs() error\n\t// Update replaces the existing Job's JobDefinition with the provided\n\t// JobDefinition. The Job's Job.ID() remains the same.\n\tUpdate(uuid.UUID, JobDefinition, Task, ...JobOption) (Job, error)\n\t// JobsWaitingInQueue number of jobs waiting in Queue in case of LimitModeWait\n\t// In case of LimitModeReschedule or no limit it will be always zero\n\tJobsWaitingInQueue() int\n}\n\n// -----------------------------------------------\n// -----------------------------------------------\n// ----------------- Scheduler -------------------\n// -----------------------------------------------\n// -----------------------------------------------\n\ntype scheduler struct {\n\t// context used for shutting down\n\tshutdownCtx context.Context\n\t// cancel used to signal scheduler should shut down\n\tshutdownCancel context.CancelFunc\n\t// the executor, which actually runs the jobs sent to it via the scheduler\n\texec executor\n\t// the map of jobs registered in the scheduler\n\tjobs map[uuid.UUID]internalJob\n\t// the location used by the scheduler for scheduling when relevant\n\tlocation *time.Location\n\t// whether the scheduler has been started or not\n\tstarted atomic.Bool\n\t// globally applied JobOption's set on all jobs added to the scheduler\n\t// note: individually set JobOption's take precedence.\n\tglobalJobOptions []JobOption\n\t// the scheduler's logger\n\tlogger Logger\n\n\t// used to tell the scheduler to start\n\tstartCh chan struct{}\n\t// used to report that the scheduler has started\n\tstartedCh chan struct{}\n\t// used to tell the scheduler to stop\n\tstopCh chan struct{}\n\t// used to report that the scheduler has stopped\n\tstopErrCh chan error\n\t// used to send all the jobs out when a request is made by the client\n\tallJobsOutRequest chan allJobsOutRequest\n\t// used to send a jobs out when a request is made by the client\n\tjobOutRequestCh chan *jobOutRequest\n\t// used to run a job on-demand when requested by the client\n\trunJobRequestCh chan runJobRequest\n\t// new jobs are received here\n\tnewJobCh chan newJobIn\n\t// requests from the client to remove jobs by ID are received here\n\tremoveJobCh chan uuid.UUID\n\t// requests from the client to remove jobs by tags are received here\n\tremoveJobsByTagsCh chan []string\n\n\t// scheduler monitor from which metrics can be collected\n\tschedulerMonitor SchedulerMonitor\n}\n\ntype newJobIn struct {\n\tctx    context.Context\n\tcancel context.CancelFunc\n\tjob    internalJob\n}\n\ntype jobOutRequest struct {\n\tid      uuid.UUID\n\toutChan chan internalJob\n}\n\ntype runJobRequest struct {\n\tid      uuid.UUID\n\toutChan chan error\n}\n\ntype allJobsOutRequest struct {\n\toutChan chan []Job\n}\n\n// NewScheduler creates a new Scheduler instance.\n// The Scheduler is not started until Start() is called.\n//\n// NewJob will add jobs to the Scheduler, but they will not\n// be scheduled until Start() is called.\nfunc NewScheduler(options ...SchedulerOption) (Scheduler, error) {\n\tschCtx, cancel := context.WithCancel(context.Background())\n\n\texec := executor{\n\t\tstopCh:           make(chan struct{}),\n\t\tstopTimeout:      time.Second * 10,\n\t\tsingletonRunners: nil,\n\t\tlogger:           &noOpLogger{},\n\t\tclock:            clockwork.NewRealClock(),\n\n\t\tjobsIn:                 make(chan jobIn),\n\t\tjobsOutForRescheduling: make(chan uuid.UUID),\n\t\tjobUpdateNextRuns:      make(chan uuid.UUID),\n\t\tjobsOutCompleted:       make(chan uuid.UUID),\n\t\tjobOutRequest:          make(chan *jobOutRequest, 100),\n\t\tdone:                   make(chan error, 1),\n\t}\n\n\ts := &scheduler{\n\t\tshutdownCtx:    schCtx,\n\t\tshutdownCancel: cancel,\n\t\tjobs:           make(map[uuid.UUID]internalJob),\n\t\tlocation:       time.Local,\n\t\tlogger:         &noOpLogger{},\n\n\t\tnewJobCh:           make(chan newJobIn),\n\t\tremoveJobCh:        make(chan uuid.UUID),\n\t\tremoveJobsByTagsCh: make(chan []string),\n\t\tstartCh:            make(chan struct{}),\n\t\tstartedCh:          make(chan struct{}),\n\t\tstopCh:             make(chan struct{}),\n\t\tstopErrCh:          make(chan error, 1),\n\t\tjobOutRequestCh:    make(chan *jobOutRequest),\n\t\trunJobRequestCh:    make(chan runJobRequest),\n\t\tallJobsOutRequest:  make(chan allJobsOutRequest),\n\t}\n\texec.scheduler = s\n\ts.exec = exec\n\n\tfor _, option := range options {\n\t\terr := option(s)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tgo func() {\n\t\ts.logger.Info(\"gocron: new scheduler created\")\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase id := <-s.exec.jobsOutForRescheduling:\n\t\t\t\ts.selectExecJobsOutForRescheduling(id)\n\t\t\tcase id := <-s.exec.jobUpdateNextRuns:\n\t\t\t\ts.updateNextScheduled(id)\n\t\t\tcase id := <-s.exec.jobsOutCompleted:\n\t\t\t\ts.selectExecJobsOutCompleted(id)\n\n\t\t\tcase in := <-s.newJobCh:\n\t\t\t\ts.selectNewJob(in)\n\n\t\t\tcase id := <-s.removeJobCh:\n\t\t\t\ts.selectRemoveJob(id)\n\n\t\t\tcase tags := <-s.removeJobsByTagsCh:\n\t\t\t\ts.selectRemoveJobsByTags(tags)\n\n\t\t\tcase out := <-s.exec.jobOutRequest:\n\t\t\t\ts.selectJobOutRequest(out)\n\n\t\t\tcase out := <-s.jobOutRequestCh:\n\t\t\t\ts.selectJobOutRequest(out)\n\n\t\t\tcase out := <-s.allJobsOutRequest:\n\t\t\t\ts.selectAllJobsOutRequest(out)\n\n\t\t\tcase run := <-s.runJobRequestCh:\n\t\t\t\ts.selectRunJobRequest(run)\n\n\t\t\tcase <-s.startCh:\n\t\t\t\ts.selectStart()\n\n\t\t\tcase <-s.stopCh:\n\t\t\t\ts.stopScheduler()\n\n\t\t\tcase <-s.shutdownCtx.Done():\n\t\t\t\ts.stopScheduler()\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn s, nil\n}\n\n// -----------------------------------------------\n// -----------------------------------------------\n// --------- Scheduler Channel Methods -----------\n// -----------------------------------------------\n// -----------------------------------------------\n\n// The scheduler's channel functions are broken out here\n// to allow prioritizing within the select blocks. The idea\n// being that we want to make sure that scheduling tasks\n// are not blocked by requests from the caller for information\n// about jobs.\n\nfunc (s *scheduler) stopScheduler() {\n\ts.logger.Debug(\"gocron: stopping scheduler\")\n\n\tif s.started.Load() {\n\t\ts.exec.stopCh <- struct{}{}\n\t}\n\n\tfor _, j := range s.jobs {\n\t\tj.stop()\n\t}\n\tfor _, j := range s.jobs {\n\t\t<-j.ctx.Done()\n\t}\n\tvar err error\n\tif s.started.Load() {\n\t\tt := time.NewTimer(s.exec.stopTimeout + 1*time.Second)\n\t\tselect {\n\t\tcase err = <-s.exec.done:\n\t\t\tt.Stop()\n\t\tcase <-t.C:\n\t\t\terr = ErrStopExecutorTimedOut\n\t\t}\n\t}\n\tfor id, j := range s.jobs {\n\t\toldCtx := j.ctx\n\t\tif j.parentCtx == nil {\n\t\t\tj.parentCtx = s.shutdownCtx\n\t\t}\n\t\tj.ctx, j.cancel = context.WithCancel(j.parentCtx)\n\n\t\t// also replace the old context with the new one in the parameters\n\t\tif len(j.parameters) > 0 && j.parameters[0] == oldCtx {\n\t\t\tj.parameters[0] = j.ctx\n\t\t}\n\n\t\ts.jobs[id] = j\n\t}\n\n\ts.stopErrCh <- err\n\ts.started.Store(false)\n\ts.logger.Debug(\"gocron: scheduler stopped\")\n\n\t// Notify monitor that scheduler has stopped\n\ts.notifySchedulerStopped()\n}\n\nfunc (s *scheduler) selectAllJobsOutRequest(out allJobsOutRequest) {\n\toutJobs := make([]Job, len(s.jobs))\n\tvar counter int\n\tfor _, j := range s.jobs {\n\t\toutJobs[counter] = s.jobFromInternalJob(j)\n\t\tcounter++\n\t}\n\tslices.SortFunc(outJobs, func(a, b Job) int {\n\t\taID, bID := a.ID().String(), b.ID().String()\n\t\treturn strings.Compare(aID, bID)\n\t})\n\tselect {\n\tcase <-s.shutdownCtx.Done():\n\tcase out.outChan <- outJobs:\n\t}\n}\n\nfunc (s *scheduler) selectRunJobRequest(run runJobRequest) {\n\tj, ok := s.jobs[run.id]\n\tif !ok {\n\t\tselect {\n\t\tcase run.outChan <- ErrJobNotFound:\n\t\tdefault:\n\t\t}\n\t\treturn\n\t}\n\tselect {\n\tcase <-s.shutdownCtx.Done():\n\t\tselect {\n\t\tcase run.outChan <- ErrJobRunNowFailed:\n\t\tdefault:\n\t\t}\n\tcase s.exec.jobsIn <- jobIn{\n\t\tid:            j.id,\n\t\tshouldSendOut: false,\n\t}:\n\t\tselect {\n\t\tcase run.outChan <- nil:\n\t\tdefault:\n\t\t}\n\t}\n}\n\nfunc (s *scheduler) selectRemoveJob(id uuid.UUID) {\n\tj, ok := s.jobs[id]\n\tif !ok {\n\t\treturn\n\t}\n\tif s.schedulerMonitor != nil {\n\t\tout := s.jobFromInternalJob(j)\n\t\ts.notifyJobUnregistered(out)\n\t}\n\tj.stop()\n\tdelete(s.jobs, id)\n}\n\n// Jobs coming back from the executor to the scheduler that\n// need to be evaluated for rescheduling.\nfunc (s *scheduler) selectExecJobsOutForRescheduling(id uuid.UUID) {\n\tselect {\n\tcase <-s.shutdownCtx.Done():\n\t\treturn\n\tdefault:\n\t}\n\tj, ok := s.jobs[id]\n\tif !ok {\n\t\t// the job was removed while it was running, and\n\t\t// so we don't need to reschedule it.\n\t\treturn\n\t}\n\n\tif j.stopTimeReached(s.now()) {\n\t\treturn\n\t}\n\n\tvar scheduleFrom time.Time\n\n\t// If intervalFromCompletion is enabled, calculate the next run time\n\t// from when the job completed (lastRun) rather than when it was scheduled.\n\tif j.intervalFromCompletion {\n\t\t// Use the completion time (lastRun is set when the job completes)\n\t\tscheduleFrom = j.lastRun\n\t\tif scheduleFrom.IsZero() {\n\t\t\t// For the first run, use the start time or current time\n\t\t\tscheduleFrom = j.startTime\n\t\t\tif scheduleFrom.IsZero() {\n\t\t\t\tscheduleFrom = s.now()\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// Default behavior: use the scheduled time\n\t\tif len(j.nextScheduled) > 0 {\n\t\t\t// always grab the last element in the slice as that is the furthest\n\t\t\t// out in the future and the time from which we want to calculate\n\t\t\t// the subsequent next run time.\n\t\t\tslices.SortStableFunc(j.nextScheduled, ascendingTime)\n\t\t\tscheduleFrom = j.nextScheduled[len(j.nextScheduled)-1]\n\t\t}\n\n\t\tif scheduleFrom.IsZero() {\n\t\t\tscheduleFrom = j.startTime\n\t\t}\n\t}\n\n\tnext := j.next(scheduleFrom)\n\tif next.IsZero() {\n\t\t// the job's next function will return zero for OneTime jobs.\n\t\t// since they are one time only, they do not need rescheduling.\n\t\treturn\n\t}\n\n\tif next.Before(s.now()) {\n\t\t// in some cases the next run time can be in the past, for example:\n\t\t// - the time on the machine was incorrect and has been synced with ntp\n\t\t// - the machine went to sleep, and woke up some time later\n\t\t// in those cases, we want to increment to the next run in the future\n\t\t// and schedule the job for that time.\n\t\tfor next.Before(s.now()) {\n\t\t\tnext = j.next(next)\n\t\t}\n\t}\n\n\tif slices.Contains(j.nextScheduled, next) {\n\t\t// if the next value is a duplicate of what's already in the nextScheduled slice, for example:\n\t\t// - the job is being rescheduled off the same next run value as before\n\t\t// increment to the next, next value\n\t\tfor slices.Contains(j.nextScheduled, next) {\n\t\t\tnext = j.next(next)\n\t\t}\n\t}\n\n\t// Clean up any existing timer to prevent leaks\n\tif j.timer != nil {\n\t\tj.timer.Stop()\n\t\tj.timer = nil // Ensure timer is cleared for GC\n\t}\n\n\tj.nextScheduled = append(j.nextScheduled, next)\n\tj.timer = s.exec.clock.AfterFunc(next.Sub(s.now()), func() {\n\t\t// set the actual timer on the job here and listen for\n\t\t// shut down events so that the job doesn't attempt to\n\t\t// run if the scheduler has been shutdown.\n\t\tselect {\n\t\tcase <-s.shutdownCtx.Done():\n\t\t\treturn\n\t\tcase s.exec.jobsIn <- jobIn{\n\t\t\tid:            j.id,\n\t\t\tshouldSendOut: true,\n\t\t}:\n\t\t}\n\t})\n\t// update the job with its new next and last run times and timer.\n\ts.jobs[id] = j\n}\n\nfunc (s *scheduler) updateNextScheduled(id uuid.UUID) {\n\tj, ok := s.jobs[id]\n\tif !ok {\n\t\treturn\n\t}\n\tvar newNextScheduled []time.Time\n\tnow := s.now()\n\tfor _, t := range j.nextScheduled {\n\t\tif t.After(now) { // Changed to match selectExecJobsOutCompleted\n\t\t\tnewNextScheduled = append(newNextScheduled, t)\n\t\t}\n\t}\n\tj.nextScheduled = newNextScheduled\n\ts.jobs[id] = j\n}\n\nfunc (s *scheduler) selectExecJobsOutCompleted(id uuid.UUID) {\n\tj, ok := s.jobs[id]\n\tif !ok {\n\t\treturn\n\t}\n\n\t// if the job has nextScheduled time in the past,\n\t// we need to remove any that are in the past or at the current time (just executed).\n\tvar newNextScheduled []time.Time\n\tnow := s.now()\n\tfor _, t := range j.nextScheduled {\n\t\tif t.After(now) {\n\t\t\tnewNextScheduled = append(newNextScheduled, t)\n\t\t}\n\t}\n\tj.nextScheduled = newNextScheduled\n\n\t// if the job has a limited number of runs set, we need to\n\t// check how many runs have occurred and stop running this\n\t// job if it has reached the limit.\n\tif j.limitRunsTo != nil {\n\t\tj.limitRunsTo.runCount = j.limitRunsTo.runCount + 1\n\t\tif j.limitRunsTo.runCount == j.limitRunsTo.limit {\n\t\t\tgo func() {\n\t\t\t\tselect {\n\t\t\t\tcase <-s.shutdownCtx.Done():\n\t\t\t\t\treturn\n\t\t\t\tcase s.removeJobCh <- id:\n\t\t\t\t}\n\t\t\t}()\n\t\t\treturn\n\t\t}\n\t}\n\n\tj.lastRun = s.now()\n\ts.jobs[id] = j\n}\n\nfunc (s *scheduler) selectJobOutRequest(out *jobOutRequest) {\n\tif j, ok := s.jobs[out.id]; ok {\n\t\tselect {\n\t\tcase out.outChan <- j:\n\t\tcase <-s.shutdownCtx.Done():\n\t\t}\n\t}\n\tclose(out.outChan)\n}\n\nfunc (s *scheduler) selectNewJob(in newJobIn) {\n\tj := in.job\n\tif s.started.Load() {\n\t\tnext := j.startTime\n\t\tif j.startImmediately {\n\t\t\tnext = s.now()\n\t\t\tselect {\n\t\t\tcase <-s.shutdownCtx.Done():\n\t\t\tcase s.exec.jobsIn <- jobIn{\n\t\t\t\tid:            j.id,\n\t\t\t\tshouldSendOut: true,\n\t\t\t}:\n\t\t\t}\n\t\t} else {\n\t\t\tif next.IsZero() {\n\t\t\t\tnext = j.next(s.now())\n\t\t\t}\n\n\t\t\tif next.Before(s.now()) {\n\t\t\t\tfor next.Before(s.now()) {\n\t\t\t\t\tnext = j.next(next)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tid := j.id\n\t\t\tj.timer = s.exec.clock.AfterFunc(next.Sub(s.now()), func() {\n\t\t\t\tselect {\n\t\t\t\tcase <-s.shutdownCtx.Done():\n\t\t\t\tcase s.exec.jobsIn <- jobIn{\n\t\t\t\t\tid:            id,\n\t\t\t\t\tshouldSendOut: true,\n\t\t\t\t}:\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t\tj.startTime = next\n\t\tj.nextScheduled = append(j.nextScheduled, next)\n\t}\n\n\ts.jobs[j.id] = j\n\tin.cancel()\n}\n\nfunc (s *scheduler) selectRemoveJobsByTags(tags []string) {\n\tfor _, j := range s.jobs {\n\t\tfor _, tag := range tags {\n\t\t\tif slices.Contains(j.tags, tag) {\n\t\t\t\tif s.schedulerMonitor != nil {\n\t\t\t\t\tout := s.jobFromInternalJob(j)\n\t\t\t\t\ts.notifyJobUnregistered(out)\n\t\t\t\t}\n\t\t\t\tj.stop()\n\t\t\t\tdelete(s.jobs, j.id)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (s *scheduler) selectStart() {\n\ts.logger.Debug(\"gocron: scheduler starting\")\n\tgo s.exec.start()\n\n\ts.started.Store(true)\n\tfor id, j := range s.jobs {\n\t\tnext := j.startTime\n\t\tif j.startImmediately {\n\t\t\tnext = s.now()\n\t\t\tselect {\n\t\t\tcase <-s.shutdownCtx.Done():\n\t\t\tcase s.exec.jobsIn <- jobIn{\n\t\t\t\tid:            id,\n\t\t\t\tshouldSendOut: true,\n\t\t\t}:\n\t\t\t}\n\t\t} else {\n\t\t\tif next.IsZero() {\n\t\t\t\tnext = j.next(s.now())\n\t\t\t}\n\t\t\tif next.Before(s.now()) {\n\t\t\t\tfor next.Before(s.now()) {\n\t\t\t\t\tnext = j.next(next)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tjobID := id\n\t\t\tj.timer = s.exec.clock.AfterFunc(next.Sub(s.now()), func() {\n\t\t\t\tselect {\n\t\t\t\tcase <-s.shutdownCtx.Done():\n\t\t\t\tcase s.exec.jobsIn <- jobIn{\n\t\t\t\t\tid:            jobID,\n\t\t\t\t\tshouldSendOut: true,\n\t\t\t\t}:\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t\tj.startTime = next\n\t\tj.nextScheduled = append(j.nextScheduled, next)\n\t\ts.jobs[id] = j\n\t}\n\tselect {\n\tcase <-s.shutdownCtx.Done():\n\tcase s.startedCh <- struct{}{}:\n\t\ts.logger.Info(\"gocron: scheduler started\")\n\t}\n}\n\n// -----------------------------------------------\n// -----------------------------------------------\n// ------------- Scheduler Methods ---------------\n// -----------------------------------------------\n// -----------------------------------------------\n\nfunc (s *scheduler) now() time.Time {\n\treturn s.exec.clock.Now().In(s.location)\n}\n\nfunc (s *scheduler) jobFromInternalJob(in internalJob) job {\n\treturn job{\n\t\tin.id,\n\t\tin.name,\n\t\tslices.Clone(in.tags),\n\t\ts.jobOutRequestCh,\n\t\ts.runJobRequestCh,\n\t}\n}\n\nfunc (s *scheduler) Jobs() []Job {\n\toutChan := make(chan []Job)\n\tselect {\n\tcase <-s.shutdownCtx.Done():\n\tcase s.allJobsOutRequest <- allJobsOutRequest{outChan: outChan}:\n\t}\n\n\tvar jobs []Job\n\tselect {\n\tcase <-s.shutdownCtx.Done():\n\tcase jobs = <-outChan:\n\t}\n\n\treturn jobs\n}\n\nfunc (s *scheduler) NewJob(jobDefinition JobDefinition, task Task, options ...JobOption) (Job, error) {\n\treturn s.addOrUpdateJob(uuid.Nil, jobDefinition, task, options)\n}\n\nfunc (s *scheduler) verifyInterfaceVariadic(taskFunc reflect.Value, tsk task, variadicStart int) error {\n\tifaceType := taskFunc.Type().In(variadicStart).Elem()\n\tfor i := variadicStart; i < len(tsk.parameters); i++ {\n\t\tif !reflect.TypeOf(tsk.parameters[i]).Implements(ifaceType) {\n\t\t\treturn ErrNewJobWrongTypeOfParameters\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *scheduler) verifyVariadic(taskFunc reflect.Value, tsk task, variadicStart int) error {\n\tif err := s.verifyNonVariadic(taskFunc, tsk, variadicStart); err != nil {\n\t\treturn err\n\t}\n\tparameterType := taskFunc.Type().In(variadicStart)\n\tparameterTypeKind := parameterType.Elem().Kind()\n\tif parameterTypeKind == reflect.Interface {\n\t\treturn s.verifyInterfaceVariadic(taskFunc, tsk, variadicStart)\n\t}\n\tif parameterTypeKind == reflect.Pointer {\n\t\tparameterTypeKind = reflect.Indirect(reflect.ValueOf(parameterType)).Kind()\n\t}\n\n\tfor i := variadicStart; i < len(tsk.parameters); i++ {\n\t\targumentType := reflect.TypeOf(tsk.parameters[i])\n\t\targumentTypeKind := argumentType.Kind()\n\t\tif argumentTypeKind == reflect.Interface || argumentTypeKind == reflect.Pointer {\n\t\t\targumentTypeKind = argumentType.Elem().Kind()\n\t\t}\n\t\tif argumentTypeKind != parameterTypeKind {\n\t\t\treturn ErrNewJobWrongTypeOfParameters\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *scheduler) verifyNonVariadic(taskFunc reflect.Value, tsk task, length int) error {\n\tfor i := 0; i < length; i++ {\n\t\targumentType := reflect.TypeOf(tsk.parameters[i])\n\t\tt1 := argumentType.Kind()\n\t\tif t1 == reflect.Interface || t1 == reflect.Pointer {\n\t\t\tt1 = argumentType.Elem().Kind()\n\t\t}\n\t\tparameterType := taskFunc.Type().In(i)\n\t\tt2 := reflect.New(parameterType).Elem().Kind()\n\t\tif t2 == reflect.Interface || t2 == reflect.Pointer {\n\t\t\tt2 = reflect.Indirect(reflect.ValueOf(parameterType)).Kind()\n\t\t}\n\t\tif t1 != t2 {\n\t\t\treturn ErrNewJobWrongTypeOfParameters\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *scheduler) verifyParameterType(taskFunc reflect.Value, tsk task) error {\n\ttaskFuncType := taskFunc.Type()\n\tisVariadic := taskFuncType.IsVariadic()\n\tif isVariadic {\n\t\tvariadicStart := taskFuncType.NumIn() - 1\n\t\treturn s.verifyVariadic(taskFunc, tsk, variadicStart)\n\t}\n\texpectedParameterLength := taskFuncType.NumIn()\n\tif len(tsk.parameters) != expectedParameterLength {\n\t\treturn ErrNewJobWrongNumberOfParameters\n\t}\n\treturn s.verifyNonVariadic(taskFunc, tsk, expectedParameterLength)\n}\n\nvar contextType = reflect.TypeOf((*context.Context)(nil)).Elem()\n\nfunc (s *scheduler) addOrUpdateJob(id uuid.UUID, definition JobDefinition, taskWrapper Task, options []JobOption) (Job, error) {\n\tj := internalJob{}\n\tif id == uuid.Nil {\n\t\tj.id = uuid.New()\n\t} else {\n\t\tcurrentJob := requestJobCtx(s.shutdownCtx, id, s.jobOutRequestCh)\n\t\tif currentJob != nil && currentJob.id != uuid.Nil {\n\t\t\tselect {\n\t\t\tcase <-s.shutdownCtx.Done():\n\t\t\t\treturn nil, nil\n\t\t\tcase s.removeJobCh <- id:\n\t\t\t\t<-currentJob.ctx.Done()\n\t\t\t}\n\t\t}\n\n\t\tj.id = id\n\t}\n\n\tif taskWrapper == nil {\n\t\treturn nil, ErrNewJobTaskNil\n\t}\n\n\ttsk := taskWrapper()\n\ttaskFunc := reflect.ValueOf(tsk.function)\n\tfor taskFunc.Kind() == reflect.Ptr {\n\t\ttaskFunc = taskFunc.Elem()\n\t}\n\n\tif taskFunc.Kind() != reflect.Func {\n\t\treturn nil, ErrNewJobTaskNotFunc\n\t}\n\n\tj.name = runtime.FuncForPC(taskFunc.Pointer()).Name()\n\tj.function = tsk.function\n\tj.parameters = tsk.parameters\n\n\t// apply global job options\n\tfor _, option := range s.globalJobOptions {\n\t\tif err := option(&j, s.now()); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// apply job specific options, which take precedence\n\tfor _, option := range options {\n\t\tif err := option(&j, s.now()); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif j.parentCtx == nil {\n\t\tj.parentCtx = s.shutdownCtx\n\t}\n\tj.ctx, j.cancel = context.WithCancel(j.parentCtx)\n\n\tif !taskFunc.IsZero() && taskFunc.Type().NumIn() > 0 {\n\t\t// if the first parameter is a context.Context and params have no context.Context, add current ctx to the params\n\t\tif taskFunc.Type().In(0) == contextType {\n\t\t\tif len(tsk.parameters) == 0 {\n\t\t\t\ttsk.parameters = []any{j.ctx}\n\t\t\t\tj.parameters = []any{j.ctx}\n\t\t\t} else if _, ok := tsk.parameters[0].(context.Context); !ok {\n\t\t\t\ttsk.parameters = append([]any{j.ctx}, tsk.parameters...)\n\t\t\t\tj.parameters = append([]any{j.ctx}, j.parameters...)\n\t\t\t}\n\t\t}\n\t}\n\n\tif err := s.verifyParameterType(taskFunc, tsk); err != nil {\n\t\tj.cancel()\n\t\treturn nil, err\n\t}\n\n\tif err := definition.setup(&j, s.location, s.exec.clock.Now()); err != nil {\n\t\tj.cancel()\n\t\treturn nil, err\n\t}\n\n\tnewJobCtx, newJobCancel := context.WithCancel(context.Background())\n\tselect {\n\tcase <-s.shutdownCtx.Done():\n\t\tnewJobCancel()\n\tcase s.newJobCh <- newJobIn{\n\t\tctx:    newJobCtx,\n\t\tcancel: newJobCancel,\n\t\tjob:    j,\n\t}:\n\t}\n\n\tselect {\n\tcase <-newJobCtx.Done():\n\tcase <-s.shutdownCtx.Done():\n\t\tnewJobCancel()\n\t}\n\n\tout := s.jobFromInternalJob(j)\n\tif s.schedulerMonitor != nil {\n\t\ts.notifyJobRegistered(out)\n\t}\n\treturn &out, nil\n}\n\nfunc (s *scheduler) RemoveByTags(tags ...string) {\n\tselect {\n\tcase <-s.shutdownCtx.Done():\n\tcase s.removeJobsByTagsCh <- tags:\n\t}\n}\n\nfunc (s *scheduler) RemoveJob(id uuid.UUID) error {\n\tj := requestJobCtx(s.shutdownCtx, id, s.jobOutRequestCh)\n\tif j == nil || j.id == uuid.Nil {\n\t\treturn ErrJobNotFound\n\t}\n\tselect {\n\tcase <-s.shutdownCtx.Done():\n\tcase s.removeJobCh <- id:\n\t}\n\n\treturn nil\n}\n\nfunc (s *scheduler) Start() {\n\tif s.started.Load() {\n\t\ts.logger.Warn(\"gocron: scheduler already started\")\n\t\treturn\n\t}\n\n\tselect {\n\tcase <-s.shutdownCtx.Done():\n\t\t// Scheduler already shut down, don't notify\n\t\treturn\n\tcase s.startCh <- struct{}{}:\n\t\t<-s.startedCh // Wait for scheduler to actually start\n\n\t\t// Scheduler has started\n\t\ts.notifySchedulerStarted()\n\t}\n}\n\nfunc (s *scheduler) StopJobs() error {\n\tselect {\n\tcase <-s.shutdownCtx.Done():\n\t\treturn nil\n\tcase s.stopCh <- struct{}{}:\n\t}\n\n\tt := time.NewTimer(s.exec.stopTimeout + 2*time.Second)\n\tselect {\n\tcase err := <-s.stopErrCh:\n\t\tt.Stop()\n\t\treturn err\n\tcase <-t.C:\n\t\treturn ErrStopSchedulerTimedOut\n\t}\n}\n\nfunc (s *scheduler) Shutdown() error {\n\ts.logger.Debug(\"scheduler shutting down\")\n\n\ts.shutdownCancel()\n\tif !s.started.Load() {\n\t\treturn nil\n\t}\n\n\tt := time.NewTimer(s.exec.stopTimeout + 2*time.Second)\n\tselect {\n\tcase err := <-s.stopErrCh:\n\t\tt.Stop()\n\n\t\t// notify monitor that scheduler stopped\n\t\ts.notifySchedulerShutdown()\n\t\treturn err\n\tcase <-t.C:\n\t\treturn ErrStopSchedulerTimedOut\n\t}\n}\n\nfunc (s *scheduler) Update(id uuid.UUID, jobDefinition JobDefinition, task Task, options ...JobOption) (Job, error) {\n\treturn s.addOrUpdateJob(id, jobDefinition, task, options)\n}\n\nfunc (s *scheduler) JobsWaitingInQueue() int {\n\tif s.exec.limitMode != nil && s.exec.limitMode.mode == LimitModeWait {\n\t\treturn len(s.exec.limitMode.in)\n\t}\n\treturn 0\n}\n\n// -----------------------------------------------\n// -----------------------------------------------\n// ------------- Scheduler Options ---------------\n// -----------------------------------------------\n// -----------------------------------------------\n\n// SchedulerOption defines the function for setting\n// options on the Scheduler.\ntype SchedulerOption func(*scheduler) error\n\n// WithClock sets the clock used by the Scheduler\n// to the clock provided. See https://github.com/jonboulle/clockwork\nfunc WithClock(clock clockwork.Clock) SchedulerOption {\n\treturn func(s *scheduler) error {\n\t\tif clock == nil {\n\t\t\treturn ErrWithClockNil\n\t\t}\n\t\ts.exec.clock = clock\n\t\treturn nil\n\t}\n}\n\n// WithDistributedElector sets the elector to be used by multiple\n// Scheduler instances to determine who should be the leader.\n// Only the leader runs jobs, while non-leaders wait and continue\n// to check if a new leader has been elected.\nfunc WithDistributedElector(elector Elector) SchedulerOption {\n\treturn func(s *scheduler) error {\n\t\tif elector == nil {\n\t\t\treturn ErrWithDistributedElectorNil\n\t\t}\n\t\ts.exec.elector = elector\n\t\treturn nil\n\t}\n}\n\n// WithDistributedLocker sets the locker to be used by multiple\n// Scheduler instances to ensure that only one instance of each\n// job is run.\n// To disable this global locker for specific jobs, see\n// WithDisabledDistributedJobLocker.\nfunc WithDistributedLocker(locker Locker) SchedulerOption {\n\treturn func(s *scheduler) error {\n\t\tif locker == nil {\n\t\t\treturn ErrWithDistributedLockerNil\n\t\t}\n\t\ts.exec.locker = locker\n\t\treturn nil\n\t}\n}\n\n// WithGlobalJobOptions sets JobOption's that will be applied to\n// all jobs added to the scheduler. JobOption's set on the job\n// itself will override if the same JobOption is set globally.\nfunc WithGlobalJobOptions(jobOptions ...JobOption) SchedulerOption {\n\treturn func(s *scheduler) error {\n\t\ts.globalJobOptions = jobOptions\n\t\treturn nil\n\t}\n}\n\n// LimitMode defines the modes used for handling jobs that reach\n// the limit provided in WithLimitConcurrentJobs\ntype LimitMode int\n\nconst (\n\t// LimitModeReschedule causes jobs reaching the limit set in\n\t// WithLimitConcurrentJobs or WithSingletonMode to be skipped\n\t// and rescheduled for the next run time rather than being\n\t// queued up to wait.\n\tLimitModeReschedule = 1\n\n\t// LimitModeWait causes jobs reaching the limit set in\n\t// WithLimitConcurrentJobs or WithSingletonMode to wait\n\t// in a queue until a slot becomes available to run.\n\t//\n\t// Note: this mode can produce unpredictable results as\n\t// job execution order isn't guaranteed. For example, a job that\n\t// executes frequently may pile up in the wait queue and be executed\n\t// many times back to back when the queue opens.\n\t//\n\t// Warning: do not use this mode if your jobs will continue to stack\n\t// up beyond the ability of the limit workers to keep up. An example of\n\t// what NOT to do:\n\t//\n\t//     s, _ := gocron.NewScheduler(gocron.WithLimitConcurrentJobs)\n\t//     s.NewJob(\n\t//         gocron.DurationJob(\n\t//\t\t\t\ttime.Second,\n\t//\t\t\t\tTask{\n\t//\t\t\t\t\tFunction: func() {\n\t//\t\t\t\t\t\ttime.Sleep(10 * time.Second)\n\t//\t\t\t\t\t},\n\t//\t\t\t\t},\n\t//\t\t\t),\n\t//      )\n\tLimitModeWait = 2\n)\n\n// WithLimitConcurrentJobs sets the limit and mode to be used by the\n// Scheduler for limiting the number of jobs that may be running at\n// a given time.\n//\n// Note: the limit mode selected for WithLimitConcurrentJobs takes initial\n// precedence in the event you are also running a limit mode at the job level\n// using WithSingletonMode.\n//\n// Warning: a single time consuming job can dominate your limit in the event\n// you are running both the scheduler limit WithLimitConcurrentJobs(1, LimitModeWait)\n// and a job limit WithSingletonMode(LimitModeReschedule).\nfunc WithLimitConcurrentJobs(limit uint, mode LimitMode) SchedulerOption {\n\treturn func(s *scheduler) error {\n\t\tif limit == 0 {\n\t\t\treturn ErrWithLimitConcurrentJobsZero\n\t\t}\n\t\ts.exec.limitMode = &limitModeConfig{\n\t\t\tmode:          mode,\n\t\t\tlimit:         limit,\n\t\t\tin:            make(chan jobIn, 1000),\n\t\t\tsingletonJobs: make(map[uuid.UUID]struct{}),\n\t\t}\n\t\tif mode == LimitModeReschedule {\n\t\t\ts.exec.limitMode.rescheduleLimiter = make(chan struct{}, limit)\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// WithLocation sets the location (i.e. timezone) that the scheduler\n// should operate within. In many systems time.Local is UTC.\n// Default: time.Local\nfunc WithLocation(location *time.Location) SchedulerOption {\n\treturn func(s *scheduler) error {\n\t\tif location == nil {\n\t\t\treturn ErrWithLocationNil\n\t\t}\n\t\ts.location = location\n\t\treturn nil\n\t}\n}\n\n// WithLogger sets the logger to be used by the Scheduler.\nfunc WithLogger(logger Logger) SchedulerOption {\n\treturn func(s *scheduler) error {\n\t\tif logger == nil {\n\t\t\treturn ErrWithLoggerNil\n\t\t}\n\t\ts.logger = logger\n\t\ts.exec.logger = logger\n\t\treturn nil\n\t}\n}\n\n// WithStopTimeout sets the amount of time the Scheduler should\n// wait gracefully for jobs to complete before returning when\n// StopJobs() or Shutdown() are called.\n// Default: 10 * time.Second\nfunc WithStopTimeout(timeout time.Duration) SchedulerOption {\n\treturn func(s *scheduler) error {\n\t\tif timeout <= 0 {\n\t\t\treturn ErrWithStopTimeoutZeroOrNegative\n\t\t}\n\t\ts.exec.stopTimeout = timeout\n\t\treturn nil\n\t}\n}\n\n// WithMonitor sets the metrics provider to be used by the Scheduler.\nfunc WithMonitor(monitor Monitor) SchedulerOption {\n\treturn func(s *scheduler) error {\n\t\tif monitor == nil {\n\t\t\treturn ErrWithMonitorNil\n\t\t}\n\t\ts.exec.monitor = monitor\n\t\treturn nil\n\t}\n}\n\n// WithMonitorStatus sets the metrics provider to be used by the Scheduler.\nfunc WithMonitorStatus(monitor MonitorStatus) SchedulerOption {\n\treturn func(s *scheduler) error {\n\t\tif monitor == nil {\n\t\t\treturn ErrWithMonitorNil\n\t\t}\n\t\ts.exec.monitorStatus = monitor\n\t\treturn nil\n\t}\n}\n\n// WithSchedulerMonitor sets a monitor that will be called with scheduler-level events.\nfunc WithSchedulerMonitor(monitor SchedulerMonitor) SchedulerOption {\n\treturn func(s *scheduler) error {\n\t\tif monitor == nil {\n\t\t\treturn ErrSchedulerMonitorNil\n\t\t}\n\t\ts.schedulerMonitor = monitor\n\t\treturn nil\n\t}\n}\n\n// notifySchedulerStarted notifies the monitor that scheduler has started\nfunc (s *scheduler) notifySchedulerStarted() {\n\tif s.schedulerMonitor != nil {\n\t\ts.schedulerMonitor.SchedulerStarted()\n\t}\n}\n\n// notifySchedulerShutdown notifies the monitor that scheduler has stopped\nfunc (s *scheduler) notifySchedulerShutdown() {\n\tif s.schedulerMonitor != nil {\n\t\ts.schedulerMonitor.SchedulerShutdown()\n\t}\n}\n\n// notifyJobRegistered notifies the monitor that a job has been registered\nfunc (s *scheduler) notifyJobRegistered(job Job) {\n\tif s.schedulerMonitor != nil {\n\t\ts.schedulerMonitor.JobRegistered(job)\n\t}\n}\n\n// notifyJobUnregistered notifies the monitor that a job has been unregistered\nfunc (s *scheduler) notifyJobUnregistered(job Job) {\n\tif s.schedulerMonitor != nil {\n\t\ts.schedulerMonitor.JobUnregistered(job)\n\t}\n}\n\n// notifyJobStarted notifies the monitor that a job has started\nfunc (s *scheduler) notifyJobStarted(job Job) {\n\tif s.schedulerMonitor != nil {\n\t\ts.schedulerMonitor.JobStarted(job)\n\t}\n}\n\n// notifyJobRunning notifies the monitor that a job is running.\nfunc (s *scheduler) notifyJobRunning(job Job) {\n\tif s.schedulerMonitor != nil {\n\t\ts.schedulerMonitor.JobRunning(job)\n\t}\n}\n\n// notifyJobCompleted notifies the monitor that a job has completed.\nfunc (s *scheduler) notifyJobCompleted(job Job) {\n\tif s.schedulerMonitor != nil {\n\t\ts.schedulerMonitor.JobCompleted(job)\n\t}\n}\n\n// notifyJobFailed notifies the monitor that a job has failed.\nfunc (s *scheduler) notifyJobFailed(job Job, err error) {\n\tif s.schedulerMonitor != nil {\n\t\ts.schedulerMonitor.JobFailed(job, err)\n\t}\n}\n\n// notifySchedulerStopped notifies the monitor that the scheduler has stopped\nfunc (s *scheduler) notifySchedulerStopped() {\n\tif s.schedulerMonitor != nil {\n\t\ts.schedulerMonitor.SchedulerStopped()\n\t}\n}\n\n// notifyJobExecutionTime notifies the monitor of a job's execution time\nfunc (s *scheduler) notifyJobExecutionTime(job Job, duration time.Duration) {\n\tif s.schedulerMonitor != nil {\n\t\ts.schedulerMonitor.JobExecutionTime(job, duration)\n\t}\n}\n\n// notifyJobSchedulingDelay notifies the monitor of scheduling delay\nfunc (s *scheduler) notifyJobSchedulingDelay(job Job, scheduledTime time.Time, actualStartTime time.Time) {\n\tif s.schedulerMonitor != nil {\n\t\ts.schedulerMonitor.JobSchedulingDelay(job, scheduledTime, actualStartTime)\n\t}\n}\n\n// notifyConcurrencyLimitReached notifies the monitor that a concurrency limit was reached\nfunc (s *scheduler) notifyConcurrencyLimitReached(limitType string, job Job) {\n\tif s.schedulerMonitor != nil {\n\t\ts.schedulerMonitor.ConcurrencyLimitReached(limitType, job)\n\t}\n}\n"
  },
  {
    "path": "scheduler_monitor.go",
    "content": "package gocron\n\nimport \"time\"\n\n// SchedulerMonitor is called by the Scheduler to provide scheduler-level\n// metrics and events.\ntype SchedulerMonitor interface {\n\t// SchedulerStarted is called when Start() is invoked on the scheduler.\n\tSchedulerStarted()\n\n\t// SchedulerStopped is called when the scheduler's main loop stops,\n\t// but before final cleanup in Shutdown().\n\tSchedulerStopped()\n\n\t// SchedulerShutdown is called when Shutdown() completes successfully.\n\tSchedulerShutdown()\n\n\t// JobRegistered is called when a job is registered with the scheduler.\n\tJobRegistered(job Job)\n\n\t// JobUnregistered is called when a job is unregistered from the scheduler.\n\tJobUnregistered(job Job)\n\n\t// JobStarted is called when a job starts running.\n\tJobStarted(job Job)\n\n\t// JobRunning is called when a job is running.\n\tJobRunning(job Job)\n\n\t// JobFailed is called when a job fails to complete successfully.\n\tJobFailed(job Job, err error)\n\n\t// JobCompleted is called when a job has completed running.\n\tJobCompleted(job Job)\n\n\t// JobExecutionTime is called after a job completes (success or failure)\n\t// with the time it took to execute. This enables calculation of metrics\n\t// like AverageExecutionTime.\n\tJobExecutionTime(job Job, duration time.Duration)\n\n\t// JobSchedulingDelay is called when a job starts running, providing both\n\t// the scheduled time and actual start time. This enables calculation of\n\t// SchedulingLag metrics to detect when jobs are running behind schedule.\n\tJobSchedulingDelay(job Job, scheduledTime time.Time, actualStartTime time.Time)\n\n\t// ConcurrencyLimitReached is called when a job cannot start immediately\n\t// due to concurrency limits (singleton or limit mode).\n\t// limitType will be \"singleton\" or \"limit\".\n\tConcurrencyLimitReached(limitType string, job Job)\n}\n"
  },
  {
    "path": "scheduler_monitor_test.go",
    "content": "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\"github.com/stretchr/testify/require\"\n)\n\n// testSchedulerMonitor is a test implementation of SchedulerMonitor\n// that tracks scheduler lifecycle events\ntype testSchedulerMonitor struct {\n\tmu                    sync.RWMutex\n\tstartedCount          int64\n\tstoppedCount          int64\n\tshutdownCount         int64\n\tjobRegCount           int64\n\tjobUnregCount         int64\n\tjobStartCount         int64\n\tjobRunningCount       int64\n\tjobCompletedCount     int64\n\tjobFailedCount        int64\n\tconcurrencyLimitCount int64\n\tstartedCalls          []time.Time\n\tstoppedCalls          []time.Time\n\tshutdownCalls         []time.Time\n\tjobRegCalls           []Job\n\tjobUnregCalls         []Job\n\tjobStartCalls         []Job\n\tjobRunningCalls       []Job\n\tjobCompletedCalls     []Job\n\tjobExecutionTimes     []time.Duration\n\tjobSchedulingDelays   []time.Duration\n\tconcurrencyLimitCalls []string\n\tjobFailedCalls        struct {\n\t\tjobs []Job\n\t\terrs []error\n\t}\n}\n\nfunc newTestSchedulerMonitor() *testSchedulerMonitor {\n\treturn &testSchedulerMonitor{\n\t\tstartedCalls:          make([]time.Time, 0),\n\t\tstoppedCalls:          make([]time.Time, 0),\n\t\tshutdownCalls:         make([]time.Time, 0),\n\t\tjobRegCalls:           make([]Job, 0),\n\t\tjobUnregCalls:         make([]Job, 0),\n\t\tjobStartCalls:         make([]Job, 0),\n\t\tjobRunningCalls:       make([]Job, 0),\n\t\tjobCompletedCalls:     make([]Job, 0),\n\t\tjobExecutionTimes:     make([]time.Duration, 0),\n\t\tjobSchedulingDelays:   make([]time.Duration, 0),\n\t\tconcurrencyLimitCalls: make([]string, 0),\n\t\tjobFailedCalls: struct {\n\t\t\tjobs []Job\n\t\t\terrs []error\n\t\t}{\n\t\t\tjobs: make([]Job, 0),\n\t\t\terrs: make([]error, 0),\n\t\t},\n\t}\n}\n\nfunc (t *testSchedulerMonitor) SchedulerStarted() {\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\tatomic.AddInt64(&t.startedCount, 1)\n\tt.startedCalls = append(t.startedCalls, time.Now())\n}\n\nfunc (t *testSchedulerMonitor) SchedulerStopped() {\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\tatomic.AddInt64(&t.stoppedCount, 1)\n\tt.stoppedCalls = append(t.stoppedCalls, time.Now())\n}\n\nfunc (t *testSchedulerMonitor) SchedulerShutdown() {\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\tatomic.AddInt64(&t.shutdownCount, 1)\n\tt.shutdownCalls = append(t.shutdownCalls, time.Now())\n}\n\nfunc (t *testSchedulerMonitor) getStartedCount() int64 {\n\treturn atomic.LoadInt64(&t.startedCount)\n}\n\nfunc (t *testSchedulerMonitor) getShutdownCount() int64 {\n\treturn atomic.LoadInt64(&t.shutdownCount)\n}\n\nfunc (t *testSchedulerMonitor) getStartedCalls() []time.Time {\n\tt.mu.RLock()\n\tdefer t.mu.RUnlock()\n\treturn append([]time.Time{}, t.startedCalls...)\n}\n\nfunc (t *testSchedulerMonitor) getShutdownCalls() []time.Time {\n\tt.mu.RLock()\n\tdefer t.mu.RUnlock()\n\treturn append([]time.Time{}, t.shutdownCalls...)\n}\n\nfunc (t *testSchedulerMonitor) JobRegistered(job Job) {\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\tatomic.AddInt64(&t.jobRegCount, 1)\n\tt.jobRegCalls = append(t.jobRegCalls, job)\n}\n\nfunc (t *testSchedulerMonitor) JobUnregistered(job Job) {\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\tatomic.AddInt64(&t.jobUnregCount, 1)\n\tt.jobUnregCalls = append(t.jobUnregCalls, job)\n}\n\nfunc (t *testSchedulerMonitor) JobStarted(job Job) {\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\tatomic.AddInt64(&t.jobStartCount, 1)\n\tt.jobStartCalls = append(t.jobStartCalls, job)\n}\n\nfunc (t *testSchedulerMonitor) JobRunning(job Job) {\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\tatomic.AddInt64(&t.jobRunningCount, 1)\n\tt.jobRunningCalls = append(t.jobRunningCalls, job)\n}\n\nfunc (t *testSchedulerMonitor) JobCompleted(job Job) {\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\tatomic.AddInt64(&t.jobCompletedCount, 1)\n\tt.jobCompletedCalls = append(t.jobCompletedCalls, job)\n}\n\nfunc (t *testSchedulerMonitor) JobFailed(job Job, err error) {\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\tatomic.AddInt64(&t.jobFailedCount, 1)\n\tt.jobFailedCalls.jobs = append(t.jobFailedCalls.jobs, job)\n\tt.jobFailedCalls.errs = append(t.jobFailedCalls.errs, err)\n}\n\nfunc (t *testSchedulerMonitor) JobExecutionTime(_ Job, duration time.Duration) {\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\tt.jobExecutionTimes = append(t.jobExecutionTimes, duration)\n}\n\nfunc (t *testSchedulerMonitor) JobSchedulingDelay(_ Job, scheduledTime time.Time, actualStartTime time.Time) {\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\tdelay := actualStartTime.Sub(scheduledTime)\n\tif delay > 0 {\n\t\tt.jobSchedulingDelays = append(t.jobSchedulingDelays, delay)\n\t}\n}\n\nfunc (t *testSchedulerMonitor) ConcurrencyLimitReached(limitType string, _ Job) {\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\tatomic.AddInt64(&t.concurrencyLimitCount, 1)\n\tt.concurrencyLimitCalls = append(t.concurrencyLimitCalls, limitType)\n}\n\nfunc (t *testSchedulerMonitor) getJobRegCount() int64 {\n\treturn atomic.LoadInt64(&t.jobRegCount)\n}\n\nfunc (t *testSchedulerMonitor) getJobUnregCount() int64 {\n\treturn atomic.LoadInt64(&t.jobUnregCount)\n}\n\nfunc (t *testSchedulerMonitor) getJobStartCount() int64 {\n\treturn atomic.LoadInt64(&t.jobStartCount)\n}\n\nfunc (t *testSchedulerMonitor) getJobRunningCount() int64 {\n\treturn atomic.LoadInt64(&t.jobRunningCount)\n}\n\nfunc (t *testSchedulerMonitor) getJobCompletedCount() int64 {\n\treturn atomic.LoadInt64(&t.jobCompletedCount)\n}\n\nfunc (t *testSchedulerMonitor) getJobFailedCount() int64 {\n\treturn atomic.LoadInt64(&t.jobFailedCount)\n}\n\n// func (t *testSchedulerMonitor) getJobRegCalls() []Job {\n// \tt.mu.RLock()\n// \tdefer t.mu.RUnlock()\n// \treturn append([]Job{}, t.jobRegCalls...)\n// }\n\n// func (t *testSchedulerMonitor) getJobUnregCalls() []Job {\n// \tt.mu.RLock()\n// \tdefer t.mu.RUnlock()\n// \treturn append([]Job{}, t.jobUnregCalls...)\n// }\n\n// func (t *testSchedulerMonitor) getJobStartCalls() []Job {\n// \tt.mu.RLock()\n// \tdefer t.mu.RUnlock()\n// \treturn append([]Job{}, t.jobStartCalls...)\n// }\n\n// func (t *testSchedulerMonitor) getJobRunningCalls() []Job {\n// \tt.mu.RLock()\n// \tdefer t.mu.RUnlock()\n// \treturn append([]Job{}, t.jobRunningCalls...)\n// }\n\n// func (t *testSchedulerMonitor) getJobCompletedCalls() []Job {\n// \tt.mu.RLock()\n// \tdefer t.mu.RUnlock()\n// \treturn append([]Job{}, t.jobCompletedCalls...)\n// }\n\nfunc (t *testSchedulerMonitor) getJobFailedCalls() ([]Job, []error) {\n\tt.mu.RLock()\n\tdefer t.mu.RUnlock()\n\tjobs := append([]Job{}, t.jobFailedCalls.jobs...)\n\terrs := append([]error{}, t.jobFailedCalls.errs...)\n\treturn jobs, errs\n}\n\nfunc TestSchedulerMonitor_Basic(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\tmonitor := newTestSchedulerMonitor()\n\ts := newTestScheduler(t, WithSchedulerMonitor(monitor))\n\n\t// Before starting, monitor should not have been called\n\tassert.Equal(t, int64(0), monitor.getStartedCount())\n\tassert.Equal(t, int64(0), monitor.getShutdownCount())\n\n\t// Add a simple job\n\t_, err := s.NewJob(\n\t\tDurationJob(time.Second),\n\t\tNewTask(func() {}),\n\t)\n\trequire.NoError(t, err)\n\n\t// Start the scheduler\n\ts.Start()\n\n\t// Wait a bit for the start to complete\n\ttime.Sleep(50 * time.Millisecond)\n\n\t// SchedulerStarted should have been called once\n\tassert.Equal(t, int64(1), monitor.getStartedCount())\n\tassert.Equal(t, int64(0), monitor.getShutdownCount())\n\n\t// Shutdown the scheduler\n\terr = s.Shutdown()\n\trequire.NoError(t, err)\n\n\t// SchedulerShutdown should have been called once\n\tassert.Equal(t, int64(1), monitor.getStartedCount())\n\tassert.Equal(t, int64(1), monitor.getShutdownCount())\n\n\t// Verify the order of calls\n\tstartedCalls := monitor.getStartedCalls()\n\tshutdownCalls := monitor.getShutdownCalls()\n\trequire.Len(t, startedCalls, 1)\n\trequire.Len(t, shutdownCalls, 1)\n\tassert.True(t, startedCalls[0].Before(shutdownCalls[0]),\n\t\t\"SchedulerStarted should be called before SchedulerShutdown\")\n}\n\nfunc TestSchedulerMonitor_MultipleStartStop(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\tmonitor := newTestSchedulerMonitor()\n\ts := newTestScheduler(t, WithSchedulerMonitor(monitor))\n\n\t_, err := s.NewJob(\n\t\tDurationJob(time.Second),\n\t\tNewTask(func() {}),\n\t)\n\trequire.NoError(t, err)\n\n\t// Start and stop multiple times\n\ts.Start()\n\ttime.Sleep(50 * time.Millisecond)\n\tassert.Equal(t, int64(1), monitor.getStartedCount())\n\n\terr = s.StopJobs()\n\trequire.NoError(t, err)\n\t// StopJobs shouldn't call SchedulerShutdown\n\tassert.Equal(t, int64(0), monitor.getShutdownCount())\n\n\t// Start again\n\ts.Start()\n\ttime.Sleep(50 * time.Millisecond)\n\tassert.Equal(t, int64(2), monitor.getStartedCount())\n\n\t// Final shutdown\n\terr = s.Shutdown()\n\trequire.NoError(t, err)\n\tassert.Equal(t, int64(1), monitor.getShutdownCount())\n}\n\nfunc TestSchedulerMonitor_WithoutMonitor(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\t// Create scheduler without monitor - should not panic\n\ts := newTestScheduler(t)\n\n\t_, err := s.NewJob(\n\t\tDurationJob(time.Second),\n\t\tNewTask(func() {}),\n\t)\n\trequire.NoError(t, err)\n\n\ts.Start()\n\ttime.Sleep(50 * time.Millisecond)\n\n\terr = s.Shutdown()\n\trequire.NoError(t, err)\n}\n\nfunc TestSchedulerMonitor_NilMonitor(t *testing.T) {\n\t// Attempting to create a scheduler with nil monitor should error\n\t_, err := NewScheduler(WithSchedulerMonitor(nil))\n\tassert.Error(t, err)\n\tassert.Equal(t, ErrSchedulerMonitorNil, err)\n}\n\nfunc TestSchedulerMonitor_ConcurrentAccess(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\tmonitor := newTestSchedulerMonitor()\n\ts := newTestScheduler(t, WithSchedulerMonitor(monitor))\n\n\t// Add multiple jobs\n\tfor i := 0; i < 10; i++ {\n\t\t_, err := s.NewJob(\n\t\t\tDurationJob(100*time.Millisecond),\n\t\t\tNewTask(func() {}),\n\t\t)\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Start scheduler once (normal use case)\n\ts.Start()\n\ttime.Sleep(150 * time.Millisecond)\n\n\t// Verify monitor was called\n\tassert.Equal(t, int64(1), monitor.getStartedCount())\n\n\terr := s.Shutdown()\n\trequire.NoError(t, err)\n\n\t// Monitor should be called for shutdown\n\tassert.Equal(t, int64(1), monitor.getShutdownCount())\n}\n\nfunc TestSchedulerMonitor_StartWithoutJobs(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\tmonitor := newTestSchedulerMonitor()\n\ts := newTestScheduler(t, WithSchedulerMonitor(monitor))\n\n\t// Start scheduler without any jobs\n\ts.Start()\n\ttime.Sleep(50 * time.Millisecond)\n\n\t// Monitor should still be called\n\tassert.Equal(t, int64(1), monitor.getStartedCount())\n\n\terr := s.Shutdown()\n\trequire.NoError(t, err)\n\tassert.Equal(t, int64(1), monitor.getShutdownCount())\n}\n\nfunc TestSchedulerMonitor_ShutdownWithoutStart(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\tmonitor := newTestSchedulerMonitor()\n\ts := newTestScheduler(t, WithSchedulerMonitor(monitor))\n\n\t_, err := s.NewJob(\n\t\tDurationJob(time.Second),\n\t\tNewTask(func() {}),\n\t)\n\trequire.NoError(t, err)\n\n\t// Shutdown without starting\n\terr = s.Shutdown()\n\trequire.NoError(t, err)\n\n\t// SchedulerStarted should not be called\n\tassert.Equal(t, int64(0), monitor.getStartedCount())\n\t// SchedulerShutdown should not be called if scheduler was never started\n\tassert.Equal(t, int64(0), monitor.getShutdownCount())\n}\n\nfunc TestSchedulerMonitor_ThreadSafety(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\tmonitor := newTestSchedulerMonitor()\n\n\t// Simulate concurrent calls to the monitor from multiple goroutines\n\tvar wg sync.WaitGroup\n\titerations := 100\n\n\tfor i := 0; i < iterations; i++ {\n\t\twg.Add(2)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tmonitor.SchedulerStarted()\n\t\t}()\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tmonitor.SchedulerShutdown()\n\t\t}()\n\t}\n\n\twg.Wait()\n\n\t// Verify all calls were recorded\n\tassert.Equal(t, int64(iterations), monitor.getStartedCount())\n\tassert.Equal(t, int64(iterations), monitor.getShutdownCount())\n\tassert.Len(t, monitor.getStartedCalls(), iterations)\n\tassert.Len(t, monitor.getShutdownCalls(), iterations)\n}\n\nfunc TestSchedulerMonitor_IntegrationWithJobs(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\tmonitor := newTestSchedulerMonitor()\n\ts := newTestScheduler(t, WithSchedulerMonitor(monitor))\n\n\t// Test successful job\n\tjobRunCount := atomic.Int32{}\n\tj, err := s.NewJob(\n\t\tDurationJob(50*time.Millisecond),\n\t\tNewTask(func() {\n\t\t\tjobRunCount.Add(1)\n\t\t}),\n\t\tWithStartAt(WithStartImmediately()),\n\t)\n\trequire.NoError(t, err)\n\n\t// Test failing job\n\t_, err = s.NewJob(\n\t\tDurationJob(50*time.Millisecond),\n\t\tNewTask(func() error {\n\t\t\treturn fmt.Errorf(\"test error\")\n\t\t}),\n\t\tWithStartAt(WithStartImmediately()),\n\t)\n\trequire.NoError(t, err)\n\n\t// Start scheduler\n\ts.Start()\n\ttime.Sleep(150 * time.Millisecond) // Wait for jobs to execute\n\n\t// Verify scheduler lifecycle events\n\tassert.Equal(t, int64(1), monitor.getStartedCount())\n\tassert.GreaterOrEqual(t, jobRunCount.Load(), int32(1))\n\n\t// Verify job registration\n\tassert.Equal(t, int64(2), monitor.getJobRegCount(), \"Should have registered 2 jobs\")\n\n\t// Verify job execution events\n\tassert.GreaterOrEqual(t, monitor.getJobStartCount(), int64(1), \"Jobs should have started\")\n\tassert.GreaterOrEqual(t, monitor.getJobRunningCount(), int64(1), \"Jobs should be running\")\n\tassert.GreaterOrEqual(t, monitor.getJobCompletedCount(), int64(1), \"Successful job should complete\")\n\tassert.GreaterOrEqual(t, monitor.getJobFailedCount(), int64(1), \"Failing job should fail\")\n\n\t// Get failed job details\n\tfailedJobs, errors := monitor.getJobFailedCalls()\n\tassert.NotEmpty(t, failedJobs, \"Should have recorded failed jobs\")\n\tassert.NotEmpty(t, errors, \"Should have recorded job errors\")\n\tassert.Contains(t, errors[0].Error(), \"test error\", \"Should record the correct error\")\n\n\t// Test unregistration\n\terr = s.RemoveJob(j.ID())\n\trequire.NoError(t, err)\n\ttime.Sleep(50 * time.Millisecond) // Wait for async removal\n\tassert.Equal(t, int64(1), monitor.getJobUnregCount(), \"Should have unregistered 1 job\")\n\n\t// Shutdown\n\terr = s.Shutdown()\n\trequire.NoError(t, err)\n\tassert.Equal(t, int64(1), monitor.getShutdownCount())\n}\n"
  },
  {
    "path": "scheduler_test.go",
    "content": "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/uuid\"\n\t\"github.com/jonboulle/clockwork\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.uber.org/goleak\"\n)\n\n// ci/cd produces a lot of false positive goroutine leaks for reasons\n// I have not been able to pin down. All tests pass locally without leaks.\n// Tests run in ci will use the TEST_ENV 'ci' to skip running leak detection.\nconst testEnvLocal = \"local\"\n\nvar testEnv = testEnvLocal\n\nfunc init() {\n\ttmp := os.Getenv(\"TEST_ENV\")\n\tif tmp != \"\" {\n\t\ttestEnv = tmp\n\t}\n}\n\nvar verifyNoGoroutineLeaks = func(t *testing.T) {\n\tif testEnv != testEnvLocal {\n\t\treturn\n\t}\n\tgoleak.VerifyNone(t)\n}\n\nfunc newTestScheduler(t *testing.T, options ...SchedulerOption) Scheduler {\n\t// default test options\n\tout := []SchedulerOption{\n\t\tWithLogger(NewLogger(LogLevelDebug)),\n\t\tWithStopTimeout(time.Second),\n\t}\n\n\t// append any additional options 2nd to override defaults if needed\n\tout = append(out, options...)\n\ts, err := NewScheduler(out...)\n\trequire.NoError(t, err)\n\treturn s\n}\n\nvar _ Locker = new(errorLocker)\n\ntype errorLocker struct{}\n\nfunc (e errorLocker) Lock(_ context.Context, _ string) (Lock, error) {\n\treturn nil, errors.New(\"locked\")\n}\n\nfunc TestScheduler_OneSecond_NoOptions(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\tcronNoOptionsCh := make(chan struct{}, 10)\n\tdurationNoOptionsCh := make(chan struct{}, 10)\n\n\ttests := []struct {\n\t\tname string\n\t\tch   chan struct{}\n\t\tjd   JobDefinition\n\t\ttsk  Task\n\t}{\n\t\t{\n\t\t\t\"cron\",\n\t\t\tcronNoOptionsCh,\n\t\t\tCronJob(\n\t\t\t\t\"* * * * * *\",\n\t\t\t\ttrue,\n\t\t\t),\n\t\t\tNewTask(\n\t\t\t\tfunc() {\n\t\t\t\t\tcronNoOptionsCh <- struct{}{}\n\t\t\t\t},\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\t\"duration\",\n\t\t\tdurationNoOptionsCh,\n\t\t\tDurationJob(\n\t\t\t\ttime.Second,\n\t\t\t),\n\t\t\tNewTask(\n\t\t\t\tfunc() {\n\t\t\t\t\tdurationNoOptionsCh <- struct{}{}\n\t\t\t\t},\n\t\t\t),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := newTestScheduler(t)\n\n\t\t\t_, err := s.NewJob(tt.jd, tt.tsk)\n\t\t\trequire.NoError(t, err)\n\n\t\t\ts.Start()\n\n\t\t\tstartTime := time.Now()\n\t\t\tvar runCount int\n\t\t\tfor runCount < 1 {\n\t\t\t\t<-tt.ch\n\t\t\t\trunCount++\n\t\t\t}\n\t\t\trequire.NoError(t, s.Shutdown())\n\t\t\tstopTime := time.Now()\n\n\t\t\tselect {\n\t\t\tcase <-tt.ch:\n\t\t\t\tt.Fatal(\"job ran after scheduler was stopped\")\n\t\t\tcase <-time.After(time.Millisecond * 50):\n\t\t\t}\n\n\t\t\trunDuration := stopTime.Sub(startTime)\n\t\t\tassert.GreaterOrEqual(t, runDuration, time.Millisecond)\n\t\t\tassert.LessOrEqual(t, runDuration, 1500*time.Millisecond)\n\t\t})\n\t}\n}\n\nfunc TestScheduler_LongRunningJobs(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\tif testEnv != testEnvLocal {\n\t\t// this test is flaky in ci, but always passes locally\n\t\tt.SkipNow()\n\t}\n\n\tdurationCh := make(chan struct{}, 10)\n\tdurationSingletonCh := make(chan struct{}, 10)\n\n\ttests := []struct {\n\t\tname         string\n\t\tch           chan struct{}\n\t\tjd           JobDefinition\n\t\ttsk          Task\n\t\topts         []JobOption\n\t\toptions      []SchedulerOption\n\t\texpectedRuns int\n\t}{\n\t\t{\n\t\t\t\"duration with stop time between executions\",\n\t\t\tdurationCh,\n\t\t\tDurationJob(\n\t\t\t\ttime.Millisecond * 500,\n\t\t\t),\n\t\t\tNewTask(\n\t\t\t\tfunc() {\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\t\t\t\t\tdurationCh <- struct{}{}\n\t\t\t\t}),\n\t\t\t[]JobOption{WithStopAt(WithStopDateTime(time.Now().Add(time.Millisecond * 1100)))},\n\t\t\t[]SchedulerOption{WithStopTimeout(time.Second * 2)},\n\t\t\t2,\n\t\t},\n\t\t{\n\t\t\t\"duration\",\n\t\t\tdurationCh,\n\t\t\tDurationJob(\n\t\t\t\ttime.Millisecond * 500,\n\t\t\t),\n\t\t\tNewTask(\n\t\t\t\tfunc() {\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\t\t\t\t\tdurationCh <- struct{}{}\n\t\t\t\t},\n\t\t\t),\n\t\t\tnil,\n\t\t\t[]SchedulerOption{WithStopTimeout(time.Second * 2)},\n\t\t\t3,\n\t\t},\n\t\t{\n\t\t\t\"duration singleton\",\n\t\t\tdurationSingletonCh,\n\t\t\tDurationJob(\n\t\t\t\ttime.Millisecond * 500,\n\t\t\t),\n\t\t\tNewTask(\n\t\t\t\tfunc() {\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\t\t\t\t\tdurationSingletonCh <- struct{}{}\n\t\t\t\t},\n\t\t\t),\n\t\t\t[]JobOption{WithSingletonMode(LimitModeWait)},\n\t\t\t[]SchedulerOption{WithStopTimeout(time.Second * 5)},\n\t\t\t2,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := newTestScheduler(t, tt.options...)\n\n\t\t\t_, err := s.NewJob(tt.jd, tt.tsk, tt.opts...)\n\t\t\trequire.NoError(t, err)\n\n\t\t\ts.Start()\n\t\t\ttime.Sleep(1600 * time.Millisecond)\n\t\t\trequire.NoError(t, s.Shutdown())\n\n\t\t\tvar runCount int\n\t\t\ttimeout := make(chan struct{})\n\t\t\tgo func() {\n\t\t\t\ttime.Sleep(2 * time.Second)\n\t\t\t\tclose(timeout)\n\t\t\t}()\n\t\tOuter:\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase <-tt.ch:\n\t\t\t\t\trunCount++\n\t\t\t\tcase <-timeout:\n\t\t\t\t\tbreak Outer\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.expectedRuns, runCount)\n\t\t})\n\t}\n}\n\nfunc TestScheduler_Update(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\tdurationJobCh := make(chan struct{})\n\n\ttests := []struct {\n\t\tname               string\n\t\tinitialJob         JobDefinition\n\t\tupdateJob          JobDefinition\n\t\ttsk                Task\n\t\tch                 chan struct{}\n\t\trunCount           int\n\t\tupdateAfterCount   int\n\t\texpectedMinTime    time.Duration\n\t\texpectedMaxRunTime time.Duration\n\t}{\n\t\t{\n\t\t\t\"duration, updated to another duration\",\n\t\t\tDurationJob(\n\t\t\t\ttime.Millisecond * 500,\n\t\t\t),\n\t\t\tDurationJob(\n\t\t\t\ttime.Second,\n\t\t\t),\n\t\t\tNewTask(\n\t\t\t\tfunc() {\n\t\t\t\t\tdurationJobCh <- struct{}{}\n\t\t\t\t},\n\t\t\t),\n\t\t\tdurationJobCh,\n\t\t\t2,\n\t\t\t1,\n\t\t\ttime.Second * 1,\n\t\t\ttime.Second * 2,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := newTestScheduler(t)\n\n\t\t\tj, err := s.NewJob(tt.initialJob, tt.tsk)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tstartTime := time.Now()\n\t\t\ts.Start()\n\n\t\t\tvar runCount int\n\t\t\tfor runCount < tt.runCount {\n\t\t\t\tselect {\n\t\t\t\tcase <-tt.ch:\n\t\t\t\t\trunCount++\n\t\t\t\t\tif runCount == tt.updateAfterCount {\n\t\t\t\t\t\t_, err = s.Update(j.ID(), tt.updateJob, tt.tsk)\n\t\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t}\n\t\t\trequire.NoError(t, s.Shutdown())\n\t\t\tstopTime := time.Now()\n\n\t\t\tselect {\n\t\t\tcase <-tt.ch:\n\t\t\t\tt.Fatal(\"job ran after scheduler was stopped\")\n\t\t\tcase <-time.After(time.Millisecond * 50):\n\t\t\t}\n\n\t\t\trunDuration := stopTime.Sub(startTime)\n\t\t\tassert.GreaterOrEqual(t, runDuration, tt.expectedMinTime)\n\t\t\tassert.LessOrEqual(t, runDuration, tt.expectedMaxRunTime)\n\t\t})\n\t}\n}\n\nfunc TestScheduler_StopTimeout(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\ttests := []struct {\n\t\tname string\n\t\tjd   JobDefinition\n\t\tf    any\n\t\topts []JobOption\n\t}{\n\t\t{\n\t\t\t\"duration\",\n\t\t\tDurationJob(\n\t\t\t\ttime.Millisecond * 100,\n\t\t\t),\n\t\t\tfunc(testDoneCtx context.Context) {\n\t\t\t\tselect {\n\t\t\t\tcase <-time.After(1 * time.Second):\n\t\t\t\tcase <-testDoneCtx.Done():\n\t\t\t\t}\n\t\t\t},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"duration singleton\",\n\t\t\tDurationJob(\n\t\t\t\ttime.Millisecond * 100,\n\t\t\t),\n\t\t\tfunc(testDoneCtx context.Context) {\n\t\t\t\tselect {\n\t\t\t\tcase <-time.After(1 * time.Second):\n\t\t\t\tcase <-testDoneCtx.Done():\n\t\t\t\t}\n\t\t\t},\n\t\t\t[]JobOption{WithSingletonMode(LimitModeWait)},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttestDoneCtx, cancel := context.WithCancel(context.Background())\n\t\t\ts := newTestScheduler(t,\n\t\t\t\tWithStopTimeout(time.Millisecond*100),\n\t\t\t)\n\n\t\t\t_, err := s.NewJob(tt.jd, NewTask(tt.f, testDoneCtx), tt.opts...)\n\t\t\trequire.NoError(t, err)\n\n\t\t\ts.Start()\n\t\t\tassert.ErrorIs(t, err, s.Shutdown())\n\t\t\tcancel()\n\t\t\ttime.Sleep(2 * time.Second)\n\t\t})\n\t}\n}\n\nfunc TestScheduler_StopLongRunningJobs(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\tt.Run(\"start, run job, stop jobs before job is completed\", func(t *testing.T) {\n\t\ts := newTestScheduler(t,\n\t\t\tWithStopTimeout(50*time.Millisecond),\n\t\t)\n\n\t\t_, err := s.NewJob(\n\t\t\tDurationJob(\n\t\t\t\t50*time.Millisecond,\n\t\t\t),\n\t\t\tNewTask(\n\t\t\t\tfunc(ctx context.Context) {\n\t\t\t\t\tselect {\n\t\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\tcase <-time.After(100 * time.Millisecond):\n\t\t\t\t\t\tt.Fatal(\"job can not been canceled\")\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t),\n\t\t\tWithStartAt(\n\t\t\t\tWithStartImmediately(),\n\t\t\t),\n\t\t\tWithSingletonMode(LimitModeReschedule),\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\ts.Start()\n\n\t\ttime.Sleep(20 * time.Millisecond)\n\t\t// the running job is canceled, no unexpected timeout error\n\t\trequire.NoError(t, s.StopJobs())\n\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\trequire.NoError(t, s.Shutdown())\n\t})\n\tt.Run(\"start, run job, stop jobs before job is completed - manual context cancel\", func(t *testing.T) {\n\t\ts := newTestScheduler(t,\n\t\t\tWithStopTimeout(50*time.Millisecond),\n\t\t)\n\n\t\tctx, cancel := context.WithCancel(context.Background())\n\n\t\t_, err := s.NewJob(\n\t\t\tDurationJob(\n\t\t\t\t50*time.Millisecond,\n\t\t\t),\n\t\t\tNewTask(\n\t\t\t\tfunc(ctx context.Context) {\n\t\t\t\t\tselect {\n\t\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\tcase <-time.After(100 * time.Millisecond):\n\t\t\t\t\t\tt.Fatal(\"job can not been canceled\")\n\t\t\t\t\t}\n\t\t\t\t}, ctx,\n\t\t\t),\n\t\t\tWithStartAt(\n\t\t\t\tWithStartImmediately(),\n\t\t\t),\n\t\t\tWithSingletonMode(LimitModeReschedule),\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\ts.Start()\n\n\t\ttime.Sleep(20 * time.Millisecond)\n\t\t// the running job is canceled, no unexpected timeout error\n\t\tcancel()\n\t\trequire.NoError(t, s.StopJobs())\n\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\trequire.NoError(t, s.Shutdown())\n\t})\n\tt.Run(\"start, run job, stop jobs before job is completed - manual context cancel WithContext\", func(t *testing.T) {\n\t\ts := newTestScheduler(t,\n\t\t\tWithStopTimeout(50*time.Millisecond),\n\t\t)\n\n\t\tctx, cancel := context.WithCancel(context.Background())\n\n\t\t_, err := s.NewJob(\n\t\t\tDurationJob(\n\t\t\t\t50*time.Millisecond,\n\t\t\t),\n\t\t\tNewTask(\n\t\t\t\tfunc(ctx context.Context) {\n\t\t\t\t\tselect {\n\t\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\tcase <-time.After(100 * time.Millisecond):\n\t\t\t\t\t\tt.Fatal(\"job can not been canceled\")\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t),\n\t\t\tWithStartAt(\n\t\t\t\tWithStartImmediately(),\n\t\t\t),\n\t\t\tWithSingletonMode(LimitModeReschedule),\n\t\t\tWithContext(ctx),\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\ts.Start()\n\n\t\ttime.Sleep(20 * time.Millisecond)\n\t\t// the running job is canceled, no unexpected timeout error\n\t\tcancel()\n\t\trequire.NoError(t, s.StopJobs())\n\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\trequire.NoError(t, s.Shutdown())\n\t})\n}\n\nfunc TestScheduler_StopAndStartLongRunningJobs(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\tt.Run(\"start, run job, stop jobs before job is completed\", func(t *testing.T) {\n\t\ts := newTestScheduler(t,\n\t\t\tWithStopTimeout(50*time.Millisecond),\n\t\t)\n\n\t\t_, err := s.NewJob(\n\t\t\tDurationJob(\n\t\t\t\t50*time.Millisecond,\n\t\t\t),\n\t\t\tNewTask(\n\t\t\t\tfunc(ctx context.Context) {\n\t\t\t\t\tselect {\n\t\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\tcase <-time.After(100 * time.Millisecond):\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t),\n\t\t\tWithStartAt(\n\t\t\t\tWithStartImmediately(),\n\t\t\t),\n\t\t\tWithSingletonMode(LimitModeReschedule),\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\ts.Start()\n\n\t\ttime.Sleep(20 * time.Millisecond)\n\t\t// the running job is canceled, no unexpected timeout error\n\t\trequire.NoError(t, s.StopJobs())\n\n\t\ts.Start()\n\n\t\ttime.Sleep(200 * time.Millisecond)\n\t\trequire.NoError(t, s.Shutdown())\n\t})\n}\n\nfunc TestScheduler_Shutdown(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\tt.Run(\"start, stop, start, shutdown\", func(t *testing.T) {\n\t\ts := newTestScheduler(t,\n\t\t\tWithStopTimeout(time.Second),\n\t\t)\n\n\t\t_, err := s.NewJob(\n\t\t\tDurationJob(\n\t\t\t\t50*time.Millisecond,\n\t\t\t),\n\t\t\tNewTask(\n\t\t\t\tfunc() {},\n\t\t\t),\n\t\t\tWithStartAt(\n\t\t\t\tWithStartImmediately(),\n\t\t\t),\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\ts.Start()\n\t\trequire.NoError(t, s.StopJobs())\n\n\t\ts.Start()\n\n\t\trequire.NoError(t, s.Shutdown())\n\t})\n\n\tt.Run(\"calling Job methods after shutdown errors\", func(t *testing.T) {\n\t\ts := newTestScheduler(t,\n\t\t\tWithStopTimeout(time.Second),\n\t\t)\n\t\tj, err := s.NewJob(\n\t\t\tDurationJob(\n\t\t\t\t100*time.Millisecond,\n\t\t\t),\n\t\t\tNewTask(\n\t\t\t\tfunc() {},\n\t\t\t),\n\t\t\tWithStartAt(\n\t\t\t\tWithStartImmediately(),\n\t\t\t),\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\ts.Start()\n\t\trequire.NoError(t, s.Shutdown())\n\n\t\t_, err = j.LastRun()\n\t\tassert.ErrorIs(t, err, ErrJobNotFound)\n\n\t\t_, err = j.NextRun()\n\t\tassert.ErrorIs(t, err, ErrJobNotFound)\n\t})\n\n\tt.Run(\"calling shutdown multiple times is a no-op\", func(t *testing.T) {\n\t\ts := newTestScheduler(t)\n\n\t\ts.Start()\n\n\t\tassert.NoError(t, s.Shutdown())\n\t\tassert.NoError(t, s.Shutdown())\n\t})\n}\n\nfunc TestScheduler_Start(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\tt.Run(\"calling start multiple times is a no-op\", func(t *testing.T) {\n\t\ts := newTestScheduler(t)\n\n\t\tvar counter int\n\t\tvar mu sync.Mutex\n\n\t\t_, err := s.NewJob(\n\t\t\tDurationJob(\n\t\t\t\t100*time.Millisecond,\n\t\t\t),\n\t\t\tNewTask(\n\t\t\t\tfunc() {\n\t\t\t\t\tmu.Lock()\n\t\t\t\t\tcounter++\n\t\t\t\t\tmu.Unlock()\n\t\t\t\t},\n\t\t\t),\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\ts.Start()\n\t\ts.Start()\n\t\ts.Start()\n\n\t\ttime.Sleep(1000 * time.Millisecond)\n\n\t\trequire.NoError(t, s.Shutdown())\n\n\t\tassert.Contains(t, []int{9, 10}, counter)\n\t})\n}\n\nfunc TestScheduler_NewJob(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\ttests := []struct {\n\t\tname string\n\t\tjd   JobDefinition\n\t\ttsk  Task\n\t\topts []JobOption\n\t}{\n\t\t{\n\t\t\t\"cron with timezone\",\n\t\t\tCronJob(\n\t\t\t\t\"CRON_TZ=America/Chicago * * * * * *\",\n\t\t\t\ttrue,\n\t\t\t),\n\t\t\tNewTask(\n\t\t\t\tfunc() {},\n\t\t\t),\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"cron with timezone, no seconds\",\n\t\t\tCronJob(\n\t\t\t\t\"CRON_TZ=America/Chicago * * * * *\",\n\t\t\t\tfalse,\n\t\t\t),\n\t\t\tNewTask(\n\t\t\t\tfunc() {},\n\t\t\t),\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"random duration\",\n\t\t\tDurationRandomJob(\n\t\t\t\ttime.Second,\n\t\t\t\ttime.Second*5,\n\t\t\t),\n\t\t\tNewTask(\n\t\t\t\tfunc() {},\n\t\t\t),\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"daily\",\n\t\t\tDailyJob(\n\t\t\t\t1,\n\t\t\t\tNewAtTimes(\n\t\t\t\t\tNewAtTime(1, 0, 0),\n\t\t\t\t),\n\t\t\t),\n\t\t\tNewTask(\n\t\t\t\tfunc() {},\n\t\t\t),\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"weekly\",\n\t\t\tWeeklyJob(\n\t\t\t\t1,\n\t\t\t\tNewWeekdays(time.Monday),\n\t\t\t\tNewAtTimes(\n\t\t\t\t\tNewAtTime(1, 0, 0),\n\t\t\t\t),\n\t\t\t),\n\t\t\tNewTask(\n\t\t\t\tfunc() {},\n\t\t\t),\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"monthly\",\n\t\t\tMonthlyJob(\n\t\t\t\t1,\n\t\t\t\tNewDaysOfTheMonth(1, -1),\n\t\t\t\tNewAtTimes(\n\t\t\t\t\tNewAtTime(1, 0, 0),\n\t\t\t\t),\n\t\t\t),\n\t\t\tNewTask(\n\t\t\t\tfunc() {},\n\t\t\t),\n\t\t\tnil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := newTestScheduler(t)\n\n\t\t\t_, err := s.NewJob(tt.jd, tt.tsk, tt.opts...)\n\t\t\trequire.NoError(t, err)\n\n\t\t\ts.Start()\n\t\t\trequire.NoError(t, s.Shutdown())\n\t\t})\n\t}\n}\n\nfunc TestScheduler_NewJobErrors(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\ttests := []struct {\n\t\tname string\n\t\tjd   JobDefinition\n\t\topts []JobOption\n\t\terr  error\n\t}{\n\t\t{\n\t\t\t\"cron with timezone\",\n\t\t\tCronJob(\n\t\t\t\t\"bad cron\",\n\t\t\t\ttrue,\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrCronJobParse,\n\t\t},\n\t\t{\n\t\t\t\"cron invalid date\",\n\t\t\tCronJob(\n\t\t\t\t\"* * * 31 FEB *\",\n\t\t\t\ttrue,\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrCronJobInvalid,\n\t\t},\n\t\t{\n\t\t\t\"context nil\",\n\t\t\tDurationJob(time.Second),\n\t\t\t[]JobOption{WithContext(nil)}, //nolint:staticcheck\n\t\t\tErrWithContextNil,\n\t\t},\n\t\t{\n\t\t\t\"duration job time interval is zero\",\n\t\t\tDurationJob(0 * time.Second),\n\t\t\tnil,\n\t\t\tErrDurationJobIntervalZero,\n\t\t},\n\t\t{\n\t\t\t\"duration job time interval is negative\",\n\t\t\tDurationJob(-1 * time.Second),\n\t\t\tnil,\n\t\t\tErrDurationJobIntervalNegative,\n\t\t},\n\t\t{\n\t\t\t\"random with bad min/max\",\n\t\t\tDurationRandomJob(\n\t\t\t\ttime.Second*5,\n\t\t\t\ttime.Second,\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrDurationRandomJobMinMax,\n\t\t},\n\t\t{\n\t\t\t\"random with negative min\",\n\t\t\tDurationRandomJob(\n\t\t\t\t-time.Second,\n\t\t\t\ttime.Second,\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrDurationRandomJobPositive,\n\t\t},\n\t\t{\n\t\t\t\"random with negative max\",\n\t\t\tDurationRandomJob(\n\t\t\t\t-2*time.Second,\n\t\t\t\t-time.Second,\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrDurationRandomJobPositive,\n\t\t},\n\t\t{\n\t\t\t\"daily job at times nil\",\n\t\t\tDailyJob(\n\t\t\t\t1,\n\t\t\t\tnil,\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrDailyJobAtTimesNil,\n\t\t},\n\t\t{\n\t\t\t\"daily job at time nil\",\n\t\t\tDailyJob(\n\t\t\t\t1,\n\t\t\t\tNewAtTimes(nil),\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrDailyJobAtTimeNil,\n\t\t},\n\t\t{\n\t\t\t\"daily job hours out of range\",\n\t\t\tDailyJob(\n\t\t\t\t1,\n\t\t\t\tNewAtTimes(\n\t\t\t\t\tNewAtTime(100, 0, 0),\n\t\t\t\t),\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrDailyJobHours,\n\t\t},\n\t\t{\n\t\t\t\"daily job minutes out of range\",\n\t\t\tDailyJob(\n\t\t\t\t1,\n\t\t\t\tNewAtTimes(\n\t\t\t\t\tNewAtTime(1, 100, 0),\n\t\t\t\t),\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrDailyJobMinutesSeconds,\n\t\t},\n\t\t{\n\t\t\t\"daily job seconds out of range\",\n\t\t\tDailyJob(\n\t\t\t\t1,\n\t\t\t\tNewAtTimes(\n\t\t\t\t\tNewAtTime(1, 0, 100),\n\t\t\t\t),\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrDailyJobMinutesSeconds,\n\t\t},\n\t\t{\n\t\t\t\"daily job interval 0\",\n\t\t\tDailyJob(\n\t\t\t\t0,\n\t\t\t\tNewAtTimes(\n\t\t\t\t\tNewAtTime(1, 0, 0),\n\t\t\t\t),\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrDailyJobZeroInterval,\n\t\t},\n\t\t{\n\t\t\t\"weekly job at times nil\",\n\t\t\tWeeklyJob(\n\t\t\t\t1,\n\t\t\t\tNewWeekdays(time.Monday),\n\t\t\t\tnil,\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrWeeklyJobAtTimesNil,\n\t\t},\n\t\t{\n\t\t\t\"weekly job at time nil\",\n\t\t\tWeeklyJob(\n\t\t\t\t1,\n\t\t\t\tNewWeekdays(time.Monday),\n\t\t\t\tNewAtTimes(nil),\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrWeeklyJobAtTimeNil,\n\t\t},\n\t\t{\n\t\t\t\"weekly job weekdays nil\",\n\t\t\tWeeklyJob(\n\t\t\t\t1,\n\t\t\t\tnil,\n\t\t\t\tNewAtTimes(\n\t\t\t\t\tNewAtTime(1, 0, 0),\n\t\t\t\t),\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrWeeklyJobDaysOfTheWeekNil,\n\t\t},\n\t\t{\n\t\t\t\"weekly job hours out of range\",\n\t\t\tWeeklyJob(\n\t\t\t\t1,\n\t\t\t\tNewWeekdays(time.Monday),\n\t\t\t\tNewAtTimes(\n\t\t\t\t\tNewAtTime(100, 0, 0),\n\t\t\t\t),\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrWeeklyJobHours,\n\t\t},\n\t\t{\n\t\t\t\"weekly job minutes out of range\",\n\t\t\tWeeklyJob(\n\t\t\t\t1,\n\t\t\t\tNewWeekdays(time.Monday),\n\t\t\t\tNewAtTimes(\n\t\t\t\t\tNewAtTime(1, 100, 0),\n\t\t\t\t),\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrWeeklyJobMinutesSeconds,\n\t\t},\n\t\t{\n\t\t\t\"weekly job seconds out of range\",\n\t\t\tWeeklyJob(\n\t\t\t\t1,\n\t\t\t\tNewWeekdays(time.Monday),\n\t\t\t\tNewAtTimes(\n\t\t\t\t\tNewAtTime(1, 0, 100),\n\t\t\t\t),\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrWeeklyJobMinutesSeconds,\n\t\t},\n\t\t{\n\t\t\t\"weekly job interval zero\",\n\t\t\tWeeklyJob(\n\t\t\t\t0,\n\t\t\t\tNewWeekdays(time.Monday),\n\t\t\t\tNewAtTimes(\n\t\t\t\t\tNewAtTime(1, 0, 0),\n\t\t\t\t),\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrWeeklyJobZeroInterval,\n\t\t},\n\t\t{\n\t\t\t\"monthly job at times nil\",\n\t\t\tMonthlyJob(\n\t\t\t\t1,\n\t\t\t\tNewDaysOfTheMonth(1),\n\t\t\t\tnil,\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrMonthlyJobAtTimesNil,\n\t\t},\n\t\t{\n\t\t\t\"monthly job at time nil\",\n\t\t\tMonthlyJob(\n\t\t\t\t1,\n\t\t\t\tNewDaysOfTheMonth(1),\n\t\t\t\tNewAtTimes(nil),\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrMonthlyJobAtTimeNil,\n\t\t},\n\t\t{\n\t\t\t\"monthly job days out of range\",\n\t\t\tMonthlyJob(\n\t\t\t\t1,\n\t\t\t\tNewDaysOfTheMonth(0),\n\t\t\t\tNewAtTimes(\n\t\t\t\t\tNewAtTime(1, 0, 0),\n\t\t\t\t),\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrMonthlyJobDays,\n\t\t},\n\t\t{\n\t\t\t\"monthly job days out of range\",\n\t\t\tMonthlyJob(\n\t\t\t\t1,\n\t\t\t\tnil,\n\t\t\t\tNewAtTimes(\n\t\t\t\t\tNewAtTime(1, 0, 0),\n\t\t\t\t),\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrMonthlyJobDaysNil,\n\t\t},\n\t\t{\n\t\t\t\"monthly job hours out of range\",\n\t\t\tMonthlyJob(\n\t\t\t\t1,\n\t\t\t\tNewDaysOfTheMonth(1),\n\t\t\t\tNewAtTimes(\n\t\t\t\t\tNewAtTime(100, 0, 0),\n\t\t\t\t),\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrMonthlyJobHours,\n\t\t},\n\t\t{\n\t\t\t\"monthly job minutes out of range\",\n\t\t\tMonthlyJob(\n\t\t\t\t1,\n\t\t\t\tNewDaysOfTheMonth(1),\n\t\t\t\tNewAtTimes(\n\t\t\t\t\tNewAtTime(1, 100, 0),\n\t\t\t\t),\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrMonthlyJobMinutesSeconds,\n\t\t},\n\t\t{\n\t\t\t\"monthly job seconds out of range\",\n\t\t\tMonthlyJob(\n\t\t\t\t1,\n\t\t\t\tNewDaysOfTheMonth(1),\n\t\t\t\tNewAtTimes(\n\t\t\t\t\tNewAtTime(1, 0, 100),\n\t\t\t\t),\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrMonthlyJobMinutesSeconds,\n\t\t},\n\t\t{\n\t\t\t\"monthly job interval zero\",\n\t\t\tMonthlyJob(\n\t\t\t\t0,\n\t\t\t\tNewDaysOfTheMonth(1),\n\t\t\t\tNewAtTimes(\n\t\t\t\t\tNewAtTime(1, 0, 0),\n\t\t\t\t),\n\t\t\t),\n\t\t\tnil,\n\t\t\tErrMonthlyJobZeroInterval,\n\t\t},\n\t\t{\n\t\t\t\"WithName no name\",\n\t\t\tDurationJob(\n\t\t\t\ttime.Second,\n\t\t\t),\n\t\t\t[]JobOption{WithName(\"\")},\n\t\t\tErrWithNameEmpty,\n\t\t},\n\t\t{\n\t\t\t\"WithStartDateTime is zero\",\n\t\t\tDurationJob(\n\t\t\t\ttime.Second,\n\t\t\t),\n\t\t\t[]JobOption{WithStartAt(WithStartDateTime(time.Time{}))},\n\t\t\tErrWithStartDateTimePast,\n\t\t},\n\t\t{\n\t\t\t\"WithStartDateTime is in the past\",\n\t\t\tDurationJob(\n\t\t\t\ttime.Second,\n\t\t\t),\n\t\t\t[]JobOption{WithStartAt(WithStartDateTime(time.Now().Add(-time.Second)))},\n\t\t\tErrWithStartDateTimePast,\n\t\t},\n\t\t{\n\t\t\t\"WithStartDateTimePast is zero\",\n\t\t\tDurationJob(\n\t\t\t\ttime.Second,\n\t\t\t),\n\t\t\t[]JobOption{WithStartAt(WithStartDateTimePast(time.Time{}))},\n\t\t\tErrWithStartDateTimePastZero,\n\t\t},\n\t\t{\n\t\t\t\"WithStartDateTime is later than the end\",\n\t\t\tDurationJob(\n\t\t\t\ttime.Second,\n\t\t\t),\n\t\t\t[]JobOption{WithStopAt(WithStopDateTime(time.Now().Add(time.Second))), WithStartAt(WithStartDateTime(time.Now().Add(time.Hour)))},\n\t\t\tErrStartTimeLaterThanEndTime,\n\t\t},\n\t\t{\n\t\t\t\"WithStopDateTime is earlier than the start\",\n\t\t\tDurationJob(\n\t\t\t\ttime.Second,\n\t\t\t),\n\t\t\t[]JobOption{WithStartAt(WithStartDateTime(time.Now().Add(time.Hour))), WithStopAt(WithStopDateTime(time.Now().Add(time.Second)))},\n\t\t\tErrStopTimeEarlierThanStartTime,\n\t\t},\n\t\t{\n\t\t\t\"oneTimeJob start at is zero\",\n\t\t\tOneTimeJob(OneTimeJobStartDateTime(time.Time{})),\n\t\t\tnil,\n\t\t\tErrOneTimeJobStartDateTimePast,\n\t\t},\n\t\t{\n\t\t\t\"oneTimeJob start at is in past\",\n\t\t\tOneTimeJob(OneTimeJobStartDateTime(time.Now().Add(-time.Second))),\n\t\t\tnil,\n\t\t\tErrOneTimeJobStartDateTimePast,\n\t\t},\n\t\t{\n\t\t\t\"WithDistributedJobLocker is nil\",\n\t\t\tDurationJob(\n\t\t\t\ttime.Second,\n\t\t\t),\n\t\t\t[]JobOption{WithDistributedJobLocker(nil)},\n\t\t\tErrWithDistributedJobLockerNil,\n\t\t},\n\t\t{\n\t\t\t\"WithIdentifier is nil\",\n\t\t\tDurationJob(\n\t\t\t\ttime.Second,\n\t\t\t),\n\t\t\t[]JobOption{WithIdentifier(uuid.Nil)},\n\t\t\tErrWithIdentifierNil,\n\t\t},\n\t\t{\n\t\t\t\"WithLimitedRuns is zero\",\n\t\t\tDurationJob(\n\t\t\t\ttime.Second,\n\t\t\t),\n\t\t\t[]JobOption{WithLimitedRuns(0)},\n\t\t\tErrWithLimitedRunsZero,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := newTestScheduler(t,\n\t\t\t\tWithStopTimeout(time.Millisecond*50),\n\t\t\t)\n\n\t\t\t_, err := s.NewJob(tt.jd, NewTask(func() {}), tt.opts...)\n\t\t\tassert.ErrorIs(t, err, tt.err)\n\t\t\trequire.NoError(t, s.Shutdown())\n\t\t})\n\t\tt.Run(tt.name+\" global\", func(t *testing.T) {\n\t\t\ts := newTestScheduler(t,\n\t\t\t\tWithStopTimeout(time.Millisecond*50),\n\t\t\t\tWithGlobalJobOptions(tt.opts...),\n\t\t\t)\n\n\t\t\t_, err := s.NewJob(tt.jd, NewTask(func() {}))\n\t\t\tassert.ErrorIs(t, err, tt.err)\n\t\t\trequire.NoError(t, s.Shutdown())\n\t\t})\n\t}\n}\n\nfunc TestScheduler_NewJobTask(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\ttestFuncPtr := func() {}\n\ttestFuncWithParams := func(_, _ string) {}\n\ttestStruct := struct{}{}\n\n\ttests := []struct {\n\t\tname string\n\t\ttsk  Task\n\t\terr  error\n\t}{\n\t\t{\n\t\t\t\"task nil\",\n\t\t\tnil,\n\t\t\tErrNewJobTaskNil,\n\t\t},\n\t\t{\n\t\t\t\"task not func - nil\",\n\t\t\tNewTask(nil),\n\t\t\tErrNewJobTaskNotFunc,\n\t\t},\n\t\t{\n\t\t\t\"task not func - string\",\n\t\t\tNewTask(\"not a func\"),\n\t\t\tErrNewJobTaskNotFunc,\n\t\t},\n\t\t{\n\t\t\t\"task func is pointer\",\n\t\t\tNewTask(&testFuncPtr),\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"parameter number does not match\",\n\t\t\tNewTask(testFuncWithParams, \"one\"),\n\t\t\tErrNewJobWrongNumberOfParameters,\n\t\t},\n\t\t{\n\t\t\t\"parameter type does not match\",\n\t\t\tNewTask(testFuncWithParams, \"one\", 2),\n\t\t\tErrNewJobWrongTypeOfParameters,\n\t\t},\n\t\t{\n\t\t\t\"parameter number does not match - ptr\",\n\t\t\tNewTask(&testFuncWithParams, \"one\"),\n\t\t\tErrNewJobWrongNumberOfParameters,\n\t\t},\n\t\t{\n\t\t\t\"parameter type does not match - ptr\",\n\t\t\tNewTask(&testFuncWithParams, \"one\", 2),\n\t\t\tErrNewJobWrongTypeOfParameters,\n\t\t},\n\t\t{\n\t\t\t\"all good struct\",\n\t\t\tNewTask(func(_ struct{}) {}, struct{}{}),\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"all good interface\",\n\t\t\tNewTask(func(_ interface{}) {}, struct{}{}),\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"all good any\",\n\t\t\tNewTask(func(_ any) {}, struct{}{}),\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"all good slice\",\n\t\t\tNewTask(func(_ []struct{}) {}, []struct{}{}),\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"all good chan\",\n\t\t\tNewTask(func(_ chan struct{}) {}, make(chan struct{})),\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"all good pointer\",\n\t\t\tNewTask(func(_ *struct{}) {}, &testStruct),\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"all good map\",\n\t\t\tNewTask(func(_ map[string]struct{}) {}, make(map[string]struct{})),\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"all good\",\n\t\t\tNewTask(&testFuncWithParams, \"one\", \"two\"),\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"parameter type does not match - different argument types against variadic parameters\",\n\t\t\tNewTask(func(_ ...string) {}, \"one\", 2),\n\t\t\tErrNewJobWrongTypeOfParameters,\n\t\t},\n\t\t{\n\t\t\t\"all good string - variadic\",\n\t\t\tNewTask(func(_ ...string) {}, \"one\", \"two\"),\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"all good mixed variadic\",\n\t\t\tNewTask(func(_ int, _ ...string) {}, 1, \"one\", \"two\"),\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"all good struct - variadic\",\n\t\t\tNewTask(func(_ ...interface{}) {}, struct{}{}),\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"all good no arguments passed in - variadic\",\n\t\t\tNewTask(func(_ ...interface{}) {}),\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"all good - interface variadic, int, string\",\n\t\t\tNewTask(func(_ ...interface{}) {}, 1, \"2\", 3.0),\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"parameter type does not match - different argument types against interface variadic parameters\",\n\t\t\tNewTask(func(_ ...io.Reader) {}, os.Stdout, any(3.0)),\n\t\t\tErrNewJobWrongTypeOfParameters,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := newTestScheduler(t)\n\n\t\t\t_, err := s.NewJob(DurationJob(time.Second), tt.tsk)\n\t\t\tassert.ErrorIs(t, err, tt.err)\n\t\t\trequire.NoError(t, s.Shutdown())\n\t\t})\n\t}\n}\n\nfunc TestScheduler_WithOptionsErrors(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\ttests := []struct {\n\t\tname string\n\t\topt  SchedulerOption\n\t\terr  error\n\t}{\n\t\t{\n\t\t\t\"WithClock nil\",\n\t\t\tWithClock(nil),\n\t\t\tErrWithClockNil,\n\t\t},\n\t\t{\n\t\t\t\"WithDistributedElector nil\",\n\t\t\tWithDistributedElector(nil),\n\t\t\tErrWithDistributedElectorNil,\n\t\t},\n\t\t{\n\t\t\t\"WithDistributedLocker nil\",\n\t\t\tWithDistributedLocker(nil),\n\t\t\tErrWithDistributedLockerNil,\n\t\t},\n\t\t{\n\t\t\t\"WithLimitConcurrentJobs limit 0\",\n\t\t\tWithLimitConcurrentJobs(0, LimitModeWait),\n\t\t\tErrWithLimitConcurrentJobsZero,\n\t\t},\n\t\t{\n\t\t\t\"WithLocation nil\",\n\t\t\tWithLocation(nil),\n\t\t\tErrWithLocationNil,\n\t\t},\n\t\t{\n\t\t\t\"WithLogger nil\",\n\t\t\tWithLogger(nil),\n\t\t\tErrWithLoggerNil,\n\t\t},\n\t\t{\n\t\t\t\"WithStopTimeout 0\",\n\t\t\tWithStopTimeout(0),\n\t\t\tErrWithStopTimeoutZeroOrNegative,\n\t\t},\n\t\t{\n\t\t\t\"WithStopTimeout -1\",\n\t\t\tWithStopTimeout(-1),\n\t\t\tErrWithStopTimeoutZeroOrNegative,\n\t\t},\n\t\t{\n\t\t\t\"WithMonitorer nil\",\n\t\t\tWithMonitor(nil),\n\t\t\tErrWithMonitorNil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t_, err := NewScheduler(tt.opt)\n\t\t\tassert.ErrorIs(t, err, tt.err)\n\t\t})\n\t}\n}\n\nfunc TestScheduler_Singleton(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\ttests := []struct {\n\t\tname        string\n\t\tduration    time.Duration\n\t\tlimitMode   LimitMode\n\t\trunCount    int\n\t\texpectedMin time.Duration\n\t\texpectedMax time.Duration\n\t}{\n\t\t{\n\t\t\t\"singleton mode reschedule\",\n\t\t\ttime.Millisecond * 100,\n\t\t\tLimitModeReschedule,\n\t\t\t3,\n\t\t\ttime.Millisecond * 600,\n\t\t\ttime.Millisecond * 1100,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tjobRanCh := make(chan struct{}, 10)\n\n\t\t\ts := newTestScheduler(t,\n\t\t\t\tWithStopTimeout(1*time.Second),\n\t\t\t\tWithLocation(time.Local),\n\t\t\t)\n\n\t\t\t_, err := s.NewJob(\n\t\t\t\tDurationJob(\n\t\t\t\t\ttt.duration,\n\t\t\t\t),\n\t\t\t\tNewTask(func() {\n\t\t\t\t\ttime.Sleep(tt.duration * 2)\n\t\t\t\t\tjobRanCh <- struct{}{}\n\t\t\t\t}),\n\t\t\t\tWithSingletonMode(tt.limitMode),\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tstart := time.Now()\n\t\t\ts.Start()\n\n\t\t\tvar runCount int\n\t\t\tfor runCount < tt.runCount {\n\t\t\t\tselect {\n\t\t\t\tcase <-jobRanCh:\n\t\t\t\t\trunCount++\n\t\t\t\tcase <-time.After(time.Second):\n\t\t\t\t\tt.Fatalf(\"timed out waiting for jobs to run\")\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tstop := time.Now()\n\t\t\trequire.NoError(t, s.Shutdown())\n\n\t\t\tassert.GreaterOrEqual(t, stop.Sub(start), tt.expectedMin)\n\t\t\tassert.LessOrEqual(t, stop.Sub(start), tt.expectedMax)\n\t\t})\n\t}\n}\n\nfunc TestScheduler_LimitMode(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\ttests := []struct {\n\t\tname        string\n\t\tnumJobs     int\n\t\tlimit       uint\n\t\tlimitMode   LimitMode\n\t\tduration    time.Duration\n\t\texpectedMin time.Duration\n\t\texpectedMax time.Duration\n\t}{\n\t\t{\n\t\t\t\"limit mode reschedule\",\n\t\t\t10,\n\t\t\t2,\n\t\t\tLimitModeReschedule,\n\t\t\ttime.Millisecond * 100,\n\t\t\ttime.Millisecond * 400,\n\t\t\ttime.Millisecond * 700,\n\t\t},\n\t\t{\n\t\t\t\"limit mode wait\",\n\t\t\t10,\n\t\t\t2,\n\t\t\tLimitModeWait,\n\t\t\ttime.Millisecond * 100,\n\t\t\ttime.Millisecond * 200,\n\t\t\ttime.Millisecond * 500,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := newTestScheduler(t,\n\t\t\t\tWithLimitConcurrentJobs(tt.limit, tt.limitMode),\n\t\t\t\tWithStopTimeout(2*time.Second),\n\t\t\t)\n\n\t\t\tjobRanCh := make(chan struct{}, 20)\n\n\t\t\tfor i := 0; i < tt.numJobs; i++ {\n\t\t\t\t_, err := s.NewJob(\n\t\t\t\t\tDurationJob(tt.duration),\n\t\t\t\t\tNewTask(func() {\n\t\t\t\t\t\ttime.Sleep(tt.duration / 2)\n\t\t\t\t\t\tjobRanCh <- struct{}{}\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tstart := time.Now()\n\t\t\ts.Start()\n\n\t\t\tvar runCount int\n\t\t\tfor runCount < tt.numJobs {\n\t\t\t\tselect {\n\t\t\t\tcase <-jobRanCh:\n\t\t\t\t\trunCount++\n\t\t\t\tcase <-time.After(time.Second):\n\t\t\t\t\tt.Fatalf(\"timed out waiting for jobs to run\")\n\t\t\t\t}\n\t\t\t}\n\t\t\tstop := time.Now()\n\t\t\trequire.NoError(t, s.Shutdown())\n\n\t\t\tassert.GreaterOrEqual(t, stop.Sub(start), tt.expectedMin)\n\t\t\tassert.LessOrEqual(t, stop.Sub(start), tt.expectedMax)\n\t\t})\n\t}\n}\n\nfunc TestScheduler_LimitModeAndSingleton(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\ttests := []struct {\n\t\tname          string\n\t\tnumJobs       int\n\t\tlimit         uint\n\t\tlimitMode     LimitMode\n\t\tsingletonMode LimitMode\n\t\tduration      time.Duration\n\t\texpectedMin   time.Duration\n\t\texpectedMax   time.Duration\n\t}{\n\t\t{\n\t\t\t\"limit mode reschedule\",\n\t\t\t10,\n\t\t\t2,\n\t\t\tLimitModeReschedule,\n\t\t\tLimitModeReschedule,\n\t\t\ttime.Millisecond * 100,\n\t\t\ttime.Millisecond * 400,\n\t\t\ttime.Millisecond * 700,\n\t\t},\n\t\t{\n\t\t\t\"limit mode wait\",\n\t\t\t10,\n\t\t\t2,\n\t\t\tLimitModeWait,\n\t\t\tLimitModeWait,\n\t\t\ttime.Millisecond * 100,\n\t\t\ttime.Millisecond * 200,\n\t\t\ttime.Millisecond * 500,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := newTestScheduler(t,\n\t\t\t\tWithLimitConcurrentJobs(tt.limit, tt.limitMode),\n\t\t\t\tWithStopTimeout(2*time.Second),\n\t\t\t)\n\n\t\t\tjobRanCh := make(chan int, 20)\n\n\t\t\tfor i := 0; i < tt.numJobs; i++ {\n\t\t\t\tjobNum := i\n\t\t\t\t_, err := s.NewJob(\n\t\t\t\t\tDurationJob(tt.duration),\n\t\t\t\t\tNewTask(func() {\n\t\t\t\t\t\ttime.Sleep(tt.duration / 2)\n\t\t\t\t\t\tjobRanCh <- jobNum\n\t\t\t\t\t}),\n\t\t\t\t\tWithSingletonMode(tt.singletonMode),\n\t\t\t\t)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tstart := time.Now()\n\t\t\ts.Start()\n\n\t\t\tjobsRan := make(map[int]int)\n\t\t\tvar runCount int\n\t\t\tfor runCount < tt.numJobs {\n\t\t\t\tselect {\n\t\t\t\tcase jobNum := <-jobRanCh:\n\t\t\t\t\trunCount++\n\t\t\t\t\tjobsRan[jobNum]++\n\t\t\t\tcase <-time.After(time.Second):\n\t\t\t\t\tt.Fatalf(\"timed out waiting for jobs to run\")\n\t\t\t\t}\n\t\t\t}\n\t\t\tstop := time.Now()\n\t\t\trequire.NoError(t, s.Shutdown())\n\n\t\t\tassert.GreaterOrEqual(t, stop.Sub(start), tt.expectedMin)\n\t\t\tassert.LessOrEqual(t, stop.Sub(start), tt.expectedMax)\n\t\t\tfor _, count := range jobsRan {\n\t\t\t\tif tt.singletonMode == LimitModeWait {\n\t\t\t\t\tassert.Equal(t, 1, count)\n\t\t\t\t} else {\n\t\t\t\t\tassert.LessOrEqual(t, count, 5)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestScheduler_OneTimeJob_DoesNotCleanupNext(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\tschedulerStartTime := time.Date(2024, time.April, 3, 4, 5, 0, 0, time.UTC)\n\n\ttests := []struct {\n\t\tname      string\n\t\trunAt     time.Time\n\t\tfakeClock *clockwork.FakeClock\n\t\tassertErr require.ErrorAssertionFunc\n\t\t// asserts things about schedules, advance time and perform new assertions\n\t\tadvanceAndAsserts []func(\n\t\t\tt *testing.T,\n\t\t\tj Job,\n\t\t\tclock *clockwork.FakeClock,\n\t\t\truns *atomic.Uint32,\n\t\t)\n\t}{\n\t\t{\n\t\t\tname:      \"exhausted run do does not cleanup next item\",\n\t\t\trunAt:     time.Date(2024, time.April, 22, 4, 5, 0, 0, time.UTC),\n\t\t\tfakeClock: clockwork.NewFakeClockAt(schedulerStartTime),\n\t\t\tadvanceAndAsserts: []func(t *testing.T, j Job, clock *clockwork.FakeClock, runs *atomic.Uint32){\n\t\t\t\tfunc(t *testing.T, j Job, clock *clockwork.FakeClock, runs *atomic.Uint32) {\n\t\t\t\t\trequire.Equal(t, uint32(0), runs.Load())\n\n\t\t\t\t\t// last not initialized\n\t\t\t\t\tlastRunAt, err := j.LastRun()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.Equal(t, time.Time{}, lastRunAt)\n\n\t\t\t\t\t// next is now\n\t\t\t\t\texpected := time.Date(2024, time.April, 22, 4, 5, 0, 0, time.UTC)\n\t\t\t\t\tnextRunAt, err := j.NextRun()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.Equal(t, expected, nextRunAt.UTC())\n\n\t\t\t\t\t// advance and eventually run\n\t\t\t\t\toneSecondAfterNextRun := expected.Add(1 * time.Second)\n\n\t\t\t\t\tclock.Advance(oneSecondAfterNextRun.Sub(schedulerStartTime))\n\t\t\t\t\trequire.Eventually(t, func() bool {\n\t\t\t\t\t\treturn uint32(1) == runs.Load()\n\t\t\t\t\t}, 3*time.Second, 100*time.Millisecond)\n\n\t\t\t\t\t// last was run\n\t\t\t\t\tlastRunAt, err = j.LastRun()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.WithinDuration(t, expected, lastRunAt, 1*time.Second)\n\n\t\t\t\t\tnextRunAt, err = j.NextRun()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.Equal(t, time.Time{}, nextRunAt)\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := newTestScheduler(t, WithClock(tt.fakeClock), WithLocation(time.UTC))\n\t\t\tt.Cleanup(func() {\n\t\t\t\trequire.NoError(t, s.Shutdown())\n\t\t\t})\n\n\t\t\truns := atomic.Uint32{}\n\t\t\tj, err := s.NewJob(\n\t\t\t\tOneTimeJob(OneTimeJobStartDateTime(tt.runAt)),\n\t\t\t\tNewTask(func() {\n\t\t\t\t\truns.Add(1)\n\t\t\t\t}),\n\t\t\t)\n\t\t\tif tt.assertErr != nil {\n\t\t\t\ttt.assertErr(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\ts.Start()\n\n\t\t\t\tfor _, advanceAndAssert := range tt.advanceAndAsserts {\n\t\t\t\t\tadvanceAndAssert(t, j, tt.fakeClock, &runs)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nvar _ Elector = (*testElector)(nil)\n\ntype testElector struct {\n\tmu            sync.Mutex\n\tleaderElected bool\n\tnotLeader     chan struct{}\n}\n\nfunc (t *testElector) IsLeader(ctx context.Context) error {\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn errors.New(\"done\")\n\tdefault:\n\t}\n\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\tif t.leaderElected {\n\t\tt.notLeader <- struct{}{}\n\t\treturn errors.New(\"already elected leader\")\n\t}\n\tt.leaderElected = true\n\treturn nil\n}\n\nvar _ Locker = (*testLocker)(nil)\n\ntype testLocker struct {\n\tmu        sync.Mutex\n\tjobLocked bool\n\tnotLocked chan struct{}\n}\n\nfunc (t *testLocker) Lock(_ context.Context, _ string) (Lock, error) {\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\tif t.jobLocked {\n\t\tt.notLocked <- struct{}{}\n\t\treturn nil, errors.New(\"job already locked\")\n\t}\n\tt.jobLocked = true\n\treturn &testLock{}, nil\n}\n\nvar _ Lock = (*testLock)(nil)\n\ntype testLock struct{}\n\nfunc (t testLock) Unlock(_ context.Context) error {\n\treturn nil\n}\n\nfunc TestScheduler_WithDistributed(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\tnotLocked := make(chan struct{}, 10)\n\tnotLeader := make(chan struct{}, 10)\n\n\ttests := []struct {\n\t\tname          string\n\t\tcount         int\n\t\trunCount      int\n\t\tschedulerOpts []SchedulerOption\n\t\tjobOpts       []JobOption\n\t\tassertions    func(*testing.T)\n\t}{\n\t\t{\n\t\t\t\"3 schedulers with elector\",\n\t\t\t3,\n\t\t\t1,\n\t\t\t[]SchedulerOption{\n\t\t\t\tWithDistributedElector(&testElector{notLeader: notLeader}),\n\t\t\t},\n\t\t\tnil,\n\t\t\tfunc(t *testing.T) {\n\t\t\t\ttimeout := time.Now().Add(1 * time.Second)\n\t\t\t\tvar notLeaderCount int\n\t\t\t\tfor !time.Now().After(timeout) {\n\t\t\t\t\tselect {\n\t\t\t\t\tcase <-notLeader:\n\t\t\t\t\t\tnotLeaderCount++\n\t\t\t\t\tdefault:\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tassert.Equal(t, 2, notLeaderCount)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"3 schedulers with locker\",\n\t\t\t3,\n\t\t\t1,\n\t\t\t[]SchedulerOption{\n\t\t\t\tWithDistributedLocker(&testLocker{notLocked: notLocked}),\n\t\t\t},\n\t\t\tnil,\n\t\t\tfunc(_ *testing.T) {\n\t\t\t\ttimeout := time.Now().Add(1 * time.Second)\n\t\t\t\tvar notLockedCount int\n\t\t\t\tfor !time.Now().After(timeout) {\n\t\t\t\t\tselect {\n\t\t\t\t\tcase <-notLocked:\n\t\t\t\t\t\tnotLockedCount++\n\t\t\t\t\tdefault:\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tassert.Equal(t, 2, notLockedCount)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"3 schedulers and job with Distributed locker\",\n\t\t\t3,\n\t\t\t1,\n\t\t\tnil,\n\t\t\t[]JobOption{\n\t\t\t\tWithDistributedJobLocker(&testLocker{notLocked: notLocked}),\n\t\t\t},\n\t\t\tfunc(_ *testing.T) {\n\t\t\t\ttimeout := time.Now().Add(1 * time.Second)\n\t\t\t\tvar notLockedCount int\n\t\t\t\tfor !time.Now().After(timeout) {\n\t\t\t\t\tselect {\n\t\t\t\t\tcase <-notLocked:\n\t\t\t\t\t\tnotLockedCount++\n\t\t\t\t\tdefault:\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tassert.Equal(t, 2, notLockedCount)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"3 schedulers and job with disabled Distributed locker\",\n\t\t\t3,\n\t\t\t3,\n\t\t\t[]SchedulerOption{\n\t\t\t\tWithDistributedLocker(&testLocker{notLocked: notLocked}),\n\t\t\t},\n\t\t\t[]JobOption{\n\t\t\t\tWithDisabledDistributedJobLocker(true),\n\t\t\t},\n\t\t\tfunc(_ *testing.T) {\n\t\t\t\ttimeout := time.Now().Add(1 * time.Second)\n\t\t\t\tvar notLockedCount int\n\t\t\t\tfor !time.Now().After(timeout) {\n\t\t\t\t\tselect {\n\t\t\t\t\tcase <-notLocked:\n\t\t\t\t\t\tnotLockedCount++\n\t\t\t\t\tdefault:\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tassert.Equal(t, 0, notLockedCount)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tjobsRan := make(chan struct{}, 20)\n\t\t\tctx, cancel := context.WithCancel(context.Background())\n\t\t\tschedulersDone := make(chan struct{}, tt.count)\n\n\t\t\tvar (\n\t\t\t\trunCount  int\n\t\t\t\tdoneCount int\n\t\t\t)\n\n\t\t\tfor i := tt.count; i > 0; i-- {\n\t\t\t\ts := newTestScheduler(t,\n\t\t\t\t\ttt.schedulerOpts...,\n\t\t\t\t)\n\t\t\t\tjobOpts := []JobOption{\n\t\t\t\t\tWithStartAt(\n\t\t\t\t\t\tWithStartImmediately(),\n\t\t\t\t\t),\n\t\t\t\t\tWithLimitedRuns(1),\n\t\t\t\t}\n\t\t\t\tjobOpts = append(jobOpts, tt.jobOpts...)\n\n\t\t\t\tgo func() {\n\t\t\t\t\ts.Start()\n\t\t\t\t\t_, err := s.NewJob(\n\t\t\t\t\t\tDurationJob(\n\t\t\t\t\t\t\ttime.Second,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tNewTask(\n\t\t\t\t\t\t\tfunc() {\n\t\t\t\t\t\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\t\t\t\t\t\tjobsRan <- struct{}{}\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t),\n\t\t\t\t\t\tjobOpts...,\n\t\t\t\t\t)\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\t<-ctx.Done()\n\t\t\t\t\terr = s.Shutdown()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tschedulersDone <- struct{}{}\n\t\t\t\t}()\n\t\t\t}\n\n\t\tRunCountLoop:\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase <-jobsRan:\n\t\t\t\t\trunCount++\n\t\t\t\t\tif runCount >= tt.runCount {\n\t\t\t\t\t\tbreak RunCountLoop\n\t\t\t\t\t}\n\t\t\t\tcase <-time.After(time.Second):\n\t\t\t\t\tt.Error(\"timed out waiting for job to run\")\n\t\t\t\t\tbreak RunCountLoop\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcancel()\n\t\t\tassert.Equal(t, tt.runCount, runCount)\n\n\t\tDoneCountLoop:\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase <-schedulersDone:\n\t\t\t\t\tdoneCount++\n\t\t\t\t\tif doneCount >= tt.count {\n\t\t\t\t\t\tbreak DoneCountLoop\n\t\t\t\t\t}\n\t\t\t\tcase <-time.After(3 * time.Second):\n\t\t\t\t\tt.Error(\"timed out waiting for schedulers to shutdown\")\n\t\t\t\t\tbreak DoneCountLoop\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.count, doneCount)\n\n\t\t\ttime.Sleep(time.Second)\n\t\t\ttt.assertions(t)\n\t\t})\n\t}\n}\n\nfunc TestScheduler_RemoveJob(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\ttests := []struct {\n\t\tname   string\n\t\taddJob bool\n\t\terr    error\n\t}{\n\t\t{\n\t\t\t\"success\",\n\t\t\ttrue,\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"job not found\",\n\t\t\tfalse,\n\t\t\tErrJobNotFound,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := newTestScheduler(t)\n\n\t\t\tvar id uuid.UUID\n\t\t\tif tt.addJob {\n\t\t\t\tj, err := s.NewJob(DurationJob(time.Second), NewTask(func() {}))\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tid = j.ID()\n\t\t\t} else {\n\t\t\t\tid = uuid.New()\n\t\t\t}\n\n\t\t\terr := s.RemoveJob(id)\n\t\t\tassert.ErrorIs(t, err, tt.err)\n\t\t\trequire.NoError(t, s.Shutdown())\n\t\t})\n\t}\n}\n\nfunc TestScheduler_JobsWaitingInQueue(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\ttests := []struct {\n\t\tname            string\n\t\tlimit           uint\n\t\tmode            LimitMode\n\t\tstartAt         func() OneTimeJobStartAtOption\n\t\texpectedInQueue int\n\t}{\n\t\t{\n\t\t\t\"with mode wait limit 1\",\n\t\t\t1,\n\t\t\tLimitModeWait,\n\t\t\tfunc() OneTimeJobStartAtOption {\n\t\t\t\treturn OneTimeJobStartDateTime(time.Now().Add(10 * time.Millisecond))\n\t\t\t},\n\t\t\t4,\n\t\t},\n\t\t{\n\t\t\t\"with mode wait limit 10\",\n\t\t\t10,\n\t\t\tLimitModeWait,\n\t\t\tfunc() OneTimeJobStartAtOption {\n\t\t\t\treturn OneTimeJobStartDateTime(time.Now().Add(10 * time.Millisecond))\n\t\t\t},\n\t\t\t0,\n\t\t},\n\t\t{\n\t\t\t\"with mode Reschedule\",\n\t\t\t1,\n\t\t\tLimitModeReschedule,\n\t\t\tfunc() OneTimeJobStartAtOption {\n\t\t\t\treturn OneTimeJobStartDateTime(time.Now().Add(10 * time.Millisecond))\n\t\t\t},\n\t\t\t0,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := newTestScheduler(t, WithLimitConcurrentJobs(tt.limit, tt.mode))\n\t\t\tfor i := 0; i <= 4; i++ {\n\t\t\t\t_, err := s.NewJob(OneTimeJob(tt.startAt()), NewTask(func() { time.Sleep(500 * time.Millisecond) }))\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t\ts.Start()\n\t\t\ttime.Sleep(20 * time.Millisecond)\n\t\t\tassert.Equal(t, tt.expectedInQueue, s.JobsWaitingInQueue())\n\t\t\trequire.NoError(t, s.Shutdown())\n\t\t})\n\t}\n}\n\nfunc TestScheduler_RemoveLotsOfJobs(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\ttests := []struct {\n\t\tname    string\n\t\tnumJobs int\n\t}{\n\t\t{\n\t\t\t\"10 successes\",\n\t\t\t10,\n\t\t},\n\t\t{\n\t\t\t\"100 successes\",\n\t\t\t100,\n\t\t},\n\t\t{\n\t\t\t\"1000 successes\",\n\t\t\t1000,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := newTestScheduler(t)\n\n\t\t\tvar ids []uuid.UUID\n\t\t\tfor i := 0; i < tt.numJobs; i++ {\n\t\t\t\tj, err := s.NewJob(DurationJob(time.Second), NewTask(func() { time.Sleep(20 * time.Second) }))\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tids = append(ids, j.ID())\n\t\t\t}\n\n\t\t\tfor _, id := range ids {\n\t\t\t\terr := s.RemoveJob(id)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tassert.Len(t, s.Jobs(), 0)\n\t\t\trequire.NoError(t, s.Shutdown())\n\t\t})\n\t}\n}\n\nfunc TestScheduler_RemoveJob_RemoveSelf(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\ts := newTestScheduler(t)\n\ts.Start()\n\n\t_, err := s.NewJob(\n\t\tDurationJob(100*time.Millisecond),\n\t\tNewTask(func() {}),\n\t\tWithEventListeners(\n\t\t\tBeforeJobRuns(\n\t\t\t\tfunc(_ uuid.UUID, _ string) {\n\t\t\t\t\ts.RemoveByTags(\"tag1\")\n\t\t\t\t},\n\t\t\t),\n\t\t),\n\t\tWithTags(\"tag1\"),\n\t)\n\trequire.NoError(t, err)\n\n\ttime.Sleep(time.Millisecond * 400)\n\tassert.NoError(t, s.Shutdown())\n}\n\nfunc TestScheduler_WithEventListeners(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\tlistenerRunCh := make(chan error, 1)\n\ttestErr := errors.New(\"test error\")\n\ttests := []struct {\n\t\tname      string\n\t\ttsk       Task\n\t\tel        EventListener\n\t\texpectRun bool\n\t\texpectErr error\n\t}{\n\t\t{\n\t\t\t\"AfterJobRuns\",\n\t\t\tNewTask(func() {}),\n\t\t\tAfterJobRuns(func(_ uuid.UUID, _ string) {\n\t\t\t\tlistenerRunCh <- nil\n\t\t\t}),\n\t\t\ttrue,\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"AfterJobRunsWithError - error\",\n\t\t\tNewTask(func() error { return testErr }),\n\t\t\tAfterJobRunsWithError(func(_ uuid.UUID, _ string, err error) {\n\t\t\t\tlistenerRunCh <- err\n\t\t\t}),\n\t\t\ttrue,\n\t\t\ttestErr,\n\t\t},\n\t\t{\n\t\t\t\"AfterJobRunsWithError - multiple return values, including error\",\n\t\t\tNewTask(func() (bool, error) { return false, testErr }),\n\t\t\tAfterJobRunsWithError(func(_ uuid.UUID, _ string, err error) {\n\t\t\t\tlistenerRunCh <- err\n\t\t\t}),\n\t\t\ttrue,\n\t\t\ttestErr,\n\t\t},\n\t\t{\n\t\t\t\"AfterJobRunsWithError - no error\",\n\t\t\tNewTask(func() error { return nil }),\n\t\t\tAfterJobRunsWithError(func(_ uuid.UUID, _ string, err error) {\n\t\t\t\tlistenerRunCh <- err\n\t\t\t}),\n\t\t\tfalse,\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"BeforeJobRuns\",\n\t\t\tNewTask(func() {}),\n\t\t\tBeforeJobRuns(func(_ uuid.UUID, _ string) {\n\t\t\t\tlistenerRunCh <- nil\n\t\t\t}),\n\t\t\ttrue,\n\t\t\tnil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := newTestScheduler(t)\n\t\t\t_, err := s.NewJob(\n\t\t\t\tDurationJob(time.Minute*10),\n\t\t\t\ttt.tsk,\n\t\t\t\tWithStartAt(\n\t\t\t\t\tWithStartImmediately(),\n\t\t\t\t),\n\t\t\t\tWithEventListeners(tt.el),\n\t\t\t\tWithLimitedRuns(1),\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\ts.Start()\n\t\t\tif tt.expectRun {\n\t\t\t\tselect {\n\t\t\t\tcase err = <-listenerRunCh:\n\t\t\t\t\tassert.ErrorIs(t, err, tt.expectErr)\n\t\t\t\tcase <-time.After(time.Second):\n\t\t\t\t\tt.Fatal(\"timed out waiting for listener to run\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tselect {\n\t\t\t\tcase <-listenerRunCh:\n\t\t\t\t\tt.Fatal(\"listener ran when it shouldn't have\")\n\t\t\t\tcase <-time.After(time.Millisecond * 100):\n\t\t\t\t}\n\t\t\t}\n\n\t\t\trequire.NoError(t, s.Shutdown())\n\t\t})\n\t}\n}\n\nfunc TestScheduler_WithLocker_WithEventListeners(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\tlistenerRunCh := make(chan error, 1)\n\ttests := []struct {\n\t\tname      string\n\t\tlocker    Locker\n\t\ttsk       Task\n\t\tel        EventListener\n\t\texpectRun bool\n\t\texpectErr error\n\t}{\n\t\t{\n\t\t\t\"AfterLockError\",\n\t\t\terrorLocker{},\n\t\t\tNewTask(func() {}),\n\t\t\tAfterLockError(func(_ uuid.UUID, _ string, _ error) {\n\t\t\t\tlistenerRunCh <- nil\n\t\t\t}),\n\t\t\ttrue,\n\t\t\tnil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := newTestScheduler(t)\n\t\t\t_, err := s.NewJob(\n\t\t\t\tDurationJob(time.Minute*10),\n\t\t\t\ttt.tsk,\n\t\t\t\tWithStartAt(\n\t\t\t\t\tWithStartImmediately(),\n\t\t\t\t),\n\t\t\t\tWithDistributedJobLocker(tt.locker),\n\t\t\t\tWithEventListeners(tt.el),\n\t\t\t\tWithLimitedRuns(1),\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\ts.Start()\n\t\t\tif tt.expectRun {\n\t\t\t\tselect {\n\t\t\t\tcase err = <-listenerRunCh:\n\t\t\t\t\tassert.ErrorIs(t, err, tt.expectErr)\n\t\t\t\tcase <-time.After(time.Second):\n\t\t\t\t\tt.Fatal(\"timed out waiting for listener to run\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tselect {\n\t\t\t\tcase <-listenerRunCh:\n\t\t\t\t\tt.Fatal(\"listener ran when it shouldn't have\")\n\t\t\t\tcase <-time.After(time.Millisecond * 100):\n\t\t\t\t}\n\t\t\t}\n\n\t\t\trequire.NoError(t, s.Shutdown())\n\t\t})\n\t}\n}\n\nfunc TestScheduler_ManyJobs(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\ts := newTestScheduler(t)\n\tjobsRan := make(chan struct{}, 20000)\n\n\tfor i := 1; i <= 1000; i++ {\n\t\t_, err := s.NewJob(\n\t\t\tDurationJob(\n\t\t\t\ttime.Millisecond*100,\n\t\t\t),\n\t\t\tNewTask(\n\t\t\t\tfunc() {\n\t\t\t\t\tjobsRan <- struct{}{}\n\t\t\t\t},\n\t\t\t),\n\t\t\tWithStartAt(WithStartImmediately()),\n\t\t)\n\t\trequire.NoError(t, err)\n\t}\n\n\ts.Start()\n\ttime.Sleep(1 * time.Second)\n\trequire.NoError(t, s.Shutdown())\n\tclose(jobsRan)\n\n\tvar count int\n\tfor range jobsRan {\n\t\tcount++\n\t}\n\n\tassert.GreaterOrEqual(t, count, 9900)\n\tassert.LessOrEqual(t, count, 11000)\n}\n\nfunc TestScheduler_RunJobNow(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\tchDuration := make(chan struct{}, 10)\n\tchMonthly := make(chan struct{}, 10)\n\tchDurationImmediate := make(chan struct{}, 10)\n\tchDurationSingleton := make(chan struct{}, 10)\n\tchOneTime := make(chan struct{}, 10)\n\n\ttests := []struct {\n\t\tname         string\n\t\tch           chan struct{}\n\t\tj            JobDefinition\n\t\tfun          any\n\t\topts         []JobOption\n\t\texpectedDiff func() time.Duration\n\t\texpectedRuns int\n\t}{\n\t\t{\n\t\t\t\"duration job\",\n\t\t\tchDuration,\n\t\t\tDurationJob(time.Second * 10),\n\t\t\tfunc() {\n\t\t\t\tchDuration <- struct{}{}\n\t\t\t},\n\t\t\tnil,\n\t\t\tfunc() time.Duration {\n\t\t\t\treturn 0\n\t\t\t},\n\t\t\t1,\n\t\t},\n\t\t{\n\t\t\t\"monthly job\",\n\t\t\tchMonthly,\n\t\t\tMonthlyJob(1, NewDaysOfTheMonth(1), NewAtTimes(NewAtTime(0, 0, 0))),\n\t\t\tfunc() {\n\t\t\t\tchMonthly <- struct{}{}\n\t\t\t},\n\t\t\tnil,\n\t\t\tfunc() time.Duration {\n\t\t\t\treturn 0\n\t\t\t},\n\t\t\t1,\n\t\t},\n\t\t{\n\t\t\t\"duration job - start immediately\",\n\t\t\tchDurationImmediate,\n\t\t\tDurationJob(time.Second * 5),\n\t\t\tfunc() {\n\t\t\t\tchDurationImmediate <- struct{}{}\n\t\t\t},\n\t\t\t[]JobOption{\n\t\t\t\tWithStartAt(\n\t\t\t\t\tWithStartImmediately(),\n\t\t\t\t),\n\t\t\t},\n\t\t\tfunc() time.Duration {\n\t\t\t\treturn 5 * time.Second\n\t\t\t},\n\t\t\t2,\n\t\t},\n\t\t{\n\t\t\t\"duration job - singleton\",\n\t\t\tchDurationSingleton,\n\t\t\tDurationJob(time.Second * 10),\n\t\t\tfunc() {\n\t\t\t\tchDurationSingleton <- struct{}{}\n\t\t\t\ttime.Sleep(200 * time.Millisecond)\n\t\t\t},\n\t\t\t[]JobOption{\n\t\t\t\tWithStartAt(\n\t\t\t\t\tWithStartImmediately(),\n\t\t\t\t),\n\t\t\t\tWithSingletonMode(LimitModeReschedule),\n\t\t\t},\n\t\t\tfunc() time.Duration {\n\t\t\t\treturn 10 * time.Second\n\t\t\t},\n\t\t\t1,\n\t\t},\n\t\t{\n\t\t\t\"one time job\",\n\t\t\tchOneTime,\n\t\t\tOneTimeJob(OneTimeJobStartImmediately()),\n\t\t\tfunc() {\n\t\t\t\tchOneTime <- struct{}{}\n\t\t\t},\n\t\t\tnil,\n\t\t\tnil,\n\t\t\t2,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := newTestScheduler(t)\n\n\t\t\t_, err := s.NewJob(tt.j, NewTask(tt.fun), tt.opts...)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tj := s.Jobs()[0]\n\t\t\ts.Start()\n\n\t\t\tvar nextRunBefore time.Time\n\t\t\tif tt.expectedDiff != nil {\n\t\t\t\tfor ; nextRunBefore.IsZero() || err != nil; nextRunBefore, err = j.NextRun() { //nolint:revive\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\trequire.NoError(t, j.RunNow())\n\t\t\tvar runCount int\n\n\t\t\tselect {\n\t\t\tcase <-tt.ch:\n\t\t\t\trunCount++\n\t\t\tcase <-time.After(time.Second):\n\t\t\t\tt.Fatal(\"timed out waiting for job to run\")\n\t\t\t}\n\n\t\t\ttimeout := time.Now().Add(time.Second)\n\t\t\tfor time.Now().Before(timeout) {\n\t\t\t\tselect {\n\t\t\t\tcase <-tt.ch:\n\t\t\t\t\trunCount++\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.expectedRuns, runCount)\n\n\t\t\tnextRunAfter, err := j.NextRun()\n\t\t\tif tt.expectedDiff != nil && tt.expectedDiff() > 0 {\n\t\t\t\tfor ; nextRunBefore.IsZero() || nextRunAfter.Equal(nextRunBefore); nextRunAfter, err = j.NextRun() { //nolint:revive\n\t\t\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.NoError(t, s.Shutdown())\n\n\t\t\tif tt.expectedDiff != nil {\n\t\t\t\tassert.Equal(t, tt.expectedDiff(), nextRunAfter.Sub(nextRunBefore))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestScheduler_LastRunSingleton(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\tif testEnv != testEnvLocal {\n\t\t// this test is flaky in ci, but always passes locally\n\t\tt.SkipNow()\n\t}\n\n\ttests := []struct {\n\t\tname string\n\t\tf    func(t *testing.T, j Job, jobRan chan struct{})\n\t}{\n\t\t{\n\t\t\t\"simple\",\n\t\t\tfunc(_ *testing.T, _ Job, _ chan struct{}) {},\n\t\t},\n\t\t{\n\t\t\t\"with runNow\",\n\t\t\tfunc(t *testing.T, j Job, jobRan chan struct{}) {\n\t\t\t\trunTime := time.Now()\n\t\t\t\tassert.NoError(t, j.RunNow())\n\n\t\t\t\t// because we're using wait mode we need to wait here\n\t\t\t\t// to make sure the job queued with RunNow has finished running\n\t\t\t\t<-jobRan\n\t\t\t\tlastRun, err := j.LastRun()\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.LessOrEqual(t, lastRun.Sub(runTime), time.Millisecond*225)\n\t\t\t\tassert.GreaterOrEqual(t, lastRun.Sub(runTime), time.Millisecond*175)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tjobRan := make(chan struct{}, 2)\n\t\t\ts := newTestScheduler(t)\n\t\t\tj, err := s.NewJob(\n\t\t\t\tDurationJob(time.Millisecond*100),\n\t\t\t\tNewTask(func() {\n\t\t\t\t\tjobRan <- struct{}{}\n\t\t\t\t\ttime.Sleep(time.Millisecond * 200)\n\t\t\t\t}),\n\t\t\t\tWithSingletonMode(LimitModeWait),\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tstartTime := time.Now()\n\t\t\ts.Start()\n\n\t\t\tlastRun, err := j.LastRun()\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.True(t, lastRun.IsZero())\n\n\t\t\t<-jobRan\n\n\t\t\tlastRun, err = j.LastRun()\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.LessOrEqual(t, lastRun.Sub(startTime), time.Millisecond*125)\n\t\t\tassert.GreaterOrEqual(t, lastRun.Sub(startTime), time.Millisecond*75)\n\n\t\t\ttt.f(t, j, jobRan)\n\n\t\t\tassert.NoError(t, s.Shutdown())\n\t\t})\n\t}\n}\n\nfunc TestScheduler_OneTimeJob(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\ttests := []struct {\n\t\tname    string\n\t\tstartAt func() OneTimeJobStartAtOption\n\t}{\n\t\t{\n\t\t\t\"start now\",\n\t\t\tfunc() OneTimeJobStartAtOption {\n\t\t\t\treturn OneTimeJobStartImmediately()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"start in 100 ms\",\n\t\t\tfunc() OneTimeJobStartAtOption {\n\t\t\t\treturn OneTimeJobStartDateTime(time.Now().Add(100 * time.Millisecond))\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tjobRan := make(chan struct{}, 2)\n\n\t\t\ts := newTestScheduler(t)\n\n\t\t\t_, err := s.NewJob(\n\t\t\t\tOneTimeJob(tt.startAt()),\n\t\t\t\tNewTask(func() {\n\t\t\t\t\tjobRan <- struct{}{}\n\t\t\t\t}),\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\ts.Start()\n\n\t\t\tselect {\n\t\t\tcase <-jobRan:\n\t\t\tcase <-time.After(500 * time.Millisecond):\n\t\t\t\tt.Fatal(\"timed out waiting for job to run\")\n\t\t\t}\n\n\t\t\tassert.NoError(t, s.Shutdown())\n\t\t})\n\t}\n}\n\nfunc TestScheduler_AtTimesJob(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\tn := time.Now().UTC()\n\n\ttests := []struct {\n\t\tname      string\n\t\tatTimes   []time.Time\n\t\tfakeClock *clockwork.FakeClock\n\t\tassertErr require.ErrorAssertionFunc\n\t\t// asserts things about schedules, advance time and perform new assertions\n\t\tadvanceAndAsserts []func(\n\t\t\tt *testing.T,\n\t\t\tj Job,\n\t\t\tclock *clockwork.FakeClock,\n\t\t\truns *atomic.Uint32,\n\t\t)\n\t}{\n\t\t{\n\t\t\tname:      \"no at times\",\n\t\t\tatTimes:   []time.Time{},\n\t\t\tfakeClock: clockwork.NewFakeClock(),\n\t\t\tassertErr: func(t require.TestingT, err error, _ ...interface{}) {\n\t\t\t\trequire.ErrorIs(t, err, ErrOneTimeJobStartDateTimePast)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"all in the past\",\n\t\t\tatTimes:   []time.Time{n.Add(-1 * time.Second)},\n\t\t\tfakeClock: clockwork.NewFakeClockAt(n),\n\t\t\tassertErr: func(t require.TestingT, err error, _ ...interface{}) {\n\t\t\t\trequire.ErrorIs(t, err, ErrOneTimeJobStartDateTimePast)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"one run 1 millisecond in the future\",\n\t\t\tatTimes:   []time.Time{n.Add(1 * time.Millisecond)},\n\t\t\tfakeClock: clockwork.NewFakeClockAt(n),\n\t\t\tadvanceAndAsserts: []func(t *testing.T, j Job, clock *clockwork.FakeClock, runs *atomic.Uint32){\n\t\t\t\tfunc(t *testing.T, j Job, clock *clockwork.FakeClock, runs *atomic.Uint32) {\n\t\t\t\t\trequire.Equal(t, uint32(0), runs.Load())\n\n\t\t\t\t\t// last not initialized\n\t\t\t\t\tlastRunAt, err := j.LastRun()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.Equal(t, time.Time{}, lastRunAt)\n\n\t\t\t\t\t// next is now\n\t\t\t\t\tnextRunAt, err := j.NextRun()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.Equal(t, n.Add(1*time.Millisecond), nextRunAt)\n\n\t\t\t\t\t// advance and eventually run\n\t\t\t\t\tclock.Advance(2 * time.Millisecond)\n\t\t\t\t\trequire.Eventually(t, func() bool {\n\t\t\t\t\t\treturn uint32(1) == runs.Load()\n\t\t\t\t\t}, 3*time.Second, 100*time.Millisecond)\n\n\t\t\t\t\t// last was run\n\t\t\t\t\tlastRunAt, err = j.LastRun()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.WithinDuration(t, n.Add(1*time.Millisecond), lastRunAt, 1*time.Millisecond)\n\n\t\t\t\t\tnextRunAt, err = j.NextRun()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.Equal(t, time.Time{}, nextRunAt)\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"one run in the past and one in the future\",\n\t\t\tatTimes:   []time.Time{n.Add(-1 * time.Millisecond), n.Add(1 * time.Millisecond)},\n\t\t\tfakeClock: clockwork.NewFakeClockAt(n),\n\t\t\tadvanceAndAsserts: []func(t *testing.T, j Job, clock *clockwork.FakeClock, runs *atomic.Uint32){\n\t\t\t\tfunc(t *testing.T, j Job, clock *clockwork.FakeClock, runs *atomic.Uint32) {\n\t\t\t\t\trequire.Equal(t, uint32(0), runs.Load())\n\n\t\t\t\t\t// last not initialized\n\t\t\t\t\tlastRunAt, err := j.LastRun()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.Equal(t, time.Time{}, lastRunAt)\n\n\t\t\t\t\t// next is now\n\t\t\t\t\tnextRunAt, err := j.NextRun()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.Equal(t, n.Add(1*time.Millisecond), nextRunAt)\n\n\t\t\t\t\t// advance and eventually run\n\t\t\t\t\tclock.Advance(2 * time.Millisecond)\n\t\t\t\t\trequire.Eventually(t, func() bool {\n\t\t\t\t\t\treturn uint32(1) == runs.Load()\n\t\t\t\t\t}, 3*time.Second, 100*time.Millisecond)\n\n\t\t\t\t\t// last was run\n\t\t\t\t\tlastRunAt, err = j.LastRun()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.WithinDuration(t, n.Add(1*time.Millisecond), lastRunAt, 1*time.Millisecond)\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"two runs in the future - order is maintained even if times are provided out of order\",\n\t\t\tatTimes:   []time.Time{n.Add(3 * time.Millisecond), n.Add(1 * time.Millisecond)},\n\t\t\tfakeClock: clockwork.NewFakeClockAt(n),\n\t\t\tadvanceAndAsserts: []func(t *testing.T, j Job, clock *clockwork.FakeClock, runs *atomic.Uint32){\n\t\t\t\tfunc(t *testing.T, j Job, clock *clockwork.FakeClock, runs *atomic.Uint32) {\n\t\t\t\t\trequire.Equal(t, uint32(0), runs.Load())\n\n\t\t\t\t\t// last not initialized\n\t\t\t\t\tlastRunAt, err := j.LastRun()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.Equal(t, time.Time{}, lastRunAt)\n\n\t\t\t\t\t// next is now\n\t\t\t\t\tnextRunAt, err := j.NextRun()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.Equal(t, n.Add(1*time.Millisecond), nextRunAt)\n\n\t\t\t\t\t// advance and eventually run\n\t\t\t\t\tclock.Advance(2 * time.Millisecond)\n\t\t\t\t\trequire.Eventually(t, func() bool {\n\t\t\t\t\t\treturn uint32(1) == runs.Load()\n\t\t\t\t\t}, 3*time.Second, 100*time.Millisecond)\n\n\t\t\t\t\t// last was run\n\t\t\t\t\tlastRunAt, err = j.LastRun()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.WithinDuration(t, n.Add(1*time.Millisecond), lastRunAt, 1*time.Millisecond)\n\n\t\t\t\t\tnextRunAt, err = j.NextRun()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.Equal(t, n.Add(3*time.Millisecond), nextRunAt)\n\t\t\t\t},\n\n\t\t\t\tfunc(t *testing.T, j Job, clock *clockwork.FakeClock, runs *atomic.Uint32) {\n\t\t\t\t\t// advance and eventually run\n\t\t\t\t\tclock.Advance(2 * time.Millisecond)\n\t\t\t\t\trequire.Eventually(t, func() bool {\n\t\t\t\t\t\treturn uint32(2) == runs.Load()\n\t\t\t\t\t}, 3*time.Second, 100*time.Millisecond)\n\n\t\t\t\t\t// last was run\n\t\t\t\t\tlastRunAt, err := j.LastRun()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.WithinDuration(t, n.Add(3*time.Millisecond), lastRunAt, 1*time.Millisecond)\n\n\t\t\t\t\tnextRunAt, err := j.NextRun()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.Equal(t, time.Time{}, nextRunAt)\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tname:      \"two runs in the future - order is maintained even if times are provided out of order - deduplication\",\n\t\t\tatTimes:   []time.Time{n.Add(3 * time.Millisecond), n.Add(1 * time.Millisecond), n.Add(1 * time.Millisecond), n.Add(3 * time.Millisecond)},\n\t\t\tfakeClock: clockwork.NewFakeClockAt(n),\n\t\t\tadvanceAndAsserts: []func(t *testing.T, j Job, clock *clockwork.FakeClock, runs *atomic.Uint32){\n\t\t\t\tfunc(t *testing.T, j Job, clock *clockwork.FakeClock, runs *atomic.Uint32) {\n\t\t\t\t\trequire.Equal(t, uint32(0), runs.Load())\n\n\t\t\t\t\t// last not initialized\n\t\t\t\t\tlastRunAt, err := j.LastRun()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.Equal(t, time.Time{}, lastRunAt)\n\n\t\t\t\t\t// next is now\n\t\t\t\t\tnextRunAt, err := j.NextRun()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.Equal(t, n.Add(1*time.Millisecond), nextRunAt)\n\n\t\t\t\t\t// advance and eventually run\n\t\t\t\t\tclock.Advance(2 * time.Millisecond)\n\t\t\t\t\trequire.Eventually(t, func() bool {\n\t\t\t\t\t\treturn uint32(1) == runs.Load()\n\t\t\t\t\t}, 3*time.Second, 100*time.Millisecond)\n\n\t\t\t\t\t// last was run\n\t\t\t\t\tlastRunAt, err = j.LastRun()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.WithinDuration(t, n.Add(1*time.Millisecond), lastRunAt, 1*time.Millisecond)\n\n\t\t\t\t\tnextRunAt, err = j.NextRun()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.Equal(t, n.Add(3*time.Millisecond), nextRunAt)\n\t\t\t\t},\n\n\t\t\t\tfunc(t *testing.T, j Job, clock *clockwork.FakeClock, runs *atomic.Uint32) {\n\t\t\t\t\t// advance and eventually run\n\t\t\t\t\tclock.Advance(2 * time.Millisecond)\n\t\t\t\t\trequire.Eventually(t, func() bool {\n\t\t\t\t\t\treturn uint32(2) == runs.Load()\n\t\t\t\t\t}, 3*time.Second, 100*time.Millisecond)\n\n\t\t\t\t\t// last was run\n\t\t\t\t\tlastRunAt, err := j.LastRun()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.WithinDuration(t, n.Add(3*time.Millisecond), lastRunAt, 1*time.Millisecond)\n\n\t\t\t\t\tnextRunAt, err := j.NextRun()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.Equal(t, time.Time{}, nextRunAt)\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := newTestScheduler(t, WithClock(tt.fakeClock))\n\t\t\tt.Cleanup(func() {\n\t\t\t\trequire.NoError(t, s.Shutdown())\n\t\t\t})\n\n\t\t\truns := atomic.Uint32{}\n\t\t\tj, err := s.NewJob(\n\t\t\t\tOneTimeJob(OneTimeJobStartDateTimes(tt.atTimes...)),\n\t\t\t\tNewTask(func() {\n\t\t\t\t\truns.Add(1)\n\t\t\t\t}),\n\t\t\t)\n\t\t\tif tt.assertErr != nil {\n\t\t\t\ttt.assertErr(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\ts.Start()\n\n\t\t\t\tfor _, advanceAndAssert := range tt.advanceAndAsserts {\n\t\t\t\t\tadvanceAndAssert(t, j, tt.fakeClock, &runs)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestScheduler_WithLimitedRuns(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\ttests := []struct {\n\t\tname          string\n\t\tschedulerOpts []SchedulerOption\n\t\tjob           JobDefinition\n\t\tjobOpts       []JobOption\n\t\trunLimit      uint\n\t\texpectedRuns  int\n\t}{\n\t\t{\n\t\t\t\"simple\",\n\t\t\tnil,\n\t\t\tDurationJob(time.Millisecond * 100),\n\t\t\tnil,\n\t\t\t1,\n\t\t\t1,\n\t\t},\n\t\t{\n\t\t\t\"OneTimeJob, WithLimitConcurrentJobs\",\n\t\t\t[]SchedulerOption{\n\t\t\t\tWithLimitConcurrentJobs(1, LimitModeWait),\n\t\t\t},\n\t\t\tOneTimeJob(OneTimeJobStartImmediately()),\n\t\t\tnil,\n\t\t\t1,\n\t\t\t1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := newTestScheduler(t, tt.schedulerOpts...)\n\n\t\t\tjobRan := make(chan struct{}, 10)\n\n\t\t\tjobOpts := []JobOption{\n\t\t\t\tWithLimitedRuns(tt.runLimit),\n\t\t\t}\n\t\t\tjobOpts = append(jobOpts, tt.jobOpts...)\n\n\t\t\t_, err := s.NewJob(\n\t\t\t\ttt.job,\n\t\t\t\tNewTask(func() {\n\t\t\t\t\tjobRan <- struct{}{}\n\t\t\t\t}),\n\t\t\t\tjobOpts...,\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\ts.Start()\n\t\t\ttime.Sleep(time.Millisecond * 150)\n\n\t\t\tassert.NoError(t, s.Shutdown())\n\n\t\t\tvar runCount int\n\t\t\tfor runCount < tt.expectedRuns {\n\t\t\t\tselect {\n\t\t\t\tcase <-jobRan:\n\t\t\t\t\trunCount++\n\t\t\t\tcase <-time.After(time.Second):\n\t\t\t\t\tt.Fatal(\"timed out waiting for job to run\")\n\t\t\t\t}\n\t\t\t}\n\t\t\tselect {\n\t\t\tcase <-jobRan:\n\t\t\t\tt.Fatal(\"job ran more than expected\")\n\t\t\tdefault:\n\t\t\t}\n\t\t\tassert.Equal(t, tt.expectedRuns, runCount)\n\t\t})\n\t}\n}\n\nfunc TestScheduler_Jobs(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\ttests := []struct {\n\t\tname string\n\t}{\n\t\t{\n\t\t\t\"order is equal\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := newTestScheduler(t)\n\n\t\t\tfor i := 0; i <= 20; i++ {\n\t\t\t\t_, err := s.NewJob(\n\t\t\t\t\tDurationJob(time.Second),\n\t\t\t\t\tNewTask(func() {}),\n\t\t\t\t)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tjobsFirst := s.Jobs()\n\t\t\tjobsSecond := s.Jobs()\n\n\t\t\tassert.Equal(t, jobsFirst, jobsSecond)\n\t\t\tassert.NoError(t, s.Shutdown())\n\t\t})\n\t}\n}\n\ntype testMonitor struct {\n\tmu      sync.Mutex\n\tcounter map[string]int\n\ttime    map[string][]time.Duration\n}\n\nfunc newTestMonitor() *testMonitor {\n\treturn &testMonitor{\n\t\tcounter: make(map[string]int),\n\t\ttime:    make(map[string][]time.Duration),\n\t}\n}\n\nfunc (t *testMonitor) IncrementJob(_ uuid.UUID, name string, _ []string, _ JobStatus) {\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\t_, ok := t.counter[name]\n\tif !ok {\n\t\tt.counter[name] = 0\n\t}\n\tt.counter[name]++\n}\n\nfunc (t *testMonitor) RecordJobTiming(startTime, endTime time.Time, _ uuid.UUID, name string, _ []string) {\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\t_, ok := t.time[name]\n\tif !ok {\n\t\tt.time[name] = make([]time.Duration, 0)\n\t}\n\tt.time[name] = append(t.time[name], endTime.Sub(startTime))\n}\n\nfunc TestScheduler_WithMonitor(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\ttests := []struct {\n\t\tname    string\n\t\tjd      JobDefinition\n\t\tjobName string\n\t}{\n\t\t{\n\t\t\t\"scheduler with monitor\",\n\t\t\tDurationJob(time.Millisecond * 50),\n\t\t\t\"job\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tch := make(chan struct{}, 20)\n\t\t\tmonitor := newTestMonitor()\n\t\t\ts := newTestScheduler(t, WithMonitor(monitor))\n\n\t\t\topt := []JobOption{\n\t\t\t\tWithName(tt.jobName),\n\t\t\t\tWithStartAt(\n\t\t\t\t\tWithStartImmediately(),\n\t\t\t\t),\n\t\t\t}\n\t\t\t_, err := s.NewJob(\n\t\t\t\ttt.jd,\n\t\t\t\tNewTask(func() {\n\t\t\t\t\tch <- struct{}{}\n\t\t\t\t}),\n\t\t\t\topt...,\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\t\t\ts.Start()\n\t\t\ttime.Sleep(150 * time.Millisecond)\n\t\t\trequire.NoError(t, s.Shutdown())\n\t\t\tclose(ch)\n\t\t\texpectedCount := 0\n\t\t\tfor range ch {\n\t\t\t\texpectedCount++\n\t\t\t}\n\n\t\t\tgot := monitor.counter[tt.jobName]\n\t\t\tif got != expectedCount {\n\t\t\t\tt.Fatalf(\"job %q counter expected %d, got %d\", tt.jobName, expectedCount, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestScheduler_WithStartAtDateTimePast(t *testing.T) {\n\tdefer verifyNoGoroutineLeaks(t)\n\n\t// Monday\n\ttestTime := time.Date(2024, time.January, 1, 9, 0, 0, 0, time.UTC)\n\n\tfakeClock := clockwork.NewFakeClockAt(testTime)\n\n\ts := newTestScheduler(t, WithClock(fakeClock))\n\tj, err := s.NewJob(\n\t\tWeeklyJob(2, NewWeekdays(time.Sunday), NewAtTimes(NewAtTime(10, 0, 0))),\n\t\tNewTask(func() {}),\n\t\tWithStartAt(\n\t\t\t// The start time is in the past (Dec 30, 2023 9am) which is a Saturday\n\t\t\tWithStartDateTimePast(testTime.Add(-time.Hour*24*2)),\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\ts.Start()\n\n\tnextRun, err := j.NextRun()\n\trequire.NoError(t, err)\n\n\trequire.NoError(t, s.Shutdown())\n\n\t// Because the start time was in the past - we expect it to schedule 2 intervals ahead, pasing the first available Sunday\n\t// which was in the past Dec 31, 2023, so the next is Jan 7, 2024\n\tassert.Equal(t, time.Date(2024, time.January, 7, 10, 0, 0, 0, time.UTC), nextRun)\n}\n"
  },
  {
    "path": "util.go",
    "content": "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 callJobFuncWithParams(jobFunc any, params ...any) error {\n\tif jobFunc == nil {\n\t\treturn nil\n\t}\n\tf := reflect.ValueOf(jobFunc)\n\tif f.IsZero() {\n\t\treturn nil\n\t}\n\tif len(params) != f.Type().NumIn() {\n\t\treturn nil\n\t}\n\tin := make([]reflect.Value, len(params))\n\tfor k, param := range params {\n\t\tin[k] = reflect.ValueOf(param)\n\t}\n\treturnValues := f.Call(in)\n\tfor _, val := range returnValues {\n\t\ti := val.Interface()\n\t\tif err, ok := i.(error); ok {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc requestJob(id uuid.UUID, ch chan *jobOutRequest) *internalJob {\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second)\n\tdefer cancel()\n\treturn requestJobCtx(ctx, id, ch)\n}\n\nfunc requestJobCtx(ctx context.Context, id uuid.UUID, ch chan *jobOutRequest) *internalJob {\n\tresp := make(chan internalJob, 1)\n\tselect {\n\tcase ch <- &jobOutRequest{\n\t\tid:      id,\n\t\toutChan: resp,\n\t}:\n\tcase <-ctx.Done():\n\t\treturn nil\n\t}\n\tvar j internalJob\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn nil\n\tcase jobReceived := <-resp:\n\t\tj = jobReceived\n\t}\n\treturn &j\n}\n\nfunc removeSliceDuplicatesInt(in []int) []int {\n\tslices.Sort(in)\n\treturn slices.Compact(in)\n}\n\nfunc convertAtTimesToDateTime(atTimes AtTimes, location *time.Location) ([]time.Time, error) {\n\tif atTimes == nil {\n\t\treturn nil, errAtTimesNil\n\t}\n\tvar atTimesDate []time.Time\n\tfor _, a := range atTimes() {\n\t\tif a == nil {\n\t\t\treturn nil, errAtTimeNil\n\t\t}\n\t\tat := a()\n\t\tif at.hours > 23 {\n\t\t\treturn nil, errAtTimeHours\n\t\t} else if at.minutes > 59 || at.seconds > 59 {\n\t\t\treturn nil, errAtTimeMinSec\n\t\t}\n\t\tatTimesDate = append(atTimesDate, at.time(location))\n\t}\n\tslices.SortStableFunc(atTimesDate, ascendingTime)\n\treturn atTimesDate, nil\n}\n\nfunc ascendingTime(a, b time.Time) int {\n\treturn a.Compare(b)\n}\n\ntype waitGroupWithMutex struct {\n\twg sync.WaitGroup\n\tmu sync.Mutex\n}\n\nfunc (w *waitGroupWithMutex) Add(delta int) {\n\tw.mu.Lock()\n\tdefer w.mu.Unlock()\n\tw.wg.Add(delta)\n}\n\nfunc (w *waitGroupWithMutex) Done() {\n\tw.wg.Done()\n}\n\nfunc (w *waitGroupWithMutex) Wait() {\n\tw.mu.Lock()\n\tdefer w.mu.Unlock()\n\tw.wg.Wait()\n}\n"
  },
  {
    "path": "util_test.go",
    "content": "package gocron\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestRemoveSliceDuplicatesInt(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []int\n\t\texpected []int\n\t}{\n\t\t{\n\t\t\t\"lots of duplicates\",\n\t\t\t[]int{\n\t\t\t\t1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,\n\t\t\t\t2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,\n\t\t\t\t3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,\n\t\t\t\t4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,\n\t\t\t\t5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,\n\t\t\t},\n\t\t\t[]int{1, 2, 3, 4, 5},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := removeSliceDuplicatesInt(tt.input)\n\t\t\tassert.ElementsMatch(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestCallJobFuncWithParams(t *testing.T) {\n\ttype f1 func()\n\ttests := []struct {\n\t\tname        string\n\t\tjobFunc     any\n\t\tparams      []any\n\t\texpectedErr error\n\t}{\n\t\t{\n\t\t\t\"nil jobFunc\",\n\t\t\tnil,\n\t\t\tnil,\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"zero jobFunc\",\n\t\t\tf1(nil),\n\t\t\tnil,\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"wrong number of params\",\n\t\t\tfunc(_ string, _ int) {},\n\t\t\t[]any{\"one\"},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"function that returns an error\",\n\t\t\tfunc() error {\n\t\t\t\treturn errors.New(\"test error\")\n\t\t\t},\n\t\t\tnil,\n\t\t\terrors.New(\"test error\"),\n\t\t},\n\t\t{\n\t\t\t\"function that returns no error\",\n\t\t\tfunc() error {\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tnil,\n\t\t\tnil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := callJobFuncWithParams(tt.jobFunc, tt.params...)\n\t\t\tassert.Equal(t, tt.expectedErr, err)\n\t\t})\n\t}\n}\n\nfunc TestConvertAtTimesToDateTime(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tatTimes  AtTimes\n\t\tlocation *time.Location\n\t\texpected []time.Time\n\t\terr      error\n\t}{\n\t\t{\n\t\t\t\"atTimes is nil\",\n\t\t\tnil,\n\t\t\ttime.UTC,\n\t\t\tnil,\n\t\t\terrAtTimesNil,\n\t\t},\n\t\t{\n\t\t\t\"atTime is nil\",\n\t\t\tNewAtTimes(nil),\n\t\t\ttime.UTC,\n\t\t\tnil,\n\t\t\terrAtTimeNil,\n\t\t},\n\t\t{\n\t\t\t\"atTimes hours is invalid\",\n\t\t\tNewAtTimes(\n\t\t\t\tNewAtTime(24, 0, 0),\n\t\t\t),\n\t\t\ttime.UTC,\n\t\t\tnil,\n\t\t\terrAtTimeHours,\n\t\t},\n\t\t{\n\t\t\t\"atTimes minutes are invalid\",\n\t\t\tNewAtTimes(\n\t\t\t\tNewAtTime(0, 60, 0),\n\t\t\t),\n\t\t\ttime.UTC,\n\t\t\tnil,\n\t\t\terrAtTimeMinSec,\n\t\t},\n\t\t{\n\t\t\t\"atTimes seconds are invalid\",\n\t\t\tNewAtTimes(\n\t\t\t\tNewAtTime(0, 0, 60),\n\t\t\t),\n\t\t\ttime.UTC,\n\t\t\tnil,\n\t\t\terrAtTimeMinSec,\n\t\t},\n\t\t{\n\t\t\t\"atTimes valid\",\n\t\t\tNewAtTimes(\n\t\t\t\tNewAtTime(0, 0, 3),\n\t\t\t\tNewAtTime(0, 0, 0),\n\t\t\t\tNewAtTime(0, 0, 1),\n\t\t\t\tNewAtTime(0, 0, 2),\n\t\t\t),\n\t\t\ttime.UTC,\n\t\t\t[]time.Time{\n\t\t\t\ttime.Date(0, 0, 0, 0, 0, 0, 0, time.UTC),\n\t\t\t\ttime.Date(0, 0, 0, 0, 0, 1, 0, time.UTC),\n\t\t\t\ttime.Date(0, 0, 0, 0, 0, 2, 0, time.UTC),\n\t\t\t\ttime.Date(0, 0, 0, 0, 0, 3, 0, time.UTC),\n\t\t\t},\n\t\t\tnil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult, err := convertAtTimesToDateTime(tt.atTimes, tt.location)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t\tassert.Equal(t, tt.err, err)\n\t\t})\n\t}\n}\n"
  }
]