You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dorm/vendor/github.com/godror/godror/subscr.go

354 lines
11 KiB

// Copyright 2017, 2020 The Godror Authors
//
//
// SPDX-License-Identifier: UPL-1.0 OR Apache-2.0
package godror
/*
#include <stdlib.h>
#include <stdio.h>
#include "dpiImpl.h"
void CallbackSubscrDebug(void *context, dpiSubscrMessage *message);
*/
import "C"
import (
"errors"
"fmt"
"log"
"runtime"
"strings"
"sync"
"unsafe"
)
// SubscriptionOption is for setting various parameters of the Subscription.
type SubscriptionOption func(*subscriptionParams)
// SubscrHostPort is a SubscriptionOption that sets tha IPAddress and Port to the specified values.
//
// The address is on which the subscription listens to receive notifications,
// for a server-initiated connection.
//
// The address can be an IPv4 address in dotted decimal format such as 192.1.2.34
// or an IPv6 address in hexadecimal format such as 2001:0db8:0000:0000:0217:f2ff:fe4b:4ced.
//
// By default (address is the empty string), an IP address will be selected by the Oracle client.
//
// The port number on which to receive notifications, for a server-initiated connection.
//
// The default value of 0 means that a port number will be selected by the Oracle client.
func SubscrHostPort(address string, port uint32) SubscriptionOption {
return func(p *subscriptionParams) {
p.IPAddress, p.Port = address, port
}
}
// SubscrClientInitiated sets whether the subscription is client-initated.
func SubscrClientInitiated(b bool) SubscriptionOption {
return func(p *subscriptionParams) {
p.ClientInitiated = b
}
}
// subscrParams are parameters for a new Subscription.
type subscriptionParams struct {
// IPAddress on which the subscription listens to receive notifications,
// for a server-initiated connection.
//
// The IP address can be an IPv4 address in dotted decimal format such as 192.1.2.34
// or an IPv6 address in hexadecimal format such as 2001:0db8:0000:0000:0217:f2ff:fe4b:4ced.
//
// By default, an IP address will be selected by the Oracle client.
IPAddress string
// Port number on which to receive notifications, for a server-initiated connection.
// The default value of 0 means that a port number will be selected by the Oracle client.
Port uint32
// ClientInitiated specifies whether a client or a server initiated connection should be created.
//
// This feature is only available when Oracle Client 19.4
// and Oracle Database 19.4 or higher are being used.
ClientInitiated bool
}
// Cannot pass *Subscription to C, so pass an uint64 that points to this map entry
var (
subscriptionsMu sync.Mutex
subscriptions = make(map[uint64]*Subscription)
subscriptionsID uint64
)
// CallbackSubscr is the callback for C code on subscription event.
//export CallbackSubscr
func CallbackSubscr(ctx unsafe.Pointer, message *C.dpiSubscrMessage) {
log.Printf("CB %p %+v", ctx, message)
if ctx == nil {
return
}
subscriptionsMu.Lock()
subscr := subscriptions[*((*uint64)(ctx))]
subscriptionsMu.Unlock()
getRows := func(rws *C.dpiSubscrMessageRow, rwsNum C.uint32_t) []RowEvent {
if rwsNum == 0 {
return nil
}
cRws := (*((*[maxArraySize]C.dpiSubscrMessageRow)(unsafe.Pointer(rws))))[:int(rwsNum)]
rows := make([]RowEvent, len(cRws))
for i, row := range cRws {
rows[i] = RowEvent{
Operation: Operation(row.operation),
Rowid: C.GoStringN(row.rowid, C.int(row.rowidLength)),
}
}
return rows
}
getTables := func(tbls *C.dpiSubscrMessageTable, tblsNum C.uint32_t) []TableEvent {
if tblsNum == 0 {
return nil
}
cTbls := (*((*[maxArraySize]C.dpiSubscrMessageTable)(unsafe.Pointer(tbls))))[:int(tblsNum)]
tables := make([]TableEvent, len(cTbls))
for i, tbl := range cTbls {
tables[i] = TableEvent{
Operation: Operation(tbl.operation),
Name: C.GoStringN(tbl.name, C.int(tbl.nameLength)),
Rows: getRows(tbl.rows, tbl.numRows),
}
}
return tables
}
getQueries := func(qrys *C.dpiSubscrMessageQuery, qrysNum C.uint32_t) []QueryEvent {
if qrysNum == 0 {
return nil
}
cQrys := (*((*[maxArraySize]C.dpiSubscrMessageQuery)(unsafe.Pointer(qrys))))[:int(qrysNum)]
queries := make([]QueryEvent, len(cQrys))
for i, qry := range cQrys {
queries[i] = QueryEvent{
ID: uint64(qry.id),
Operation: Operation(qry.operation),
Tables: getTables(qry.tables, qry.numTables),
}
}
return queries
}
var err error
if message.errorInfo != nil {
err = fromErrorInfo(*message.errorInfo)
}
subscr.callback(Event{
Err: err,
Type: EventType(message.eventType),
DB: C.GoStringN(message.dbName, C.int(message.dbNameLength)),
Tables: getTables(message.tables, message.numTables),
Queries: getQueries(message.queries, message.numQueries),
})
}
// Event for a subscription.
type Event struct {
Err error
DB string
Tables []TableEvent
Queries []QueryEvent
Type EventType
}
// QueryEvent is an event of a Query.
type QueryEvent struct {
Tables []TableEvent
ID uint64
Operation
}
// TableEvent is for a Table-related event.
type TableEvent struct {
Name string
Rows []RowEvent
Operation
}
// RowEvent is for row-related event.
type RowEvent struct {
Rowid string
Operation
}
// Subscription for events in the DB.
type Subscription struct {
conn *conn
dpiSubscr *C.dpiSubscr
callback func(Event)
ID uint64
}
// NewSubscription creates a new Subscription in the DB.
//
// Make sure your user has CHANGE NOTIFICATION privilege!
//
// This code is EXPERIMENTAL yet!
func (c *conn) NewSubscription(name string, cb func(Event), options ...SubscriptionOption) (*Subscription, error) {
if !c.params.EnableEvents {
return nil, errors.New("subscription must be allowed by specifying \"enableEvents=1\" in the connection parameters")
}
var p subscriptionParams
for _, o := range options {
o(&p)
}
subscr := Subscription{conn: c, callback: cb}
params := (*C.dpiSubscrCreateParams)(C.malloc(C.sizeof_dpiSubscrCreateParams))
defer func() { C.free(unsafe.Pointer(params)) }()
C.dpiContext_initSubscrCreateParams(c.drv.dpiContext, params)
params.subscrNamespace = C.DPI_SUBSCR_NAMESPACE_DBCHANGE
params.protocol = C.DPI_SUBSCR_PROTO_CALLBACK
params.qos = C.DPI_SUBSCR_QOS_BEST_EFFORT | C.DPI_SUBSCR_QOS_QUERY | C.DPI_SUBSCR_QOS_ROWIDS
params.operations = C.DPI_OPCODE_ALL_OPS
if name != "" || p.IPAddress != "" {
if name != "" {
params.name = C.CString(name)
params.nameLength = C.uint32_t(len(name))
}
if p.IPAddress != "" {
params.ipAddress = C.CString(p.IPAddress)
params.ipAddressLength = C.uint32_t(len(p.IPAddress))
}
defer func() {
if params.name != nil {
C.free(unsafe.Pointer(params.name))
}
if params.ipAddress != nil {
C.free(unsafe.Pointer(params.ipAddress))
}
}()
}
if p.Port != 0 {
params.portNumber = C.uint32_t(p.Port)
}
if p.ClientInitiated {
params.clientInitiated = C.int(1)
}
// typedef void (*dpiSubscrCallback)(void* context, dpiSubscrMessage *message);
params.callback = C.dpiSubscrCallback(C.CallbackSubscrDebug)
// cannot pass &subscr to C, so pass indirectly
subscriptionsMu.Lock()
subscriptionsID++
subscr.ID = subscriptionsID
subscriptions[subscr.ID] = &subscr
subscriptionsMu.Unlock()
subscrID := (*C.uint64_t)(C.malloc(8))
*subscrID = C.uint64_t(subscriptionsID)
params.callbackContext = unsafe.Pointer(subscrID)
dpiSubscr := (*C.dpiSubscr)(C.malloc(C.sizeof_void))
if err := c.checkExec(func() C.int {
return C.dpiConn_subscribe(c.dpiConn, params, (**C.dpiSubscr)(unsafe.Pointer(&dpiSubscr)))
}); err != nil {
C.free(unsafe.Pointer(dpiSubscr))
err = fmt.Errorf("newSubscription: %w", err)
if strings.Contains(errors.Unwrap(err).Error(), "DPI-1065:") {
err = fmt.Errorf("specify \"enableEvents=1\" connection parameter on connection to be able to use subscriptions: %w", err)
}
return nil, err
}
subscr.dpiSubscr = dpiSubscr
return &subscr, nil
}
// Register a query for Change Notification.
//
// This code is EXPERIMENTAL yet!
func (s *Subscription) Register(qry string, params ...interface{}) error {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
cQry := C.CString(qry)
defer func() { C.free(unsafe.Pointer(cQry)) }()
var dpiStmt *C.dpiStmt
if C.dpiSubscr_prepareStmt(s.dpiSubscr, cQry, C.uint32_t(len(qry)), &dpiStmt) == C.DPI_FAILURE {
return fmt.Errorf("prepareStmt[%p]: %w", s.dpiSubscr, s.conn.getError())
}
defer func() { C.dpiStmt_release(dpiStmt) }()
mode := C.dpiExecMode(C.DPI_MODE_EXEC_DEFAULT)
var qCols C.uint32_t
if C.dpiStmt_execute(dpiStmt, mode, &qCols) == C.DPI_FAILURE {
return fmt.Errorf("executeStmt: %w", s.conn.getError())
}
var queryID C.uint64_t
if C.dpiStmt_getSubscrQueryId(dpiStmt, &queryID) == C.DPI_FAILURE {
return fmt.Errorf("getSubscrQueryId: %w", s.conn.getError())
}
logger := getLogger()
if logger != nil {
logger.Log("msg", "subscribed", "query", qry, "id", queryID)
}
return nil
}
// Close the subscription.
//
// This code is EXPERIMENTAL yet!
func (s *Subscription) Close() error {
subscriptionsMu.Lock()
delete(subscriptions, s.ID)
subscriptionsMu.Unlock()
dpiSubscr := s.dpiSubscr
conn := s.conn
s.conn = nil
s.dpiSubscr = nil
s.callback = nil
if dpiSubscr == nil || conn == nil || conn.dpiConn == nil {
return nil
}
if err := conn.checkExec(func() C.int { return C.dpiConn_unsubscribe(conn.dpiConn, dpiSubscr) }); err != nil {
return fmt.Errorf("close: %w", err)
}
return nil
}
// EventType is the type of an event.
type EventType C.dpiEventType
// Events that can be watched.
const (
EvtStartup = EventType(C.DPI_EVENT_STARTUP)
EvtShutdown = EventType(C.DPI_EVENT_SHUTDOWN)
EvtShutdownAny = EventType(C.DPI_EVENT_SHUTDOWN_ANY)
EvtDereg = EventType(C.DPI_EVENT_DEREG)
EvtObjChange = EventType(C.DPI_EVENT_OBJCHANGE)
EvtQueryChange = EventType(C.DPI_EVENT_QUERYCHANGE)
EvtAQ = EventType(C.DPI_EVENT_AQ)
)
// Operation in the DB.
type Operation C.dpiOpCode
const (
// OpAll Indicates that notifications should be sent for all operations on the table or query.
OpAll = Operation(C.DPI_OPCODE_ALL_OPS)
// OpAllRows Indicates that all rows have been changed in the table or query (or too many rows were changed or row information was not requested).
OpAllRows = Operation(C.DPI_OPCODE_ALL_ROWS)
// OpInsert Indicates that an insert operation has taken place in the table or query.
OpInsert = Operation(C.DPI_OPCODE_INSERT)
// OpUpdate Indicates that an update operation has taken place in the table or query.
OpUpdate = Operation(C.DPI_OPCODE_UPDATE)
// OpDelete Indicates that a delete operation has taken place in the table or query.
OpDelete = Operation(C.DPI_OPCODE_DELETE)
// OpAlter Indicates that the registered table or query has been altered.
OpAlter = Operation(C.DPI_OPCODE_ALTER)
// OpDrop Indicates that the registered table or query has been dropped.
OpDrop = Operation(C.DPI_OPCODE_DROP)
// OpUnknown An unknown operation has taken place.
OpUnknown = Operation(C.DPI_OPCODE_UNKNOWN)
)