pax_global_header00006660000000000000000000000064131656331000014510gustar00rootroot0000000000000052 comment=c12e786950c070066b948e4dfe42fd1004f3af32 pg_cron-1.0.2/000077500000000000000000000000001316563310000131375ustar00rootroot00000000000000pg_cron-1.0.2/.gitignore000066400000000000000000000007051316563310000151310ustar00rootroot00000000000000# 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.0.2/CHANGELOG.md000066400000000000000000000011741316563310000147530ustar00rootroot00000000000000### 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.0.2/LICENSE000066400000000000000000000016261316563310000141510ustar00rootroot00000000000000Copyright (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.0.2/META.json000066400000000000000000000024671316563310000145710ustar00rootroot00000000000000{ "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.0.2/Makefile000066400000000000000000000012041316563310000145740ustar00rootroot00000000000000# src/test/modules/pg_cron/Makefile EXTENSION = pg_cron EXTVERSION = 1.0 DATA_built = $(EXTENSION)--$(EXTVERSION).sql DATA = $(wildcard $(EXTENSION)--*--*.sql) # compilation configuration MODULE_big = $(EXTENSION) OBJS = $(patsubst %.c,%.o,$(wildcard src/*.c)) PG_CPPFLAGS = -std=c99 -Wall -Wextra -Werror -Wno-unused-parameter -Wno-implicit-fallthrough -Iinclude -I$(libpq_srcdir) 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 $(EXTENSION)--0.1--1.0.sql cat $^ > $@ pg_cron-1.0.2/README.md000066400000000000000000000121431316563310000144170ustar00rootroot00000000000000[![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('0 10 * * *', 'VACUUM'); schedule ---------- 43 -- Stop scheduling a job SELECT cron.unschedule(43); 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 9.6: ```bash # Add Citus Data package repository curl https://install.citusdata.com/community/rpm.sh | sudo bash # Install the pg_cron extension sudo yum install -y pg_cron_96 ``` 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-9.6/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 and restart PostgreSQL. Note that pg_cron does not run any jobs as a long a server is in [hot standby](https://www.postgresql.org/docs/current/static/hot-standby.html) mode, but it automatically starts when the server is promoted. ``` # add to postgresql.conf: shared_preload_libraries = 'pg_cron' ``` After restarting PostgreSQL, you can create the pg_cron functions and metadata tables using `CREATE EXTENSION pg_cron`. By default, the pg_cron background worker expects its metadata tables to be created in the "postgres" database. However, you can configure this by setting the `cron.database_name` configuration parameter in postgresql.conf. ```sql -- run as superuser: CREATE EXTENSION pg_cron; -- optionally, grant usage to regular users: GRANT USAGE ON SCHEMA cron TO marco; ``` 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. ## Advanced usage Since pg_cron uses libpq, you can also run periodic jobs on other databases or other machines. This can be especially useful when you are using the [Citus extension](https://www.citusdata.com/product) to distribute tables across many PostgreSQL servers and need to run periodic jobs across all of them. If you are superuser, then you can manually modify the `cron.job` table and use custom values for nodename and nodeport to connect to a different machine: ```sql INSERT INTO cron.job (schedule, command, nodename, nodeport, database, username) VALUES ('0 4 * * *', 'VACUUM', 'worker-node-1', 5432, 'postgres', 'marco'); ``` You can use [.pgpass](https://www.postgresql.org/docs/current/static/libpq-pgpass.html) to allow pg_cron to authenticate with the remote server. pg_cron-1.0.2/github-banner.png000066400000000000000000000100531316563310000163710ustar00rootroot00000000000000PNG  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.0.2/include/000077500000000000000000000000001316563310000145625ustar00rootroot00000000000000pg_cron-1.0.2/include/bitstring.h000066400000000000000000000075651316563310000167550ustar00rootroot00000000000000/* * 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.0.2/include/cron.h000066400000000000000000000172741316563310000157070ustar00rootroot00000000000000/* 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.0.2/include/cron_job.h000066400000000000000000000021661316563310000165330ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * 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; #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 7 #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 #endif /* CRON_JOB_H */ pg_cron-1.0.2/include/job_metadata.h000066400000000000000000000014741316563310000173530ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * 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" /* job metadata data structure */ typedef struct CronJob { int64 jobId; char *scheduleText; entry schedule; char *command; char *nodeName; int nodePort; char *database; char *userName; } CronJob; /* global settings */ extern bool CronJobCacheValid; /* functions for retrieving job metadata */ extern void InitializeJobMetadataCache(void); extern void ResetJobMetadataCache(void); extern List * LoadCronJobList(void); extern CronJob * GetCronJob(int64 jobId); #endif pg_cron-1.0.2/include/pathnames.h000066400000000000000000000062251316563310000167200ustar00rootroot00000000000000/* 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.0.2/include/pg_cron.h000066400000000000000000000005571316563310000163710ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * 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.0.2/include/task_states.h000066400000000000000000000021431316563310000172600ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * 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 "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 } CronTaskState; 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; } 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.0.2/pg_cron--0.1--1.0.sql000066400000000000000000000001311316563310000162240ustar00rootroot00000000000000/* pg_cron--0.1--1.0.sql */ SELECT pg_catalog.pg_extension_config_dump('cron.job', ''); pg_cron-1.0.2/pg_cron.control000066400000000000000000000001711316563310000161670ustar00rootroot00000000000000comment = 'Job scheduler for PostgreSQL' default_version = '1.0' module_pathname = '$libdir/pg_cron' relocatable = false pg_cron-1.0.2/pg_cron.sql000066400000000000000000000037031316563310000153120ustar00rootroot00000000000000DO $$ BEGIN IF current_database() <> current_setting('cron.database_name') 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.0.2/src/000077500000000000000000000000001316563310000137265ustar00rootroot00000000000000pg_cron-1.0.2/src/entry.c000066400000000000000000000263231316563310000152410ustar00rootroot00000000000000/* 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(e) entry *e; { 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 = NULL; 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) { 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) 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. */ e = (entry *) calloc(sizeof(entry), sizeof(char)); 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)) { 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 equivilent */ 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); if (e->cmd) free(e->cmd); free(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 implmented 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.0.2/src/job_metadata.c000066400000000000000000000370571316563310000165200ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * 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/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/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 #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" /* forward declarations */ static HTAB * CreateCronJobHash(void); static int64 NextJobId(void); static Oid CronExtensionOwner(void); 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); /* SQL-callable functions */ PG_FUNCTION_INFO_V1(cron_schedule); PG_FUNCTION_INFO_V1(cron_unschedule); PG_FUNCTION_INFO_V1(cron_job_cache_invalidate); /* global variables */ static MemoryContext CronJobContext = NULL; static HTAB *CronJobHash = NULL; static Oid CachedCronJobRelationId = InvalidOid; bool CronJobCacheValid = false; /* * 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; } /* * cluster_schedule schedules a cron job. */ Datum cron_schedule(PG_FUNCTION_ARGS) { text *scheduleText = PG_GETARG_TEXT_P(0); text *commandText = PG_GETARG_TEXT_P(1); char *schedule = text_to_cstring(scheduleText); char *command = text_to_cstring(commandText); entry *parsedSchedule = NULL; int64 jobId = 0; Datum jobIdDatum = 0; Oid cronSchemaId = InvalidOid; Oid cronJobsRelationId = InvalidOid; Relation cronJobsTable = NULL; TupleDesc tupleDescriptor = NULL; HeapTuple heapTuple = NULL; Datum values[Natts_cron_job]; bool isNulls[Natts_cron_job]; Oid userId = GetUserId(); char *userName = GetUserNameFromId(userId, false); parsedSchedule = parse_cron_entry(schedule); if (parsedSchedule == NULL) { ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid schedule: %s", schedule))); } free_entry(parsedSchedule); /* form new job tuple */ memset(values, 0, sizeof(values)); memset(isNulls, false, sizeof(isNulls)); jobId = NextJobId(); jobIdDatum = Int64GetDatum(jobId); values[Anum_cron_job_jobid - 1] = jobIdDatum; values[Anum_cron_job_schedule - 1] = CStringGetTextDatum(schedule); values[Anum_cron_job_command - 1] = CStringGetTextDatum(command); values[Anum_cron_job_nodename - 1] = CStringGetTextDatum("localhost"); values[Anum_cron_job_nodeport - 1] = Int32GetDatum(PostPortNumber); values[Anum_cron_job_database - 1] = CStringGetTextDatum(CronTableDatabaseName); values[Anum_cron_job_username - 1] = CStringGetTextDatum(userName); cronSchemaId = get_namespace_oid(CRON_SCHEMA_NAME, false); cronJobsRelationId = get_relname_relid(JOBS_TABLE_NAME, cronSchemaId); /* open jobs relation and insert new tuple */ cronJobsTable = heap_open(cronJobsRelationId, RowExclusiveLock); tupleDescriptor = RelationGetDescr(cronJobsTable); heapTuple = heap_form_tuple(tupleDescriptor, values, isNulls); #if (PG_VERSION_NUM >= 100000) CatalogTupleInsert(cronJobsTable, heapTuple); #else simple_heap_insert(cronJobsTable, heapTuple); CatalogUpdateIndexes(cronJobsTable, heapTuple); #endif CommandCounterIncrement(); /* close relation and invalidate previous cache entry */ heap_close(cronJobsTable, NoLock); InvalidateJobCache(); PG_RETURN_INT64(jobId); } /* * NextJobId returns a new, unique job ID using the job ID sequence. */ static int64 NextJobId(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; /* resolve relationId from passed in schema and relation name */ sequenceName = cstring_to_text(JOB_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 = DatumGetUInt32(jobIdDatum); 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 = heap_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); heap_close(extensionRelation, AccessShareLock); return extensionOwner; } /* * cluster_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; TupleDesc tupleDescriptor = NULL; HeapTuple heapTuple = NULL; bool isNull = false; Oid userId = InvalidOid; char *userName = NULL; Datum ownerNameDatum = 0; char *ownerName = NULL; cronSchemaId = get_namespace_oid(CRON_SCHEMA_NAME, false); cronJobIndexId = get_relname_relid(JOB_ID_INDEX_NAME, cronSchemaId); cronJobsTable = heap_open(CronJobRelationId(), RowExclusiveLock); ScanKeyInit(&scanKey[0], Anum_cron_job_jobid, BTEqualStrategyNumber, F_INT8EQ, Int64GetDatum(jobId)); scanDescriptor = systable_beginscan(cronJobsTable, cronJobIndexId, indexOK, NULL, scanKeyCount, scanKey); tupleDescriptor = RelationGetDescr(cronJobsTable); heapTuple = systable_getnext(scanDescriptor); if (!HeapTupleIsValid(heapTuple)) { ereport(ERROR, (errmsg("could not find valid entry for job " UINT64_FORMAT, jobId))); } /* check if the current user owns the row */ userId = GetUserId(); userName = GetUserNameFromId(userId, false); ownerNameDatum = heap_getattr(heapTuple, Anum_cron_job_username, tupleDescriptor, &isNull); 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, ACL_KIND_CLASS, get_rel_name(CronJobRelationId())); } } simple_heap_delete(cronJobsTable, &heapTuple->t_self); systable_endscan(scanDescriptor); heap_close(cronJobsTable, NoLock); CommandCounterIncrement(); InvalidateJobCache(); PG_RETURN_BOOL(true); } /* * 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(); pgstat_report_activity(STATE_IDLE, NULL); return NIL; } cronJobTable = heap_open(CronJobRelationId(), AccessShareLock); scanDescriptor = systable_beginscan(cronJobTable, InvalidOid, false, NULL, scanKeyCount, scanKey); tupleDescriptor = RelationGetDescr(cronJobTable); heapTuple = systable_getnext(scanDescriptor); while (HeapTupleIsValid(heapTuple)) { MemoryContext oldContext = NULL; CronJob *job = NULL; oldContext = MemoryContextSwitchTo(CronJobContext); job = TupleToCronJob(tupleDescriptor, heapTuple); jobList = lappend(jobList, job); MemoryContextSwitchTo(oldContext); heapTuple = systable_getnext(scanDescriptor); } systable_endscan(scanDescriptor); heap_close(cronJobTable, AccessShareLock); PopActiveSnapshot(); CommitTransactionCommand(); pgstat_report_activity(STATE_IDLE, NULL); MemoryContextSwitchTo(originalContext); 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); Assert(!HeapTupleHasNulls(heapTuple)); jobKey = DatumGetUInt32(jobId); job = hash_search(CronJobHash, &jobKey, HASH_ENTER, &isPresent); job->jobId = DatumGetUInt32(jobId); job->scheduleText = TextDatumGetCString(schedule); job->command = TextDatumGetCString(command); job->nodeName = TextDatumGetCString(nodeName); job->nodePort = DatumGetUInt32(nodePort); job->userName = TextDatumGetCString(userName); job->database = TextDatumGetCString(database); 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 %ld: %s", 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; } pg_cron-1.0.2/src/misc.c000066400000000000000000000065371316563310000150400ustar00rootroot00000000000000/* 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'; } 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.0.2/src/pg_cron.c000066400000000000000000000662721316563310000155360ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * src/pg_cron.c * * Implementation of the pg_cron task scheduler. * * 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/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" #include "poll.h" #include "sys/time.h" #include "sys/poll.h" #include "time.h" #include "access/genam.h" #include "access/heapam.h" #include "access/htup_details.h" #include "access/xact.h" #include "access/xlog.h" #include "catalog/pg_extension.h" #include "catalog/indexing.h" #include "catalog/namespace.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/pqsignal.h" #include "mb/pg_wchar.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/rel.h" #include "utils/snapmgr.h" #include "utils/syscache.h" #include "utils/timestamp.h" #if (PG_VERSION_NUM >= 100000) #include "utils/varlena.h" #endif #include "tcop/utility.h" PG_MODULE_MAGIC; /* 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); void PgCronWorkerMain(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); /* global settings */ char *CronTableDatabaseName = "postgres"; static bool CronLogStatement = true; /* flags set by signal handlers */ static volatile sig_atomic_t got_sigterm = false; /* global variables */ static int64 RunCount = 0; /* counter for assigning unique run IDs */ 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; /* * _PG_init gets called when the extension is loaded. */ void _PG_init(void) { BackgroundWorker worker; 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); 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); /* 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 = PgCronWorkerMain; #endif worker.bgw_main_arg = Int32GetDatum(0); worker.bgw_notify_pid = 0; sprintf(worker.bgw_library_name, "pg_cron"); sprintf(worker.bgw_function_name, "PgCronWorkerMain"); snprintf(worker.bgw_name, BGW_MAXLEN, "pg_cron_scheduler"); 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; if (MyProc != NULL) { SetLatch(&MyProc->procLatch); } } /* * PgCronWorkerMain is the main entry-point for the background worker * that performs tasks. */ void PgCronWorkerMain(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 */ BackgroundWorkerInitializeConnection(CronTableDatabaseName, NULL); /* 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 (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"))); MemoryContextSwitchTo(CronLoopContext); while (!got_sigterm) { List *taskList = NIL; TimestampTz currentTime = 0; AcceptInvalidationMessages(); if (!CronJobCacheValid) { RefreshTaskHash(); } taskList = CurrentTaskList(); currentTime = GetCurrentTimestamp(); StartAllPendingRuns(taskList, currentTime); WaitForCronTasks(taskList); ManageCronTasks(taskList, currentTime); MemoryContextReset(CronLoopContext); } ereport(LOG, (errmsg("pg_cron scheduler shutting down"))); 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); 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_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; 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->runId = RunCount++; task->pendingRunCount -= 1; task->state = CRON_TASK_START; RunningTaskCount++; } case CRON_TASK_START: { 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 %ld starting: %s", jobId, 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; break; } case CRON_TASK_CONNECTING: { PostgresPollingStatusType pollingStatus = 0; /* check if job has been removed */ if (!task->isActive) { task->errorMessage = "job cancelled"; task->pollingStatus = 0; task->state = CRON_TASK_ERROR; break; } /* check if timeout has been reached */ if (TimestampDifferenceExceeds(task->startDeadline, currentTime, 0)) { task->errorMessage = "connection timeout"; task->pollingStatus = 0; task->state = CRON_TASK_ERROR; 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) { /* wait for socket to be ready to send a query */ task->pollingStatus = PGRES_POLLING_WRITING; task->state = CRON_TASK_SENDING; } 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; /* check if job has been removed */ if (!task->isActive) { task->errorMessage = "job cancelled"; task->pollingStatus = 0; task->state = CRON_TASK_ERROR; break; } /* check if timeout has been reached */ if (TimestampDifferenceExceeds(task->startDeadline, currentTime, 0)) { task->errorMessage = "connection timeout"; task->pollingStatus = 0; task->state = CRON_TASK_ERROR; 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; } else { /* not yet ready to send */ } break; } case CRON_TASK_RUNNING: { int connectionBusy = 0; PGresult *result = NULL; /* check if job has been removed */ if (!task->isActive) { task->errorMessage = "job cancelled"; task->pollingStatus = 0; task->state = CRON_TASK_ERROR; 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) { ExecStatusType executionStatus = PQresultStatus(result); switch (executionStatus) { case PGRES_COMMAND_OK: { if (CronLogStatement) { char *cmdStatus = PQcmdStatus(result); char *cmdTuples = PQcmdTuples(result); ereport(LOG, (errmsg("cron job %ld completed: %s %s", 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; 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; PQclear(result); return; } case PGRES_TUPLES_OK: case PGRES_EMPTY_QUERY: case PGRES_SINGLE_TUPLE: case PGRES_NONFATAL_ERROR: default: { if (CronLogStatement) { int tupleCount = PQntuples(result); char *rowString = ngettext("row", "rows", tupleCount); ereport(LOG, (errmsg("cron job %ld completed: " "%d %s", jobId, tupleCount, rowString))); } break; } } PQclear(result); } PQfinish(connection); task->connection = NULL; task->pollingStatus = 0; task->isSocketReady = false; task->state = CRON_TASK_DONE; RunningTaskCount--; break; } case CRON_TASK_ERROR: { if (connection != NULL) { PQfinish(connection); task->connection = NULL; } if (!task->isActive) { RemoveTask(jobId); } if (task->errorMessage != NULL) { ereport(LOG, (errmsg("cron job %ld %s", jobId, task->errorMessage))); if (task->freeErrorMessage) { free(task->errorMessage); } } else { ereport(LOG, (errmsg("cron job %ld failed", jobId))); } task->startDeadline = 0; task->isSocketReady = false; task->state = CRON_TASK_DONE; RunningTaskCount--; /* fall through to CRON_TASK_DONE */ } case CRON_TASK_DONE: default: { InitializeCronTask(task, jobId); } } } pg_cron-1.0.2/src/task_states.c000066400000000000000000000067531316563310000164320ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * 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 "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 = true; } 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); }