pax_global_header00006660000000000000000000000064141236521640014516gustar00rootroot0000000000000052 comment=005d20c54c7adb55fe7e55454881150d3f6038d1 pg_cron-1.4.1/000077500000000000000000000000001412365216400131505ustar00rootroot00000000000000pg_cron-1.4.1/.gitignore000066400000000000000000000007051412365216400151420ustar00rootroot00000000000000# 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 pg_cron-1.4.1/CHANGELOG.md000066400000000000000000000053611412365216400147660ustar00rootroot00000000000000### 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.4.1/LICENSE000066400000000000000000000016261412365216400141620ustar00rootroot00000000000000Copyright (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.4.1/META.json000066400000000000000000000024671412365216400146020ustar00rootroot00000000000000{ "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.4.1/Makefile000066400000000000000000000014551412365216400146150ustar00rootroot00000000000000# src/test/modules/pg_cron/Makefile EXTENSION = pg_cron EXTVERSION = 1.3 DATA_built = $(EXTENSION)--1.0.sql DATA = $(wildcard $(EXTENSION)--*--*.sql) 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-maybe-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 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.4.1/README.md000066400000000000000000000144431412365216400144350ustar00rootroot00000000000000[![Citus Banner](/github-banner.png)](https://www.citusdata.com/) [![Slack Status](http://slack.citusdata.com/badge.svg)](https://slack.citusdata.com) ## What is pg_cron? pg_cron is a simple cron-based job scheduler for PostgreSQL (9.5 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: ```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 (1 row) SELECT cron.unschedule(42); unschedule ------------ t ``` 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) │ │ │ ┌──────────────── 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. Be aware that pg_cron always uses GMT! ## Installing pg_cron Install on Red Hat, CentOS, Fedora, Amazon Linux with PostgreSQL 12 using [PGDG](https://yum.postgresql.org/repopackages/): ```bash # Install the pg_cron extension sudo yum install -y pg_cron_12 ``` Install on Debian, Ubuntu with PostgreSQL 12 using [apt.postgresql.org](https://wiki.postgresql.org/wiki/Apt): ```bash # Install the pg_cron extension sudo apt-get -y install postgresql-12-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-12/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. 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: shared_preload_libraries = 'pg_cron' cron.database_name = 'postgres' ``` 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; ``` **Important**: Internally, pg_cron uses libpq to open a new connection to the local database. It may be necessary to enable `trust` authentication for connections coming from localhost in [pg_hba.conf](https://www.postgresql.org/docs/current/static/auth-pg-hba-conf.html) for the user running the cron job. Alternatively, 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. 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. ## 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) ## 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: | | [Citus Cloud](https://www.citusdata.com/product/cloud) | :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/docs/postgres/) | :x: | | [Heroku](https://elements.heroku.com/addons/heroku-postgresql) | :x: | | | [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: | pg_cron-1.4.1/expected/000077500000000000000000000000001412365216400147515ustar00rootroot00000000000000pg_cron-1.4.1/expected/pg_cron-test.out000066400000000000000000000201101412365216400201000ustar00rootroot00000000000000CREATE EXTENSION pg_cron VERSION '1.0'; SELECT extversion FROM pg_extension WHERE extname='pg_cron'; extversion ------------ 1.0 (1 row) ALTER EXTENSION pg_cron UPDATE TO '1.4'; SELECT extversion FROM pg_extension WHERE extname='pg_cron'; extversion ------------ 1.4 (1 row) SET cron.enable_superuser_jobs TO on; -- 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 -- Try to update pg_cron on restart SELECT cron.schedule('@restar', 'ALTER EXTENSION pg_cron UPDATE'); ERROR: invalid schedule: @restar 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) -- Try to schedule a job as superuser when it is not allowed SET cron.enable_superuser_jobs TO off; SELECT cron.schedule(job_name:='disallowed-superuser', schedule:='* * * * *', command:='drop database pg_crondbno'); ERROR: cannot schedule jobs as superuser DETAIL: Scheduling jobs as superuser is disallowed when cron.enable_superuser_jobs is set to off. SELECT cron.alter_job(7, username := current_user); ERROR: cannot schedule jobs as superuser DETAIL: Scheduling jobs as superuser is disallowed when cron.enable_superuser_jobs is set to off. -- Scheduling as other users is allowed as superuser SELECT cron.schedule_in_database(job_name:='more vacuum', schedule:='0 12 * * *', command:='VACUUM', database:=current_database(), username:='pgcron_cront'); schedule_in_database ---------------------- 8 (1 row) SELECT cron.alter_job(7, username := 'pgcron_cront'); alter_job ----------- (1 row) -- cleaning DROP EXTENSION pg_cron; drop user pgcron_cront; drop database pgcron_dbno; drop database pgcron_dbyes; pg_cron-1.4.1/github-banner.png000066400000000000000000000100531412365216400164020ustar00rootroot00000000000000PNG  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.4.1/include/000077500000000000000000000000001412365216400145735ustar00rootroot00000000000000pg_cron-1.4.1/include/bitstring.h000066400000000000000000000075651412365216400167660ustar00rootroot00000000000000/* * 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.4.1/include/cron.h000066400000000000000000000172741412365216400157200ustar00rootroot00000000000000/* 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 Skip_Blanks(c, f) \ while (c == '\t' || 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 SECONDS_PER_MINUTE 60 #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; char *cmd; 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 } 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.4.1/include/cron_job.h000066400000000000000000000037161412365216400165460ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * 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; Name 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.4.1/include/job_metadata.h000066400000000000000000000030111412365216400173510ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * 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; Name 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); #endif pg_cron-1.4.1/include/pathnames.h000066400000000000000000000062251412365216400167310ustar00rootroot00000000000000/* 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.4.1/include/pg_cron.h000066400000000000000000000005561412365216400164010ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * 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; #endif pg_cron-1.4.1/include/task_states.h000066400000000000000000000025111412365216400172700ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * 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 "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; bool isSocketReady; bool isActive; char *errorMessage; bool freeErrorMessage; 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.4.1/pg_cron--1.0--1.1.sql000066400000000000000000000001451412365216400162430ustar00rootroot00000000000000/* pg_cron--1.0--1.1.sql */ ALTER TABLE cron.job ADD COLUMN active boolean not null default 'true'; pg_cron-1.4.1/pg_cron--1.1--1.2.sql000066400000000000000000000002331412365216400162430ustar00rootroot00000000000000/* 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.4.1/pg_cron--1.2--1.3.sql000066400000000000000000000025771412365216400162620ustar00rootroot00000000000000/* 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 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 = 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.4.1/pg_cron--1.3--1.4.sql000066400000000000000000000031461412365216400162550ustar00rootroot00000000000000/* 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.4.1/pg_cron.control000066400000000000000000000001711412365216400162000ustar00rootroot00000000000000comment = 'Job scheduler for PostgreSQL' default_version = '1.4' module_pathname = '$libdir/pg_cron' relocatable = false pg_cron-1.4.1/pg_cron.sql000066400000000000000000000037621412365216400153300ustar00rootroot00000000000000DO $$ BEGIN IF current_database() <> current_setting('cron.database_name') AND current_database() <> 'contrib_regression' THEN RAISE EXCEPTION 'can only create extension in database %', current_setting('cron.database_name') USING DETAIL = 'Jobs must be scheduled from the database configured in '|| 'cron.database_name, since the pg_cron background worker '|| 'reads job descriptions from this database.', HINT = format('Add cron.database_name = ''%s'' in postgresql.conf '|| 'to use the current database.', current_database()); END IF; END; $$; CREATE SCHEMA cron; CREATE SEQUENCE cron.jobid_seq; CREATE TABLE cron.job ( jobid bigint primary key default nextval('cron.jobid_seq'), schedule text not null, command text not null, nodename text not null default 'localhost', nodeport int not null default inet_server_port(), database text not null default 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 = 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.4.1/sql/000077500000000000000000000000001412365216400137475ustar00rootroot00000000000000pg_cron-1.4.1/sql/pg_cron-test.sql000066400000000000000000000107301412365216400170750ustar00rootroot00000000000000CREATE EXTENSION pg_cron VERSION '1.0'; SELECT extversion FROM pg_extension WHERE extname='pg_cron'; ALTER EXTENSION pg_cron UPDATE TO '1.4'; SELECT extversion FROM pg_extension WHERE extname='pg_cron'; SET cron.enable_superuser_jobs TO on; -- 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), ''); -- 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; -- Try to schedule a job as superuser when it is not allowed SET cron.enable_superuser_jobs TO off; SELECT cron.schedule(job_name:='disallowed-superuser', schedule:='* * * * *', command:='drop database pg_crondbno'); SELECT cron.alter_job(7, username := current_user); -- Scheduling as other users is allowed as superuser SELECT cron.schedule_in_database(job_name:='more vacuum', schedule:='0 12 * * *', command:='VACUUM', database:=current_database(), username:='pgcron_cront'); SELECT cron.alter_job(7, username := 'pgcron_cront'); -- cleaning DROP EXTENSION pg_cron; drop user pgcron_cront; drop database pgcron_dbno; drop database pgcron_dbyes; pg_cron-1.4.1/src/000077500000000000000000000000001412365216400137375ustar00rootroot00000000000000pg_cron-1.4.1/src/entry.c000066400000000000000000000263671412365216400152620ustar00rootroot00000000000000/* 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 char 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); void free_entry(entry *e) { if (e->cmd) free(e->cmd); 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)){ bit_set(e->minute, 0); bit_set(e->hour, 0); bit_set(e->dom, 0); bit_set(e->month, 0); bit_nset(e->dow, 0, (LAST_DOW-FIRST_DOW+1)); e->flags |= DOW_STAR; } else if (!strcmp("monthly", cmd)) { bit_set(e->minute, 0); bit_set(e->hour, 0); bit_set(e->dom, 0); bit_nset(e->month, 0, (LAST_MONTH-FIRST_MONTH+1)); bit_nset(e->dow, 0, (LAST_DOW-FIRST_DOW+1)); e->flags |= DOW_STAR; } else if (!strcmp("weekly", cmd)) { bit_set(e->minute, 0); bit_set(e->hour, 0); bit_nset(e->dom, 0, (LAST_DOM-FIRST_DOM+1)); e->flags |= DOM_STAR; bit_nset(e->month, 0, (LAST_MONTH-FIRST_MONTH+1)); bit_nset(e->dow, 0,0); } else if (!strcmp("daily", cmd) || !strcmp("midnight", cmd)) { bit_set(e->minute, 0); bit_set(e->hour, 0); bit_nset(e->dom, 0, (LAST_DOM-FIRST_DOM+1)); bit_nset(e->month, 0, (LAST_MONTH-FIRST_MONTH+1)); bit_nset(e->dow, 0, (LAST_DOW-FIRST_DOW+1)); } else if (!strcmp("hourly", cmd)) { bit_set(e->minute, 0); bit_nset(e->hour, 0, (LAST_HOUR-FIRST_HOUR+1)); bit_nset(e->dom, 0, (LAST_DOM-FIRST_DOM+1)); bit_nset(e->month, 0, (LAST_MONTH-FIRST_MONTH+1)); bit_nset(e->dow, 0, (LAST_DOW-FIRST_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 == '*') 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(LOG, "failed to parse entry %d", ecode); free_entry(e); while (ch != EOF && ch != '\n') ch = get_char(file); return NULL; } static char get_list(bits, low, high, names, ch, 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+1)); /* 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 char get_range(bits, low, high, names, ch, 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 ] */ register int i; 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) 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). */ for (i = num1; i <= num2; i += num3) if (EOF == set_element(bits, low, high, i)) return EOF; return ch; } static char get_number(numptr, low, names, ch, 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; 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(bits, low, high, 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; bit_set(bits, (number-low)); return OK; } pg_cron-1.4.1/src/job_metadata.c000066400000000000000000001070121412365216400165160ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * 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" #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/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 InvalidateJobCacheCallback(Datum argument, Oid relationId); 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); /* 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) { /* watch for invalidation events */ CacheRegisterRelcacheCallback(InvalidateJobCacheCallback, (Datum) 0); 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) { MemoryContextResetAndDeleteChildren(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 = parse_cron_entry(schedule); if (parsedSchedule == NULL) { ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid schedule: %s", schedule))); } 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"); } 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 */ aclresult = pg_database_aclcheck(get_database_oid(database_name, false), userIdcheckacl, ACL_CONNECT); 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 jobIdDatum = 0; int64 jobId = 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 */ jobIdDatum = DirectFunctionCall1(nextval_oid, sequenceIdDatum); SetUserIdAndSecContext(savedUserId, savedSecurityContext); jobId = DatumGetInt64(jobIdDatum); PopActiveSnapshot(); CommitTransactionCommand(); MemoryContextSwitchTo(originalContext); return jobId; } /* * 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); Name jobName = DatumGetName(jobNameDatum); 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; cronJobsTable = table_open(CronJobRelationId(), RowExclusiveLock); ScanKeyInit(&scanKey[0], Anum_cron_job_jobname, BTEqualStrategyNumber, F_NAMEEQ, 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'", NameStr(*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. */ static 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 and adds each job to the CronJobHash. */ 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); pgstat_report_activity(STATE_IDLE, NULL); 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; Oid jobOwnerId = InvalidOid; oldContext = MemoryContextSwitchTo(CronJobContext); job = TupleToCronJob(tupleDescriptor, heapTuple); jobOwnerId = get_role_oid(job->userName, 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))); } else { jobList = lappend(jobList, job); } MemoryContextSwitchTo(oldContext); heapTuple = systable_getnext(scanDescriptor); } systable_endscan(scanDescriptor); table_close(cronJobTable, AccessShareLock); PopActiveSnapshot(); CommitTransactionCommand(); MemoryContextSwitchTo(originalContext); pgstat_report_activity(STATE_IDLE, NULL); return jobList; } /* * TupleToCronJob takes a heap tuple and converts it into a CronJob * struct. */ static CronJob * TupleToCronJob(TupleDesc tupleDescriptor, HeapTuple heapTuple) { CronJob *job = NULL; int64 jobKey = 0; bool isNull = false; bool isPresent = false; entry *parsedSchedule = NULL; 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); 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 = TextDatumGetCString(userName); 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) { job->jobName = DatumGetName(jobName); } else { job->jobName = NULL; } } parsedSchedule = parse_cron_entry(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); pgstat_report_activity(STATE_RUNNING, querybuf.data); 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); pgstat_report_activity(STATE_IDLE, NULL); } 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); pgstat_report_activity(STATE_RUNNING, querybuf.data); 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); pgstat_report_activity(STATE_IDLE, NULL); } 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 */ aclresult = pg_database_aclcheck(get_database_oid(database_name, false), userIdcheckacl, ACL_CONNECT); 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 = parse_cron_entry(schedule); if (parsedSchedule == NULL) { ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid schedule: %s", schedule))); } 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 %ld 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)); pgstat_report_activity(STATE_RUNNING, querybuf.data); 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); pgstat_report_activity(STATE_IDLE, NULL); } 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; } pg_cron-1.4.1/src/misc.c000066400000000000000000000066621412365216400150500ustar00rootroot00000000000000/* 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 *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(ch, file) 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 >= 1024) { 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(string, size, file, terms) 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 *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.4.1/src/pg_cron.c000066400000000000000000001562231412365216400155430ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * 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" #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); static void pg_cron_background_worker_sigterm(SIGNAL_ARGS); void PgCronLauncherMain(Datum arg); 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 GetBgwTaskFeedback(shm_mq_handle *responseq, 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; 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."))); } 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_USERSET, 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); if (!UseBackgroundWorkers) DefineCustomIntVariable( "cron.max_running_jobs", gettext_noop("Maximum number of jobs that can run concurrently."), NULL, &MaxRunningTasks, 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, 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); /* 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); } /* * Signal handler for SIGTERM for background workers * When we receive a SIGTERM, we set InterruptPending and ProcDiePending * just like a normal backend. The next CHECK_FOR_INTERRUPTS() will do the * right thing. */ static void pg_cron_background_worker_sigterm(SIGNAL_ARGS) { int save_errno = errno; if (MyProc) SetLatch(&MyProc->procLatch); if (!proc_exit_inprogress) { InterruptPending = true; ProcDiePending = true; } errno = save_errno; } /* * 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; AcceptInvalidationMessages(); if (!CronJobCacheValid) { RefreshTaskHash(); } 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; } taskList = CurrentTaskList(); currentTime = GetCurrentTimestamp(); StartAllPendingRuns(taskList, currentTime); WaitForCronTasks(taskList); ManageCronTasks(taskList, currentTime); MemoryContextReset(CronLoopContext); } ereport(LOG, (errmsg("pg_cron scheduler shutting down"))); proc_exit(0); } /* * 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->pendingRunCount += 1; } } RebootJobsScheduled = true; } 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) { time_t currentTime_t = timestamptz_to_time_t(currentTime); struct tm *tm = gmtime(¤tTime_t); int minute = tm->tm_min -FIRST_MINUTE; int hour = tm->tm_hour -FIRST_HOUR; int dayOfMonth = tm->tm_mday -FIRST_DOM; int month = tm->tm_mon +1 -FIRST_MONTH; int dayOfWeek = tm->tm_wday -FIRST_DOW; if (bit_test(schedule->minute, minute) && bit_test(schedule->hour, hour) && bit_test(schedule->month, month) && ( ((schedule->flags & DOM_STAR) || (schedule->flags & DOW_STAR)) ? (bit_test(schedule->dow,dayOfWeek) && bit_test(schedule->dom,dayOfMonth)) : (bit_test(schedule->dow,dayOfWeek) || bit_test(schedule->dom,dayOfMonth)))) { if ((doNonWild && !(schedule->flags & (MIN_STAR|HR_STAR))) || (doWild && (schedule->flags & (MIN_STAR|HR_STAR)))) { 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); 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) { /* 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) { pfree(polledTasks); pfree(pollFDs); return; } 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; TimestampTz start_time; 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; 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); 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; } start_time = GetCurrentTimestamp(); if (CronLogRun) UpdateJobRunDetail(task->runId, &pid, GetCronStatus(CRON_STATUS_RUNNING), NULL, &start_time, 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; start_time = GetCurrentTimestamp(); if (CronLogRun) UpdateJobRunDetail(task->runId, NULL, GetCronStatus(CRON_STATUS_RUNNING), NULL, &start_time, 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; shm_mq_handle *responseq; shm_mq *mq; shm_toc *toc; 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; } toc = shm_toc_attach(PG_CRON_MAGIC, dsm_segment_address(task->seg)); #if PG_VERSION_NUM < 100000 mq = shm_toc_lookup(toc, PG_CRON_KEY_QUEUE); #else mq = shm_toc_lookup(toc, PG_CRON_KEY_QUEUE, false); #endif responseq = shm_mq_attach(mq, task->seg, NULL); /* still waiting for job to complete */ if (GetBackgroundWorkerPid(&task->handle, &pid) != BGWH_STOPPED) { GetBgwTaskFeedback(responseq, task, true); shm_mq_detach(responseq); break; } GetBgwTaskFeedback(responseq, task, false); 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) UpdateJobRunDetail(task->runId, NULL, GetCronStatus(CRON_STATUS_FAILED), task->errorMessage, NULL, NULL); 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; InitializeCronTask(task, jobId); /* * 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); } static void GetBgwTaskFeedback(shm_mq_handle *responseq, CronTask *task, bool running) { 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 (;;) { /* Get next message. */ res = shm_mq_receive(responseq, &nbytes, &data, false); 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; pqsignal(SIGTERM, pg_cron_background_worker_sigterm); 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(); ProcessCompletedNotifies(); 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 >= 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.4.1/src/task_states.c000066400000000000000000000070231412365216400164320ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * 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; /* * 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); CronTask *task = GetCronTask(job->jobId); task->isActive = job->active; } 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); } 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); }