pax_global_header00006660000000000000000000000064147265465220014527gustar00rootroot0000000000000052 comment=7936ef5a93675c8c33c9c152f8c13859d35db88d pg_cron-1.6.5/000077500000000000000000000000001472654652200131675ustar00rootroot00000000000000pg_cron-1.6.5/.gitignore000066400000000000000000000010111472654652200151500ustar00rootroot00000000000000# Global excludes across all subdirectories *.o *.so *.so.[0-9] *.so.[0-9].[0-9] *.sl *.sl.[0-9] *.sl.[0-9].[0-9] *.dylib *.dll *.a *.mo *.pot objfiles.txt .deps/ *.gcno *.gcda *.gcov *.gcov.out lcov.info coverage/ *.vcproj *.vcxproj win32ver.rc *.exe lib*dll.def lib*.pc # Local excludes in root directory /config.log /config.status /pgsql.sln /pgsql.sln.cache /Debug/ /Release/ /autom4te.cache /Makefile.global /src/Makefile.custom pg_cron--?.?.sql log/ results/ src/*.bc regression.diffs regression.out tmp_check/ pg_cron-1.6.5/CHANGELOG.md000066400000000000000000000142421472654652200150030ustar00rootroot00000000000000### pg_cron v1.6.5 (December 12, 2024) ### * Fix superuser check before adding job to CronJobHash by @CyberDem0n in https://github.com/citusdata/pg_cron/pull/367 * Fix leap year scheduling problem by @zhjwpku in https://github.com/citusdata/pg_cron/pull/365 * Fix possible buffer underflow issue in cron parsing by @marcoslot in https://github.com/citusdata/pg_cron/commit/5c10a8a24527b79c301eaeb04317846f2426dcd5 * Fix compilation warnings by @reshke in https://github.com/citusdata/pg_cron/pull/363 ### pg_cron v1.6.4 (August 9, 2024) ### * Fix bug with invalidation of CachedCronJobRelationId by @CyberDem0n in https://github.com/citusdata/pg_cron/pull/346 * Select procedure based on argument type of cron_unschedule_named by @CyberDem0n in https://github.com/citusdata/pg_cron/pull/347 * Revert "Remove unnecessary lastStartTime reset" d90843de92d5e517a23b1e17da56dc08c496c774 per https://github.com/citusdata/pg_cron/issues/342 ### pg_cron v1.6.3 (July 23, 2024) ### * Fix pointer reuse bug causing off-by-1 day of month by @marcoslot in https://github.com/citusdata/pg_cron/pull/292 * Update the database when scheduling an existing job by @nuno-faria in https://github.com/citusdata/pg_cron/pull/293 * Make cron_unschedule_named() accept v1.4 SQL signature. by @nmisch in https://github.com/citusdata/pg_cron/pull/299 * Allow interrupts in pg_cron launcher loop to avoid deadlock by @Ngalstyan4 in https://github.com/citusdata/pg_cron/pull/319 * Fixes possible overflow by @sminux in https://github.com/citusdata/pg_cron/pull/326 * Log start & end time for all failed runs by @kketch in https://github.com/citusdata/pg_cron/pull/324 * Replace MemoryContextResetAndDeleteChildren macro with MemoryContextReset for PG 17 compatibility by @esiaero in https://github.com/citusdata/pg_cron/pull/332 * Fix compiler error on Illumos by @japinli in https://github.com/citusdata/pg_cron/pull/317 ### pg_cron v1.6.2 (October 20, 2023) ### * Fixes off-by-1 issue in day of month ### pg_cron v1.6.1 (September 26, 2023) ### * Restart the pg_cron scheduler if cancelled * Fix the schema version to 1.6 (requires ALTER EXTENSION pg_cron UPDATE) ### pg_cron v1.6.0 (August 29, 2023) ### * Adds a cron.launch_active_jobs setting, by Bertrand Drouvot * Adds support for PostgreSQL 16, by Cristoph Berg & zhjwpku * Adds scheduling on the last day of the month, by zhjwpku * Fixes a possible memory corruption bug, by zhjwpku ### pg_cron v1.5.2 (April 9, 2023) ### * Fixes a bug that caused crashes after upgrading binaries to 1.5, by Polina Bungina ### pg_cron v1.5.1 (February 9, 2023) ### * Fixes a bug that caused incorrect parsing of some crons schedules ### pg_cron v1.5.0 (February 7, 2023) ### * Adds the possibility of scheduling a job with a 1-59 second interval * Adds a cron.timezone setting to configure the timezone of cron schedules * Removes pg_stat_activity reporting of internal pg_cron metadata queries * Fixes a bug that caused issues with long job names * Fixes a bug that caused inactive @reboot jobs to still run * Fixes a bug that could limit concurrency for background workers * Fixes a bug that prevented compiling on ARM * Fixes regression tests for PostgreSQL <= 12 ### pg_cron v1.4.2 (July 15, 2022) ### * Fixes a bug that could lead to privilege escalation if users can trigger CREATE EXTENSION * Add compatibility for PostgreSQL 15 * Fixes a bug that could cause unschedule to crash * Ensures that cron.max_running_jobs is not higher than possible connection count ### pg_cron v1.4.1 (September 25, 2021) ### * Fixes PostgreSQL 11- support ### pg_cron v1.4.0 (September 16, 2021) ### * Adds a cron.alter_job function to change job properties, by Bertrand Drouvot * Adds a cron.schedule_in_database function to schedule in a custom database, by Bertrand Drouvot * Adds a cron.log_min_messages setting to control log_min_messages in pg_cron launcher, by Bertrand Drouvot * Adds a cron.enable_superuser_jobs setting to disallow superuser jobs * Fixes a bug that could cause jobs to hang when using cron.use_background_workers, by Bertrand Drouvot * Fixes a small memory allocation bug, by @mrdrivingduck * PostgreSQL 14 is supported (no changes were needed) ### pg_cron v1.3.1 (March 29, 2021) ### * Fixes a memory leak ### pg_cron v1.3.0 (September 30, 2020) ### * Background worker support by Bertrand Drouvot * Audit table support by Bertrand Drouvot * PostgreSQL 13 support by Alexander Kukushkin * Schedule jobs by name * Fixes a bug that could cause cron.schedule to crash with long schedules * Fixes a bug that could cause cron.schedule to get into an infinite loop * Fixes a bug that caused overlapping runs not to start ### pg_cron v1.2.0 (August 30, 2019) ### * PostgreSQL 12 support by dverite * Fixes a bug that caused the cron.job table to not appear in pg_dump ### pg_cron v1.1.4 (April 4, 2019) ### * Adds a cron.host setting to make the postgres host configurable * Fixes a bug that could cause segmentation fault after cron.unschedule ### pg_cron v1.1.3 (November 15, 2018) ### * Fixes a bug that causes pg_cron to run during pg_upgrade * Fixes a bug that causes pg_cron to show up incorrectly in pg_stat_activity in PG11 ### pg_cron v1.1.2 (July 10, 2018) ### * PostgreSQL 11 support by dverite * Fix a clang build error by kxjhlele ### pg_cron v1.1.1 (June 7, 2018) ### * Fixed a bug that would cause new jobs to be created as inactive ### pg_cron v1.1.0 (March 22, 2018) ### * Add new 'active' column on cron.job table to enable or disable job(s). * Added a regression test, simply run 'make installcheck' * Set relevant application_name in pg_stat_activity * Increased pg_cron version to 1.1 ### pg_cron v1.0.2 (October 6, 2017) ### * PostgreSQL 10 support * Restrict the maximum number of concurrent tasks * Ensure table locks on cron.job are kept after schedule/unschedule ### pg_cron v1.0.1 (June 30, 2017) ### * Fixes a memory leak that occurs when a connection fails immediately * Fixes a memory leak due to switching memory context when loading metadata * Fixes a segmentation fault that can occur when using an error message after PQclear ### pg_cron v1.0.0 (January 27, 2017) ### * Use WaitLatch instead of pg_usleep when there are no tasks ### pg_cron v1.0.0-rc.1 (December 14, 2016) ### * Initial 1.0 candidate pg_cron-1.6.5/LICENSE000066400000000000000000000016261472654652200142010ustar00rootroot00000000000000Copyright (c) 2015, Citus Data Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies. IN NO EVENT SHALL CITUS DATA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF CITUS DATA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. CITUS DATA SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND CITUS DATA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. pg_cron-1.6.5/META.json000066400000000000000000000024671472654652200146210ustar00rootroot00000000000000{ "name": "pg_cron", "abstract": "Periodic job scheduler for PostgreSQL", "description": "Sets up a background worker that periodically runs queries in the background", "version": "1.0", "maintainer": "\"Marco Slot\" ", "license": { "PostgreSQL": "http://www.postgresql.org/about/licence" }, "prereqs": { "runtime": { "requires": { "PostgreSQL": "9.5.0" } } }, "provides": { "pg_cron": { "abstract": "Periodic background job scheduler", "file": "pg_cron--1.0.sql", "docfile": "README.md", "version": "1.0" } }, "release_status": "stable", "resources": { "homepage": "https://citusdata.com/", "bugtracker": { "web": "https://github.com/citusdata/pg_cron/issues", "mailto": "support@citusdata.com" }, "repository": { "url": "git://github.com/citusdata/pg_cron.git", "web": "https://github.com/citusdata/pg_cron", "type": "git" } }, "generated_by": "\"Marco Slot\" ", "tags": [ "cron", "background worker" ], "meta-spec": { "version": "1.0.0", "url": "http://pgxn.org/meta/spec.txt" } } pg_cron-1.6.5/Makefile000066400000000000000000000016711472654652200146340ustar00rootroot00000000000000# src/test/modules/pg_cron/Makefile EXTENSION = pg_cron DATA_built = $(EXTENSION)--1.0.sql DATA = $(wildcard $(EXTENSION)--*--*.sql) REGRESS_OPTS =--temp-config=./pg_cron.conf --temp-instance=./tmp_check REGRESS = pg_cron-test # compilation configuration MODULE_big = $(EXTENSION) OBJS = $(patsubst %.c,%.o,$(wildcard src/*.c)) ifeq ($(CC),gcc) PG_CPPFLAGS = -std=c99 -Wall -Wextra -Werror -Wno-unused-parameter -Wno-uninitialized -Wno-implicit-fallthrough -Iinclude -I$(libpq_srcdir) else PG_CPPFLAGS = -std=c99 -Wall -Wextra -Werror -Wno-unused-parameter -Wno-implicit-fallthrough -Iinclude -I$(libpq_srcdir) endif ifeq ($(shell uname -s),SunOS) PG_CPPFLAGS += -Wno-sign-compare -D__EXTENSIONS__ endif SHLIB_LINK = $(libpq) EXTRA_CLEAN += $(addprefix src/,*.gcno *.gcda) # clean up after profiling runs PG_CONFIG ?= pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) $(EXTENSION)--1.0.sql: $(EXTENSION).sql cat $^ > $@ pg_cron-1.6.5/README.md000066400000000000000000000304221472654652200144470ustar00rootroot00000000000000[![Citus Banner](/github-banner.png)](https://www.citusdata.com/) ## What is pg_cron? pg_cron is a simple cron-based job scheduler for PostgreSQL (10 or higher) that runs inside the database as an extension. It uses the same syntax as regular cron, but it allows you to schedule PostgreSQL commands directly from the database. You can also use '[1-59] seconds' to schedule a job based on an interval. pg_cron also allows you using '$' to indicate last day of the month. ```sql -- Delete old data on Saturday at 3:30am (GMT) SELECT cron.schedule('30 3 * * 6', $$DELETE FROM events WHERE event_time < now() - interval '1 week'$$); schedule ---------- 42 -- Vacuum every day at 10:00am (GMT) SELECT cron.schedule('nightly-vacuum', '0 10 * * *', 'VACUUM'); schedule ---------- 43 -- Change to vacuum at 3:00am (GMT) SELECT cron.schedule('nightly-vacuum', '0 3 * * *', 'VACUUM'); schedule ---------- 43 -- Stop scheduling jobs SELECT cron.unschedule('nightly-vacuum' ); unschedule ------------ t SELECT cron.unschedule(42); unschedule ------------ t -- Vacuum every Sunday at 4:00am (GMT) in a database other than the one pg_cron is installed in SELECT cron.schedule_in_database('weekly-vacuum', '0 4 * * 0', 'VACUUM', 'some_other_database'); schedule ---------- 44 -- Call a stored procedure every 5 seconds SELECT cron.schedule('process-updates', '5 seconds', 'CALL process_updates()'); -- Process payroll at 12:00 of the last day of each month SELECT cron.schedule('process-payroll', '0 12 $ * *', 'CALL process_payroll()'); ``` pg_cron can run multiple jobs in parallel, but it runs at most one instance of a job at a time. If a second run is supposed to start before the first one finishes, then the second run is queued and started as soon as the first run completes. The schedule uses the standard cron syntax, in which * means "run every time period", and a specific number means "but only at this time": ``` ┌───────────── min (0 - 59) │ ┌────────────── hour (0 - 23) │ │ ┌─────────────── day of month (1 - 31) or last day of the month ($) │ │ │ ┌──────────────── month (1 - 12) │ │ │ │ ┌───────────────── day of week (0 - 6) (0 to 6 are Sunday to │ │ │ │ │ Saturday, or use names; 7 is also Sunday) │ │ │ │ │ │ │ │ │ │ * * * * * ``` An easy way to create a cron schedule is: [crontab.guru](http://crontab.guru/). The code in pg_cron that handles parsing and scheduling comes directly from the cron source code by Paul Vixie, hence the same options are supported. ## Installing pg_cron Install on Red Hat, CentOS, Fedora, Amazon Linux with PostgreSQL 16 using [PGDG](https://yum.postgresql.org/repopackages/): ```bash # Install the pg_cron extension sudo yum install -y pg_cron_16 ``` Install on Debian, Ubuntu with PostgreSQL 16 using [apt.postgresql.org](https://wiki.postgresql.org/wiki/Apt): ```bash # Install the pg_cron extension sudo apt-get -y install postgresql-16-cron ``` You can also install pg_cron by building it from source: ```bash git clone https://github.com/citusdata/pg_cron.git cd pg_cron # Ensure pg_config is in your path, e.g. export PATH=/usr/pgsql-16/bin:$PATH make && sudo PATH=$PATH make install ``` ## Setting up pg_cron To start the pg_cron background worker when PostgreSQL starts, you need to add pg_cron to `shared_preload_libraries` in postgresql.conf. Note that pg_cron does not run any jobs as a long a server is in [hot standby](https://www.postgresql.org/docs/current/static/hot-standby.html) mode, but it automatically starts when the server is promoted. ``` # add to postgresql.conf # required to load pg_cron background worker on start-up shared_preload_libraries = 'pg_cron' ``` By default, the pg_cron background worker expects its metadata tables to be created in the "postgres" database. However, you can configure this by setting the `cron.database_name` configuration parameter in postgresql.conf. ``` # add to postgresql.conf # optionally, specify the database in which the pg_cron background worker should run (defaults to postgres) cron.database_name = 'postgres' ``` `pg_cron` may only be installed to one database in a cluster. If you need to run jobs in multiple databases, use `cron.schedule_in_database()`. Previously pg_cron could only use GMT time, but now you can adapt your time by setting `cron.timezone` in postgresql.conf. ``` # add to postgresql.conf # optionally, specify the timezone in which the pg_cron background worker should run (defaults to GMT). E.g: cron.timezone = 'PRC' ``` After restarting PostgreSQL, you can create the pg_cron functions and metadata tables using `CREATE EXTENSION pg_cron`. ```sql -- run as superuser: CREATE EXTENSION pg_cron; -- optionally, grant usage to regular users: GRANT USAGE ON SCHEMA cron TO marco; ``` ### Ensuring pg_cron can start jobs **Important**: By default, pg_cron uses libpq to open a new connection to the local database, which needs to be allowed by [pg_hba.conf](https://www.postgresql.org/docs/current/static/auth-pg-hba-conf.html). It may be necessary to enable `trust` authentication for connections coming from localhost in for the user running the cron job, or you can add the password to a [.pgpass file](https://www.postgresql.org/docs/current/static/libpq-pgpass.html), which libpq will use when opening a connection. You can also use a unix domain socket directory as the hostname and enable `trust` authentication for local connections in [pg_hba.conf](https://www.postgresql.org/docs/current/static/auth-pg-hba-conf.html), which is normally safe: ``` # Connect via a unix domain socket: cron.host = '/tmp' # Can also be an empty string to look for the default directory: cron.host = '' ``` Alternatively, pg_cron can be configured to use background workers. In that case, the number of concurrent jobs is limited by the `max_worker_processes` setting, so you may need to raise that. ``` # Schedule jobs via background workers instead of localhost connections cron.use_background_workers = on # Increase the number of available background workers from the default of 8 max_worker_processes = 20 ``` For security, jobs are executed in the database in which the `cron.schedule` function is called with the same permissions as the current user. In addition, users are only able to see their own jobs in the `cron.job` table. ```sql -- View active jobs select * from cron.job; ``` ## Viewing job run details You can view the status of running and recently completed job runs in the `cron.job_run_details`: ```sql select * from cron.job_run_details order by start_time desc limit 5; ┌───────┬───────┬─────────┬──────────┬──────────┬───────────────────┬───────────┬──────────────────┬───────────────────────────────┬───────────────────────────────┐ │ jobid │ runid │ job_pid │ database │ username │ command │ status │ return_message │ start_time │ end_time │ ├───────┼───────┼─────────┼──────────┼──────────┼───────────────────┼───────────┼──────────────────┼───────────────────────────────┼───────────────────────────────┤ │ 10 │ 4328 │ 2610 │ postgres │ marco │ select process() │ succeeded │ SELECT 1 │ 2023-02-07 09:30:00.098164+01 │ 2023-02-07 09:30:00.130729+01 │ │ 10 │ 4327 │ 2609 │ postgres │ marco │ select process() │ succeeded │ SELECT 1 │ 2023-02-07 09:29:00.015168+01 │ 2023-02-07 09:29:00.832308+01 │ │ 10 │ 4321 │ 2603 │ postgres │ marco │ select process() │ succeeded │ SELECT 1 │ 2023-02-07 09:28:00.011965+01 │ 2023-02-07 09:28:01.420901+01 │ │ 10 │ 4320 │ 2602 │ postgres │ marco │ select process() │ failed │ server restarted │ 2023-02-07 09:27:00.011833+01 │ 2023-02-07 09:27:00.72121+01 │ │ 9 │ 4320 │ 2602 │ postgres │ marco │ select do_stuff() │ failed │ job canceled │ 2023-02-07 09:26:00.011833+01 │ 2023-02-07 09:26:00.22121+01 │ └───────┴───────┴─────────┴──────────┴──────────┴───────────────────┴───────────┴──────────────────┴───────────────────────────────┴───────────────────────────────┘ (10 rows) ``` The records in `cron.job_run_details` are not cleaned automatically, but every user that can schedule cron jobs also has permission to delete their own `cron.job_run_details` records. Especially when you have jobs that run every few seconds, it can be a good idea to clean up regularly, which can easily be done using pg_cron itself: ```sql -- Delete old cron.job_run_details records of the current user every day at noon SELECT cron.schedule('delete-job-run-details', '0 12 * * *', $$DELETE FROM cron.job_run_details WHERE end_time < now() - interval '7 days'$$); ``` If you do not want to use `cron.job_run_details` at all, then you can add `cron.log_run = off` to `postgresql.conf`. ## Example use cases Articles showing possible ways of using pg_cron: * [Auto-partitioning using pg_partman](https://www.citusdata.com/blog/2018/01/24/citus-and-pg-partman-creating-a-scalable-time-series-database-on-postgresql/) * [Computing rollups in an analytical dashboard](https://www.citusdata.com/blog/2017/12/27/real-time-analytics-dashboards-with-citus/) * [Deleting old data, vacuum](https://www.citusdata.com/blog/2016/09/09/pgcron-run-periodic-jobs-in-postgres/) * [Feeding cats](http://bonesmoses.org/2016/09/09/pg-phriday-irrelevant-inclinations/) * [Routinely invoking a function](https://fluca1978.github.io/2019/05/21/pgcron.html) * [Postgres as a cron server](https://supabase.io/blog/2021/03/05/postgres-as-a-cron-server) ## Managed services The following table keeps track of which of the major managed Postgres services support pg_cron. | Service | Supported | | ------------- |:-------------:| | [Aiven](https://aiven.io/postgresql) | :heavy_check_mark: | | [Alibaba Cloud](https://www.alibabacloud.com/help/doc-detail/150355.htm) | :heavy_check_mark: | | [Amazon RDS](https://aws.amazon.com/rds/postgresql/) | :heavy_check_mark: | | | [Azure](https://azure.microsoft.com/en-us/services/postgresql/) | :heavy_check_mark: | | [Crunchy Bridge](https://www.crunchydata.com/products/crunchy-bridge/?ref=producthunt) | :heavy_check_mark: | | [DigitalOcean](https://www.digitalocean.com/products/managed-databases/) | :heavy_check_mark: | | [Google Cloud](https://cloud.google.com/sql/postgresql/) | :heavy_check_mark: | | [Heroku](https://elements.heroku.com/addons/heroku-postgresql) | :x: | | [Instaclustr](https://instaclustr.com) | :heavy_check_mark: | | [ScaleGrid](https://scalegrid.io/postgresql.html) | :heavy_check_mark: | | [Scaleway](https://www.scaleway.com/en/database/) | :heavy_check_mark: | | [Supabase](https://supabase.io/docs/guides/database) | :heavy_check_mark: | | [Tembo](https://tembo.io) | :heavy_check_mark: | | [YugabyteDB](https://www.yugabyte.com/) | :heavy_check_mark: | ## Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. pg_cron-1.6.5/expected/000077500000000000000000000000001472654652200147705ustar00rootroot00000000000000pg_cron-1.6.5/expected/pg_cron-test.out000066400000000000000000000305531472654652200201330ustar00rootroot00000000000000CREATE EXTENSION pg_cron VERSION '1.0'; SELECT extversion FROM pg_extension WHERE extname='pg_cron'; extversion ------------ 1.0 (1 row) -- Test binary compatibility with v1.4 function signature. ALTER EXTENSION pg_cron UPDATE TO '1.4'; SELECT cron.unschedule(job_name := 'no_such_job'); ERROR: could not find valid entry for job 'no_such_job' SELECT cron.schedule('testjob', '* * * * *', 'SELECT 1'); schedule ---------- 1 (1 row) SELECT cron.unschedule('testjob'); unschedule ------------ t (1 row) -- Test cache invalidation DROP EXTENSION pg_cron; CREATE EXTENSION pg_cron VERSION '1.4'; ALTER EXTENSION pg_cron UPDATE; -- Vacuum every day at 10:00am (GMT) SELECT cron.schedule('0 10 * * *', 'VACUUM'); schedule ---------- 1 (1 row) -- Stop scheduling a job SELECT cron.unschedule(1); unschedule ------------ t (1 row) -- Invalid input: input too long SELECT cron.schedule(repeat('a', 1000), ''); ERROR: invalid schedule: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa HINT: Use cron format (e.g. 5 4 * * *), or interval format '[1-59] seconds' -- Invalid input: missing parts SELECT cron.schedule('* * * *', 'SELECT 1'); ERROR: invalid schedule: * * * * HINT: Use cron format (e.g. 5 4 * * *), or interval format '[1-59] seconds' -- Invalid input: trailing characters SELECT cron.schedule('5 secondc', 'SELECT 1'); ERROR: invalid schedule: 5 secondc HINT: Use cron format (e.g. 5 4 * * *), or interval format '[1-59] seconds' SELECT cron.schedule('50 seconds c', 'SELECT 1'); ERROR: invalid schedule: 50 seconds c HINT: Use cron format (e.g. 5 4 * * *), or interval format '[1-59] seconds' -- Invalid input: step out of range SELECT cron.schedule('*/0 10 * * *', 'SELECT 1'); ERROR: invalid schedule: */0 10 * * * HINT: Use cron format (e.g. 5 4 * * *), or interval format '[1-59] seconds' SELECT cron.schedule('-1 * * * *', 'SELECT 1'); ERROR: invalid schedule: -1 * * * * HINT: Use cron format (e.g. 5 4 * * *), or interval format '[1-59] seconds' SELECT cron.schedule('*/-1 10 * * *', 'SELECT 1'); ERROR: invalid schedule: */-1 10 * * * HINT: Use cron format (e.g. 5 4 * * *), or interval format '[1-59] seconds' SELECT cron.schedule('*/60 10 * * *', 'SELECT 1'); ERROR: invalid schedule: */60 10 * * * HINT: Use cron format (e.g. 5 4 * * *), or interval format '[1-59] seconds' SELECT cron.schedule('* * * 13 *', 'SELECT 1'); ERROR: invalid schedule: * * * 13 * HINT: Use cron format (e.g. 5 4 * * *), or interval format '[1-59] seconds' SELECT cron.schedule('* * * 0 *', 'SELECT 1'); ERROR: invalid schedule: * * * 0 * HINT: Use cron format (e.g. 5 4 * * *), or interval format '[1-59] seconds' SELECT cron.schedule('*/5000000000 10 * * *', 'SELECT 1'); ERROR: invalid schedule: */5000000000 10 * * * HINT: Use cron format (e.g. 5 4 * * *), or interval format '[1-59] seconds' -- Invalid input: seconds out of range SELECT cron.schedule('-1 seconds', 'SELECT 1'); ERROR: invalid schedule: -1 seconds HINT: Use cron format (e.g. 5 4 * * *), or interval format '[1-59] seconds' SELECT cron.schedule('0 seconds', 'SELECT 1'); ERROR: invalid schedule: 0 seconds HINT: Use cron format (e.g. 5 4 * * *), or interval format '[1-59] seconds' SELECT cron.schedule('60 seconds', 'SELECT 1'); ERROR: invalid schedule: 60 seconds HINT: Use cron format (e.g. 5 4 * * *), or interval format '[1-59] seconds' SELECT cron.schedule('10000000000 seconds', 'SELECT 1'); ERROR: invalid schedule: 10000000000 seconds HINT: Use cron format (e.g. 5 4 * * *), or interval format '[1-59] seconds' -- Try to update pg_cron on restart SELECT cron.schedule('@restar', 'ALTER EXTENSION pg_cron UPDATE'); ERROR: invalid schedule: @restar HINT: Use cron format (e.g. 5 4 * * *), or interval format '[1-59] seconds' SELECT cron.schedule('@restart', 'ALTER EXTENSION pg_cron UPDATE'); schedule ---------- 2 (1 row) -- Vacuum every day at 10:00am (GMT) SELECT cron.schedule('myvacuum', '0 10 * * *', 'VACUUM'); schedule ---------- 3 (1 row) SELECT jobid, jobname, schedule, command FROM cron.job ORDER BY jobid; jobid | jobname | schedule | command -------+----------+------------+-------------------------------- 2 | | @restart | ALTER EXTENSION pg_cron UPDATE 3 | myvacuum | 0 10 * * * | VACUUM (2 rows) -- Make that 11:00am (GMT) SELECT cron.schedule('myvacuum', '0 11 * * *', 'VACUUM'); schedule ---------- 3 (1 row) SELECT jobid, jobname, schedule, command FROM cron.job ORDER BY jobid; jobid | jobname | schedule | command -------+----------+------------+-------------------------------- 2 | | @restart | ALTER EXTENSION pg_cron UPDATE 3 | myvacuum | 0 11 * * * | VACUUM (2 rows) -- Make that VACUUM FULL SELECT cron.schedule('myvacuum', '0 11 * * *', 'VACUUM FULL'); schedule ---------- 3 (1 row) SELECT jobid, jobname, schedule, command FROM cron.job ORDER BY jobid; jobid | jobname | schedule | command -------+----------+------------+-------------------------------- 2 | | @restart | ALTER EXTENSION pg_cron UPDATE 3 | myvacuum | 0 11 * * * | VACUUM FULL (2 rows) -- Stop scheduling a job SELECT cron.unschedule('myvacuum'); unschedule ------------ t (1 row) SELECT jobid, jobname, schedule, command FROM cron.job ORDER BY jobid; jobid | jobname | schedule | command -------+---------+----------+-------------------------------- 2 | | @restart | ALTER EXTENSION pg_cron UPDATE (1 row) -- Testing version >= 1.4 new APIs -- First as superuser -- Update a job without one job attribute to change SELECT cron.alter_job(2); ERROR: no updates specified HINT: You must specify at least one job attribute to change when calling alter_job -- Update to a non existing database select cron.alter_job(job_id:=2,database:='hopedoesnotexist'); ERROR: database "hopedoesnotexist" does not exist -- Create a database that does not allow connection create database pgcron_dbno; revoke connect on database pgcron_dbno from public; -- create a test user create user pgcron_cront with password 'pwd'; GRANT USAGE ON SCHEMA cron TO pgcron_cront; -- Schedule a job for this user on the database that does not accept connections SELECT cron.schedule_in_database(job_name:='can not connect', schedule:='0 11 * * *', command:='VACUUM',database:='pgcron_dbno',username:='pgcron_cront'); ERROR: User pgcron_cront does not have CONNECT privilege on pgcron_dbno -- Create a database that does allow connections create database pgcron_dbyes; -- Schedule a job on the database that does accept connections for a non existing user SELECT cron.schedule_in_database(job_name:='user does not exist', schedule:='0 11 * * *', command:='VACUUM',database:='pgcron_dbyes',username:='pgcron_useraqwxszedc'); ERROR: role "pgcron_useraqwxszedc" does not exist -- Alter an existing job on a database that does not accept connections SELECT cron.alter_job(job_id:=2,database:='pgcron_dbno',username:='pgcron_cront'); ERROR: User pgcron_cront does not have CONNECT privilege on pgcron_dbno -- Make sure pgcron_cront can execute alter_job GRANT EXECUTE ON FUNCTION cron.alter_job(bigint,text,text,text,text,boolean) TO public; -- Second as non superuser SET SESSION AUTHORIZATION pgcron_cront; -- Create a job SELECT cron.schedule('My vacuum', '0 11 * * *', 'VACUUM'); schedule ---------- 6 (1 row) -- Create a job for another user SELECT cron.schedule_in_database(job_name:='his vacuum', schedule:='0 11 * * *', command:='VACUUM',database:=current_database(),username:='anotheruser'); ERROR: permission denied for function schedule_in_database -- Change the username of an existing job that the user own select cron.alter_job(job_id:=6,username:='anotheruser'); ERROR: must be superuser to alter username -- Update a job that the user does not own select cron.alter_job(job_id:=2,database:='pgcron_dbyes'); ERROR: Job 2 does not exist or you don't own it -- change the database for a job that the user own and can connect to select cron.alter_job(job_id:=6,database:='pgcron_dbyes'); alter_job ----------- (1 row) SELECT database FROM cron.job; database -------------- pgcron_dbyes (1 row) -- change the database for a job that the user own but can not connect to select cron.alter_job(job_id:=6,database:='pgcron_dbno'); ERROR: User pgcron_cront does not have CONNECT privilege on pgcron_dbno SELECT database FROM cron.job; database -------------- pgcron_dbyes (1 row) -- back to superuser RESET SESSION AUTHORIZATION; -- Change the username of an existing job select cron.alter_job(job_id:=2,username:='pgcron_cront'); alter_job ----------- (1 row) SELECT username FROM cron.job where jobid=2; username -------------- pgcron_cront (1 row) -- Create a job for another user SELECT cron.schedule_in_database(job_name:='his vacuum', schedule:='0 11 * * *', command:='VACUUM',database:=current_database(), username:='pgcron_cront'); schedule_in_database ---------------------- 7 (1 row) SELECT username FROM cron.job where jobid=7; username -------------- pgcron_cront (1 row) -- Override function DROP EXTENSION IF EXISTS pg_cron cascade; CREATE TABLE test (data text); DROP TYPE IF EXISTS current_setting cascade; NOTICE: type "current_setting" does not exist, skipping CREATE TYPE current_setting AS ENUM ('cron.database_name'); CREATE OR REPLACE FUNCTION public.func1(text, current_setting) RETURNS text LANGUAGE sql volatile AS 'INSERT INTO test(data) VALUES (current_user); SELECT current_database()::text;'; CREATE OR REPLACE FUNCTION public.func1(current_setting) RETURNS text LANGUAGE sql volatile AS 'INSERT INTO test(data) VALUES (current_user); SELECT current_database()::text;'; CREATE CAST (current_setting AS text) WITH FUNCTION public.func1(current_setting) AS IMPLICIT; CREATE EXTENSION pg_cron; select * from public.test; data ------ (0 rows) -- valid interval jobs SELECT cron.schedule('1 second', 'SELECT 1'); schedule ---------- 1 (1 row) SELECT cron.schedule(' 30 sEcOnDs ', 'SELECT 1'); schedule ---------- 2 (1 row) SELECT cron.schedule('59 seconds', 'SELECT 1'); schedule ---------- 3 (1 row) SELECT cron.schedule('17 seconds ', 'SELECT 1'); schedule ---------- 4 (1 row) SELECT jobid, jobname, schedule, command FROM cron.job ORDER BY jobid; jobid | jobname | schedule | command -------+---------+--------------+---------- 1 | | 1 second | SELECT 1 2 | | 30 sEcOnDs | SELECT 1 3 | | 59 seconds | SELECT 1 4 | | 17 seconds | SELECT 1 (4 rows) -- valid last of day job SELECT cron.schedule('last-day-of-month-job1', '0 11 $ * *', 'SELECT 1'); schedule ---------- 5 (1 row) SELECT jobid, jobname, schedule, command FROM cron.job ORDER BY jobid; jobid | jobname | schedule | command -------+------------------------+--------------+---------- 1 | | 1 second | SELECT 1 2 | | 30 sEcOnDs | SELECT 1 3 | | 59 seconds | SELECT 1 4 | | 17 seconds | SELECT 1 5 | last-day-of-month-job1 | 0 11 $ * * | SELECT 1 (5 rows) -- invalid last of day job SELECT cron.schedule('bad-last-dom-job1', '0 11 $foo * *', 'VACUUM FULL'); ERROR: invalid schedule: 0 11 $foo * * HINT: Use cron format (e.g. 5 4 * * *), or interval format '[1-59] seconds' -- cleaning DROP EXTENSION pg_cron; drop user pgcron_cront; drop database pgcron_dbno; drop database pgcron_dbyes; pg_cron-1.6.5/github-banner.png000066400000000000000000000100531472654652200164210ustar00rootroot00000000000000PNG  IHDR3ƬIDATx]xUENB3 &< ]B^ׅX`" q-(삈"` AIw^r;?ݹ3sg?s3(L2'?d#@6L2ITm!߀g@OEDLHKd~IXwbf_I*v+ph7b %5tkIJ;@ê.󳢁v@gt:@Gy'A"p zUT,ن.@BMV8̆_0IP,(8? mj)ΑmUQy D!:S;sAowGr݀6@㨌"g =v*j (b~(ZJQɯ*-!m"_Z5E1Q}A e7`2$;aCJ9 o P ƁT#XyNM^;X+~SIIFt΂ lv;M ,.z!=l{u6,R*%X a&nk" $-Țy((L󀫁Ӂ3u"@[t!X~$ @\ZA6ULt3@r@y,1z[K0?h[p.[[F`6m ƖбX J5 * Dv>q rGjMz@bdH'ǫ}E)?e~.6뀗xBj;fpk=j0!O{i-M\>cAwPb\ᠢ Y|[i+%w!Ϟ)4nV\k6$;XӀ\Zn-칎X͙"!rۀ0|HY4" y|{OhMO<;DmX!ygywmHqP_44QI@|V7$pY4І 4ޣȤ9H]|l(L;FJOyww*>4MXq+ k ;'6#e~.YZ{^w[?mT Y8Ѧ|g,6۠Y6L aWA>}GtI Gc! ״g'rHh֕S<3mq!,- \AP$Gs8I زi]6c|RWE(XVH f#7x&\t~MiZ<<€A zY_-FfkF.z] \4xW=6էjyP}li !]$ve~\Ej/$}_.{".t>5NcrI}O!#T|pRcGx@skȕLp) ?RZi4V3 ;'z AlR56JxomY' o&B&Ld{>O*LV_oSa DinWy訰..t1B$yޖde}bYE+pOLz熧`Ir.iqF1`~Fq[ж AY"l%v'@Cw]i9+`$C>G abQ |^ǎ>qAF0wGϿT:Ҟ`à1WZQMMNgJS+DAΦǝLoTUa+{ :t^?}r>c˧%V{]o+&i+40OecF{[Hq2&i ݃z]2ҭi92+$pf-'X T#(EUwӥVr5\( z,Y_81.YRs))] YA,%ϟא1"d*\؈Ue<*Bx>%cXNj6{\TxE57؏D`wd[6Sw76ly8wO۟0,f`v |SU.'2Gs{Pg|])Le~ʺJކ!]WMQ ME%\0|E'h,Z_ ZnN U,#= dEWd,(-Ֆ╍%ycuT9)Fl$HqY. t&k;%"9tF/\D֮r $|*]^/jGXi!dWx3uV!NҌJ X1.&Ar*#2MtIfl$?U;h AJԕF|ClӵQa&=V ##t qN"LZȖV)ИhŜτOI/8xlK*^DTWw9Թ3<(-7TCbd].tІb0Ϻ ℨ*N(GNwG<ʱy9a>)ޥ 7},|uYSm)jFb/n$u72*tr+FL~'Dxs8NW ne89¾qH-({&_~o˗wxn*XRƷXRF<\zLI҆N\coQEjٌjjIWbMMzE5G 񬲩|{3Z&Dqn"=,wHTS vkՏ|_E0#>jqȥ> cFɤLd? xv ߮DiA4d{mE9u/4ɤJ¼ʬ!t'f^B)oVI&}hFŤDh"dR ދ};|\(TIvOwI k?7{1L2?4뚻^6x'  1LR}XI&K:=v>gTG0lo6&dI&dI>gq1)IENDB`pg_cron-1.6.5/include/000077500000000000000000000000001472654652200146125ustar00rootroot00000000000000pg_cron-1.6.5/include/bitstring.h000066400000000000000000000075651472654652200170050ustar00rootroot00000000000000/* * Copyright (c) 1989 The Regents of the University of California. * All rights reserved. * * This code is derived from software contributed to Berkeley by * Paul Vixie. * * Redistribution and use in source and binary forms are permitted * provided that the above copyright notice and this paragraph are * duplicated in all such forms and that any documentation, * advertising materials, and other materials related to such * distribution and use acknowledge that the software was developed * by the University of California, Berkeley. The name of the * University may not be used to endorse or promote products derived * from this software without specific prior written permission. * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. * * @(#)bitstring.h 5.2 (Berkeley) 4/4/90 */ typedef unsigned char bitstr_t; /* internal macros */ /* byte of the bitstring bit is in */ #define _bit_byte(bit) \ ((bit) >> 3) /* mask for the bit within its byte */ #define _bit_mask(bit) \ (1 << ((bit)&0x7)) /* external macros */ /* bytes in a bitstring of nbits bits */ #define bitstr_size(nbits) \ ((((nbits) - 1) >> 3) + 1) /* allocate a bitstring */ #define bit_alloc(nbits) \ (bitstr_t *)malloc(1, \ (unsigned int)bitstr_size(nbits) * sizeof(bitstr_t)) /* allocate a bitstring on the stack */ #define bit_decl(name, nbits) \ (name)[bitstr_size(nbits)] /* is bit N of bitstring name set? */ #define bit_test(name, bit) \ ((name)[_bit_byte(bit)] & _bit_mask(bit)) /* set bit N of bitstring name */ #define bit_set(name, bit) \ (name)[_bit_byte(bit)] |= _bit_mask(bit) /* clear bit N of bitstring name */ #define bit_clear(name, bit) \ (name)[_bit_byte(bit)] &= ~_bit_mask(bit) /* clear bits start ... stop in bitstring */ #define bit_nclear(name, start, stop) { \ register bitstr_t *_name = name; \ register int _start = start, _stop = stop; \ register int _startbyte = _bit_byte(_start); \ register int _stopbyte = _bit_byte(_stop); \ if (_startbyte == _stopbyte) { \ _name[_startbyte] &= ((0xff >> (8 - (_start&0x7))) | \ (0xff << ((_stop&0x7) + 1))); \ } else { \ _name[_startbyte] &= 0xff >> (8 - (_start&0x7)); \ while (++_startbyte < _stopbyte) \ _name[_startbyte] = 0; \ _name[_stopbyte] &= 0xff << ((_stop&0x7) + 1); \ } \ } /* set bits start ... stop in bitstring */ #define bit_nset(name, start, stop) { \ register bitstr_t *_name = name; \ register int _start = start, _stop = stop; \ register int _startbyte = _bit_byte(_start); \ register int _stopbyte = _bit_byte(_stop); \ if (_startbyte == _stopbyte) { \ _name[_startbyte] |= ((0xff << (_start&0x7)) & \ (0xff >> (7 - (_stop&0x7)))); \ } else { \ _name[_startbyte] |= 0xff << ((_start)&0x7); \ while (++_startbyte < _stopbyte) \ _name[_startbyte] = 0xff; \ _name[_stopbyte] |= 0xff >> (7 - (_stop&0x7)); \ } \ } /* find first bit clear in name */ #define bit_ffc(name, nbits, value) { \ register bitstr_t *_name = name; \ register int _byte, _nbits = nbits; \ register int _stopbyte = _bit_byte(_nbits), _value = -1; \ for (_byte = 0; _byte <= _stopbyte; ++_byte) \ if (_name[_byte] != 0xff) { \ _value = _byte << 3; \ for (_stopbyte = _name[_byte]; (_stopbyte&0x1); \ ++_value, _stopbyte >>= 1); \ break; \ } \ *(value) = _value; \ } /* find first bit set in name */ #define bit_ffs(name, nbits, value) { \ register bitstr_t *_name = name; \ register int _byte, _nbits = nbits; \ register int _stopbyte = _bit_byte(_nbits), _value = -1; \ for (_byte = 0; _byte <= _stopbyte; ++_byte) \ if (_name[_byte]) { \ _value = _byte << 3; \ for (_stopbyte = _name[_byte]; !(_stopbyte&0x1); \ ++_value, _stopbyte >>= 1); \ break; \ } \ *(value) = _value; \ } pg_cron-1.6.5/include/cron.h000066400000000000000000000173531472654652200157350ustar00rootroot00000000000000/* Copyright 1988,1990,1993,1994 by Paul Vixie * All rights reserved * * Distribute freely, except: don't remove my name from the source or * documentation (don't take credit for my work), mark your changes (don't * get me blamed for your possible bugs), don't alter or remove this * notice. May be sold if buildable source is provided to buyer. No * warrantee of any kind, express or implied, is included with this * software; use at your own risk, responsibility for damages (if any) to * anyone resulting from the use of this software rests entirely with the * user. * * Send bug reports, bug fixes, enhancements, requests, flames, etc., and * I'll try to keep a version up to date. I can be reached as follows: * Paul Vixie uunet!decwrl!vixie!paul */ /* cron.h - header for vixie's cron * * $Id: cron.h,v 2.10 1994/01/15 20:43:43 vixie Exp $ * * marco 07nov16 [remove code not needed by pg_cron] * marco 04sep16 [integrate into pg_cron] * vix 14nov88 [rest of log is in RCS] * vix 14jan87 [0 or 7 can be sunday; thanks, mwm@berkeley] * vix 30dec86 [written] */ /* reorder these #include's at your peril */ #include #include #include #include #if SYS_TIME_H # include #else # include #endif /* these are really immutable, and are * defined for symbolic convenience only * TRUE, FALSE, and ERR must be distinct * ERR must be < OK. */ #define TRUE 1 #define FALSE 0 /* system calls return this on success */ #define OK 0 /* or this on error */ #define ERR (-1) /* turn this on to get '-x' code */ #ifndef DEBUGGING #define DEBUGGING FALSE #endif #define READ_PIPE 0 /* which end of a pipe pair do you read? */ #define WRITE_PIPE 1 /* or write to? */ #define STDIN 0 /* what is stdin's file descriptor? */ #define STDOUT 1 /* stdout's? */ #define STDERR 2 /* stderr's? */ #define ERROR_EXIT 1 /* exit() with this will scare the shell */ #define OK_EXIT 0 /* exit() with this is considered 'normal' */ #define MAX_FNAME 100 /* max length of internally generated fn */ #define MAX_COMMAND 1000 /* max length of internally generated cmd */ #define MAX_TEMPSTR 1000 /* max length of envvar=value\0 strings */ #define MAX_ENVSTR MAX_TEMPSTR /* DO NOT change - buffer overruns otherwise */ #define MAX_UNAME 20 /* max length of username, should be overkill */ #define ROOT_UID 0 /* don't change this, it really must be root */ #define ROOT_USER "root" /* ditto */ /* NOTE: these correspond to DebugFlagNames, * defined below. */ #define DEXT 0x0001 /* extend flag for other debug masks */ #define DSCH 0x0002 /* scheduling debug mask */ #define DPROC 0x0004 /* process control debug mask */ #define DPARS 0x0008 /* parsing debug mask */ #define DLOAD 0x0010 /* database loading debug mask */ #define DMISC 0x0020 /* misc debug mask */ #define DTEST 0x0040 /* test mode: don't execute any commands */ #define DBIT 0x0080 /* bit twiddling shown (long) */ #define CRON_TAB(u) "%s/%s", SPOOL_DIR, u #define REG register #define PPC_NULL ((char **)NULL) #ifndef MAXHOSTNAMELEN #define MAXHOSTNAMELEN 64 #endif #define Is_Blank(c) ((c) == '\t' || (c) == ' ') #define Skip_Blanks(c, f) \ while (Is_Blank(c)) \ c = get_char(f); #define Skip_Nonblanks(c, f) \ while (c!='\t' && c!=' ' && c!='\n' && c != EOF && c != '\0') \ c = get_char(f); #define Skip_Line(c, f) \ do {c = get_char(f);} while (c != '\n' && c != EOF); #if DEBUGGING # define Debug(mask, message) \ if ( (DebugFlags & (mask) ) ) \ printf message; #else /* !DEBUGGING */ # define Debug(mask, message) \ ; #endif /* DEBUGGING */ #define MkLower(ch) (isupper(ch) ? tolower(ch) : ch) #define MkUpper(ch) (islower(ch) ? toupper(ch) : ch) #define Set_LineNum(ln) {Debug(DPARS|DEXT,("linenum=%d\n",ln)); \ LineNumber = ln; \ } typedef int time_min; /* Log levels */ #define CRON_LOG_JOBSTART 0x01 #define CRON_LOG_JOBEND 0x02 #define CRON_LOG_JOBFAILED 0x04 #define CRON_LOG_JOBPID 0x08 #define FIRST_MINUTE 0 #define LAST_MINUTE 59 #define MINUTE_COUNT (LAST_MINUTE - FIRST_MINUTE + 1) #define FIRST_HOUR 0 #define LAST_HOUR 23 #define HOUR_COUNT (LAST_HOUR - FIRST_HOUR + 1) #define FIRST_DOM 1 #define LAST_DOM 31 #define DOM_COUNT (LAST_DOM - FIRST_DOM + 1) #define FIRST_MONTH 1 #define LAST_MONTH 12 #define MONTH_COUNT (LAST_MONTH - FIRST_MONTH + 1) /* note on DOW: 0 and 7 are both Sunday, for compatibility reasons. */ #define FIRST_DOW 0 #define LAST_DOW 7 #define DOW_COUNT (LAST_DOW - FIRST_DOW + 1) /* each user's crontab will be held as a list of * the following structure. * * These are the cron commands. */ typedef struct _entry { struct _entry *next; uid_t uid; gid_t gid; char **envp; int secondsInterval; bitstr_t bit_decl(minute, MINUTE_COUNT); bitstr_t bit_decl(hour, HOUR_COUNT); bitstr_t bit_decl(dom, DOM_COUNT); bitstr_t bit_decl(month, MONTH_COUNT); bitstr_t bit_decl(dow, DOW_COUNT); int flags; #define DOM_STAR 0x01 #define DOW_STAR 0x02 #define WHEN_REBOOT 0x04 #define MIN_STAR 0x08 #define HR_STAR 0x10 #define DOM_LAST 0x20 } entry; /* the crontab database will be a list of the * following structure, one element per user * plus one for the system. * * These are the crontabs. */ typedef struct _user { struct _user *next, *prev; /* links */ char *name; time_t mtime; /* last modtime of crontab */ entry *crontab; /* this person's crontab */ #ifdef WITH_SELINUX security_context_t scontext; /* SELinux security context */ #endif } user; typedef struct _cron_db { user *head, *tail; /* links */ time_t user_mtime; /* last modtime on spooldir */ time_t sys_mtime; /* last modtime on system crontab */ #ifdef DEBIAN time_t sysd_mtime; /* last modtime on system crondir */ #endif } cron_db; typedef struct _orphan { struct _orphan *next; /* link */ char *uname; char *fname; char *tabname; } orphan; /* * Buffer used to mimick getc(FILE*) and ungetc(FILE*) */ #define MAX_FILE_BUFFER_LENGTH 1000 typedef struct _file_buffer { char data[MAX_FILE_BUFFER_LENGTH]; int length; int pointer; char unget_data[MAX_FILE_BUFFER_LENGTH]; int unget_count; } file_buffer; void unget_char(int, FILE *), free_entry(entry *), skip_comments(FILE *); int get_char(FILE *), get_string(char *, int, FILE *, char *); entry * parse_cron_entry(char *); /* in the C tradition, we only create * variables for the main program, just * extern them elsewhere. */ #ifdef MAIN_PROGRAM # if !defined(LINT) && !defined(lint) char *copyright[] = { "@(#) Copyright 1988,1989,1990,1993,1994 by Paul Vixie", "@(#) All rights reserved" }; # endif char *MonthNames[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", NULL }; char *DowNames[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", NULL }; char *ecodes[] = { "no error", "bad minute", "bad hour", "bad day-of-month", "bad month", "bad day-of-week", "bad command", "bad time specifier", "bad username", "command too long", NULL }; char *ProgramName; int LineNumber; time_t StartTime; time_min virtualTime; time_min clockTime; # if DEBUGGING int DebugFlags; char *DebugFlagNames[] = { /* sync with #defines */ "ext", "sch", "proc", "pars", "load", "misc", "test", "bit", NULL /* NULL must be last element */ }; # endif /* DEBUGGING */ #else /*MAIN_PROGRAM*/ extern char *copyright[], *MonthNames[], *DowNames[], *ProgramName; extern int LineNumber; extern time_t StartTime; extern time_min virtualTime; extern time_min clockTime; # if DEBUGGING extern int DebugFlags; extern char *DebugFlagNames[]; # endif /* DEBUGGING */ #endif /*MAIN_PROGRAM*/ pg_cron-1.6.5/include/cron_job.h000066400000000000000000000037161472654652200165650ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * cron_job.h * definition of the relation that holds cron jobs (cron.job). * * Copyright (c) 2016, Citus Data, Inc. * *------------------------------------------------------------------------- */ #ifndef CRON_JOB_H #define CRON_JOB_H /* ---------------- * cron_job definition. * ---------------- */ typedef struct FormData_cron_job { int64 jobId; #ifdef CATALOG_VARLEN text schedule; text command; text nodeName; int nodePort; text database; text userName; bool active; text jobName; #endif } FormData_cron_job; /* ---------------- * Form_cron_jobs corresponds to a pointer to a tuple with * the format of cron_job relation. * ---------------- */ typedef FormData_cron_job *Form_cron_job; /* ---------------- * compiler constants for cron_job * ---------------- */ #define Natts_cron_job 9 #define Anum_cron_job_jobid 1 #define Anum_cron_job_schedule 2 #define Anum_cron_job_command 3 #define Anum_cron_job_nodename 4 #define Anum_cron_job_nodeport 5 #define Anum_cron_job_database 6 #define Anum_cron_job_username 7 #define Anum_cron_job_active 8 #define Anum_cron_job_jobname 9 typedef struct FormData_job_run_details { int64 jobId; int64 runId; int32 job_pid; #ifdef CATALOG_VARLEN text database; text username; text command; text status; text return_message; timestamptz start_time; timestamptz end_time; #endif } FormData_job_run_details; typedef FormData_job_run_details *Form_job_run_details; #define Natts_job_run_details 10 #define Anum_job_run_details_jobid 1 #define Anum_job_run_details_runid 2 #define Anum_job_run_details_job_pid 3 #define Anum_job_run_details_database 4 #define Anum_job_run_details_username 5 #define Anum_job_run_details_command 6 #define Anum_job_run_details_status 7 #define Anum_job_run_details_return_message 8 #define Anum_job_run_details_start_time 9 #define Anum_job_run_details_end_time 10 #endif /* CRON_JOB_H */ pg_cron-1.6.5/include/job_metadata.h000066400000000000000000000031231472654652200173740ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * job_metadata.h * definition of job metadata functions * * Copyright (c) 2010-2015, Citus Data, Inc. * *------------------------------------------------------------------------- */ #ifndef JOB_METADATA_H #define JOB_METADATA_H #include "nodes/pg_list.h" #if (PG_VERSION_NUM < 120000) #include "datatype/timestamp.h" #endif typedef enum { CRON_STATUS_STARTING, CRON_STATUS_RUNNING, CRON_STATUS_SENDING, CRON_STATUS_CONNECTING, CRON_STATUS_SUCCEEDED, CRON_STATUS_FAILED } CronStatus; /* job metadata data structure */ typedef struct CronJob { int64 jobId; char *scheduleText; entry schedule; char *command; char *nodeName; int nodePort; char *database; char *userName; bool active; char *jobName; } CronJob; /* global settings */ extern char *CronHost; extern bool CronJobCacheValid; extern bool EnableSuperuserJobs; /* functions for retrieving job metadata */ extern void InitializeJobMetadataCache(void); extern void ResetJobMetadataCache(void); extern List * LoadCronJobList(void); extern CronJob * GetCronJob(int64 jobId); extern void InsertJobRunDetail(int64 runId, int64 *jobId, char *database, char *username, char *command, char *status); extern void UpdateJobRunDetail(int64 runId, int32 *job_pid, char *status, char *return_message, TimestampTz *start_time, TimestampTz *end_time); extern int64 NextRunId(void); extern void MarkPendingRunsAsFailed(void); extern char *GetCronStatus(CronStatus cronstatus); extern void InvalidateJobCacheCallback(Datum argument, Oid relationId); #endif pg_cron-1.6.5/include/pathnames.h000066400000000000000000000062251472654652200167500ustar00rootroot00000000000000/* Copyright 1993,1994 by Paul Vixie * All rights reserved * * Distribute freely, except: don't remove my name from the source or * documentation (don't take credit for my work), mark your changes (don't * get me blamed for your possible bugs), don't alter or remove this * notice. May be sold if buildable source is provided to buyer. No * warrantee of any kind, express or implied, is included with this * software; use at your own risk, responsibility for damages (if any) to * anyone resulting from the use of this software rests entirely with the * user. * * Send bug reports, bug fixes, enhancements, requests, flames, etc., and * I'll try to keep a version up to date. I can be reached as follows: * Paul Vixie uunet!decwrl!vixie!paul */ /* * $Id: pathnames.h,v 1.3 1994/01/15 20:43:43 vixie Exp $ */ #ifndef CRONDIR /* CRONDIR is where crond(8) and crontab(1) both chdir * to; SPOOL_DIR, ALLOW_FILE, DENY_FILE, and LOG_FILE * are all relative to this directory. */ #define CRONDIR "/var/spool/cron" #endif /* SPOOLDIR is where the crontabs live. * This directory will have its modtime updated * whenever crontab(1) changes a crontab; this is * the signal for crond(8) to look at each individual * crontab file and reload those whose modtimes are * newer than they were last time around (or which * didn't exist last time around...) */ #define SPOOL_DIR "crontabs" /* undefining these turns off their features. note * that ALLOW_FILE and DENY_FILE must both be defined * in order to enable the allow/deny code. If neither * LOG_FILE or SYSLOG is defined, we don't log. If * both are defined, we log both ways. */ #ifdef DEBIAN #define ALLOW_FILE "/etc/cron.allow" /*-*/ #define DENY_FILE "/etc/cron.deny" /*-*/ #else #define ALLOW_FILE "allow" /*-*/ #define DENY_FILE "deny" /*-*/ #endif /* #define LOG_FILE "log" -*/ /* where should the daemon stick its PID? */ #ifdef _PATH_VARRUN # define PIDDIR _PATH_VARRUN #else # define PIDDIR "/etc/" #endif #define PIDFILE "%scrond.pid" /* 4.3BSD-style crontab */ #define SYSCRONTAB "/etc/crontab" #ifdef DEBIAN /* where package specific crontabs live */ #define SYSCRONDIR "/etc/cron.d" #endif /* what editor to use if no EDITOR or VISUAL * environment variable specified. */ #if defined(DEBIAN) # define EDITOR "/usr/bin/sensible-editor" #elif defined(_PATH_VI) # define EDITOR _PATH_VI #else # define EDITOR "/usr/ucb/vi" #endif #ifndef _PATH_BSHELL # define _PATH_BSHELL "/bin/sh" #endif #ifndef _PATH_DEFPATH # define _PATH_DEFPATH "/usr/bin:/bin" #endif #ifndef _PATH_DEFPATH_ROOT # define _PATH_DEFPATH_ROOT "/usr/sbin:/usr/bin:/sbin:/bin" #endif #ifndef CRONDIR_MODE /* Create mode for CRONDIR; must be in sync with * packaging */ #define CRONDIR_MODE 0755 #endif #ifndef SPOOL_DIR_MODE /* Create mode for SPOOL_DIR; must be in sync with * packaging */ #define SPOOL_DIR_MODE 01730 #endif #ifndef SPOOL_DIR_GROUP /* Chown SPOOL_DIR to this group (needed by Debian's * SGID crontab feature) */ #define SPOOL_DIR_GROUP "crontab" #endif pg_cron-1.6.5/include/pg_cron.h000066400000000000000000000006141472654652200164130ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * pg_cron.h * definition of pg_cron data types * * Copyright (c) 2010-2015, Citus Data, Inc. * *------------------------------------------------------------------------- */ #ifndef PG_CRON_H #define PG_CRON_H /* global settings */ extern char *CronTableDatabaseName; extern bool LaunchActiveJobs; #endif pg_cron-1.6.5/include/task_states.h000066400000000000000000000026751472654652200173220ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * task_states.h * definition of task state functions * * Copyright (c) 2010-2015, Citus Data, Inc. * *------------------------------------------------------------------------- */ #ifndef TASK_STATES_H #define TASK_STATES_H #include "job_metadata.h" #include "libpq-fe.h" #include "postmaster/bgworker.h" #include "storage/dsm.h" #include "storage/shm_mq.h" #include "utils/timestamp.h" typedef enum { CRON_TASK_WAITING = 0, CRON_TASK_START = 1, CRON_TASK_CONNECTING = 2, CRON_TASK_SENDING = 3, CRON_TASK_RUNNING = 4, CRON_TASK_RECEIVING = 5, CRON_TASK_DONE = 6, CRON_TASK_ERROR = 7, CRON_TASK_BGW_START = 8, CRON_TASK_BGW_RUNNING = 9 } CronTaskState; struct BackgroundWorkerHandle { int slot; uint64 generation; }; typedef struct CronTask { int64 jobId; int64 runId; CronTaskState state; uint pendingRunCount; PGconn *connection; PostgresPollingStatusType pollingStatus; TimestampTz startDeadline; TimestampTz lastStartTime; uint32 secondsInterval; bool isSocketReady; bool isActive; char *errorMessage; bool freeErrorMessage; shm_mq_handle *sharedMemoryQueue; dsm_segment *seg; BackgroundWorkerHandle handle; } CronTask; extern void InitializeTaskStateHash(void); extern void RefreshTaskHash(void); extern List * CurrentTaskList(void); extern void InitializeCronTask(CronTask *task, int64 jobId); extern void RemoveTask(int64 jobId); #endif pg_cron-1.6.5/pg_cron--1.0--1.1.sql000066400000000000000000000001451472654652200162620ustar00rootroot00000000000000/* pg_cron--1.0--1.1.sql */ ALTER TABLE cron.job ADD COLUMN active boolean not null default 'true'; pg_cron-1.6.5/pg_cron--1.1--1.2.sql000066400000000000000000000002331472654652200162620ustar00rootroot00000000000000/* pg_cron--1.1--1.2.sql */ SELECT pg_catalog.pg_extension_config_dump('cron.job', ''); SELECT pg_catalog.pg_extension_config_dump('cron.jobid_seq', ''); pg_cron-1.6.5/pg_cron--1.2--1.3.sql000066400000000000000000000026371472654652200162760ustar00rootroot00000000000000/* pg_cron--1.2--1.3.sql */ CREATE SEQUENCE cron.runid_seq; CREATE TABLE cron.job_run_details ( jobid bigint, runid bigint primary key default pg_catalog.nextval('cron.runid_seq'), job_pid integer, database text, username text, command text, status text, return_message text, start_time timestamptz, end_time timestamptz ); GRANT SELECT ON cron.job_run_details TO public; GRANT DELETE ON cron.job_run_details TO public; ALTER TABLE cron.job_run_details ENABLE ROW LEVEL SECURITY; CREATE POLICY cron_job_run_details_policy ON cron.job_run_details USING (username OPERATOR(pg_catalog.=) current_user); SELECT pg_catalog.pg_extension_config_dump('cron.job_run_details', ''); SELECT pg_catalog.pg_extension_config_dump('cron.runid_seq', ''); ALTER TABLE cron.job ADD COLUMN jobname name; CREATE UNIQUE INDEX jobname_username_idx ON cron.job (jobname, username); ALTER TABLE cron.job ADD CONSTRAINT jobname_username_uniq UNIQUE USING INDEX jobname_username_idx; CREATE FUNCTION cron.schedule(job_name name, schedule text, command text) RETURNS bigint LANGUAGE C STRICT AS 'MODULE_PATHNAME', $$cron_schedule_named$$; COMMENT ON FUNCTION cron.schedule(name,text,text) IS 'schedule a pg_cron job'; CREATE FUNCTION cron.unschedule(job_name name) RETURNS bool LANGUAGE C STRICT AS 'MODULE_PATHNAME', $$cron_unschedule_named$$; COMMENT ON FUNCTION cron.unschedule(name) IS 'unschedule a pg_cron job'; pg_cron-1.6.5/pg_cron--1.3--1.4.sql000066400000000000000000000031461472654652200162740ustar00rootroot00000000000000/* pg_cron--1.3--1.4.sql */ /* cron_schedule_named expects job name to be text */ DROP FUNCTION cron.schedule(name,text,text); CREATE FUNCTION cron.schedule(job_name text, schedule text, command text) RETURNS bigint LANGUAGE C AS 'MODULE_PATHNAME', $$cron_schedule_named$$; COMMENT ON FUNCTION cron.schedule(text,text,text) IS 'schedule a pg_cron job'; CREATE FUNCTION cron.alter_job(job_id bigint, schedule text default null, command text default null, database text default null, username text default null, active boolean default null) RETURNS void LANGUAGE C AS 'MODULE_PATHNAME', $$cron_alter_job$$; COMMENT ON FUNCTION cron.alter_job(bigint,text,text,text,text,boolean) IS 'Alter the job identified by job_id. Any option left as NULL will not be modified.'; /* admin should decide whether alter_job is safe by explicitly granting execute */ REVOKE ALL ON FUNCTION cron.alter_job(bigint,text,text,text,text,boolean) FROM public; CREATE FUNCTION cron.schedule_in_database(job_name text, schedule text, command text, database text, username text default null, active boolean default 'true') RETURNS bigint LANGUAGE C AS 'MODULE_PATHNAME', $$cron_schedule_named$$; COMMENT ON FUNCTION cron.schedule_in_database(text,text,text,text,text,boolean) IS 'schedule a pg_cron job'; /* admin should decide whether cron.schedule_in_database is safe by explicitly granting execute */ REVOKE ALL ON FUNCTION cron.schedule_in_database(text,text,text,text,text,boolean) FROM public; pg_cron-1.6.5/pg_cron--1.4--1.4-1.sql000066400000000000000000000005471472654652200164350ustar00rootroot00000000000000/* pg_cron--1.4--1.4-1.sql */ /* * pg_dump will read from these sequences. Grant everyone permission * to read from the sequence. That way, a user with usage on the cron * schema can also do pg_dump. This does not grant write/nextval * permission. */ GRANT SELECT ON SEQUENCE cron.jobid_seq TO public; GRANT SELECT ON SEQUENCE cron.runid_seq TO public; pg_cron-1.6.5/pg_cron--1.4-1--1.5.sql000066400000000000000000000004631472654652200164330ustar00rootroot00000000000000ALTER TABLE cron.job ALTER COLUMN jobname TYPE text; DROP FUNCTION cron.unschedule(name); CREATE FUNCTION cron.unschedule(job_name text) RETURNS bool LANGUAGE C STRICT AS 'MODULE_PATHNAME', $$cron_unschedule_named$$; COMMENT ON FUNCTION cron.unschedule(text) IS 'unschedule a pg_cron job'; pg_cron-1.6.5/pg_cron--1.5--1.6.sql000066400000000000000000000000341472654652200162710ustar00rootroot00000000000000/* no SQL changes in 1.6 */ pg_cron-1.6.5/pg_cron.conf000066400000000000000000000000451472654652200154640ustar00rootroot00000000000000shared_preload_libraries = 'pg_cron' pg_cron-1.6.5/pg_cron.control000066400000000000000000000002151472654652200162160ustar00rootroot00000000000000comment = 'Job scheduler for PostgreSQL' default_version = '1.6' module_pathname = '$libdir/pg_cron' relocatable = false schema = pg_catalog pg_cron-1.6.5/pg_cron.sql000066400000000000000000000043231472654652200153410ustar00rootroot00000000000000DO $$ BEGIN IF pg_catalog.current_database() OPERATOR(pg_catalog.<>) pg_catalog.current_setting('cron.database_name') AND pg_catalog.current_database() OPERATOR(pg_catalog.<>) 'contrib_regression' THEN RAISE EXCEPTION 'can only create extension in database %', pg_catalog.current_setting('cron.database_name') USING DETAIL = 'Jobs must be scheduled from the database configured in 'OPERATOR(pg_catalog.||) 'cron.database_name, since the pg_cron background worker 'OPERATOR(pg_catalog.||) 'reads job descriptions from this database.', HINT = pg_catalog.format('Add cron.database_name = ''%s'' in postgresql.conf 'OPERATOR(pg_catalog.||) 'to use the current database.', pg_catalog.current_database()); END IF; END; $$; CREATE SCHEMA cron; CREATE SEQUENCE cron.jobid_seq; CREATE TABLE cron.job ( jobid bigint primary key default pg_catalog.nextval('cron.jobid_seq'), schedule text not null, command text not null, nodename text not null default 'localhost', nodeport int not null default pg_catalog.inet_server_port(), database text not null default pg_catalog.current_database(), username text not null default current_user ); GRANT SELECT ON cron.job TO public; ALTER TABLE cron.job ENABLE ROW LEVEL SECURITY; CREATE POLICY cron_job_policy ON cron.job USING (username OPERATOR(pg_catalog.=) current_user); CREATE FUNCTION cron.schedule(schedule text, command text) RETURNS bigint LANGUAGE C STRICT AS 'MODULE_PATHNAME', $$cron_schedule$$; COMMENT ON FUNCTION cron.schedule(text,text) IS 'schedule a pg_cron job'; CREATE FUNCTION cron.unschedule(job_id bigint) RETURNS bool LANGUAGE C STRICT AS 'MODULE_PATHNAME', $$cron_unschedule$$; COMMENT ON FUNCTION cron.unschedule(bigint) IS 'unschedule a pg_cron job'; CREATE FUNCTION cron.job_cache_invalidate() RETURNS trigger LANGUAGE C AS 'MODULE_PATHNAME', $$cron_job_cache_invalidate$$; COMMENT ON FUNCTION cron.job_cache_invalidate() IS 'invalidate job cache'; CREATE TRIGGER cron_job_cache_invalidate AFTER INSERT OR UPDATE OR DELETE OR TRUNCATE ON cron.job FOR STATEMENT EXECUTE PROCEDURE cron.job_cache_invalidate(); pg_cron-1.6.5/sql/000077500000000000000000000000001472654652200137665ustar00rootroot00000000000000pg_cron-1.6.5/sql/pg_cron-test.sql000066400000000000000000000143761472654652200171260ustar00rootroot00000000000000CREATE EXTENSION pg_cron VERSION '1.0'; SELECT extversion FROM pg_extension WHERE extname='pg_cron'; -- Test binary compatibility with v1.4 function signature. ALTER EXTENSION pg_cron UPDATE TO '1.4'; SELECT cron.unschedule(job_name := 'no_such_job'); SELECT cron.schedule('testjob', '* * * * *', 'SELECT 1'); SELECT cron.unschedule('testjob'); -- Test cache invalidation DROP EXTENSION pg_cron; CREATE EXTENSION pg_cron VERSION '1.4'; ALTER EXTENSION pg_cron UPDATE; -- Vacuum every day at 10:00am (GMT) SELECT cron.schedule('0 10 * * *', 'VACUUM'); -- Stop scheduling a job SELECT cron.unschedule(1); -- Invalid input: input too long SELECT cron.schedule(repeat('a', 1000), ''); -- Invalid input: missing parts SELECT cron.schedule('* * * *', 'SELECT 1'); -- Invalid input: trailing characters SELECT cron.schedule('5 secondc', 'SELECT 1'); SELECT cron.schedule('50 seconds c', 'SELECT 1'); -- Invalid input: step out of range SELECT cron.schedule('*/0 10 * * *', 'SELECT 1'); SELECT cron.schedule('-1 * * * *', 'SELECT 1'); SELECT cron.schedule('*/-1 10 * * *', 'SELECT 1'); SELECT cron.schedule('*/60 10 * * *', 'SELECT 1'); SELECT cron.schedule('* * * 13 *', 'SELECT 1'); SELECT cron.schedule('* * * 0 *', 'SELECT 1'); SELECT cron.schedule('*/5000000000 10 * * *', 'SELECT 1'); -- Invalid input: seconds out of range SELECT cron.schedule('-1 seconds', 'SELECT 1'); SELECT cron.schedule('0 seconds', 'SELECT 1'); SELECT cron.schedule('60 seconds', 'SELECT 1'); SELECT cron.schedule('10000000000 seconds', 'SELECT 1'); -- Try to update pg_cron on restart SELECT cron.schedule('@restar', 'ALTER EXTENSION pg_cron UPDATE'); SELECT cron.schedule('@restart', 'ALTER EXTENSION pg_cron UPDATE'); -- Vacuum every day at 10:00am (GMT) SELECT cron.schedule('myvacuum', '0 10 * * *', 'VACUUM'); SELECT jobid, jobname, schedule, command FROM cron.job ORDER BY jobid; -- Make that 11:00am (GMT) SELECT cron.schedule('myvacuum', '0 11 * * *', 'VACUUM'); SELECT jobid, jobname, schedule, command FROM cron.job ORDER BY jobid; -- Make that VACUUM FULL SELECT cron.schedule('myvacuum', '0 11 * * *', 'VACUUM FULL'); SELECT jobid, jobname, schedule, command FROM cron.job ORDER BY jobid; -- Stop scheduling a job SELECT cron.unschedule('myvacuum'); SELECT jobid, jobname, schedule, command FROM cron.job ORDER BY jobid; -- Testing version >= 1.4 new APIs -- First as superuser -- Update a job without one job attribute to change SELECT cron.alter_job(2); -- Update to a non existing database select cron.alter_job(job_id:=2,database:='hopedoesnotexist'); -- Create a database that does not allow connection create database pgcron_dbno; revoke connect on database pgcron_dbno from public; -- create a test user create user pgcron_cront with password 'pwd'; GRANT USAGE ON SCHEMA cron TO pgcron_cront; -- Schedule a job for this user on the database that does not accept connections SELECT cron.schedule_in_database(job_name:='can not connect', schedule:='0 11 * * *', command:='VACUUM',database:='pgcron_dbno',username:='pgcron_cront'); -- Create a database that does allow connections create database pgcron_dbyes; -- Schedule a job on the database that does accept connections for a non existing user SELECT cron.schedule_in_database(job_name:='user does not exist', schedule:='0 11 * * *', command:='VACUUM',database:='pgcron_dbyes',username:='pgcron_useraqwxszedc'); -- Alter an existing job on a database that does not accept connections SELECT cron.alter_job(job_id:=2,database:='pgcron_dbno',username:='pgcron_cront'); -- Make sure pgcron_cront can execute alter_job GRANT EXECUTE ON FUNCTION cron.alter_job(bigint,text,text,text,text,boolean) TO public; -- Second as non superuser SET SESSION AUTHORIZATION pgcron_cront; -- Create a job SELECT cron.schedule('My vacuum', '0 11 * * *', 'VACUUM'); -- Create a job for another user SELECT cron.schedule_in_database(job_name:='his vacuum', schedule:='0 11 * * *', command:='VACUUM',database:=current_database(),username:='anotheruser'); -- Change the username of an existing job that the user own select cron.alter_job(job_id:=6,username:='anotheruser'); -- Update a job that the user does not own select cron.alter_job(job_id:=2,database:='pgcron_dbyes'); -- change the database for a job that the user own and can connect to select cron.alter_job(job_id:=6,database:='pgcron_dbyes'); SELECT database FROM cron.job; -- change the database for a job that the user own but can not connect to select cron.alter_job(job_id:=6,database:='pgcron_dbno'); SELECT database FROM cron.job; -- back to superuser RESET SESSION AUTHORIZATION; -- Change the username of an existing job select cron.alter_job(job_id:=2,username:='pgcron_cront'); SELECT username FROM cron.job where jobid=2; -- Create a job for another user SELECT cron.schedule_in_database(job_name:='his vacuum', schedule:='0 11 * * *', command:='VACUUM',database:=current_database(), username:='pgcron_cront'); SELECT username FROM cron.job where jobid=7; -- Override function DROP EXTENSION IF EXISTS pg_cron cascade; CREATE TABLE test (data text); DROP TYPE IF EXISTS current_setting cascade; CREATE TYPE current_setting AS ENUM ('cron.database_name'); CREATE OR REPLACE FUNCTION public.func1(text, current_setting) RETURNS text LANGUAGE sql volatile AS 'INSERT INTO test(data) VALUES (current_user); SELECT current_database()::text;'; CREATE OR REPLACE FUNCTION public.func1(current_setting) RETURNS text LANGUAGE sql volatile AS 'INSERT INTO test(data) VALUES (current_user); SELECT current_database()::text;'; CREATE CAST (current_setting AS text) WITH FUNCTION public.func1(current_setting) AS IMPLICIT; CREATE EXTENSION pg_cron; select * from public.test; -- valid interval jobs SELECT cron.schedule('1 second', 'SELECT 1'); SELECT cron.schedule(' 30 sEcOnDs ', 'SELECT 1'); SELECT cron.schedule('59 seconds', 'SELECT 1'); SELECT cron.schedule('17 seconds ', 'SELECT 1'); SELECT jobid, jobname, schedule, command FROM cron.job ORDER BY jobid; -- valid last of day job SELECT cron.schedule('last-day-of-month-job1', '0 11 $ * *', 'SELECT 1'); SELECT jobid, jobname, schedule, command FROM cron.job ORDER BY jobid; -- invalid last of day job SELECT cron.schedule('bad-last-dom-job1', '0 11 $foo * *', 'VACUUM FULL'); -- cleaning DROP EXTENSION pg_cron; drop user pgcron_cront; drop database pgcron_dbno; drop database pgcron_dbyes; pg_cron-1.6.5/src/000077500000000000000000000000001472654652200137565ustar00rootroot00000000000000pg_cron-1.6.5/src/entry.c000066400000000000000000000316401472654652200152670ustar00rootroot00000000000000/* Copyright 1988,1990,1993,1994 by Paul Vixie * All rights reserved * * Distribute freely, except: don't remove my name from the source or * documentation (don't take credit for my work), mark your changes (don't * get me blamed for your possible bugs), don't alter or remove this * notice. May be sold if buildable source is provided to buyer. No * warrantee of any kind, express or implied, is included with this * software; use at your own risk, responsibility for damages (if any) to * anyone resulting from the use of this software rests entirely with the * user. * * Send bug reports, bug fixes, enhancements, requests, flames, etc., and * I'll try to keep a version up to date. I can be reached as follows: * Paul Vixie uunet!decwrl!vixie!paul */ /* marco 04sep16 [integrated into pg_cron] * vix 26jan87 [RCS'd; rest of log is in RCS file] * vix 01jan87 [added line-level error recovery] * vix 31dec86 [added /step to the from-to range, per bob@acornrc] * vix 30dec86 [written] */ #include "postgres.h" #include "stdlib.h" #include "string.h" #include "cron.h" typedef enum ecode { e_none, e_minute, e_hour, e_dom, e_month, e_dow, e_cmd, e_timespec, e_username, e_cmd_len } ecode_e; static int get_list(bitstr_t *, int, int, char *[], int, FILE *), get_range(bitstr_t *, int, int, char *[], int, FILE *), get_number(int *, int, char *[], int, FILE *); static int set_element(bitstr_t *, int, int, int), set_range(bitstr_t *, int, int, int, int, int); void free_entry(entry *e) { free(e); } /* return NULL if eof or syntax error occurs; * otherwise return a pointer to a new entry. * * Note: This function is a modified version of load_entry in Vixie * cron. It only parses the schedule part of a cron entry and uses * an in-memry buffer. */ entry * parse_cron_entry(char *schedule) { /* this function reads one crontab entry -- the next -- from a file. * it skips any leading blank lines, ignores comments, and returns * EOF if for any reason the entry can't be read and parsed. * * the entry is also parsed here. * * syntax: * user crontab: * minutes hours doms months dows cmd\n * system crontab (/etc/crontab): * minutes hours doms months dows USERNAME cmd\n */ ecode_e ecode = e_none; entry *e = (entry *) calloc(sizeof(entry), sizeof(char)); int ch = 0; char cmd[MAX_COMMAND]; file_buffer buffer = {{},0,0,{},0}; FILE *file = (FILE *) &buffer; int scheduleLength = strlen(schedule); if (scheduleLength >= MAX_FILE_BUFFER_LENGTH) { ch = EOF; ecode = e_cmd_len; goto eof; } strcpy(buffer.data, schedule); buffer.length = scheduleLength; buffer.pointer = 0; Debug(DPARS, ("load_entry()...about to eat comments\n")) skip_comments(file); ch = get_char(file); if (ch == EOF) { free_entry(e); return NULL; } /* ch is now the first useful character of a useful line. * it may be an @special or it may be the first character * of a list of minutes. */ if (ch == '@') { /* all of these should be flagged and load-limited; i.e., * instead of @hourly meaning "0 * * * *" it should mean * "close to the front of every hour but not 'til the * system load is low". Problems are: how do you know * what "low" means? (save me from /etc/cron.conf!) and: * how to guarantee low variance (how low is low?), which * means how to we run roughly every hour -- seems like * we need to keep a history or let the first hour set * the schedule, which means we aren't load-limited * anymore. too much for my overloaded brain. (vix, jan90) * HINT */ ch = get_string(cmd, MAX_COMMAND, file, " \t\n"); if (!strcmp("reboot", cmd) || !strcmp("restart", cmd)) { e->flags |= WHEN_REBOOT; } else if (!strcmp("yearly", cmd) || !strcmp("annually", cmd)){ set_element(e->minute, FIRST_MINUTE, LAST_MINUTE, FIRST_MINUTE); set_element(e->hour, FIRST_HOUR, LAST_HOUR, FIRST_HOUR); set_element(e->dom, FIRST_DOM, LAST_DOM, FIRST_DOM); set_element(e->month, FIRST_MONTH, LAST_MONTH, FIRST_MINUTE); set_range(e->dow, FIRST_DOW, LAST_DOW, FIRST_DOW, LAST_DOW, 1); e->flags |= DOW_STAR; } else if (!strcmp("monthly", cmd)) { set_element(e->minute, FIRST_MINUTE, LAST_MINUTE, FIRST_MINUTE); set_element(e->hour, FIRST_HOUR, LAST_HOUR, FIRST_HOUR); set_element(e->dom, FIRST_DOM, LAST_DOM, FIRST_DOM); set_range(e->month, FIRST_MONTH, LAST_MONTH, FIRST_MONTH, LAST_MONTH, 1); set_range(e->dow, FIRST_DOW, LAST_DOW, FIRST_DOW, LAST_DOW, 1); e->flags |= DOW_STAR; } else if (!strcmp("weekly", cmd)) { set_element(e->minute, FIRST_MINUTE, LAST_MINUTE, FIRST_MINUTE); set_element(e->hour, FIRST_HOUR, LAST_HOUR, FIRST_HOUR); set_range(e->dom, FIRST_DOM, LAST_DOM, FIRST_DOM, LAST_DOM, 1); set_range(e->month, FIRST_MONTH, LAST_MONTH, FIRST_MONTH, LAST_MONTH, 1); set_element(e->dow, FIRST_DOW, LAST_DOW, FIRST_DOW); e->flags |= DOW_STAR; } else if (!strcmp("daily", cmd) || !strcmp("midnight", cmd)) { set_element(e->minute, FIRST_MINUTE, LAST_MINUTE, FIRST_MINUTE); set_element(e->hour, FIRST_HOUR, LAST_HOUR, FIRST_HOUR); set_range(e->dom, FIRST_DOM, LAST_DOM, FIRST_DOM, LAST_DOM, 1); set_range(e->month, FIRST_MONTH, LAST_MONTH, FIRST_MONTH, LAST_MONTH, 1); set_range(e->dow, FIRST_DOW, LAST_DOW, FIRST_DOW, LAST_DOW, 1); } else if (!strcmp("hourly", cmd)) { set_element(e->minute, FIRST_MINUTE, LAST_MINUTE, FIRST_MINUTE); set_range(e->hour, FIRST_HOUR, LAST_HOUR, FIRST_HOUR, LAST_HOUR, 1); set_range(e->dom, FIRST_DOM, LAST_DOM, FIRST_DOM, LAST_DOM, 1); set_range(e->month, FIRST_MONTH, LAST_MONTH, FIRST_MONTH, LAST_MONTH, 1); set_range(e->dow, FIRST_DOW, LAST_DOW, FIRST_DOW, LAST_DOW, 1); e->flags |= HR_STAR; } else { ecode = e_timespec; goto eof; } } else { Debug(DPARS, ("load_entry()...about to parse numerics\n")) if (ch == '*') e->flags |= MIN_STAR; ch = get_list(e->minute, FIRST_MINUTE, LAST_MINUTE, PPC_NULL, ch, file); if (ch == EOF) { ecode = e_minute; goto eof; } /* hours */ if (ch == '*') e->flags |= HR_STAR; ch = get_list(e->hour, FIRST_HOUR, LAST_HOUR, PPC_NULL, ch, file); if (ch == EOF) { ecode = e_hour; goto eof; } /* DOM (days of month) */ if (ch == '$') { ch = get_char(file); if (!Is_Blank(ch)) { ecode = e_dom; goto eof; } Skip_Blanks(ch, file); e->flags |= DOM_LAST; } else { if (ch == '*') e->flags |= DOM_STAR; ch = get_list(e->dom, FIRST_DOM, LAST_DOM, PPC_NULL, ch, file); } if (ch == EOF) { ecode = e_dom; goto eof; } /* month */ ch = get_list(e->month, FIRST_MONTH, LAST_MONTH, MonthNames, ch, file); if (ch == EOF) { ecode = e_month; goto eof; } /* DOW (days of week) */ if (ch == '*') e->flags |= DOW_STAR; ch = get_list(e->dow, FIRST_DOW, LAST_DOW, DowNames, ch, file); if (ch == EOF) { ecode = e_month; goto eof; } } /* make sundays equivalent */ if (bit_test(e->dow, 0) || bit_test(e->dow, 7)) { bit_set(e->dow, 0); bit_set(e->dow, 7); } /* success, fini, return pointer to the entry we just created... */ return e; eof: elog(DEBUG1, "failed to parse entry %d", ecode); free_entry(e); while (ch != EOF && ch != '\n') ch = get_char(file); return NULL; } static int get_list(bitstr_t *bits, int low, int high, char *names[], int ch, FILE *file) /* bitstr_t *bits; */ /* one bit per flag, default=FALSE */ /* int low, high; */ /* bounds, impl. offset for bitstr */ /* char *names[]; */ /* NULL or *[] of names for these elements */ /* int ch; */ /* current character being processed */ /* FILE *file; */ /* file being read */ { register int done; /* we know that we point to a non-blank character here; * must do a Skip_Blanks before we exit, so that the * next call (or the code that picks up the cmd) can * assume the same thing. */ Debug(DPARS|DEXT, ("get_list()...entered\n")) /* list = range {"," range} */ /* clear the bit string, since the default is 'off'. */ bit_nclear(bits, 0, (high-low)); /* process all ranges */ done = FALSE; while (!done) { ch = get_range(bits, low, high, names, ch, file); if (ch == ',') ch = get_char(file); else done = TRUE; } /* exiting. skip to some blanks, then skip over the blanks. */ Skip_Nonblanks(ch, file) Skip_Blanks(ch, file) Debug(DPARS|DEXT, ("get_list()...exiting w/ %02x\n", ch)) return ch; } static int get_range(bitstr_t *bits, int low, int high, char *names[], int ch, FILE *file) /* bitstr_t *bits; one bit per flag, default=FALSE */ /* int low, high; bounds, impl. offset for bitstr */ /* char *names[]; NULL or names of elements */ /* int ch; current character being processed */ /* FILE *file; file being read */ { /* range = number | number "-" number [ "/" number ] */ auto int num1, num2, num3; Debug(DPARS|DEXT, ("get_range()...entering, exit won't show\n")) if (ch == '*') { /* '*' means "first-last" but can still be modified by /step */ num1 = low; num2 = high; ch = get_char(file); if (ch == EOF) return EOF; } else { if (EOF == (ch = get_number(&num1, low, names, ch, file))) return EOF; if (ch != '-') { /* not a range, it's a single number. */ /* Unsupported syntax: Step specified without range, eg: 1/20 * * * * /bin/echo "this fails" */ if (ch == '/') return EOF; if (EOF == set_element(bits, low, high, num1)) return EOF; return ch; } else { /* eat the dash */ ch = get_char(file); if (ch == EOF) return EOF; /* get the number following the dash */ ch = get_number(&num2, low, names, ch, file); if (ch == EOF) return EOF; } } /* check for step size */ if (ch == '/') { /* eat the slash */ ch = get_char(file); if (ch == EOF) return EOF; /* get the step size -- note: we don't pass the * names here, because the number is not an * element id, it's a step size. 'low' is * sent as a 0 since there is no offset either. */ ch = get_number(&num3, 0, PPC_NULL, ch, file); if (ch == EOF || num3 <= 0) return EOF; } else { /* no step. default==1. */ num3 = 1; } /* Explicitly check for sane values. Certain combinations of ranges and * steps which should return EOF don't get picked up by the code below, * eg: * 5-64/30 * * * * touch /dev/null * * Code adapted from set_elements() where this error was probably intended * to be catched. */ if (num1 < low || num1 > high || num2 < low || num2 > high || num3 < 0 || num3 > high) return EOF; /* range. set all elements from num1 to num2, stepping * by num3. (the step is a downward-compatible extension * proposed conceptually by bob@acornrc, syntactically * designed then implemented by paul vixie). */ if (EOF == set_range(bits, low, high, num1, num2, num3)) { unget_char(ch, file); return EOF; } return ch; } static int get_number(int *numptr, int low, char *names[], int ch, FILE *file) /* int *numptr; where does the result go? */ /* int low;/ offset applied to result if symbolic enum used */ /* char *names[]; symbolic names, if any, for enums */ /* int ch; current character */ /* FILE *file; source */ { char temp[MAX_TEMPSTR], *pc; int len, i, all_digits; /* collect alphanumerics into our fixed-size temp array */ pc = temp; len = 0; /* keep compiler quiet */ *numptr = 0; all_digits = TRUE; while (isalnum(ch)) { if (++len >= MAX_TEMPSTR) return EOF; *pc++ = ch; if (!isdigit(ch)) all_digits = FALSE; ch = get_char(file); } *pc = '\0'; if (len == 0) { return EOF; } /* try to find the name in the name list */ if (names) { for (i = 0; names[i] != NULL; i++) { Debug(DPARS|DEXT, ("get_num, compare(%s,%s)\n", names[i], temp)) if (!strcasecmp(names[i], temp)) { *numptr = i+low; return ch; } } } /* no name list specified, or there is one and our string isn't * in it. either way: if it's all digits, use its magnitude. * otherwise, it's an error. */ if (all_digits) { *numptr = atoi(temp); return ch; } return EOF; } static int set_element(bitstr_t *bits, int low, int high, int number) /* bitstr_t *bits; one bit per flag, default=FALSE */ /* int low; */ /* int high; */ /* int number; */ { Debug(DPARS|DEXT, ("set_element(?,%d,%d,%d)\n", low, high, number)) if (number < low || number > high) return EOF; number -= low; bit_set(bits, number); return OK; } static int set_range(bitstr_t *bits, int low, int high, int start, int stop, int step) { Debug(DPARS|DEXT, ("set_range(?,%d,%d,%d,%d,%d)\n", low, high, start, stop, step)) if (start < low || stop > high || step <= 0) return EOF; start -= low; stop -= low; if (step == 1) { bit_nset(bits, start, stop); } else { for (int i = start; i <= stop; i += step) bit_set(bits, i); } return OK; } pg_cron-1.6.5/src/job_metadata.c000066400000000000000000001140451472654652200165410ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * src/job_metadata.c * * Functions for reading and manipulating pg_cron metadata. * * Copyright (c) 2016, Citus Data, Inc. * *------------------------------------------------------------------------- */ #include "postgres.h" #include "fmgr.h" #include "miscadmin.h" #include "cron.h" #include "pg_cron.h" #include "job_metadata.h" #include "cron_job.h" #include "access/genam.h" #include "access/hash.h" #include "access/heapam.h" #include "access/htup_details.h" #include "access/skey.h" #include "access/xact.h" #include "access/xlog.h" #include "catalog/pg_extension.h" #if (PG_VERSION_NUM >= 160000) #include "catalog/pg_database.h" #endif #include "catalog/indexing.h" #include "catalog/namespace.h" #include "commands/extension.h" #include "commands/sequence.h" #include "commands/trigger.h" #include "postmaster/postmaster.h" #include "pgstat.h" #include "storage/lock.h" #include "utils/acl.h" #include "utils/builtins.h" #include "utils/fmgroids.h" #include "utils/formatting.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/rel.h" #include "utils/relcache.h" #include "utils/snapmgr.h" #include "utils/syscache.h" #if (PG_VERSION_NUM >= 100000) #include "utils/varlena.h" #endif #include "executor/spi.h" #include "catalog/pg_type.h" #include "commands/dbcommands.h" #include "catalog/pg_authid.h" #if (PG_VERSION_NUM < 120000) #define table_open(r, l) heap_open(r, l) #define table_close(r, l) heap_close(r, l) #endif #define EXTENSION_NAME "pg_cron" #define CRON_SCHEMA_NAME "cron" #define JOBS_TABLE_NAME "job" #define JOB_ID_INDEX_NAME "job_pkey" #define JOB_ID_SEQUENCE_NAME "cron.jobid_seq" #define JOB_RUN_DETAILS_TABLE_NAME "job_run_details" #define RUN_ID_SEQUENCE_NAME "cron.runid_seq" /* forward declarations */ static HTAB * CreateCronJobHash(void); static int64 ScheduleCronJob(text *scheduleText, text *commandText, text *databaseText, text *usernameText, bool active, text *jobnameText); static Oid CronExtensionOwner(void); static void EnsureDeletePermission(Relation cronJobsTable, HeapTuple heapTuple); static void InvalidateJobCache(void); static Oid CronJobRelationId(void); static CronJob * TupleToCronJob(TupleDesc tupleDescriptor, HeapTuple heapTuple); static bool PgCronHasBeenLoaded(void); static bool JobRunDetailsTableExists(void); static bool JobTableExists(void); static void AlterJob(int64 jobId, text *scheduleText, text *commandText, text *databaseText, text *usernameText, bool *active); static Oid GetRoleOidIfCanLogin(char *username); static entry * ParseSchedule(char *scheduleText); static bool TryParseInterval(char *scheduleText, uint32 *secondsInterval); /* SQL-callable functions */ PG_FUNCTION_INFO_V1(cron_schedule); PG_FUNCTION_INFO_V1(cron_schedule_named); PG_FUNCTION_INFO_V1(cron_unschedule); PG_FUNCTION_INFO_V1(cron_unschedule_named); PG_FUNCTION_INFO_V1(cron_job_cache_invalidate); PG_FUNCTION_INFO_V1(cron_alter_job); /* global variables */ static MemoryContext CronJobContext = NULL; static HTAB *CronJobHash = NULL; static Oid CachedCronJobRelationId = InvalidOid; bool CronJobCacheValid = false; char *CronHost = "localhost"; bool EnableSuperuserJobs = true; /* * InitializeJobMetadataCache initializes the data structures for caching * job metadata. */ void InitializeJobMetadataCache(void) { CronJobContext = AllocSetContextCreate(CurrentMemoryContext, "pg_cron job context", ALLOCSET_DEFAULT_MINSIZE, ALLOCSET_DEFAULT_INITSIZE, ALLOCSET_DEFAULT_MAXSIZE); CronJobHash = CreateCronJobHash(); } /* * ResetJobMetadataCache resets the job metadata cache to its initial * state. */ void ResetJobMetadataCache(void) { MemoryContextReset(CronJobContext); CronJobHash = CreateCronJobHash(); } /* * CreateCronJobHash creates the hash for caching job metadata. */ static HTAB * CreateCronJobHash(void) { HTAB *taskHash = NULL; HASHCTL info; int hashFlags = 0; memset(&info, 0, sizeof(info)); info.keysize = sizeof(int64); info.entrysize = sizeof(CronJob); info.hash = tag_hash; info.hcxt = CronJobContext; hashFlags = (HASH_ELEM | HASH_FUNCTION | HASH_CONTEXT); taskHash = hash_create("pg_cron jobs", 32, &info, hashFlags); return taskHash; } /* * GetCronJob gets the cron job with the given id. */ CronJob * GetCronJob(int64 jobId) { CronJob *job = NULL; int64 hashKey = jobId; bool isPresent = false; job = hash_search(CronJobHash, &hashKey, HASH_FIND, &isPresent); return job; } /* * ScheduleCronJob schedules a cron job with the given name. */ static int64 ScheduleCronJob(text *scheduleText, text *commandText, text *databaseText, text *usernameText, bool active, text *jobnameText) { entry *parsedSchedule = NULL; char *schedule; char *command; char *database_name; char *jobName; char *username; AclResult aclresult; Oid userIdcheckacl; int64 jobId = 0; Datum jobIdDatum = 0; StringInfoData querybuf; Oid argTypes[8]; Datum argValues[8]; int argCount = 0; Oid savedUserId = InvalidOid; int savedSecurityContext = 0; TupleDesc returnedRowDescriptor = NULL; HeapTuple returnedRow = NULL; bool returnedJobIdIsNull = false; Oid userId = GetUserId(); userIdcheckacl = GetUserId(); username = GetUserNameFromId(userId, false); /* check schedule is valid */ schedule = text_to_cstring(scheduleText); parsedSchedule = ParseSchedule(schedule); if (parsedSchedule == NULL) { ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid schedule: %s", schedule), errhint("Use cron format (e.g. 5 4 * * *), or interval " "format '[1-59] seconds'"))); } free_entry(parsedSchedule); initStringInfo(&querybuf); appendStringInfo(&querybuf, "insert into %s (schedule, command, nodename, nodeport, database, username, active", quote_qualified_identifier(CRON_SCHEMA_NAME, JOBS_TABLE_NAME)); if (jobnameText != NULL) { appendStringInfo(&querybuf, ", jobname"); } appendStringInfo(&querybuf, ") values ($1, $2, $3, $4, $5, $6, $7"); if (jobnameText != NULL) { appendStringInfo(&querybuf, ", $8) "); appendStringInfo(&querybuf, "on conflict on constraint jobname_username_uniq "); appendStringInfo(&querybuf, "do update set "); appendStringInfo(&querybuf, "schedule = EXCLUDED.schedule, "); appendStringInfo(&querybuf, "command = EXCLUDED.command, "); appendStringInfo(&querybuf, "database = EXCLUDED.database"); } else { appendStringInfo(&querybuf, ")"); } appendStringInfo(&querybuf, " returning jobid"); argTypes[0] = TEXTOID; argValues[0] = CStringGetTextDatum(schedule); argCount++; argTypes[1] = TEXTOID; command = text_to_cstring(commandText); argValues[1] = CStringGetTextDatum(command); argCount++; argTypes[2] = TEXTOID; argValues[2] = CStringGetTextDatum(CronHost); argCount++; argTypes[3] = INT4OID; argValues[3] = Int32GetDatum(PostPortNumber); argCount++; /* username has been provided */ if (usernameText != NULL) { if (!superuser()) elog(ERROR, "must be superuser to create a job for another role"); username = text_to_cstring(usernameText); userIdcheckacl = GetRoleOidIfCanLogin(username); } /* database has been provided */ if (databaseText != NULL) database_name = text_to_cstring(databaseText); else /* use the GUC */ database_name = CronTableDatabaseName; /* first do a crude check to see whether superuser jobs are allowed */ if (!EnableSuperuserJobs && superuser_arg(userIdcheckacl)) { ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("cannot schedule jobs as superuser"), errdetail("Scheduling jobs as superuser is disallowed when " "cron.enable_superuser_jobs is set to off."))); } /* ensure the user that is used in the job can connect to the database */ #if (PG_VERSION_NUM >= 160000) aclresult = object_aclcheck(DatabaseRelationId, get_database_oid(database_name, false), userIdcheckacl, ACL_CONNECT); #else aclresult = pg_database_aclcheck(get_database_oid(database_name, false), userIdcheckacl, ACL_CONNECT); #endif if (aclresult != ACLCHECK_OK) elog(ERROR, "User %s does not have CONNECT privilege on %s", GetUserNameFromId(userIdcheckacl, false), database_name); argTypes[4] = TEXTOID; argValues[4] = CStringGetTextDatum(database_name); argCount++; argTypes[5] = TEXTOID; argValues[5] = CStringGetTextDatum(username); argCount++; argTypes[6] = BOOLOID; argValues[6] = BoolGetDatum(active); argCount++; if (jobnameText != NULL) { argTypes[7] = TEXTOID; jobName = text_to_cstring(jobnameText); argValues[7] = CStringGetTextDatum(jobName); argCount++; } GetUserIdAndSecContext(&savedUserId, &savedSecurityContext); SetUserIdAndSecContext(CronExtensionOwner(), SECURITY_LOCAL_USERID_CHANGE); /* Open SPI context. */ if (SPI_connect() != SPI_OK_CONNECT) { elog(ERROR, "SPI_connect failed"); } if (SPI_execute_with_args(querybuf.data, argCount, argTypes, argValues, NULL, false, 1) != SPI_OK_INSERT_RETURNING) { elog(ERROR, "SPI_exec failed: %s", querybuf.data); } if (SPI_processed <= 0) { elog(ERROR, "query did not return any rows: %s", querybuf.data); } returnedRowDescriptor = SPI_tuptable->tupdesc; returnedRow = SPI_tuptable->vals[0]; jobIdDatum = SPI_getbinval(returnedRow, returnedRowDescriptor, 1, &returnedJobIdIsNull); jobId = DatumGetInt64(jobIdDatum); pfree(querybuf.data); SPI_finish(); SetUserIdAndSecContext(savedUserId, savedSecurityContext); InvalidateJobCache(); return jobId; } /* * GetRoleOidIfCanLogin * Checks user exist and can log in */ static Oid GetRoleOidIfCanLogin(char *username) { HeapTuple roletup; Form_pg_authid rform; Oid roleOid = InvalidOid; roletup = SearchSysCache1(AUTHNAME, PointerGetDatum(username)); if (!HeapTupleIsValid(roletup)) ereport(ERROR, (errmsg("role \"%s\" does not exist", username))); rform = (Form_pg_authid) GETSTRUCT(roletup); if (!rform->rolcanlogin) ereport(ERROR, (errmsg("role \"%s\" can not log in", username), errdetail("Jobs may only be run by roles that have the LOGIN attribute."))); #if (PG_VERSION_NUM < 120000) roleOid = HeapTupleGetOid(roletup); #else roleOid = rform->oid; #endif ReleaseSysCache(roletup); return roleOid; } /* * cron_alter_job alter a job */ Datum cron_alter_job(PG_FUNCTION_ARGS) { int64 jobId; text *scheduleText = NULL; text *commandText = NULL; text *databaseText = NULL; text *usernameText = NULL; bool active; if (PG_ARGISNULL(0)) ereport(ERROR, (errmsg("job_id can not be NULL"))); else jobId = PG_GETARG_INT64(0); if (!PG_ARGISNULL(1)) scheduleText = PG_GETARG_TEXT_P(1); if (!PG_ARGISNULL(2)) commandText = PG_GETARG_TEXT_P(2); if (!PG_ARGISNULL(3)) databaseText = PG_GETARG_TEXT_P(3); if (!PG_ARGISNULL(4)) usernameText = PG_GETARG_TEXT_P(4); if (!PG_ARGISNULL(5)) active = PG_GETARG_BOOL(5); AlterJob(jobId, scheduleText, commandText, databaseText, usernameText, PG_ARGISNULL(5) ? NULL : &active); PG_RETURN_VOID(); } /* * cron_schedule schedule a job */ Datum cron_schedule(PG_FUNCTION_ARGS) { text *scheduleText = NULL; text *commandText = NULL; int64 jobId; if (PG_ARGISNULL(0)) ereport(ERROR, (errmsg("schedule can not be NULL"))); else scheduleText = PG_GETARG_TEXT_P(0); if (PG_ARGISNULL(1)) ereport(ERROR, (errmsg("command can not be NULL"))); else commandText = PG_GETARG_TEXT_P(1); jobId = ScheduleCronJob(scheduleText, commandText, NULL, NULL, true, NULL); PG_RETURN_INT64(jobId); } /* * cron_schedule schedule a named job */ Datum cron_schedule_named(PG_FUNCTION_ARGS) { text *scheduleText = NULL; text *commandText = NULL; text *databaseText = NULL; text *usernameText = NULL; bool active = true; text *jobnameText = NULL; int64 jobId; if (PG_ARGISNULL(0)) ereport(ERROR, (errmsg("job_name can not be NULL"))); else jobnameText = PG_GETARG_TEXT_P(0); if (PG_ARGISNULL(1)) ereport(ERROR, (errmsg("schedule can not be NULL"))); else scheduleText = PG_GETARG_TEXT_P(1); if (PG_ARGISNULL(2)) ereport(ERROR, (errmsg("command can not be NULL"))); else commandText = PG_GETARG_TEXT_P(2); if (PG_NARGS() > 3) { if (!PG_ARGISNULL(3)) databaseText = PG_GETARG_TEXT_P(3); if (!PG_ARGISNULL(4)) usernameText = PG_GETARG_TEXT_P(4); if (!PG_ARGISNULL(5)) active = PG_GETARG_BOOL(5); } jobId = ScheduleCronJob(scheduleText, commandText, databaseText, usernameText, active, jobnameText); PG_RETURN_INT64(jobId); } /* * NextRunId draws a new run ID from cron.runid_seq. */ int64 NextRunId(void) { text *sequenceName = NULL; Oid sequenceId = InvalidOid; List *sequenceNameList = NIL; RangeVar *sequenceVar = NULL; Datum sequenceIdDatum = InvalidOid; Oid savedUserId = InvalidOid; int savedSecurityContext = 0; Datum runIdDatum = 0; int64 runId = 0; bool failOK = true; MemoryContext originalContext = CurrentMemoryContext; StartTransactionCommand(); PushActiveSnapshot(GetTransactionSnapshot()); if (!JobRunDetailsTableExists()) { PopActiveSnapshot(); CommitTransactionCommand(); MemoryContextSwitchTo(originalContext); /* if the job_run_details table is not yet created, the run ID is not used */ return 0; } /* resolve relationId from passed in schema and relation name */ sequenceName = cstring_to_text(RUN_ID_SEQUENCE_NAME); sequenceNameList = textToQualifiedNameList(sequenceName); sequenceVar = makeRangeVarFromNameList(sequenceNameList); sequenceId = RangeVarGetRelid(sequenceVar, NoLock, failOK); sequenceIdDatum = ObjectIdGetDatum(sequenceId); GetUserIdAndSecContext(&savedUserId, &savedSecurityContext); SetUserIdAndSecContext(CronExtensionOwner(), SECURITY_LOCAL_USERID_CHANGE); /* generate new and unique colocation id from sequence */ runIdDatum = DirectFunctionCall1(nextval_oid, sequenceIdDatum); SetUserIdAndSecContext(savedUserId, savedSecurityContext); runId = DatumGetInt64(runIdDatum); PopActiveSnapshot(); CommitTransactionCommand(); MemoryContextSwitchTo(originalContext); return runId; } /* * CronExtensionOwner returns the name of the user that owns the * extension. */ static Oid CronExtensionOwner(void) { Relation extensionRelation = NULL; SysScanDesc scanDescriptor; ScanKeyData entry[1]; HeapTuple extensionTuple = NULL; Form_pg_extension extensionForm = NULL; Oid extensionOwner = InvalidOid; extensionRelation = table_open(ExtensionRelationId, AccessShareLock); ScanKeyInit(&entry[0], Anum_pg_extension_extname, BTEqualStrategyNumber, F_NAMEEQ, CStringGetDatum(EXTENSION_NAME)); scanDescriptor = systable_beginscan(extensionRelation, ExtensionNameIndexId, true, NULL, 1, entry); extensionTuple = systable_getnext(scanDescriptor); if (!HeapTupleIsValid(extensionTuple)) { ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("pg_cron extension not loaded"))); } extensionForm = (Form_pg_extension) GETSTRUCT(extensionTuple); extensionOwner = extensionForm->extowner; systable_endscan(scanDescriptor); table_close(extensionRelation, AccessShareLock); return extensionOwner; } /* * cron_unschedule removes a cron job. */ Datum cron_unschedule(PG_FUNCTION_ARGS) { int64 jobId = PG_GETARG_INT64(0); Oid cronSchemaId = InvalidOid; Oid cronJobIndexId = InvalidOid; Relation cronJobsTable = NULL; SysScanDesc scanDescriptor = NULL; ScanKeyData scanKey[1]; int scanKeyCount = 1; bool indexOK = true; HeapTuple heapTuple = NULL; cronSchemaId = get_namespace_oid(CRON_SCHEMA_NAME, false); cronJobIndexId = get_relname_relid(JOB_ID_INDEX_NAME, cronSchemaId); cronJobsTable = table_open(CronJobRelationId(), RowExclusiveLock); ScanKeyInit(&scanKey[0], Anum_cron_job_jobid, BTEqualStrategyNumber, F_INT8EQ, Int64GetDatum(jobId)); scanDescriptor = systable_beginscan(cronJobsTable, cronJobIndexId, indexOK, NULL, scanKeyCount, scanKey); heapTuple = systable_getnext(scanDescriptor); if (!HeapTupleIsValid(heapTuple)) { ereport(ERROR, (errmsg("could not find valid entry for job " INT64_FORMAT, jobId))); } EnsureDeletePermission(cronJobsTable, heapTuple); simple_heap_delete(cronJobsTable, &heapTuple->t_self); systable_endscan(scanDescriptor); table_close(cronJobsTable, NoLock); CommandCounterIncrement(); InvalidateJobCache(); PG_RETURN_BOOL(true); } /* * cron_unschedule_named removes a cron job by name. */ Datum cron_unschedule_named(PG_FUNCTION_ARGS) { Datum jobNameDatum = PG_GETARG_DATUM(0); char *jobName = NULL; RegProcedure procedure; Oid userId = GetUserId(); char *userName = GetUserNameFromId(userId, false); Datum userNameDatum = CStringGetTextDatum(userName); Relation cronJobsTable = NULL; SysScanDesc scanDescriptor = NULL; ScanKeyData scanKey[2]; int scanKeyCount = 2; bool indexOK = false; HeapTuple heapTuple = NULL; if (PG_ARGISNULL(0)) { ereport(ERROR, (errmsg("job_name can not be NULL"))); } /* * v1.5 changed the first argument type from "name" to "text". Cope with * calls from "CREATE EXTENSION pg_cron VERSION '1.4'". */ if (get_fn_expr_argtype(fcinfo->flinfo, 0) == NAMEOID) { procedure = F_NAMEEQ; jobName = NameStr(*DatumGetName(jobNameDatum)); } else { procedure = F_TEXTEQ; jobName = TextDatumGetCString(jobNameDatum); } cronJobsTable = table_open(CronJobRelationId(), RowExclusiveLock); ScanKeyInit(&scanKey[0], Anum_cron_job_jobname, BTEqualStrategyNumber, procedure, jobNameDatum); ScanKeyInit(&scanKey[1], Anum_cron_job_username, BTEqualStrategyNumber, F_TEXTEQ, userNameDatum); scanDescriptor = systable_beginscan(cronJobsTable, InvalidOid, indexOK, NULL, scanKeyCount, scanKey); heapTuple = systable_getnext(scanDescriptor); if (!HeapTupleIsValid(heapTuple)) { ereport(ERROR, (errmsg("could not find valid entry for job '%s'", jobName))); } EnsureDeletePermission(cronJobsTable, heapTuple); simple_heap_delete(cronJobsTable, &heapTuple->t_self); systable_endscan(scanDescriptor); table_close(cronJobsTable, NoLock); CommandCounterIncrement(); InvalidateJobCache(); PG_RETURN_BOOL(true); } /* * EnsureDeletePermission throws an error if the current user does * not have permission to delete the given cron.job tuple. */ static void EnsureDeletePermission(Relation cronJobsTable, HeapTuple heapTuple) { TupleDesc tupleDescriptor = RelationGetDescr(cronJobsTable); /* check if the current user owns the row */ Oid userId = GetUserId(); char *userName = GetUserNameFromId(userId, false); bool isNull = false; Datum ownerNameDatum = heap_getattr(heapTuple, Anum_cron_job_username, tupleDescriptor, &isNull); char *ownerName = TextDatumGetCString(ownerNameDatum); if (pg_strcasecmp(userName, ownerName) != 0) { /* otherwise, allow if the user has DELETE permission */ AclResult aclResult = pg_class_aclcheck(CronJobRelationId(), GetUserId(), ACL_DELETE); if (aclResult != ACLCHECK_OK) { aclcheck_error(aclResult, #if (PG_VERSION_NUM < 110000) ACL_KIND_CLASS, #else OBJECT_TABLE, #endif get_rel_name(CronJobRelationId())); } } } /* * cron_job_cache_invalidate invalidates the job cache in response to * a trigger. */ Datum cron_job_cache_invalidate(PG_FUNCTION_ARGS) { if (!CALLED_AS_TRIGGER(fcinfo)) { ereport(ERROR, (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED), errmsg("must be called as trigger"))); } InvalidateJobCache(); PG_RETURN_DATUM(PointerGetDatum(NULL)); } /* * Invalidate job cache ensures the job cache is reloaded on the next * iteration of pg_cron. */ static void InvalidateJobCache(void) { HeapTuple classTuple = NULL; classTuple = SearchSysCache1(RELOID, ObjectIdGetDatum(CronJobRelationId())); if (HeapTupleIsValid(classTuple)) { CacheInvalidateRelcacheByTuple(classTuple); ReleaseSysCache(classTuple); } } /* * InvalidateJobCacheCallback invalidates the job cache in response to * an invalidation event. */ void InvalidateJobCacheCallback(Datum argument, Oid relationId) { if (relationId == CachedCronJobRelationId || CachedCronJobRelationId == InvalidOid) { CronJobCacheValid = false; CachedCronJobRelationId = InvalidOid; } } /* * CachedCronJobRelationId returns a cached oid of the cron.job relation. */ static Oid CronJobRelationId(void) { if (CachedCronJobRelationId == InvalidOid) { Oid cronSchemaId = get_namespace_oid(CRON_SCHEMA_NAME, false); CachedCronJobRelationId = get_relname_relid(JOBS_TABLE_NAME, cronSchemaId); } return CachedCronJobRelationId; } /* * LoadCronJobList loads the current list of jobs from the cron.job table. */ List * LoadCronJobList(void) { List *jobList = NIL; Relation cronJobTable = NULL; SysScanDesc scanDescriptor = NULL; ScanKeyData scanKey[1]; int scanKeyCount = 0; HeapTuple heapTuple = NULL; TupleDesc tupleDescriptor = NULL; MemoryContext originalContext = CurrentMemoryContext; SetCurrentStatementStartTimestamp(); StartTransactionCommand(); PushActiveSnapshot(GetTransactionSnapshot()); /* * If the pg_cron extension has not been created yet or * we are on a hot standby, the job table is treated as * being empty. */ if (!PgCronHasBeenLoaded() || RecoveryInProgress()) { PopActiveSnapshot(); CommitTransactionCommand(); MemoryContextSwitchTo(originalContext); return NIL; } cronJobTable = table_open(CronJobRelationId(), AccessShareLock); scanDescriptor = systable_beginscan(cronJobTable, InvalidOid, false, NULL, scanKeyCount, scanKey); tupleDescriptor = RelationGetDescr(cronJobTable); heapTuple = systable_getnext(scanDescriptor); while (HeapTupleIsValid(heapTuple)) { MemoryContext oldContext = NULL; CronJob *job = NULL; oldContext = MemoryContextSwitchTo(CronJobContext); job = TupleToCronJob(tupleDescriptor, heapTuple); if (job != NULL) { jobList = lappend(jobList, job); } MemoryContextSwitchTo(oldContext); heapTuple = systable_getnext(scanDescriptor); } systable_endscan(scanDescriptor); table_close(cronJobTable, AccessShareLock); PopActiveSnapshot(); CommitTransactionCommand(); MemoryContextSwitchTo(originalContext); return jobList; } /* * TupleToCronJob takes a heap tuple, converts it into a CronJob struct and * adds it to the CronJobHash if it satisfies EnableSuperuserJobs condition. */ static CronJob * TupleToCronJob(TupleDesc tupleDescriptor, HeapTuple heapTuple) { CronJob *job = NULL; int64 jobKey = 0; bool isNull = false; bool isPresent = false; entry *parsedSchedule = NULL; char *jobOwner; Oid jobOwnerId; Datum jobId = heap_getattr(heapTuple, Anum_cron_job_jobid, tupleDescriptor, &isNull); Datum schedule = heap_getattr(heapTuple, Anum_cron_job_schedule, tupleDescriptor, &isNull); Datum command = heap_getattr(heapTuple, Anum_cron_job_command, tupleDescriptor, &isNull); Datum nodeName = heap_getattr(heapTuple, Anum_cron_job_nodename, tupleDescriptor, &isNull); Datum nodePort = heap_getattr(heapTuple, Anum_cron_job_nodeport, tupleDescriptor, &isNull); Datum database = heap_getattr(heapTuple, Anum_cron_job_database, tupleDescriptor, &isNull); Datum userName = heap_getattr(heapTuple, Anum_cron_job_username, tupleDescriptor, &isNull); jobOwner = TextDatumGetCString(userName); jobOwnerId = get_role_oid(jobOwner, false); if (!EnableSuperuserJobs && superuser_arg(jobOwnerId)) { /* * Someone inserted a superuser into the metadata. Skip over the * job when cron.enable_superuser_jobs is disabled. The memory * will be cleaned up when CronJobContext is reset. */ ereport(WARNING, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("skipping job " INT64_FORMAT " since superuser jobs " "are currently disallowed", job->jobId))); return NULL; } jobKey = DatumGetInt64(jobId); job = hash_search(CronJobHash, &jobKey, HASH_ENTER, &isPresent); job->jobId = DatumGetInt64(jobId); job->scheduleText = TextDatumGetCString(schedule); job->command = TextDatumGetCString(command); job->nodeName = TextDatumGetCString(nodeName); job->nodePort = DatumGetInt32(nodePort); job->userName = jobOwner; job->database = TextDatumGetCString(database); if (HeapTupleHeaderGetNatts(heapTuple->t_data) >= Anum_cron_job_active) { Datum active = heap_getattr(heapTuple, Anum_cron_job_active, tupleDescriptor, &isNull); Assert(!isNull); job->active = DatumGetBool(active); } else { job->active = true; } if (tupleDescriptor->natts >= Anum_cron_job_jobname) { bool isJobNameNull = false; Datum jobName = heap_getattr(heapTuple, Anum_cron_job_jobname, tupleDescriptor, &isJobNameNull); if (!isJobNameNull) { /* Handle the column type change introduced in 1.5 */ if (TupleDescAttr(tupleDescriptor, Anum_cron_job_jobname - 1)->atttypid == NAMEOID) { job->jobName = pstrdup(NameStr(*DatumGetName(jobName))); } else { job->jobName = TextDatumGetCString(jobName); } } else { job->jobName = NULL; } } parsedSchedule = ParseSchedule(job->scheduleText); if (parsedSchedule != NULL) { /* copy the schedule and free the allocated memory immediately */ job->schedule = *parsedSchedule; free_entry(parsedSchedule); } else { ereport(LOG, (errmsg("invalid pg_cron schedule for job " INT64_FORMAT ": %s", job->jobId, job->scheduleText))); /* a zeroed out schedule never runs */ memset(&job->schedule, 0, sizeof(entry)); } return job; } /* * PgCronHasBeenLoaded returns true if the pg_cron extension has been created * in the current database and the extension script has been executed. Otherwise, * it returns false. The result is cached as this is called very frequently. */ static bool PgCronHasBeenLoaded(void) { bool extensionLoaded = false; bool extensionPresent = false; bool extensionScriptExecuted = true; Oid extensionOid = get_extension_oid(EXTENSION_NAME, true); if (extensionOid != InvalidOid) { extensionPresent = true; } if (extensionPresent) { /* check if pg_cron extension objects are still being created */ if (creating_extension && CurrentExtensionObject == extensionOid) { extensionScriptExecuted = false; } else if (IsBinaryUpgrade) { extensionScriptExecuted = false; } } extensionLoaded = extensionPresent && extensionScriptExecuted; return extensionLoaded; } void InsertJobRunDetail(int64 runId, int64 *jobId, char *database, char *username, char *command, char *status) { StringInfoData querybuf; const int argCount = 6; Oid argTypes[6]; Datum argValues[6]; MemoryContext originalContext = CurrentMemoryContext; SetCurrentStatementStartTimestamp(); StartTransactionCommand(); PushActiveSnapshot(GetTransactionSnapshot()); if (!PgCronHasBeenLoaded() || RecoveryInProgress() || !JobRunDetailsTableExists()) { PopActiveSnapshot(); CommitTransactionCommand(); MemoryContextSwitchTo(originalContext); return; } initStringInfo(&querybuf); /* Open SPI context. */ if (SPI_connect() != SPI_OK_CONNECT) elog(ERROR, "SPI_connect failed"); appendStringInfo(&querybuf, "insert into %s.%s (jobid, runid, database, username, command, status) values ($1,$2,$3,$4,$5,$6)", CRON_SCHEMA_NAME, JOB_RUN_DETAILS_TABLE_NAME); /* jobId */ argTypes[0] = INT8OID; argValues[0] = Int64GetDatum(*jobId); /* runId */ argTypes[1] = INT8OID; argValues[1] = Int64GetDatum(runId); /* database */ argTypes[2] = TEXTOID; argValues[2] = CStringGetTextDatum(database); /* username */ argTypes[3] = TEXTOID; argValues[3] = CStringGetTextDatum(username); /* command */ argTypes[4] = TEXTOID; argValues[4] = CStringGetTextDatum(command); /* status */ argTypes[5] = TEXTOID; argValues[5] = CStringGetTextDatum(status); if(SPI_execute_with_args(querybuf.data, argCount, argTypes, argValues, NULL, false, 1) != SPI_OK_INSERT) elog(ERROR, "SPI_exec failed: %s", querybuf.data); pfree(querybuf.data); SPI_finish(); PopActiveSnapshot(); CommitTransactionCommand(); MemoryContextSwitchTo(originalContext); } void UpdateJobRunDetail(int64 runId, int32 *job_pid, char *status, char *return_message, TimestampTz *start_time, TimestampTz *end_time) { StringInfoData querybuf; Oid argTypes[6]; Datum argValues[6]; int i; MemoryContext originalContext = CurrentMemoryContext; SetCurrentStatementStartTimestamp(); StartTransactionCommand(); PushActiveSnapshot(GetTransactionSnapshot()); if (!PgCronHasBeenLoaded() || RecoveryInProgress() || !JobRunDetailsTableExists()) { PopActiveSnapshot(); CommitTransactionCommand(); MemoryContextSwitchTo(originalContext); return; } initStringInfo(&querybuf); i = 0; /* Open SPI context. */ if (SPI_connect() != SPI_OK_CONNECT) elog(ERROR, "SPI_connect failed"); appendStringInfo(&querybuf, "update %s.%s set", CRON_SCHEMA_NAME, JOB_RUN_DETAILS_TABLE_NAME); /* add the fields to be updated */ if (job_pid != NULL) { argTypes[i] = INT4OID; argValues[i] = Int32GetDatum(*job_pid); i++; appendStringInfo(&querybuf, " job_pid = $%d,", i); } if (status != NULL) { argTypes[i] = TEXTOID; argValues[i] = CStringGetTextDatum(status); i++; appendStringInfo(&querybuf, " status = $%d,", i); } if (return_message != NULL) { argTypes[i] = TEXTOID; argValues[i] = CStringGetTextDatum(return_message); i++; appendStringInfo(&querybuf, " return_message = $%d,", i); } if (start_time != NULL) { argTypes[i] = TIMESTAMPTZOID; argValues[i] = TimestampTzGetDatum(*start_time); i++; appendStringInfo(&querybuf, " start_time = $%d,", i); } if (end_time != NULL) { argTypes[i] = TIMESTAMPTZOID; argValues[i] = TimestampTzGetDatum(*end_time); i++; appendStringInfo(&querybuf, " end_time = $%d,", i); } argTypes[i] = INT8OID; argValues[i] = Int64GetDatum(runId); i++; /* remove the last comma */ querybuf.len--; querybuf.data[querybuf.len] = '\0'; /* and add the where clause */ appendStringInfo(&querybuf, " where runid = $%d", i); if(SPI_execute_with_args(querybuf.data, i, argTypes, argValues, NULL, false, 1) != SPI_OK_UPDATE) elog(ERROR, "SPI_exec failed: %s", querybuf.data); pfree(querybuf.data); SPI_finish(); PopActiveSnapshot(); CommitTransactionCommand(); MemoryContextSwitchTo(originalContext); } static void AlterJob(int64 jobId, text *scheduleText, text *commandText, text *databaseText, text *usernameText, bool *active) { StringInfoData querybuf; Oid argTypes[7]; Datum argValues[7]; int i; AclResult aclresult; Oid userId; Oid userIdcheckacl; Oid savedUserId; int savedSecurityContext; char *database_name; char *schedule; char *command; char *username; char *currentuser; entry *parsedSchedule = NULL; userId = GetUserId(); userIdcheckacl = GetUserId(); currentuser = GetUserNameFromId(userId, false); savedUserId = InvalidOid; savedSecurityContext = 0; if (!PgCronHasBeenLoaded() || RecoveryInProgress() || !JobTableExists()) { return; } initStringInfo(&querybuf); i = 0; appendStringInfo(&querybuf, "update %s.%s set", CRON_SCHEMA_NAME, JOBS_TABLE_NAME); /* username has been provided */ if (usernameText != NULL) { if (!superuser()) elog(ERROR, "must be superuser to alter username"); username = text_to_cstring(usernameText); userIdcheckacl = GetRoleOidIfCanLogin(username); } else { username = currentuser; } if (!EnableSuperuserJobs && superuser_arg(userIdcheckacl)) { ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("cannot schedule jobs as superuser"), errdetail("Scheduling jobs as superuser is disallowed when " "cron.enable_superuser_jobs is set to off."))); } /* add the fields to be updated */ /* database has been provided */ if (databaseText != NULL) { database_name = text_to_cstring(databaseText); /* ensure the user that is used in the job can connect to the database */ #if (PG_VERSION_NUM >= 160000) aclresult = object_aclcheck(DatabaseRelationId, get_database_oid(database_name, false), userIdcheckacl, ACL_CONNECT); #else aclresult = pg_database_aclcheck(get_database_oid(database_name, false), userIdcheckacl, ACL_CONNECT); #endif if (aclresult != ACLCHECK_OK) elog(ERROR, "User %s does not have CONNECT privilege on %s", GetUserNameFromId(userIdcheckacl, false), database_name); argTypes[i] = TEXTOID; argValues[i] = CStringGetTextDatum(database_name); i++; appendStringInfo(&querybuf, " database = $%d,", i); } /* ensure schedule is valid */ if (scheduleText != NULL) { schedule = text_to_cstring(scheduleText); parsedSchedule = ParseSchedule(schedule); if (parsedSchedule == NULL) { ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid schedule: %s", schedule), errhint("Use cron format (e.g. 5 4 * * *), or interval " "format '[1-59] seconds'"))); } free_entry(parsedSchedule); argTypes[i] = TEXTOID; argValues[i] = CStringGetTextDatum(schedule); i++; appendStringInfo(&querybuf, " schedule = $%d,", i); } if (commandText != NULL) { argTypes[i] = TEXTOID; command = text_to_cstring(commandText); argValues[i] = CStringGetTextDatum(command); i++; appendStringInfo(&querybuf, " command = $%d,", i); } if (usernameText != NULL) { argTypes[i] = TEXTOID; argValues[i] = CStringGetTextDatum(username); i++; appendStringInfo(&querybuf, " username = $%d,", i); } if (active != NULL) { argTypes[i] = BOOLOID; argValues[i] = BoolGetDatum(*active); i++; appendStringInfo(&querybuf, " active = $%d,", i); } /* remove the last comma */ querybuf.len--; querybuf.data[querybuf.len] = '\0'; /* and add the where clause */ argTypes[i] = INT8OID; argValues[i] = Int64GetDatum(jobId); i++; appendStringInfo(&querybuf, " where jobid = $%d", i); /* ensure the caller owns the row */ argTypes[i] = TEXTOID; argValues[i] = CStringGetTextDatum(currentuser); i++; if (!superuser()) appendStringInfo(&querybuf, " and username = $%d", i); if (i <= 2) ereport(ERROR, (errmsg("no updates specified"), errhint("You must specify at least one job attribute to change when calling alter_job"))); GetUserIdAndSecContext(&savedUserId, &savedSecurityContext); SetUserIdAndSecContext(CronExtensionOwner(), SECURITY_LOCAL_USERID_CHANGE); /* Open SPI context. */ if (SPI_connect() != SPI_OK_CONNECT) elog(ERROR, "SPI_connect failed"); if(SPI_execute_with_args(querybuf.data, i, argTypes, argValues, NULL, false, 1) != SPI_OK_UPDATE) elog(ERROR, "SPI_exec failed: %s", querybuf.data); pfree(querybuf.data); if (SPI_processed <= 0) elog(ERROR, "Job " INT64_FORMAT " does not exist or you don't own it", jobId); SPI_finish(); SetUserIdAndSecContext(savedUserId, savedSecurityContext); InvalidateJobCache(); } void MarkPendingRunsAsFailed(void) { StringInfoData querybuf; MemoryContext originalContext = CurrentMemoryContext; SetCurrentStatementStartTimestamp(); StartTransactionCommand(); PushActiveSnapshot(GetTransactionSnapshot()); if (!PgCronHasBeenLoaded() || RecoveryInProgress() || !JobRunDetailsTableExists()) { PopActiveSnapshot(); CommitTransactionCommand(); MemoryContextSwitchTo(originalContext); return; } initStringInfo(&querybuf); /* Open SPI context. */ if (SPI_connect() != SPI_OK_CONNECT) elog(ERROR, "SPI_connect failed"); appendStringInfo(&querybuf, "update %s.%s set status = '%s', return_message = 'server restarted' where status in ('%s','%s')" , CRON_SCHEMA_NAME, JOB_RUN_DETAILS_TABLE_NAME, GetCronStatus(CRON_STATUS_FAILED), GetCronStatus(CRON_STATUS_STARTING), GetCronStatus(CRON_STATUS_RUNNING)); if (SPI_exec(querybuf.data, 0) != SPI_OK_UPDATE) elog(ERROR, "SPI_exec failed: %s", querybuf.data); pfree(querybuf.data); SPI_finish(); PopActiveSnapshot(); CommitTransactionCommand(); MemoryContextSwitchTo(originalContext); } char * GetCronStatus(CronStatus cronstatus) { char *statusDesc = "unknown status"; switch (cronstatus) { case CRON_STATUS_STARTING: statusDesc = "starting"; break; case CRON_STATUS_RUNNING: statusDesc = "running"; break; case CRON_STATUS_SENDING: statusDesc = "sending"; break; case CRON_STATUS_CONNECTING: statusDesc = "connecting"; break; case CRON_STATUS_SUCCEEDED: statusDesc = "succeeded"; break; case CRON_STATUS_FAILED: statusDesc = "failed"; break; default: break; } return statusDesc; } /* * JobRunDetailsTableExists returns whether the job_run_details table exists. */ static bool JobRunDetailsTableExists(void) { Oid cronSchemaId = get_namespace_oid(CRON_SCHEMA_NAME, false); Oid jobRunDetailsTableOid = get_relname_relid(JOB_RUN_DETAILS_TABLE_NAME, cronSchemaId); return jobRunDetailsTableOid != InvalidOid; } /* * JobTableExists returns whether the job table exists. */ static bool JobTableExists(void) { Oid cronSchemaId = get_namespace_oid(CRON_SCHEMA_NAME, false); Oid jobTableOid = get_relname_relid(JOBS_TABLE_NAME, cronSchemaId); return jobTableOid != InvalidOid; } /* * ParseSchedule attempts to parse a cron schedule or an interval in seconds. * The returned pointer is allocated using malloc and should be freed by the * caller. */ static entry * ParseSchedule(char *scheduleText) { uint32 secondsInterval = 0; entry *schedule; /* * First try to parse as a cron schedule. */ schedule = parse_cron_entry(scheduleText); if (schedule != NULL) { /* valid cron schedule */ return schedule; } /* * Parse as interval on seconds. */ if (TryParseInterval(scheduleText, &secondsInterval)) { schedule = calloc(sizeof(entry), sizeof(char)); schedule->secondsInterval = secondsInterval; return schedule; } elog(LOG, "failed to parse schedule: %s", scheduleText); return NULL; } /* * TryParseInterval returns whether scheduleText is of the form * second[s]. */ static bool TryParseInterval(char *scheduleText, uint32 *secondsInterval) { char lastChar = '\0'; char plural = '\0'; char extra = '\0'; char *lowercaseSchedule = asc_tolower(scheduleText, strlen(scheduleText)); int numParts = sscanf(lowercaseSchedule, " %u secon%c%c %c", secondsInterval, &lastChar, &plural, &extra); if (lastChar != 'd') { /* value did not have a "second" suffix */ return false; } if (numParts == 2) { /* second (allow "2 second") */ return 0 < *secondsInterval && *secondsInterval < 60; } else if (numParts == 3 && plural == 's') { /* seconds (allow "1 seconds") */ return 0 < *secondsInterval && *secondsInterval < 60; } return false; } pg_cron-1.6.5/src/misc.c000066400000000000000000000065711472654652200150660ustar00rootroot00000000000000/* Copyright 1988,1990,1993,1994 by Paul Vixie * All rights reserved * * Distribute freely, except: don't remove my name from the source or * documentation (don't take credit for my work), mark your changes (don't * get me blamed for your possible bugs), don't alter or remove this * notice. May be sold if buildable source is provided to buyer. No * warrantee of any kind, express or implied, is included with this * software; use at your own risk, responsibility for damages (if any) to * anyone resulting from the use of this software rests entirely with the * user. * * Send bug reports, bug fixes, enhancements, requests, flames, etc., and * I'll try to keep a version up to date. I can be reached as follows: * Paul Vixie uunet!decwrl!vixie!paul */ /* marco 07nov16 [removed code not needed by pg_cron] * marco 04sep16 [integrated into pg_cron] * vix 26jan87 [RCS has the rest of the log] * vix 30dec86 [written] */ #include #include #include #include #include #include "cron.h" /* get_char(file) : like getc() but increment LineNumber on newlines */ int get_char(FILE *file) { int ch; /* * Sneaky hack: we wrapped an in-memory buffer into a FILE* * to minimize changes to cron.c. * * This code replaces: * ch = getc(file); */ file_buffer *buffer = (file_buffer *) file; if (buffer->unget_count > 0) { ch = buffer->unget_data[--buffer->unget_count]; } else if (buffer->pointer == buffer->length) { ch = '\0'; buffer->pointer++; } else if (buffer->pointer > buffer->length) { ch = EOF; } else { ch = buffer->data[buffer->pointer++]; } if (ch == '\n') Set_LineNum(LineNumber + 1); return ch; } /* unget_char(ch, file) : like ungetc but do LineNumber processing */ void unget_char(int ch, FILE *file) { /* * Sneaky hack: we wrapped an in-memory buffer into a FILE* * to minimize changes to cron.c. * * This code replaces: * ungetc(ch, file); */ file_buffer *buffer = (file_buffer *) file; if (buffer->unget_count >= 1000) { perror("ungetc limit exceeded"); exit(ERROR_EXIT); } buffer->unget_data[buffer->unget_count++] = ch; if (ch == '\n') Set_LineNum(LineNumber - 1); } /* get_string(str, max, file, termstr) : like fgets() but * (1) has terminator string which should include \n * (2) will always leave room for the null * (3) uses get_char() so LineNumber will be accurate * (4) returns EOF or terminating character, whichever */ int get_string(char *string, int size, FILE *file, char *terms) { int ch; while (EOF != (ch = get_char(file)) && !strchr(terms, ch)) { if (size > 1) { *string++ = (char) ch; size--; } } if (size > 0) *string = '\0'; return ch; } /* skip_comments(file) : read past comment (if any) */ void skip_comments(FILE *file) { int ch; while (EOF != (ch = get_char(file))) { /* ch is now the first character of a line. */ while (ch == ' ' || ch == '\t') ch = get_char(file); if (ch == EOF) break; /* ch is now the first non-blank character of a line. */ if (ch != '\n' && ch != '#') break; /* ch must be a newline or comment as first non-blank * character on a line. */ while (ch != '\n' && ch != EOF) ch = get_char(file); /* ch is now the newline of a line which we're going to * ignore. */ } if (ch != EOF) unget_char(ch, file); } pg_cron-1.6.5/src/pg_cron.c000066400000000000000000001636611472654652200155660ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * src/pg_cron.c * * Implementation of the pg_cron task scheduler. * Wording: * - A job is a scheduling definition of a task * - A task is what is actually executed within the database engine * * Copyright (c) 2016, Citus Data, Inc. * *------------------------------------------------------------------------- */ #include #include "postgres.h" #include "fmgr.h" /* these are always necessary for a bgworker */ #include "miscadmin.h" #include "postmaster/bgworker.h" #include "storage/ipc.h" #include "storage/latch.h" #include "storage/lwlock.h" #include "storage/proc.h" #include "storage/shm_mq.h" #include "storage/shm_toc.h" #include "storage/shmem.h" /* these headers are used by this particular worker's code */ #define MAIN_PROGRAM #include "cron.h" #include "pg_cron.h" #include "task_states.h" #include "job_metadata.h" #ifdef HAVE_POLL_H #include #elif defined(HAVE_SYS_POLL_H) #include #endif #include "sys/time.h" #include "time.h" #include "access/genam.h" #include "access/heapam.h" #include "access/htup_details.h" #include "access/printtup.h" #include "access/xact.h" #include "access/xlog.h" #include "catalog/pg_extension.h" #include "catalog/indexing.h" #include "catalog/namespace.h" #include "commands/async.h" #include "commands/dbcommands.h" #include "commands/extension.h" #include "commands/sequence.h" #include "commands/trigger.h" #if (PG_VERSION_NUM >= 160000) #include "utils/guc_hooks.h" #else #include "commands/variable.h" #endif #include "lib/stringinfo.h" #include "libpq-fe.h" #include "libpq/pqmq.h" #include "libpq/pqsignal.h" #include "mb/pg_wchar.h" #include "parser/analyze.h" #include "pgstat.h" #include "postmaster/postmaster.h" #include "utils/builtins.h" #include "utils/fmgroids.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/portal.h" #include "utils/ps_status.h" #include "utils/rel.h" #include "utils/snapmgr.h" #include "utils/syscache.h" #include "utils/timeout.h" #include "utils/timestamp.h" #if (PG_VERSION_NUM >= 100000) #include "utils/varlena.h" #endif #include "tcop/pquery.h" #include "tcop/utility.h" #include "libpq/pqformat.h" #include "utils/builtins.h" PG_MODULE_MAGIC; #ifndef MAXINT8LEN #define MAXINT8LEN 20 #endif /* Table-of-contents constants for our dynamic shared memory segment. */ #define PG_CRON_MAGIC 0x51028080 #define PG_CRON_KEY_DATABASE 0 #define PG_CRON_KEY_USERNAME 1 #define PG_CRON_KEY_COMMAND 2 #define PG_CRON_KEY_QUEUE 3 #define PG_CRON_NKEYS 4 /* ways in which the clock can change between main loop iterations */ typedef enum { CLOCK_JUMP_BACKWARD = 0, CLOCK_PROGRESSED = 1, CLOCK_JUMP_FORWARD = 2, CLOCK_CHANGE = 3 } ClockProgress; /* forward declarations */ void _PG_init(void); void _PG_fini(void); static void pg_cron_sigterm(SIGNAL_ARGS); static void pg_cron_sighup(SIGNAL_ARGS); PGDLLEXPORT void PgCronLauncherMain(Datum arg); PGDLLEXPORT void CronBackgroundWorker(Datum arg); static void StartAllPendingRuns(List *taskList, TimestampTz currentTime); static void StartPendingRuns(CronTask *task, ClockProgress clockProgress, TimestampTz lastMinute, TimestampTz currentTime); static int MinutesPassed(TimestampTz startTime, TimestampTz stopTime); static TimestampTz TimestampMinuteStart(TimestampTz time); static TimestampTz TimestampMinuteEnd(TimestampTz time); static bool ShouldRunTask(entry *schedule, TimestampTz currentMinute, bool doWild, bool doNonWild); static void WaitForCronTasks(List *taskList); static void WaitForLatch(int timeoutMs); static void PollForTasks(List *taskList); static bool CanStartTask(CronTask *task); static void ManageCronTasks(List *taskList, TimestampTz currentTime); static void ManageCronTask(CronTask *task, TimestampTz currentTime); static void ExecuteSqlString(const char *sql); static void GetTaskFeedback(PGresult *result, CronTask *task); static void ProcessBgwTaskFeedback(CronTask *task, bool running); static bool jobCanceled(CronTask *task); static bool jobStartupTimeout(CronTask *task, TimestampTz currentTime); static char* pg_cron_cmdTuples(char *msg); static void bgw_generate_returned_message(StringInfoData *display_msg, ErrorData edata); /* global settings */ char *CronTableDatabaseName = "postgres"; static bool CronLogStatement = true; static bool CronLogRun = true; static bool CronReloadConfig = false; /* flags set by signal handlers */ static volatile sig_atomic_t got_sigterm = false; /* global variables */ static int CronTaskStartTimeout = 10000; /* maximum connection time */ static const int MaxWait = 1000; /* maximum time in ms that poll() can block */ static bool RebootJobsScheduled = false; static int RunningTaskCount = 0; static int MaxRunningTasks = 0; static int CronLogMinMessages = WARNING; static bool UseBackgroundWorkers = false; char *cron_timezone = NULL; static const struct config_enum_entry cron_message_level_options[] = { {"debug5", DEBUG5, false}, {"debug4", DEBUG4, false}, {"debug3", DEBUG3, false}, {"debug2", DEBUG2, false}, {"debug1", DEBUG1, false}, {"debug", DEBUG2, true}, {"info", INFO, false}, {"notice", NOTICE, false}, {"warning", WARNING, false}, {"error", ERROR, false}, {"log", LOG, false}, {"fatal", FATAL, false}, {"panic", PANIC, false}, {NULL, 0, false} }; static const char *cron_error_severity(int elevel); /* * _PG_init gets called when the extension is loaded. */ void _PG_init(void) { BackgroundWorker worker; if (IsBinaryUpgrade) { return; } if (!process_shared_preload_libraries_in_progress) { ereport(ERROR, (errmsg("pg_cron can only be loaded via shared_preload_libraries"), errhint("Add pg_cron to the shared_preload_libraries " "configuration variable in postgresql.conf."))); } /* watch for invalidation events */ CacheRegisterRelcacheCallback(InvalidateJobCacheCallback, (Datum) 0); DefineCustomStringVariable( "cron.database_name", gettext_noop("Database in which pg_cron metadata is kept."), NULL, &CronTableDatabaseName, "postgres", PGC_POSTMASTER, GUC_SUPERUSER_ONLY, NULL, NULL, NULL); DefineCustomBoolVariable( "cron.log_statement", gettext_noop("Log all cron statements prior to execution."), NULL, &CronLogStatement, true, PGC_POSTMASTER, GUC_SUPERUSER_ONLY, NULL, NULL, NULL); DefineCustomBoolVariable( "cron.log_run", gettext_noop("Log all jobs runs into the job_run_details table"), NULL, &CronLogRun, true, PGC_POSTMASTER, GUC_SUPERUSER_ONLY, NULL, NULL, NULL); DefineCustomBoolVariable( "cron.enable_superuser_jobs", gettext_noop("Allow jobs to be scheduled as superuser"), NULL, &EnableSuperuserJobs, true, PGC_POSTMASTER, GUC_SUPERUSER_ONLY, NULL, NULL, NULL); DefineCustomStringVariable( "cron.host", gettext_noop("Hostname to connect to postgres."), gettext_noop("This setting has no effect when background workers are used."), &CronHost, "localhost", PGC_POSTMASTER, GUC_SUPERUSER_ONLY, NULL, NULL, NULL); DefineCustomBoolVariable( "cron.use_background_workers", gettext_noop("Use background workers instead of client sessions."), NULL, &UseBackgroundWorkers, false, PGC_POSTMASTER, GUC_SUPERUSER_ONLY, NULL, NULL, NULL); DefineCustomBoolVariable( "cron.launch_active_jobs", gettext_noop("Launch jobs that are defined as active."), NULL, &LaunchActiveJobs, true, PGC_SIGHUP, GUC_SUPERUSER_ONLY, NULL, NULL, NULL); if (!UseBackgroundWorkers) DefineCustomIntVariable( "cron.max_running_jobs", gettext_noop("Maximum number of jobs that can run concurrently."), NULL, &MaxRunningTasks, (MaxConnections < 32) ? MaxConnections : 32, 0, MaxConnections, PGC_POSTMASTER, GUC_SUPERUSER_ONLY, NULL, NULL, NULL); else DefineCustomIntVariable( "cron.max_running_jobs", gettext_noop("Maximum number of jobs that can run concurrently."), NULL, &MaxRunningTasks, (max_worker_processes - 1 < 5) ? max_worker_processes - 1 : 5, 0, max_worker_processes - 1, PGC_POSTMASTER, GUC_SUPERUSER_ONLY, NULL, NULL, NULL); DefineCustomEnumVariable( "cron.log_min_messages", gettext_noop("log_min_messages for the launcher bgworker."), NULL, &CronLogMinMessages, WARNING, cron_message_level_options, PGC_SIGHUP, GUC_SUPERUSER_ONLY, NULL, NULL, NULL); DefineCustomStringVariable( "cron.timezone", gettext_noop("Specify timezone used for cron schedule."), NULL, &cron_timezone, "GMT", PGC_POSTMASTER, GUC_SUPERUSER_ONLY, check_timezone, NULL, NULL); /* set up common data for all our workers */ worker.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION; worker.bgw_start_time = BgWorkerStart_RecoveryFinished; worker.bgw_restart_time = 1; #if (PG_VERSION_NUM < 100000) worker.bgw_main = PgCronLauncherMain; #endif worker.bgw_main_arg = Int32GetDatum(0); worker.bgw_notify_pid = 0; sprintf(worker.bgw_library_name, "pg_cron"); sprintf(worker.bgw_function_name, "PgCronLauncherMain"); snprintf(worker.bgw_name, BGW_MAXLEN, "pg_cron launcher"); #if (PG_VERSION_NUM >= 110000) snprintf(worker.bgw_type, BGW_MAXLEN, "pg_cron launcher"); #endif RegisterBackgroundWorker(&worker); } /* * Signal handler for SIGTERM * Set a flag to let the main loop to terminate, and set our latch to wake * it up. */ static void pg_cron_sigterm(SIGNAL_ARGS) { got_sigterm = true; if (MyProc != NULL) { SetLatch(&MyProc->procLatch); } } /* * Signal handler for SIGHUP * Set a flag to tell the main loop to reload the cron jobs. */ static void pg_cron_sighup(SIGNAL_ARGS) { CronJobCacheValid = false; CronReloadConfig = true; if (MyProc != NULL) { SetLatch(&MyProc->procLatch); } } /* * pg_cron_cmdTuples - * mainly copy/pasted from PQcmdTuples * If the last command was INSERT/UPDATE/DELETE/MOVE/FETCH/COPY, return * a string containing the number of inserted/affected tuples. If not, * return "". * * XXX: this should probably return an int */ static char * pg_cron_cmdTuples(char *msg) { char *p, *c; if (!msg) return ""; if (strncmp(msg, "INSERT ", 7) == 0) { p = msg + 7; /* INSERT: skip oid and space */ while (*p && *p != ' ') p++; if (*p == 0) goto interpret_error; /* no space? */ p++; } else if (strncmp(msg, "SELECT ", 7) == 0 || strncmp(msg, "DELETE ", 7) == 0 || strncmp(msg, "UPDATE ", 7) == 0) p = msg + 7; else if (strncmp(msg, "FETCH ", 6) == 0) p = msg + 6; else if (strncmp(msg, "MOVE ", 5) == 0 || strncmp(msg, "COPY ", 5) == 0) p = msg + 5; else return ""; /* check that we have an integer (at least one digit, nothing else) */ for (c = p; *c; c++) { if (!isdigit((unsigned char) *c)) goto interpret_error; } if (c == p) goto interpret_error; return p; interpret_error: ereport(LOG, (errmsg("could not interpret result from server: %s", msg))); return ""; } /* * cron_error_severity --- get string representing elevel */ static const char * cron_error_severity(int elevel) { const char *elevel_char; switch (elevel) { case DEBUG1: elevel_char = "DEBUG1"; break; case DEBUG2: elevel_char = "DEBUG2"; break; case DEBUG3: elevel_char = "DEBUG3"; break; case DEBUG4: elevel_char = "DEBUG4"; break; case DEBUG5: elevel_char = "DEBUG5"; break; case LOG: elevel_char = "LOG"; break; case INFO: elevel_char = "INFO"; break; case NOTICE: elevel_char = "NOTICE"; break; case WARNING: elevel_char = "WARNING"; break; case ERROR: elevel_char = "ERROR"; break; case FATAL: elevel_char = "FATAL"; break; case PANIC: elevel_char = "PANIC"; break; default: elevel_char = "???"; break; } return elevel_char; } /* * bgw_generate_returned_message - * generates the message to be inserted into the job_run_details table * first part is comming from error_severity (elog.c) */ static void bgw_generate_returned_message(StringInfoData *display_msg, ErrorData edata) { const char *prefix; switch (edata.elevel) { case DEBUG1: case DEBUG2: case DEBUG3: case DEBUG4: case DEBUG5: prefix = gettext_noop("DEBUG"); break; case LOG: #if (PG_VERSION_NUM >= 100000) case LOG_SERVER_ONLY: #endif prefix = gettext_noop("LOG"); break; case INFO: prefix = gettext_noop("INFO"); break; case NOTICE: prefix = gettext_noop("NOTICE"); break; case WARNING: prefix = gettext_noop("WARNING"); break; case ERROR: prefix = gettext_noop("ERROR"); break; case FATAL: prefix = gettext_noop("FATAL"); break; case PANIC: prefix = gettext_noop("PANIC"); break; default: prefix = "???"; break; } appendStringInfo(display_msg, "%s: %s", prefix, edata.message); if (edata.detail != NULL) appendStringInfo(display_msg, "\nDETAIL: %s", edata.detail); if (edata.hint != NULL) appendStringInfo(display_msg, "\nHINT: %s", edata.hint); if (edata.context != NULL) appendStringInfo(display_msg, "\nCONTEXT: %s", edata.context); } /* * PgCronLauncherMain is the main entry-point for the background worker * that performs tasks. */ void PgCronLauncherMain(Datum arg) { MemoryContext CronLoopContext = NULL; struct rlimit limit; /* Establish signal handlers before unblocking signals. */ pqsignal(SIGHUP, pg_cron_sighup); pqsignal(SIGINT, SIG_IGN); pqsignal(SIGTERM, pg_cron_sigterm); /* We're now ready to receive signals */ BackgroundWorkerUnblockSignals(); /* Connect to our database */ #if (PG_VERSION_NUM < 110000) BackgroundWorkerInitializeConnection(CronTableDatabaseName, NULL); #else BackgroundWorkerInitializeConnection(CronTableDatabaseName, NULL, 0); #endif /* Make pg_cron recognisable in pg_stat_activity */ pgstat_report_appname("pg_cron scheduler"); /* * Mark anything that was in progress before the database restarted as * failed. */ MarkPendingRunsAsFailed(); /* Determine how many tasks we can run concurrently */ if (MaxConnections < MaxRunningTasks) { MaxRunningTasks = MaxConnections; } if (max_files_per_process < MaxRunningTasks) { MaxRunningTasks = max_files_per_process; } if (getrlimit(RLIMIT_NOFILE, &limit) != 0 && limit.rlim_cur < (uint32) MaxRunningTasks) { MaxRunningTasks = limit.rlim_cur; } if (UseBackgroundWorkers && max_worker_processes - 1 < MaxRunningTasks) { MaxRunningTasks = max_worker_processes - 1; } if (MaxRunningTasks <= 0) { MaxRunningTasks = 1; } CronLoopContext = AllocSetContextCreate(CurrentMemoryContext, "pg_cron loop context", ALLOCSET_DEFAULT_MINSIZE, ALLOCSET_DEFAULT_INITSIZE, ALLOCSET_DEFAULT_MAXSIZE); InitializeJobMetadataCache(); InitializeTaskStateHash(); ereport(LOG, (errmsg("pg_cron scheduler started"))); /* set the desired log_min_messages */ SetConfigOption("log_min_messages", cron_error_severity(CronLogMinMessages), PGC_POSTMASTER, PGC_S_OVERRIDE); MemoryContextSwitchTo(CronLoopContext); while (!got_sigterm) { List *taskList = NIL; TimestampTz currentTime = 0; CHECK_FOR_INTERRUPTS(); AcceptInvalidationMessages(); if (CronReloadConfig) { /* set the desired log_min_messages */ ProcessConfigFile(PGC_SIGHUP); SetConfigOption("log_min_messages", cron_error_severity(CronLogMinMessages), PGC_POSTMASTER, PGC_S_OVERRIDE); CronReloadConfig = false; } /* * Both CronReloadConfig and CronJobCacheValid are triggered by SIGHUP. * ProcessConfigFile should come first, because RefreshTaskHash depends * on settings that might have changed. */ if (!CronJobCacheValid) { RefreshTaskHash(); } taskList = CurrentTaskList(); currentTime = GetCurrentTimestamp(); StartAllPendingRuns(taskList, currentTime); WaitForCronTasks(taskList); ManageCronTasks(taskList, currentTime); MemoryContextReset(CronLoopContext); } ereport(LOG, (errmsg("pg_cron scheduler shutting down"))); /* return error code to trigger restart */ proc_exit(1); } /* * StartPendingRuns goes through the list of tasks and kicks of * runs for tasks that should start, taking clock changes into * into consideration. */ static void StartAllPendingRuns(List *taskList, TimestampTz currentTime) { static TimestampTz lastMinute = 0; int minutesPassed = 0; ListCell *taskCell = NULL; ClockProgress clockProgress; if (!RebootJobsScheduled) { /* find jobs with @reboot as a schedule */ foreach(taskCell, taskList) { CronTask *task = (CronTask *) lfirst(taskCell); CronJob *cronJob = GetCronJob(task->jobId); entry *schedule = &cronJob->schedule; if (schedule->flags & WHEN_REBOOT && task->isActive) { task->pendingRunCount += 1; } } RebootJobsScheduled = true; } foreach(taskCell, taskList) { CronTask *task = (CronTask *) lfirst(taskCell); if (task->secondsInterval > 0 && task->isActive) { /* * For interval jobs, if a task takes longer than the interval, * we only queue up once. So if a task that is supposed to run * every 30 seconds takes 5 minutes, we start another run * immediately after 5 minutes, but then return to regular cadence. */ if (task->pendingRunCount == 0 && TimestampDifferenceExceeds(task->lastStartTime, currentTime, task->secondsInterval * 1000)) { task->pendingRunCount += 1; } } } if (lastMinute == 0) { lastMinute = TimestampMinuteStart(currentTime); } minutesPassed = MinutesPassed(lastMinute, currentTime); if (minutesPassed == 0) { /* wait for new minute */ return; } /* use Vixie cron logic for clock jumps */ if (minutesPassed > (3*MINUTE_COUNT)) { /* clock jumped forward by more than 3 hours */ clockProgress = CLOCK_CHANGE; } else if (minutesPassed > 5) { /* clock went forward by more than 5 minutes (DST?) */ clockProgress = CLOCK_JUMP_FORWARD; } else if (minutesPassed > 0) { /* clock went forward by 1-5 minutes */ clockProgress = CLOCK_PROGRESSED; } else if (minutesPassed > -(3*MINUTE_COUNT)) { /* clock jumped backwards by less than 3 hours (DST?) */ clockProgress = CLOCK_JUMP_BACKWARD; } else { /* clock jumped backwards 3 hours or more */ clockProgress = CLOCK_CHANGE; } foreach(taskCell, taskList) { CronTask *task = (CronTask *) lfirst(taskCell); if (!task->isActive) { /* * The job has been unscheduled, so we should not schedule * new runs. The task will be safely removed on the next call * to ManageCronTask. */ continue; } StartPendingRuns(task, clockProgress, lastMinute, currentTime); } /* * If the clock jump backwards then we avoid repeating the fixed-time * tasks by preserving the last minute from before the clock jump, * until the clock has caught up (clockProgress will be * CLOCK_JUMP_BACKWARD until then). */ if (clockProgress != CLOCK_JUMP_BACKWARD) { lastMinute = TimestampMinuteStart(currentTime); } } /* * StartPendingRuns kicks off pending runs for a task if it * should start, taking clock changes into consideration. */ static void StartPendingRuns(CronTask *task, ClockProgress clockProgress, TimestampTz lastMinute, TimestampTz currentTime) { CronJob *cronJob = GetCronJob(task->jobId); entry *schedule = &cronJob->schedule; TimestampTz virtualTime = lastMinute; TimestampTz currentMinute = TimestampMinuteStart(currentTime); switch (clockProgress) { case CLOCK_PROGRESSED: { /* * case 1: minutesPassed is a small positive number * run jobs for each virtual minute until caught up. */ do { virtualTime = TimestampTzPlusMilliseconds(virtualTime, 60*1000); if (ShouldRunTask(schedule, virtualTime, true, true)) { task->pendingRunCount += 1; } } while (virtualTime < currentMinute); break; } case CLOCK_JUMP_FORWARD: { /* * case 2: minutesPassed is a medium-sized positive number, * for example because we went to DST run wildcard * jobs once, then run any fixed-time jobs that would * otherwise be skipped if we use up our minute * (possible, if there are a lot of jobs to run) go * around the loop again so that wildcard jobs have * a chance to run, and we do our housekeeping */ /* run fixed-time jobs for each minute missed */ do { virtualTime = TimestampTzPlusMilliseconds(virtualTime, 60*1000); if (ShouldRunTask(schedule, virtualTime, false, true)) { task->pendingRunCount += 1; } } while (virtualTime < currentMinute); /* run wildcard jobs for current minute */ if (ShouldRunTask(schedule, currentMinute, true, false)) { task->pendingRunCount += 1; } break; } case CLOCK_JUMP_BACKWARD: { /* * case 3: timeDiff is a small or medium-sized * negative num, eg. because of DST ending just run * the wildcard jobs. The fixed-time jobs probably * have already run, and should not be repeated * virtual time does not change until we are caught up */ if (ShouldRunTask(schedule, currentMinute, true, false)) { task->pendingRunCount += 1; } break; } default: { /* * other: time has changed a *lot*, skip over any * intermediate fixed-time jobs and go back to * normal operation. */ if (ShouldRunTask(schedule, currentMinute, true, true)) { task->pendingRunCount += 1; } } } } /* * MinutesPassed returns the number of minutes between startTime and * stopTime rounded down to the closest integer. */ static int MinutesPassed(TimestampTz startTime, TimestampTz stopTime) { int microsPassed = 0; long secondsPassed = 0; int minutesPassed = 0; TimestampDifference(startTime, stopTime, &secondsPassed, µsPassed); minutesPassed = secondsPassed / 60; return minutesPassed; } /* * TimestampMinuteEnd returns the timestamp at the start of the * current minute for the given time. */ static TimestampTz TimestampMinuteStart(TimestampTz time) { TimestampTz result = 0; #ifdef HAVE_INT64_TIMESTAMP result = time - time % 60000000; #else result = (long) time - (long) time % 60; #endif return result; } /* * TimestampMinuteEnd returns the timestamp at the start of the * next minute from the given time. */ static TimestampTz TimestampMinuteEnd(TimestampTz time) { TimestampTz result = TimestampMinuteStart(time); #ifdef HAVE_INT64_TIMESTAMP result += 60000000; #else result += 60; #endif return result; } /* * ShouldRunTask returns whether a job should run in the current * minute according to its schedule. */ static bool ShouldRunTask(entry *schedule, TimestampTz currentTime, bool doWild, bool doNonWild) { pg_time_t currentTime_t = timestamptz_to_time_t(currentTime); pg_time_t tomorrowTime_t = timestamptz_to_time_t(currentTime + USECS_PER_DAY); struct pg_tm* cur_tm = pg_localtime(¤tTime_t, pg_tzset(cron_timezone)); int minute = cur_tm->tm_min -FIRST_MINUTE; int hour = cur_tm->tm_hour -FIRST_HOUR; int dayOfMonth = cur_tm->tm_mday -FIRST_DOM; int month = cur_tm->tm_mon +1 -FIRST_MONTH; int dayOfWeek = cur_tm->tm_wday -FIRST_DOW; /* * pg_localtime returns a pointer to a global struct, * so cur_tm cannot be used after this point. */ struct pg_tm* tomorrow_tm = pg_localtime(&tomorrowTime_t, pg_tzset(cron_timezone)); bool is_lastdom = tomorrow_tm->tm_mday == 1; bool thisdom = bit_test(schedule->dom, dayOfMonth) != 0 || (is_lastdom && (schedule->flags & DOM_LAST) != 0); bool thisdow = bit_test(schedule->dow, dayOfWeek); if (bit_test(schedule->minute, minute) && bit_test(schedule->hour, hour) && bit_test(schedule->month, month) && ((schedule->flags & (DOM_STAR | DOW_STAR)) != 0 ? (thisdom && thisdow) : (thisdom || thisdow))) { if ((doNonWild && (schedule->flags & (MIN_STAR | HR_STAR)) == 0) || (doWild && (schedule->flags & (MIN_STAR | HR_STAR)) != 0)) { return true; } } return false; } /* * WaitForCronTasks blocks waiting for any active task for at most * 1 second. */ static void WaitForCronTasks(List *taskList) { int taskCount = list_length(taskList); if (taskCount > 0) { PollForTasks(taskList); } else { WaitForLatch(MaxWait); } } /* * WaitForLatch waits for the given number of milliseconds unless a signal * is received or postmaster shuts down. */ static void WaitForLatch(int timeoutMs) { int rc = 0; int waitFlags = WL_LATCH_SET | WL_POSTMASTER_DEATH | WL_TIMEOUT; /* nothing to do, wait for new jobs */ #if (PG_VERSION_NUM >= 100000) rc = WaitLatch(MyLatch, waitFlags, timeoutMs, PG_WAIT_EXTENSION); #else rc = WaitLatch(MyLatch, waitFlags, timeoutMs); #endif ResetLatch(MyLatch); CHECK_FOR_INTERRUPTS(); if (rc & WL_POSTMASTER_DEATH) { /* postmaster died and we should bail out immediately */ proc_exit(1); } } /* * PollForTasks calls poll() for the sockets of all tasks. It checks for * read or write events based on the pollingStatus of the task. */ static void PollForTasks(List *taskList) { TimestampTz currentTime = 0; TimestampTz nextEventTime = 0; int pollTimeout = 0; long waitSeconds = 0; int waitMicros = 0; CronTask **polledTasks = NULL; struct pollfd *pollFDs = NULL; int pollResult = 0; int taskIndex = 0; int taskCount = list_length(taskList); int activeTaskCount = 0; ListCell *taskCell = NULL; polledTasks = (CronTask **) palloc0(taskCount * sizeof(CronTask *)); pollFDs = (struct pollfd *) palloc0(taskCount * sizeof(struct pollfd)); currentTime = GetCurrentTimestamp(); /* * At the latest, wake up when the next minute starts. */ nextEventTime = TimestampMinuteEnd(currentTime); foreach(taskCell, taskList) { CronTask *task = (CronTask *) lfirst(taskCell); PostgresPollingStatusType pollingStatus = task->pollingStatus; struct pollfd *pollFileDescriptor = &pollFDs[activeTaskCount]; if (activeTaskCount >= MaxRunningTasks) { /* already polling the maximum number of tasks */ break; } if (task->state == CRON_TASK_ERROR || task->state == CRON_TASK_DONE || CanStartTask(task)) { /* there is work to be done, don't wait */ pfree(polledTasks); pfree(pollFDs); return; } if (task->state == CRON_TASK_WAITING && task->pendingRunCount == 0) { /* * Make sure we do not wait past the next run time of an interval * job. */ if (task->secondsInterval > 0) { TimestampTz nextRunTime = TimestampTzPlusMilliseconds(task->lastStartTime, task->secondsInterval * 1000); if (TimestampDifferenceExceeds(nextRunTime, nextEventTime, 0)) { nextEventTime = nextRunTime; } } /* don't poll idle tasks */ continue; } if (task->state == CRON_TASK_CONNECTING || task->state == CRON_TASK_SENDING) { /* * We need to wake up when a timeout expires. * Take the minimum of nextEventTime and task->startDeadline. */ if (TimestampDifferenceExceeds(task->startDeadline, nextEventTime, 0)) { nextEventTime = task->startDeadline; } } /* we plan to poll this task */ pollFileDescriptor = &pollFDs[activeTaskCount]; polledTasks[activeTaskCount] = task; if (task->state == CRON_TASK_CONNECTING || task->state == CRON_TASK_SENDING || task->state == CRON_TASK_BGW_RUNNING || task->state == CRON_TASK_RUNNING) { PGconn *connection = task->connection; int pollEventMask = 0; /* * Set the appropriate mask for poll, based on the current polling * status of the task, controlled by ManageCronTask. */ if (pollingStatus == PGRES_POLLING_READING) { pollEventMask = POLLERR | POLLIN; } else if (pollingStatus == PGRES_POLLING_WRITING) { pollEventMask = POLLERR | POLLOUT; } pollFileDescriptor->fd = PQsocket(connection); pollFileDescriptor->events = pollEventMask; } else { /* * Task is not running. */ pollFileDescriptor->fd = -1; pollFileDescriptor->events = 0; } pollFileDescriptor->revents = 0; activeTaskCount++; } /* * Find the first time-based event, which is either the start of a new * minute or a timeout. */ TimestampDifference(currentTime, nextEventTime, &waitSeconds, &waitMicros); pollTimeout = waitSeconds * 1000 + waitMicros / 1000; if (pollTimeout <= 0) { /* * Interval jobs might frequently be overdue, inject a small * 1ms wait to avoid getting into a tight loop. */ pollTimeout = 1; } else if (pollTimeout > MaxWait) { /* * We never wait more than 1 second, this gives us a chance to react * to external events like a TERM signal and job changes. */ pollTimeout = MaxWait; } if (activeTaskCount == 0) { /* turns out there's nothing to do, just wait for something to happen */ WaitForLatch(pollTimeout); pfree(polledTasks); pfree(pollFDs); return; } pollResult = poll(pollFDs, activeTaskCount, pollTimeout); if (pollResult < 0) { /* * This typically happens in case of a signal, though we should * probably check errno in case something bad happened. */ pfree(polledTasks); pfree(pollFDs); return; } for (taskIndex = 0; taskIndex < activeTaskCount; taskIndex++) { CronTask *task = polledTasks[taskIndex]; struct pollfd *pollFileDescriptor = &pollFDs[taskIndex]; task->isSocketReady = pollFileDescriptor->revents & pollFileDescriptor->events; } pfree(polledTasks); pfree(pollFDs); } /* * CanStartTask determines whether a task is ready to be started because * it has pending runs and we are running less than MaxRunningTasks. */ static bool CanStartTask(CronTask *task) { return task->state == CRON_TASK_WAITING && task->pendingRunCount > 0 && RunningTaskCount < MaxRunningTasks; } /* * ManageCronTasks proceeds the state machines of the given list of tasks. */ static void ManageCronTasks(List *taskList, TimestampTz currentTime) { ListCell *taskCell = NULL; foreach(taskCell, taskList) { CronTask *task = (CronTask *) lfirst(taskCell); ManageCronTask(task, currentTime); } } /* * ManageCronTask implements the cron task state machine. */ static void ManageCronTask(CronTask *task, TimestampTz currentTime) { CronTaskState checkState = task->state; int64 jobId = task->jobId; CronJob *cronJob = GetCronJob(jobId); PGconn *connection = task->connection; ConnStatusType connectionStatus = CONNECTION_BAD; switch (checkState) { case CRON_TASK_WAITING: { /* check if job has been removed */ if (!task->isActive) { /* remove task as well */ RemoveTask(jobId); break; } if (!CanStartTask(task)) { break; } task->pendingRunCount -= 1; if (UseBackgroundWorkers) task->state = CRON_TASK_BGW_START; else task->state = CRON_TASK_START; task->lastStartTime = currentTime; RunningTaskCount++; /* Add new entry to audit table. */ task->runId = NextRunId(); if (CronLogRun) InsertJobRunDetail(task->runId, &cronJob->jobId, cronJob->database, cronJob->userName, cronJob->command, GetCronStatus(CRON_STATUS_STARTING)); } case CRON_TASK_START: { /* as there is no break at the end of the previous case * to not add an extra second, then do another check here */ if (!UseBackgroundWorkers) { const char *clientEncoding = GetDatabaseEncodingName(); char nodePortString[12]; TimestampTz startDeadline = 0; const char *keywordArray[] = { "host", "port", "fallback_application_name", "client_encoding", "dbname", "user", NULL }; const char *valueArray[] = { cronJob->nodeName, nodePortString, "pg_cron", clientEncoding, cronJob->database, cronJob->userName, NULL }; sprintf(nodePortString, "%d", cronJob->nodePort); Assert(sizeof(keywordArray) == sizeof(valueArray)); if (CronLogStatement) { char *command = cronJob->command; ereport(LOG, (errmsg("cron job " INT64_FORMAT " %s: %s", jobId, GetCronStatus(CRON_STATUS_STARTING), command))); } connection = PQconnectStartParams(keywordArray, valueArray, false); PQsetnonblocking(connection, 1); connectionStatus = PQstatus(connection); if (connectionStatus == CONNECTION_BAD) { /* make sure we call PQfinish on the connection */ task->connection = connection; task->errorMessage = "connection failed"; task->pollingStatus = 0; task->state = CRON_TASK_ERROR; break; } startDeadline = TimestampTzPlusMilliseconds(currentTime, CronTaskStartTimeout); task->startDeadline = startDeadline; task->connection = connection; task->pollingStatus = PGRES_POLLING_WRITING; task->state = CRON_TASK_CONNECTING; if (CronLogRun) UpdateJobRunDetail(task->runId, NULL, GetCronStatus(CRON_STATUS_CONNECTING), NULL, NULL, NULL); break; } } case CRON_TASK_BGW_START: { BackgroundWorker worker; pid_t pid; shm_toc_estimator e; shm_toc *toc; char *database; char *username; char *command; MemoryContext oldcontext; shm_mq *mq; Size segsize; BackgroundWorkerHandle *handle; BgwHandleStatus status; bool registered; TimestampTz startDeadline = 0; /* break in the previous case has not been reached * checking just for extra precaution */ Assert(UseBackgroundWorkers); #if PG_VERSION_NUM < 100000 Assert(CurrentResourceOwner == NULL); CurrentResourceOwner = ResourceOwnerCreate(NULL, "pg_cron_worker"); #endif #define QUEUE_SIZE ((Size) 65536) /* * Create the shared memory that we will pass to the background * worker process. We use DSM_CREATE_NULL_IF_MAXSEGMENTS so that we * do not ERROR here. This way, we can mark the job as failed and * keep the launcher process running normally. */ shm_toc_initialize_estimator(&e); shm_toc_estimate_chunk(&e, strlen(cronJob->database) + 1); shm_toc_estimate_chunk(&e, strlen(cronJob->userName) + 1); shm_toc_estimate_chunk(&e, strlen(cronJob->command) + 1); shm_toc_estimate_chunk(&e, QUEUE_SIZE); shm_toc_estimate_keys(&e, PG_CRON_NKEYS); segsize = shm_toc_estimate(&e); task->seg = dsm_create(segsize, DSM_CREATE_NULL_IF_MAXSEGMENTS); if (task->seg == NULL) { task->state = CRON_TASK_ERROR; task->errorMessage = "unable to create a DSM segment; more " "details may be available in the server log"; ereport(WARNING, (errmsg("max number of DSM segments may has been reached"))); break; } toc = shm_toc_create(PG_CRON_MAGIC, dsm_segment_address(task->seg), segsize); database = shm_toc_allocate(toc, strlen(cronJob->database) + 1); strcpy(database, cronJob->database); shm_toc_insert(toc, PG_CRON_KEY_DATABASE, database); username = shm_toc_allocate(toc, strlen(cronJob->userName) + 1); strcpy(username, cronJob->userName); shm_toc_insert(toc, PG_CRON_KEY_USERNAME, username); command = shm_toc_allocate(toc, strlen(cronJob->command) + 1); strcpy(command, cronJob->command); shm_toc_insert(toc, PG_CRON_KEY_COMMAND, command); mq = shm_mq_create(shm_toc_allocate(toc, QUEUE_SIZE), QUEUE_SIZE); shm_toc_insert(toc, PG_CRON_KEY_QUEUE, mq); shm_mq_set_receiver(mq, MyProc); /* * Attach the queue before launching a worker, so that we'll automatically * detach the queue if we error out. (Otherwise, the worker might sit * there trying to write the queue long after we've gone away.) */ oldcontext = MemoryContextSwitchTo(TopMemoryContext); task->sharedMemoryQueue = shm_mq_attach(mq, task->seg, NULL); MemoryContextSwitchTo(oldcontext); /* * Prepare the background worker. * */ memset(&worker, 0, sizeof(BackgroundWorker)); worker.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION; worker.bgw_start_time = BgWorkerStart_ConsistentState; worker.bgw_restart_time = BGW_NEVER_RESTART; sprintf(worker.bgw_library_name, "pg_cron"); sprintf(worker.bgw_function_name, "CronBackgroundWorker"); #if (PG_VERSION_NUM >= 110000) snprintf(worker.bgw_type, BGW_MAXLEN, "pg_cron"); #endif snprintf(worker.bgw_name, BGW_MAXLEN, "pg_cron worker"); worker.bgw_main_arg = UInt32GetDatum(dsm_segment_handle(task->seg)); worker.bgw_notify_pid = MyProcPid; /* * Start the worker process. */ if (CronLogStatement) { ereport(LOG, (errmsg("cron job " INT64_FORMAT " %s: %s", jobId, GetCronStatus(CRON_STATUS_STARTING), command))); } /* If no no background worker slots are currently available * let's try until we reach jobStartupTimeout */ startDeadline = TimestampTzPlusMilliseconds(currentTime, CronTaskStartTimeout); task->startDeadline = startDeadline; do { registered = RegisterDynamicBackgroundWorker(&worker, &handle); } while (!registered && !jobStartupTimeout(task, GetCurrentTimestamp())); if (!registered) { dsm_detach(task->seg); task->seg = NULL; task->state = CRON_TASK_ERROR; task->errorMessage = "could not start background process; more " "details may be available in the server log"; ereport(WARNING, (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED), errmsg("out of background worker slots"), errhint("You might need to increase max_worker_processes."))); break; } task->startDeadline = 0; task->handle = *handle; status = WaitForBackgroundWorkerStartup(&task->handle, &pid); if (status != BGWH_STARTED && status != BGWH_STOPPED) { dsm_detach(task->seg); task->seg = NULL; task->state = CRON_TASK_ERROR; task->errorMessage = "could not start background process; more " "details may be available in the server log"; break; } task->lastStartTime = GetCurrentTimestamp(); if (CronLogRun) UpdateJobRunDetail(task->runId, &pid, GetCronStatus(CRON_STATUS_RUNNING), NULL, &task->lastStartTime, NULL); task->state = CRON_TASK_BGW_RUNNING; break; } case CRON_TASK_CONNECTING: { PostgresPollingStatusType pollingStatus = 0; Assert(!UseBackgroundWorkers); /* check if job has been removed */ if (jobCanceled(task)) break; /* check if timeout has been reached */ if (jobStartupTimeout(task, currentTime)) break; /* check if connection is still alive */ connectionStatus = PQstatus(connection); if (connectionStatus == CONNECTION_BAD) { task->errorMessage = "connection failed"; task->pollingStatus = 0; task->state = CRON_TASK_ERROR; break; } /* check if socket is ready to send */ if (!task->isSocketReady) { break; } /* check whether a connection has been established */ pollingStatus = PQconnectPoll(connection); if (pollingStatus == PGRES_POLLING_OK) { pid_t pid; /* wait for socket to be ready to send a query */ task->pollingStatus = PGRES_POLLING_WRITING; task->state = CRON_TASK_SENDING; pid = (pid_t) PQbackendPID(connection); if (CronLogRun) UpdateJobRunDetail(task->runId, &pid, GetCronStatus(CRON_STATUS_SENDING), NULL, NULL, NULL); } else if (pollingStatus == PGRES_POLLING_FAILED) { task->errorMessage = "connection failed"; task->pollingStatus = 0; task->state = CRON_TASK_ERROR; } else { /* * Connection is still being established. * * On the next WaitForTasks round, we wait for reading or writing * based on the status returned by PQconnectPoll, see: * https://www.postgresql.org/docs/9.5/static/libpq-connect.html */ task->pollingStatus = pollingStatus; } break; } case CRON_TASK_SENDING: { char *command = cronJob->command; int sendResult = 0; Assert(!UseBackgroundWorkers); /* check if job has been removed */ if (jobCanceled(task)) break; /* check if timeout has been reached */ if (jobStartupTimeout(task, currentTime)) break; /* check if socket is ready to send */ if (!task->isSocketReady) { break; } /* check if connection is still alive */ connectionStatus = PQstatus(connection); if (connectionStatus == CONNECTION_BAD) { task->errorMessage = "connection lost"; task->pollingStatus = 0; task->state = CRON_TASK_ERROR; break; } sendResult = PQsendQuery(connection, command); if (sendResult == 1) { /* wait for socket to be ready to receive results */ task->pollingStatus = PGRES_POLLING_READING; /* command is underway, stop using timeout */ task->startDeadline = 0; task->state = CRON_TASK_RUNNING; task->lastStartTime = GetCurrentTimestamp(); if (CronLogRun) UpdateJobRunDetail(task->runId, NULL, GetCronStatus(CRON_STATUS_RUNNING), NULL, &task->lastStartTime, NULL); } else { /* not yet ready to send */ } break; } case CRON_TASK_RUNNING: { int connectionBusy = 0; PGresult *result = NULL; Assert(!UseBackgroundWorkers); /* check if job has been removed */ if (jobCanceled(task)) break; /* check if connection is still alive */ connectionStatus = PQstatus(connection); if (connectionStatus == CONNECTION_BAD) { task->errorMessage = "connection lost"; task->pollingStatus = 0; task->state = CRON_TASK_ERROR; break; } /* check if socket is ready to send */ if (!task->isSocketReady) { break; } PQconsumeInput(connection); connectionBusy = PQisBusy(connection); if (connectionBusy) { /* still waiting for results */ break; } while ((result = PQgetResult(connection)) != NULL) { GetTaskFeedback(result, task); } PQfinish(connection); task->connection = NULL; task->pollingStatus = 0; task->isSocketReady = false; task->state = CRON_TASK_DONE; RunningTaskCount--; break; } case CRON_TASK_BGW_RUNNING: { pid_t pid; Assert(UseBackgroundWorkers); /* check if job has been removed */ if (jobCanceled(task)) { TerminateBackgroundWorker(&task->handle); WaitForBackgroundWorkerShutdown(&task->handle); dsm_detach(task->seg); task->seg = NULL; break; } /* still waiting for job to complete */ if (GetBackgroundWorkerPid(&task->handle, &pid) != BGWH_STOPPED) { bool isRunning = true; /* process notices and warnings */ ProcessBgwTaskFeedback(task, isRunning); } else { bool isRunning = false; /* process remaining notices and final task result */ ProcessBgwTaskFeedback(task, isRunning); task->state = CRON_TASK_DONE; dsm_detach(task->seg); task->seg = NULL; RunningTaskCount--; } break; } case CRON_TASK_ERROR: { if (connection != NULL) { PQfinish(connection); task->connection = NULL; } if (!task->isActive) { RemoveTask(jobId); } if (task->errorMessage != NULL) { if (CronLogRun) { TimestampTz end_time = GetCurrentTimestamp(); UpdateJobRunDetail(task->runId, NULL, GetCronStatus(CRON_STATUS_FAILED), task->errorMessage, &task->lastStartTime, &end_time); } ereport(LOG, (errmsg("cron job " INT64_FORMAT " %s", jobId, task->errorMessage))); if (task->freeErrorMessage) { free(task->errorMessage); } } else { ereport(LOG, (errmsg("cron job " INT64_FORMAT " %s", jobId, GetCronStatus(CRON_STATUS_FAILED)))); } task->startDeadline = 0; task->isSocketReady = false; task->state = CRON_TASK_DONE; RunningTaskCount--; /* fall through to CRON_TASK_DONE */ } case CRON_TASK_DONE: default: { int currentPendingRunCount = task->pendingRunCount; CronJob *job = GetCronJob(jobId); /* * It may happen that job was unscheduled during task execution. * In this case we keep task as-is. Otherwise, we should * re-initialize task, i.e. reset fields to initial values including * status. */ if (job != NULL && job->active) InitializeCronTask(task, jobId); else task->state = CRON_TASK_WAITING; /* * We keep the number of runs that should have started while * the task was still running. If >0, this will trigger another * run immediately. */ task->pendingRunCount = currentPendingRunCount; } } } static void GetTaskFeedback(PGresult *result, CronTask *task) { TimestampTz end_time; ExecStatusType executionStatus; end_time = GetCurrentTimestamp(); executionStatus = PQresultStatus(result); switch (executionStatus) { case PGRES_COMMAND_OK: { char *cmdStatus = PQcmdStatus(result); char *cmdTuples = PQcmdTuples(result); if (CronLogRun) UpdateJobRunDetail(task->runId, NULL, GetCronStatus(CRON_STATUS_SUCCEEDED), cmdStatus, NULL, &end_time); if (CronLogStatement) { ereport(LOG, (errmsg("cron job " INT64_FORMAT " COMMAND completed: %s %s", task->jobId, cmdStatus, cmdTuples))); } break; } case PGRES_BAD_RESPONSE: case PGRES_FATAL_ERROR: { task->errorMessage = strdup(PQresultErrorMessage(result)); task->freeErrorMessage = true; task->pollingStatus = 0; task->state = CRON_TASK_ERROR; if (CronLogRun) UpdateJobRunDetail(task->runId, NULL, GetCronStatus(CRON_STATUS_FAILED), task->errorMessage, NULL, &end_time); PQclear(result); return; } case PGRES_COPY_IN: case PGRES_COPY_OUT: case PGRES_COPY_BOTH: { /* cannot handle COPY input/output */ task->errorMessage = "COPY not supported"; task->pollingStatus = 0; task->state = CRON_TASK_ERROR; if (CronLogRun) UpdateJobRunDetail(task->runId, NULL, GetCronStatus(CRON_STATUS_FAILED), task->errorMessage, NULL, &end_time); PQclear(result); return; } case PGRES_TUPLES_OK: case PGRES_EMPTY_QUERY: case PGRES_SINGLE_TUPLE: case PGRES_NONFATAL_ERROR: default: { int tupleCount = PQntuples(result); char *rowString = ngettext("row", "rows", tupleCount); char rows[MAXINT8LEN + 1]; char outputrows[MAXINT8LEN + 4 + 1]; pg_lltoa(tupleCount, rows); snprintf(outputrows, sizeof(outputrows), "%s %s", rows, rowString); if (CronLogRun) UpdateJobRunDetail(task->runId, NULL, GetCronStatus(CRON_STATUS_SUCCEEDED), outputrows, NULL, &end_time); if (CronLogStatement) { ereport(LOG, (errmsg("cron job " INT64_FORMAT " completed: " "%d %s", task->jobId, tupleCount, rowString))); } break; } } PQclear(result); } /* * ProcessBgwTaskFeedback reads messages from a shared memory queue associated * with the background worker that is executing a given task. If the task is * still running, the function does not block if the queue is empty. Otherwise, * it reads until the end of the queue. */ static void ProcessBgwTaskFeedback(CronTask *task, bool running) { shm_mq_handle *responseq = task->sharedMemoryQueue; TimestampTz end_time; Size nbytes; void *data; char msgtype; StringInfoData msg; shm_mq_result res; end_time = GetCurrentTimestamp(); /* * Message-parsing routines operate on a null-terminated StringInfo, * so we must construct one. */ for (;;) { /* do not wait if the task is running */ bool nowait = running; /* Get next message. */ res = shm_mq_receive(responseq, &nbytes, &data, nowait); if (res != SHM_MQ_SUCCESS) break; initStringInfo(&msg); resetStringInfo(&msg); enlargeStringInfo(&msg, nbytes); msg.len = nbytes; memcpy(msg.data, data, nbytes); msg.data[nbytes] = '\0'; msgtype = pq_getmsgbyte(&msg); switch (msgtype) { case 'N': case 'E': { ErrorData edata; StringInfoData display_msg; pq_parse_errornotice(&msg, &edata); initStringInfo(&display_msg); bgw_generate_returned_message(&display_msg, edata); if (CronLogRun) { if (edata.elevel >= ERROR) UpdateJobRunDetail(task->runId, NULL, GetCronStatus(CRON_STATUS_FAILED), display_msg.data, NULL, &end_time); else if (running) UpdateJobRunDetail(task->runId, NULL, NULL, display_msg.data, NULL, NULL); else UpdateJobRunDetail(task->runId, NULL, GetCronStatus(CRON_STATUS_SUCCEEDED), display_msg.data, NULL, &end_time); } ereport(LOG, (errmsg("cron job " INT64_FORMAT ": %s", task->jobId, display_msg.data))); pfree(display_msg.data); break; } case 'T': break; case 'C': { const char *tag = pq_getmsgstring(&msg); char *nonconst_tag; char *cmdTuples; nonconst_tag = strdup(tag); if (CronLogRun) UpdateJobRunDetail(task->runId, NULL, GetCronStatus(CRON_STATUS_SUCCEEDED), nonconst_tag, NULL, &end_time); if (CronLogStatement) { cmdTuples = pg_cron_cmdTuples(nonconst_tag); ereport(LOG, (errmsg("cron job " INT64_FORMAT " COMMAND completed: %s %s", task->jobId, nonconst_tag, cmdTuples))); } free(nonconst_tag); break; } case 'A': case 'D': case 'G': case 'H': case 'W': case 'Z': break; default: elog(WARNING, "unknown message type: %c (%zu bytes)", msg.data[0], nbytes); break; } pfree(msg.data); } } /* * Background worker logic. */ void CronBackgroundWorker(Datum main_arg) { dsm_segment *seg; shm_toc *toc; char *database; char *username; char *command; shm_mq *mq; shm_mq_handle *responseq; /* handle SIGTERM like regular backend */ pqsignal(SIGTERM, die); BackgroundWorkerUnblockSignals(); /* Set up a memory context and resource owner. */ Assert(CurrentResourceOwner == NULL); CurrentResourceOwner = ResourceOwnerCreate(NULL, "pg_cron"); CurrentMemoryContext = AllocSetContextCreate(TopMemoryContext, "pg_cron worker", ALLOCSET_DEFAULT_MINSIZE, ALLOCSET_DEFAULT_INITSIZE, ALLOCSET_DEFAULT_MAXSIZE); /* Set up a dynamic shared memory segment. */ seg = dsm_attach(DatumGetInt32(main_arg)); if (seg == NULL) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("unable to map dynamic shared memory segment"))); toc = shm_toc_attach(PG_CRON_MAGIC, dsm_segment_address(seg)); if (toc == NULL) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("bad magic number in dynamic shared memory segment"))); #if PG_VERSION_NUM < 100000 database = shm_toc_lookup(toc, PG_CRON_KEY_DATABASE); username = shm_toc_lookup(toc, PG_CRON_KEY_USERNAME); command = shm_toc_lookup(toc, PG_CRON_KEY_COMMAND); mq = shm_toc_lookup(toc, PG_CRON_KEY_QUEUE); #else database = shm_toc_lookup(toc, PG_CRON_KEY_DATABASE, false); username = shm_toc_lookup(toc, PG_CRON_KEY_USERNAME, false); command = shm_toc_lookup(toc, PG_CRON_KEY_COMMAND, false); mq = shm_toc_lookup(toc, PG_CRON_KEY_QUEUE, false); #endif shm_mq_set_sender(mq, MyProc); responseq = shm_mq_attach(mq, seg, NULL); pq_redirect_to_shm_mq(seg, responseq); #if (PG_VERSION_NUM < 110000) BackgroundWorkerInitializeConnection(database, username); #else BackgroundWorkerInitializeConnection(database, username, 0); #endif /* Prepare to execute the query. */ SetCurrentStatementStartTimestamp(); debug_query_string = command; pgstat_report_activity(STATE_RUNNING, command); StartTransactionCommand(); if (StatementTimeout > 0) enable_timeout_after(STATEMENT_TIMEOUT, StatementTimeout); else disable_timeout(STATEMENT_TIMEOUT, false); /* Execute the query. */ ExecuteSqlString(command); /* Post-execution cleanup. */ disable_timeout(STATEMENT_TIMEOUT, false); CommitTransactionCommand(); pgstat_report_activity(STATE_IDLE, command); pgstat_report_stat(true); /* Signal that we are done. */ ReadyForQuery(DestRemote); dsm_detach(seg); proc_exit(0); } /* * Execute given SQL string without SPI or a libpq session. */ static void ExecuteSqlString(const char *sql) { List *raw_parsetree_list; ListCell *lc1; bool isTopLevel; int commands_remaining; MemoryContext parsecontext; MemoryContext oldcontext; /* * Parse the SQL string into a list of raw parse trees. * * Because we allow statements that perform internal transaction control, * we can't do this in TopTransactionContext; the parse trees might get * blown away before we're done executing them. */ parsecontext = AllocSetContextCreate(TopMemoryContext, "pg_cron parse/plan", ALLOCSET_DEFAULT_MINSIZE, ALLOCSET_DEFAULT_INITSIZE, ALLOCSET_DEFAULT_MAXSIZE); oldcontext = MemoryContextSwitchTo(parsecontext); raw_parsetree_list = pg_parse_query(sql); commands_remaining = list_length(raw_parsetree_list); isTopLevel = commands_remaining == 1; MemoryContextSwitchTo(oldcontext); /* * Do parse analysis, rule rewrite, planning, and execution for each raw * parsetree. We must fully execute each query before beginning parse * analysis on the next one, since there may be interdependencies. */ foreach(lc1, raw_parsetree_list) { #if PG_VERSION_NUM < 100000 Node *parsetree = (Node *) lfirst(lc1); #else RawStmt *parsetree = (RawStmt *) lfirst(lc1); #endif #if PG_VERSION_NUM < 130000 const char *commandTag; char completionTag[COMPLETION_TAG_BUFSIZE]; #else CommandTag commandTag; QueryCompletion qc; #endif List *querytree_list; List *plantree_list; bool snapshot_set = false; Portal portal; DestReceiver *receiver; int16 format = 1; /* * We don't allow transaction-control commands like COMMIT and ABORT * here. The entire SQL statement is executed as a single transaction * which commits if no errors are encountered. */ if (IsA(parsetree, TransactionStmt)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("transaction control statements are not allowed in pg_cron"))); /* * Get the command name for use in status display (it also becomes the * default completion tag, down inside PortalRun). Set ps_status and * do any special start-of-SQL-command processing needed by the * destination. */ #if PG_VERSION_NUM < 100000 commandTag = CreateCommandTag(parsetree); #else commandTag = CreateCommandTag(parsetree->stmt); #endif #if PG_VERSION_NUM < 130000 set_ps_display(commandTag, false); #else set_ps_display(GetCommandTagName(commandTag)); #endif BeginCommand(commandTag, DestNone); /* Set up a snapshot if parse analysis/planning will need one. */ if (analyze_requires_snapshot(parsetree)) { PushActiveSnapshot(GetTransactionSnapshot()); snapshot_set = true; } /* * OK to analyze, rewrite, and plan this query. * * As with parsing, we need to make sure this data outlives the * transaction, because of the possibility that the statement might * perform internal transaction control. */ oldcontext = MemoryContextSwitchTo(parsecontext); #if PG_VERSION_NUM >= 150000 querytree_list = pg_analyze_and_rewrite_fixedparams(parsetree, sql, NULL, 0, NULL); #elif PG_VERSION_NUM >= 100000 querytree_list = pg_analyze_and_rewrite(parsetree, sql, NULL, 0, NULL); #else querytree_list = pg_analyze_and_rewrite(parsetree, sql, NULL, 0); #endif #if PG_VERSION_NUM < 130000 plantree_list = pg_plan_queries(querytree_list, 0, NULL); #else plantree_list = pg_plan_queries(querytree_list, sql, 0, NULL); #endif /* Done with the snapshot used for parsing/planning */ if (snapshot_set) PopActiveSnapshot(); /* If we got a cancel signal in analysis or planning, quit */ CHECK_FOR_INTERRUPTS(); /* * Execute the query using the unnamed portal. */ portal = CreatePortal("", true, true); /* Don't display the portal in pg_cursors */ portal->visible = false; PortalDefineQuery(portal, NULL, sql, commandTag, plantree_list, NULL); PortalStart(portal, NULL, 0, InvalidSnapshot); PortalSetResultFormat(portal, 1, &format); /* binary format */ --commands_remaining; receiver = CreateDestReceiver(DestNone); /* * Only once the portal and destreceiver have been established can * we return to the transaction context. All that stuff needs to * survive an internal commit inside PortalRun! */ MemoryContextSwitchTo(oldcontext); /* Here's where we actually execute the command. */ #if PG_VERSION_NUM < 100000 (void) PortalRun(portal, FETCH_ALL, isTopLevel, receiver, receiver, completionTag); #elif PG_VERSION_NUM < 130000 (void) PortalRun(portal, FETCH_ALL, isTopLevel,true, receiver, receiver, completionTag); #else (void) PortalRun(portal, FETCH_ALL, isTopLevel, true, receiver, receiver, &qc); #endif /* Clean up the receiver. */ (*receiver->rDestroy) (receiver); /* * Send a CommandComplete message even if we suppressed the query * results. The user backend will report these in the absence of * any true query results. */ #if PG_VERSION_NUM < 130000 EndCommand(completionTag, DestRemote); #else EndCommand(&qc, DestRemote, false); #endif /* Clean up the portal. */ PortalDrop(portal, false); } /* Be sure to advance the command counter after the last script command */ CommandCounterIncrement(); } /* * If a task is not marked as active, set an appropriate error state on the task * and return true. Note that this should only be called after a task has * already been launched. */ static bool jobCanceled(CronTask *task) { Assert(task->state == CRON_TASK_CONNECTING || \ task->state == CRON_TASK_SENDING || \ task->state == CRON_TASK_BGW_RUNNING || \ task->state == CRON_TASK_RUNNING); if (task->isActive) return false; else { /* Use the American spelling for consistency with PG code. */ task->errorMessage = "job canceled"; task->state = CRON_TASK_ERROR; /* * Technically, pollingStatus is only used by when UseBackgroundWorkers * is false, but no damage in setting it in both cases. */ task->pollingStatus = 0; return true; } } /* * If a task has hit it's startup deadline, set an appropriate error state on * the task and return true. Note that this should only be called after a task * has already been launched. */ static bool jobStartupTimeout(CronTask *task, TimestampTz currentTime) { Assert(task->state == CRON_TASK_CONNECTING || \ task->state == CRON_TASK_SENDING || \ task->state == CRON_TASK_BGW_START); if (TimestampDifferenceExceeds(task->startDeadline, currentTime, 0)) { task->errorMessage = "job startup timeout"; task->pollingStatus = 0; task->state = CRON_TASK_ERROR; return true; } else return false; } pg_cron-1.6.5/src/task_states.c000066400000000000000000000075751472654652200164650ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * src/task_states.c * * Logic for storing and manipulating cron task states. * * Copyright (c) 2016, Citus Data, Inc. * *------------------------------------------------------------------------- */ #include "postgres.h" #include "fmgr.h" #include "miscadmin.h" #include "cron.h" #include "pg_cron.h" #include "task_states.h" #include "access/hash.h" #include "utils/hsearch.h" #include "utils/memutils.h" /* forward declarations */ static HTAB * CreateCronTaskHash(void); static CronTask * GetCronTask(int64 jobId); /* global variables */ static MemoryContext CronTaskContext = NULL; static HTAB *CronTaskHash = NULL; /* settings */ bool LaunchActiveJobs = true; /* * InitializeTaskStateHash initializes the hash for storing task states. */ void InitializeTaskStateHash(void) { CronTaskContext = AllocSetContextCreate(CurrentMemoryContext, "pg_cron task context", ALLOCSET_DEFAULT_MINSIZE, ALLOCSET_DEFAULT_INITSIZE, ALLOCSET_DEFAULT_MAXSIZE); CronTaskHash = CreateCronTaskHash(); } /* * CreateCronTaskHash creates the hash for storing cron task states. */ static HTAB * CreateCronTaskHash(void) { HTAB *taskHash = NULL; HASHCTL info; int hashFlags = 0; memset(&info, 0, sizeof(info)); info.keysize = sizeof(int64); info.entrysize = sizeof(CronTask); info.hash = tag_hash; info.hcxt = CronTaskContext; hashFlags = (HASH_ELEM | HASH_FUNCTION | HASH_CONTEXT); taskHash = hash_create("pg_cron tasks", 32, &info, hashFlags); return taskHash; } /* * RefreshTaskHash reloads the cron jobs from the cron.job table. * If a job that has an active task has been removed, the task * is marked as inactive by this function. */ void RefreshTaskHash(void) { List *jobList = NIL; ListCell *jobCell = NULL; CronTask *task = NULL; HASH_SEQ_STATUS status; ResetJobMetadataCache(); hash_seq_init(&status, CronTaskHash); /* mark all tasks as inactive */ while ((task = hash_seq_search(&status)) != NULL) { task->isActive = false; } jobList = LoadCronJobList(); /* mark tasks that still have a job as active */ foreach(jobCell, jobList) { CronJob *job = (CronJob *) lfirst(jobCell); task = GetCronTask(job->jobId); task->isActive = LaunchActiveJobs && job->active; task->secondsInterval = job->schedule.secondsInterval; } CronJobCacheValid = true; } /* * GetCronTask gets the current task with the given job ID. */ static CronTask * GetCronTask(int64 jobId) { CronTask *task = NULL; int64 hashKey = jobId; bool isPresent = false; task = hash_search(CronTaskHash, &hashKey, HASH_ENTER, &isPresent); if (!isPresent) { InitializeCronTask(task, jobId); /* * We only initialize last run when entering into the hash. * The net effect is that the timer for the first run of an * interval job starts when pg_cron first learns about the job. */ task->lastStartTime = GetCurrentTimestamp(); } return task; } /* * InitializeCronTask intializes a CronTask struct. */ void InitializeCronTask(CronTask *task, int64 jobId) { task->runId = 0; task->jobId = jobId; task->state = CRON_TASK_WAITING; task->pendingRunCount = 0; task->connection = NULL; task->pollingStatus = 0; task->startDeadline = 0; task->isSocketReady = false; task->isActive = true; task->errorMessage = NULL; task->freeErrorMessage = false; } /* * CurrentTaskList extracts the current list of tasks from the * cron task hash. */ List * CurrentTaskList(void) { List *taskList = NIL; CronTask *task = NULL; HASH_SEQ_STATUS status; hash_seq_init(&status, CronTaskHash); while ((task = hash_seq_search(&status)) != NULL) { taskList = lappend(taskList, task); } return taskList; } /* * RemoveTask remove the task for the given job ID. */ void RemoveTask(int64 jobId) { bool isPresent = false; hash_search(CronTaskHash, &jobId, HASH_REMOVE, &isPresent); }