blob: fb51d2eefd26df07250d01111819ae2e7e25d944 [file] [log] [blame]
/*
* Copyright 2018-present Open Networking Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Two voltha cores receive the same request; each tries to acquire ownership of the request
* by writing its identifier (e.g. container name or pod name) to the transaction key named
* after the serial number of the request. The core that loses the race for acquisition
* monitors the progress of the core actually serving the request by watching for changes
* in the value of the transaction key. Once the request is complete, the
* serving core closes the transaction by invoking the KVTransaction's Close method, which
* replaces the value of the transaction (i.e. serial number) key with the string
* TRANSACTION_COMPLETE. The standby core observes this update, stops watching the transaction,
* and then deletes the transaction key.
*
*/
package core
import (
"context"
"time"
"github.com/opencord/voltha-lib-go/v3/pkg/db/kvstore"
"github.com/opencord/voltha-lib-go/v3/pkg/log"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// Transaction acquisition results
const (
UNKNOWN = iota
SeizedBySelf
CompletedByOther
AbandonedByOther
AbandonedWatchBySelf
)
var errorTransactionNotAcquired = status.Error(codes.Canceled, "transaction-not-acquired")
// Transaction constant
const (
TransactionComplete = "TRANSACTION-COMPLETE"
)
// Transaction constants used to guarantee the Core processing a request hold on to the transaction until
// it either completes it (either successfully or times out) or the Core itself crashes (
// e.g. a server failure).
// If a request has a timeout of x seconds then the Core processing the request will renew the transaction lease
// every x/NUM_TXN_RENEWAL_PER_REQUEST seconds. After the Core completes the request it stops renewing the
// transaction and sets the transaction value to TRANSACTION_COMPLETE. If the processing Core crashes then it
// will not renew the transaction causing the KV store to delete the transaction after its renewal period. The
// Core watching the transaction will then take over.
// Since the MIN_TXN_RENEWAL_INTERVAL_IN_SEC is 3 seconds then for any transaction that completes within 3 seconds
// there won't be a transaction renewal done.
const (
NumTxnRenewalPerRequest = 2
MinTxnRenewalIntervalInSec = 3
MinTxnReservationDurationInSec = 5
)
// TransactionContext represent transaction context attributes
type TransactionContext struct {
kvClient kvstore.Client
kvOperationTimeout int
owner string
txnPrefix string
}
var ctx *TransactionContext
var txnState = []string{
"UNKNOWN",
"SEIZED-BY-SELF",
"COMPLETED-BY-OTHER",
"ABANDONED-BY-OTHER",
"ABANDONED_WATCH_BY_SELF"}
func init() {
_, err := log.AddPackage(log.JSON, log.DebugLevel, nil)
if err != nil {
log.Errorw("unable-to-register-package-to-the-log-map", log.Fields{"error": err})
}
}
// NewTransactionContext creates transaction context instance
func NewTransactionContext(
owner string,
txnPrefix string,
kvClient kvstore.Client,
kvOpTimeout int) *TransactionContext {
return &TransactionContext{
owner: owner,
txnPrefix: txnPrefix,
kvClient: kvClient,
kvOperationTimeout: kvOpTimeout}
}
/*
* Before instantiating a KVTransaction, a TransactionContext must be created.
* The parameters stored in the context govern the behavior of all KVTransaction
* instances.
*
* :param owner: The owner (i.e. voltha core name) of a transaction
* :param txnPrefix: The key prefix under which all transaction IDs, or serial numbers,
* will be created (e.g. "service/voltha/transactions")
* :param kvClient: The client API used for all interactions with the KV store. Currently
* only the etcd client is supported.
* :param: kvOpTimeout: The maximum time, in seconds, to be taken by any KV operation
* used by this package
*/
// SetTransactionContext creates new transaction context
func SetTransactionContext(owner string,
txnPrefix string,
kvClient kvstore.Client,
kvOpTimeout int) error {
ctx = NewTransactionContext(owner, txnPrefix, kvClient, kvOpTimeout)
return nil
}
// KVTransaction represent KV transaction attributes
type KVTransaction struct {
monitorCh chan int
txnID string
txnKey string
}
/*
* A KVTransaction constructor
*
* :param txnId: The serial number of a voltha request.
* :return: A KVTransaction instance
*/
// NewKVTransaction creates KV transaction instance
func NewKVTransaction(txnID string) *KVTransaction {
return &KVTransaction{
txnID: txnID,
txnKey: ctx.txnPrefix + txnID}
}
/*
* Acquired is invoked by a Core, upon reception of a request, to reserve the transaction key in the KV store. The
* request may be resource specific (i.e will include an ID for that resource) or may be generic (i.e. list a set of
* resources). If the request is resource specific then this function should be invoked with the ownedByMe flag to
* indicate whether this Core owns this resource. In the case where this Core owns this resource or it is a generic
* request then we will proceed to reserve the transaction key in the KV store for a minimum time specified by the
* minDuration param. If the reservation request fails (i.e. the other Core got the reservation before this one - this
* can happen only for generic request) then the Core will start to watch for changes to the key to determine
* whether the other Core completed the transaction successfully or the Core just died. If the Core does not own the
* resource then we will proceed to watch the transaction key.
*
* :param minDuration: minimum time to reserve the transaction key in the KV store
* :param ownedByMe: specify whether the request is about a resource owned or not. If it's absent then this is a
* generic request that has no specific resource ID (e.g. list)
*
* :return: A boolean specifying whether the resource was acquired. An error is return in case this function is invoked
* for a resource that is nonexistent.
*/
// Acquired aquires transaction status
func (c *KVTransaction) Acquired(ctx context.Context, minDuration int64, ownedByMe ...bool) (bool, error) {
var acquired bool
var currOwner string
var res int
// Convert milliseconds to seconds, rounding up
// The reservation TTL is specified in seconds
durationInSecs := minDuration / 1000
if remainder := minDuration % 1000; remainder > 0 {
durationInSecs++
}
if durationInSecs < int64(MinTxnReservationDurationInSec) {
durationInSecs = int64(MinTxnReservationDurationInSec)
}
genericRequest := true
resourceOwned := false
if len(ownedByMe) > 0 {
genericRequest = false
resourceOwned = ownedByMe[0]
}
if resourceOwned || genericRequest {
// Keep the reservation longer that the minDuration (which is really the request timeout) to ensure the
// transaction key stays in the KV store until after the Core finalize a request timeout condition (which is
// a success from a request completion perspective).
if err := c.tryToReserveTxn(ctx, durationInSecs*2); err == nil {
res = SeizedBySelf
} else {
log.Debugw("watch-other-server",
log.Fields{"transactionId": c.txnID, "owner": currOwner, "timeout": durationInSecs})
res = c.Watch(ctx, durationInSecs)
}
} else {
res = c.Watch(ctx, durationInSecs)
}
switch res {
case SeizedBySelf, AbandonedByOther:
acquired = true
default:
acquired = false
}
log.Debugw("acquire-transaction-status", log.Fields{"transactionId": c.txnID, "acquired": acquired, "result": txnState[res]})
return acquired, nil
}
func (c *KVTransaction) tryToReserveTxn(ctxt context.Context, durationInSecs int64) error {
var currOwner string
var res int
var err error
value, _ := ctx.kvClient.Reserve(ctxt, c.txnKey, ctx.owner, durationInSecs)
if value != nil {
if currOwner, err = kvstore.ToString(value); err != nil { // This should never happen
log.Errorw("unexpected-owner-type", log.Fields{"transactionId": c.txnID, "error": err})
return err
}
if currOwner == ctx.owner {
log.Debugw("acquired-transaction", log.Fields{"transactionId": c.txnID, "result": txnState[res]})
// Setup the monitoring channel
c.monitorCh = make(chan int)
go c.holdOnToTxnUntilProcessingCompleted(ctxt, c.txnKey, ctx.owner, durationInSecs)
return nil
}
}
return status.Error(codes.PermissionDenied, "reservation-denied")
}
// Watch watches transaction
func (c *KVTransaction) Watch(ctxt context.Context, durationInSecs int64) int {
var res int
events := ctx.kvClient.Watch(ctxt, c.txnKey)
defer ctx.kvClient.CloseWatch(c.txnKey, events)
transactionWasAcquiredByOther := false
//Check whether the transaction was already completed by the other Core before we got here.
if kvp, _ := ctx.kvClient.Get(ctxt, c.txnKey); kvp != nil {
transactionWasAcquiredByOther = true
if val, err := kvstore.ToString(kvp.Value); err == nil {
if val == TransactionComplete {
res = CompletedByOther
// Do an immediate delete of the transaction in the KV Store to free up KV Storage faster
err = c.Delete(ctxt)
if err != nil {
log.Errorw("unable-to-delete-the-transaction", log.Fields{"error": err})
}
return res
}
} else {
// An unexpected value - let's get out of here as something did not go according to plan
res = AbandonedWatchBySelf
log.Debugw("cannot-read-transaction-value", log.Fields{"txn": c.txnID, "error": err})
return res
}
}
for {
select {
case event := <-events:
transactionWasAcquiredByOther = true
log.Debugw("received-event", log.Fields{"txn": c.txnID, "type": event.EventType})
if event.EventType == kvstore.DELETE {
// The other core failed to process the request
res = AbandonedByOther
} else if event.EventType == kvstore.PUT {
key, e1 := kvstore.ToString(event.Key)
val, e2 := kvstore.ToString(event.Value)
if e1 == nil && e2 == nil && key == c.txnKey {
if val == TransactionComplete {
res = CompletedByOther
// Successful request completion has been detected. Remove the transaction key
err := c.Delete(ctxt)
if err != nil {
log.Errorw("unable-to-delete-the-transaction", log.Fields{"error": err})
}
} else {
log.Debugw("Ignoring-PUT-event", log.Fields{"val": val, "key": key})
continue
}
} else {
log.Warnw("received-unexpected-PUT-event", log.Fields{"txn": c.txnID, "key": key, "ctxKey": c.txnKey})
}
}
case <-time.After(time.Duration(durationInSecs) * time.Second):
// Corner case: In the case where the Core owning the device dies and before this Core takes ownership of
// this device there is a window where new requests will end up being watched instead of being processed.
// Grab the request if the other Core did not create the transaction in the KV store.
// TODO: Use a peer-monitoring probe to switch over (still relies on the probe frequency). This will
// guarantee that the peer is actually gone instead of limiting the time the peer can get hold of a
// request.
if !transactionWasAcquiredByOther {
log.Debugw("timeout-no-peer", log.Fields{"txId": c.txnID})
res = AbandonedByOther
} else {
continue
}
}
break
}
return res
}
// Close closes transaction
func (c *KVTransaction) Close(ctxt context.Context) error {
log.Debugw("close", log.Fields{"txn": c.txnID})
// Stop monitoring the key (applies only when there has been no transaction switch over)
if c.monitorCh != nil {
close(c.monitorCh)
err := ctx.kvClient.Put(ctxt, c.txnKey, TransactionComplete)
if err != nil {
log.Errorw("unable-to-write-a-key-value-pair-to-the-KV-store", log.Fields{"error": err})
}
}
return nil
}
// Delete deletes transaction
func (c *KVTransaction) Delete(ctxt context.Context) error {
log.Debugw("delete", log.Fields{"txn": c.txnID})
return ctx.kvClient.Delete(ctxt, c.txnKey)
}
// holdOnToTxnUntilProcessingCompleted renews the transaction lease until the transaction is complete. durationInSecs
// is used to calculate the frequency at which the Core processing the transaction renews the lease. This function
// exits only when the transaction is Closed, i.e completed.
func (c *KVTransaction) holdOnToTxnUntilProcessingCompleted(ctxt context.Context, key string, owner string, durationInSecs int64) {
log.Debugw("holdOnToTxnUntilProcessingCompleted", log.Fields{"txn": c.txnID})
renewInterval := durationInSecs / NumTxnRenewalPerRequest
if renewInterval < MinTxnRenewalIntervalInSec {
renewInterval = MinTxnRenewalIntervalInSec
}
forLoop:
for {
select {
case <-c.monitorCh:
log.Debugw("transaction-renewal-exits", log.Fields{"txn": c.txnID})
break forLoop
case <-time.After(time.Duration(renewInterval) * time.Second):
if err := ctx.kvClient.RenewReservation(ctxt, c.txnKey); err != nil {
// Log and continue.
log.Warnw("transaction-renewal-failed", log.Fields{"txnId": c.txnKey, "error": err})
}
}
}
}