/* * insert_event.c - C implementation of pgq.insert_event_raw(). * * Copyright (c) 2007 Marko Kreen, Skype Technologies OÜ * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include "postgres.h" #include "funcapi.h" #include "catalog/pg_type.h" #include "commands/trigger.h" #include "executor/spi.h" #include "lib/stringinfo.h" #include "utils/builtins.h" #include "utils/datetime.h" #include "utils/hsearch.h" #include "access/xact.h" /* * Module tag */ #ifdef PG_MODULE_MAGIC PG_MODULE_MAGIC; #endif #ifndef TextDatumGetCString #define TextDatumGetCString(d) DatumGetCString(DirectFunctionCall1(textout, d)) #endif /* * Function tag */ Datum pgq_insert_event_raw(PG_FUNCTION_ARGS); PG_FUNCTION_INFO_V1(pgq_insert_event_raw); /* * Queue info fetching. * * Always touch ev_id sequence, even if ev_id is given as arg, * to notify ticker about new event. */ #define QUEUE_SQL \ "select queue_id::int4, queue_data_pfx::text," \ " queue_cur_table::int4, nextval(queue_event_seq)::int8," \ " queue_disable_insert::bool," \ " queue_per_tx_limit::int4" \ " from pgq.queue where queue_name = $1" #define COL_QUEUE_ID 1 #define COL_PREFIX 2 #define COL_TBLNO 3 #define COL_EVENT_ID 4 #define COL_DISABLED 5 #define COL_LIMIT 6 /* * Support inserting into pgq 2 queues. */ #define QUEUE_SQL_OLD \ "select queue_id::int4, queue_data_pfx::text," \ " queue_cur_table::int4, nextval(queue_event_seq)::int8," \ " false::bool as queue_disable_insert," \ " null::int4 as queue_per_tx_limit" \ " from pgq.queue where queue_name = $1" #define QUEUE_CHECK_NEW \ "select 1 from pg_catalog.pg_attribute" \ " where attname = 'queue_per_tx_limit'" \ " and attrelid = 'pgq.queue'::regclass" /* * Plan cache entry in HTAB. */ struct InsertCacheEntry { Oid queue_id; /* actually int32, but we want to use oid_hash */ int cur_table; TransactionId last_xid; int last_count; void *plan; }; /* * helper structure to pass values. */ struct QueueState { int queue_id; int cur_table; char *table_prefix; Datum next_event_id; bool disabled; int per_tx_limit; }; /* * Cached plans. */ static void *queue_plan; static HTAB *insert_cache; /* * Prepare utility plans and plan cache. */ static void init_cache(void) { static int init_done = 0; Oid types[1] = { TEXTOID }; HASHCTL ctl; int flags; int res; int max_queues = 128; const char *sql; if (init_done) return; /* * Check if old (v2.x) or new (v3.x) queue table. * * Needed for upgrades. */ res = SPI_execute(QUEUE_CHECK_NEW, 1, 0); if (res < 0) elog(ERROR, "pgq.insert_event: QUEUE_CHECK_NEW failed"); if (SPI_processed > 0) { sql = QUEUE_SQL; } else { sql = QUEUE_SQL_OLD; } /* * Init plans. */ queue_plan = SPI_saveplan(SPI_prepare(sql, 1, types)); if (queue_plan == NULL) elog(ERROR, "pgq_insert: SPI_prepare() failed"); /* * init insert plan cache. */ MemSet(&ctl, 0, sizeof(ctl)); ctl.keysize = sizeof(Oid); ctl.entrysize = sizeof(struct InsertCacheEntry); ctl.hash = oid_hash; flags = HASH_ELEM | HASH_FUNCTION; insert_cache = hash_create("pgq_insert_raw plans cache", max_queues, &ctl, flags); init_done = 1; } /* * Create new plan for insertion into current queue table. */ static void *make_plan(struct QueueState *state) { void *plan; StringInfo sql; static Oid types[10] = { INT8OID, TIMESTAMPTZOID, INT4OID, INT4OID, TEXTOID, TEXTOID, TEXTOID, TEXTOID, TEXTOID, TEXTOID }; /* * create sql */ sql = makeStringInfo(); appendStringInfo(sql, "insert into %s_%d (ev_id, ev_time, ev_owner, ev_retry," " ev_type, ev_data, ev_extra1, ev_extra2, ev_extra3, ev_extra4)" " values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", state->table_prefix, state->cur_table); /* * create plan */ plan = SPI_prepare(sql->data, 10, types); return SPI_saveplan(plan); } /* * fetch insert plan from cache. */ static void *load_insert_plan(Datum qname, struct QueueState *state) { struct InsertCacheEntry *entry; Oid queue_id = state->queue_id; bool did_exist = false; entry = hash_search(insert_cache, &queue_id, HASH_ENTER, &did_exist); if (did_exist) { if (entry->plan && state->cur_table == entry->cur_table) goto valid_table; if (entry->plan) SPI_freeplan(entry->plan); } entry->cur_table = state->cur_table; entry->last_xid = 0; entry->plan = NULL; /* this can fail, struct must be valid before */ entry->plan = make_plan(state); valid_table: if (state->per_tx_limit >= 0) { TransactionId xid = GetTopTransactionId(); if (entry->last_xid != xid) { entry->last_xid = xid; entry->last_count = 0; } entry->last_count++; if (entry->last_count > state->per_tx_limit) elog(ERROR, "Queue '%s' allows max %d events from one TX", TextDatumGetCString(qname), state->per_tx_limit); } return entry->plan; } /* * load queue info from pgq.queue table. */ static void load_queue_info(Datum queue_name, struct QueueState *state) { Datum values[1]; int res; TupleDesc desc; HeapTuple row; bool isnull; values[0] = queue_name; res = SPI_execute_plan(queue_plan, values, NULL, false, 0); if (res != SPI_OK_SELECT) elog(ERROR, "Queue fetch failed"); if (SPI_processed == 0) elog(ERROR, "No such queue"); row = SPI_tuptable->vals[0]; desc = SPI_tuptable->tupdesc; state->queue_id = DatumGetInt32(SPI_getbinval(row, desc, COL_QUEUE_ID, &isnull)); if (isnull) elog(ERROR, "queue id NULL"); state->cur_table = DatumGetInt32(SPI_getbinval(row, desc, COL_TBLNO, &isnull)); if (isnull) elog(ERROR, "table nr NULL"); state->table_prefix = SPI_getvalue(row, desc, COL_PREFIX); if (state->table_prefix == NULL) elog(ERROR, "table prefix NULL"); state->next_event_id = SPI_getbinval(row, desc, COL_EVENT_ID, &isnull); if (isnull) elog(ERROR, "Seq name NULL"); state->disabled = SPI_getbinval(row, desc, COL_DISABLED, &isnull); if (isnull) elog(ERROR, "insert_disabled NULL"); state->per_tx_limit = SPI_getbinval(row, desc, COL_LIMIT, &isnull); if (isnull) state->per_tx_limit = -1; } /* * Arguments: * 0: queue_name text NOT NULL * 1: ev_id int8 if NULL take from SEQ * 2: ev_time timestamptz if NULL use now() * 3: ev_owner int4 * 4: ev_retry int4 * 5: ev_type text * 6: ev_data text * 7: ev_extra1 text * 8: ev_extra2 text * 9: ev_extra3 text * 10:ev_extra4 text */ Datum pgq_insert_event_raw(PG_FUNCTION_ARGS) { Datum values[11]; char nulls[11]; struct QueueState state; int64 ret_id; void *ins_plan; Datum ev_id, ev_time; int i, res; Datum qname; if (PG_NARGS() < 6) elog(ERROR, "Need at least 6 arguments"); if (PG_ARGISNULL(0)) elog(ERROR, "Queue name must not be NULL"); qname = PG_GETARG_DATUM(0); if (SPI_connect() < 0) elog(ERROR, "SPI_connect() failed"); init_cache(); load_queue_info(qname, &state); /* * Check if queue has disable_insert flag set. */ #if defined(PG_VERSION_NUM) && PG_VERSION_NUM >= 80300 /* 8.3+: allow insert_event() even if connection is in 'replica' role */ if (state.disabled) { if (SessionReplicationRole != SESSION_REPLICATION_ROLE_REPLICA) elog(ERROR, "Insert into queue disallowed"); } #else /* pre-8.3 */ if (state.disabled) elog(ERROR, "Insert into queue disallowed"); #endif if (PG_ARGISNULL(1)) ev_id = state.next_event_id; else ev_id = PG_GETARG_DATUM(1); if (PG_ARGISNULL(2)) ev_time = DirectFunctionCall1(now, 0); else ev_time = PG_GETARG_DATUM(2); /* * Prepare arguments for INSERT */ values[0] = ev_id; nulls[0] = ' '; values[1] = ev_time; nulls[1] = ' '; for (i = 3; i < 11; i++) { int dst = i - 1; if (i >= PG_NARGS() || PG_ARGISNULL(i)) { values[dst] = (Datum)NULL; nulls[dst] = 'n'; } else { values[dst] = PG_GETARG_DATUM(i); nulls[dst] = ' '; } } /* * Perform INSERT into queue table. */ ins_plan = load_insert_plan(qname, &state); res = SPI_execute_plan(ins_plan, values, nulls, false, 0); if (res != SPI_OK_INSERT) elog(ERROR, "Queue insert failed"); /* * ev_id cannot pass SPI_finish() */ ret_id = DatumGetInt64(ev_id); if (SPI_finish() < 0) elog(ERROR, "SPI_finish failed"); PG_RETURN_INT64(ret_id); }