Skip to content

Commit 2e5ad24

Browse files
committed
Introduce 'actions' which nest under sub-status entries, including additional information pertaining to each action
Signed-off-by: Matthew Whitehead <[email protected]>
1 parent a143d9e commit 2e5ad24

File tree

11 files changed

+351
-308
lines changed

11 files changed

+351
-308
lines changed

config.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,9 @@
241241

242242
|Key|Description|Type|Default Value|
243243
|---|-----------|----|-------------|
244-
|errorHistoryCount|The number of historical errors to retain in the operation|`int`|`25`
244+
|maxHistoryActions|The number of actions to store per historical status updates|`int`|`50`
245+
|maxHistoryCount|The number of historical status updates to retain in the operation|`int`|`50`
246+
|maxHistorySummaryCount|The number of historical status summary records to retain in the operation|`int`|`50`
245247
|maxInFlight|The maximum number of transactions to have in-flight with the policy engine / blockchain transaction pool|`int`|`100`
246248
|nonceStateTimeout|How old the most recently submitted transaction record in our local state needs to be, before we make a request to the node to query the next nonce for a signing address|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1h`
247249

internal/tmconfig/tmconfig.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ var (
3030
ConfirmationsBlockQueueLength = ffc("confirmations.blockQueueLength")
3131
ConfirmationsStaleReceiptTimeout = ffc("confirmations.staleReceiptTimeout")
3232
ConfirmationsNotificationQueueLength = ffc("confirmations.notificationQueueLength")
33-
TransactionsErrorHistoryCount = ffc("transactions.errorHistoryCount")
33+
TransactionsMaxHistoryCount = ffc("transactions.maxHistoryCount")
34+
TransactionsMaxHistorySummaryCount = ffc("transactions.maxHistorySummaryCount")
35+
TransactionsMaxHistoryActions = ffc("transactions.maxHistoryActions")
3436
TransactionsMaxInFlight = ffc("transactions.maxInFlight")
3537
TransactionsNonceStateTimeout = ffc("transactions.nonceStateTimeout")
3638
PolicyLoopInterval = ffc("policyloop.interval")
@@ -74,7 +76,9 @@ var MetricsConfig config.Section
7476

7577
func setDefaults() {
7678
viper.SetDefault(string(TransactionsMaxInFlight), 100)
77-
viper.SetDefault(string(TransactionsErrorHistoryCount), 25)
79+
viper.SetDefault(string(TransactionsMaxHistoryCount), 50)
80+
viper.SetDefault(string(TransactionsMaxHistorySummaryCount), 50)
81+
viper.SetDefault(string(TransactionsMaxHistoryActions), 50)
7882
viper.SetDefault(string(TransactionsNonceStateTimeout), "1h")
7983
viper.SetDefault(string(ConfirmationsRequired), 20)
8084
viper.SetDefault(string(ConfirmationsBlockQueueLength), 50)

internal/tmmsgs/en_config_descriptions.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,11 @@ var (
4545
ConfigConfirmationsRequired = ffc("config.confirmations.required", "Number of confirmations required to consider a transaction/event final", i18n.IntType)
4646
ConfigConfirmationsStaleReceiptTimeout = ffc("config.confirmations.staleReceiptTimeout", "Duration after which to force a receipt check for a pending transaction", i18n.TimeDurationType)
4747

48-
ConfigTransactionsErrorHistoryCount = ffc("config.transactions.errorHistoryCount", "The number of historical errors to retain in the operation", i18n.IntType)
49-
ConfigTransactionsMaxInflight = ffc("config.transactions.maxInFlight", "The maximum number of transactions to have in-flight with the policy engine / blockchain transaction pool", i18n.IntType)
50-
ConfigTransactionsNonceStateTimeout = ffc("config.transactions.nonceStateTimeout", "How old the most recently submitted transaction record in our local state needs to be, before we make a request to the node to query the next nonce for a signing address", i18n.TimeDurationType)
48+
ConfigTransactionsMaxHistoryCount = ffc("config.transactions.maxHistoryCount", "The number of historical status updates to retain in the operation", i18n.IntType)
49+
ConfigTransactionsMaxHistorySummaryCount = ffc("config.transactions.maxHistorySummaryCount", "The number of historical status summary records to retain in the operation", i18n.IntType)
50+
ConfigTransactionsMaxHistoryActions = ffc("config.transactions.maxHistoryActions", "The number of actions to store per historical status updates", i18n.IntType)
51+
ConfigTransactionsMaxInflight = ffc("config.transactions.maxInFlight", "The maximum number of transactions to have in-flight with the policy engine / blockchain transaction pool", i18n.IntType)
52+
ConfigTransactionsNonceStateTimeout = ffc("config.transactions.nonceStateTimeout", "How old the most recently submitted transaction record in our local state needs to be, before we make a request to the node to query the next nonce for a signing address", i18n.TimeDurationType)
5153

5254
ConfigPolicyEngineName = ffc("config.policyengine.name", "The name of the policy engine to use", i18n.StringType)
5355

pkg/apitypes/managed_tx.go

Lines changed: 171 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package apitypes
1818

1919
import (
2020
"context"
21+
"encoding/json"
2122
"fmt"
2223

2324
"github.com/hyperledger/firefly-common/pkg/fftypes"
@@ -38,55 +39,63 @@ const (
3839
TxStatusFailed TxStatus = "Failed"
3940
)
4041

41-
type ManagedTXUpdate struct {
42-
Time *fftypes.FFTime `json:"time"`
43-
LastOccurrence *fftypes.FFTime `json:"lastOccurrence"`
44-
Count int `json:"count"`
45-
Info string `json:"info,omitempty"`
46-
Error string `json:"error,omitempty"`
47-
MappedReason ffcapi.ErrorReason `json:"reason,omitempty"`
48-
}
49-
5042
// TxSubStatus is an intermediate status a transaction may go through
5143
type TxSubStatus string
5244

5345
const (
54-
// TxSubStatusRetrievingGasPrice indicates the operation is getting a gas price
55-
TxSubStatusRetrievingGasPrice TxSubStatus = "RetrievingGasPrice"
56-
// TxSubStatusRetrievedGasPrice indicates the operation has had gas price calculated for it
57-
TxSubStatusRetrievedGasPrice TxSubStatus = "RetrievedGasPrice"
58-
// TxSubStatusSubmitting indicates that the transaction is about to be submitted
59-
TxSubStatusSubmitting TxSubStatus = "Submitting"
60-
// TxSubStatusSubmitted indicates that the transaction has been submitted to the JSON/RPC endpoint
61-
TxSubStatusSubmitted TxSubStatus = "Submitted"
62-
// TxSubStatusReceivedReceipt indicates that we have received a receipt for the transaction
63-
TxSubStatusReceivedReceipt TxSubStatus = "ReceivedReceipt"
64-
// TxSubStatusConfirmed indicates that we have met the required number of confirmations for the transaction
46+
// TxSubStatusReceived indicates the transaction has been received by the connector
47+
TxSubStatusReceived TxSubStatus = "Received"
48+
// TxSubStatusStale indicates the transaction is now in stale
49+
TxSubStatusStale TxSubStatus = "Stale"
50+
// TxSubStatusTracking indicates we are tracking progress of the transaction
51+
TxSubStatusTracking TxSubStatus = "Tracking"
52+
// TxSubStatusConfirmed indicates we have confirmed that the transaction has been fully processed
6553
TxSubStatusConfirmed TxSubStatus = "Confirmed"
54+
// TxSubStatusFailed indicates we have failed to process the transaction and it will no longer be tracked
55+
TxSubStatusFailed TxSubStatus = "Failed"
6656
)
6757

68-
type TxSubStatusEntry struct {
69-
Time *fftypes.FFTime `json:"time"`
70-
LastOccurrence *fftypes.FFTime `json:"lastOccurrence"`
71-
Status TxSubStatus `json:"subStatus"`
72-
Count int `json:"count"`
58+
type TxHistoryRecord struct {
59+
Time *fftypes.FFTime `json:"time"`
60+
Status TxSubStatus `json:"subStatus"`
61+
Actions []*TxActionEntry `json:"actions"`
62+
Info string `json:"info,omitempty"`
63+
Error string `json:"error,omitempty"`
7364
}
7465

75-
// MsgString is assured to be the same, as long as the type/message is the same.
76-
// Does not change if the count/times are different - so allows comparison.
77-
func (mtu *ManagedTXUpdate) MsgString() string {
78-
if mtu == nil {
79-
return ""
80-
}
81-
msg := ""
82-
if mtu.Error != "" {
83-
msg = fmt.Sprintf("error: %s, ", mtu.Error)
84-
}
85-
if mtu.MappedReason != "" {
86-
msg += fmt.Sprintf("reason: %s, ", mtu.MappedReason)
87-
}
88-
msg += fmt.Sprintf("info: %s", mtu.Info)
89-
return msg
66+
type TxHistorySummaryRecord struct {
67+
FirstOccurrence *fftypes.FFTime `json:"firstOccurence"`
68+
Status TxSubStatus `json:"subStatus"`
69+
Count int `json:"count"`
70+
}
71+
72+
// TxAction is an action taken while attempting to progress a transaction between sub-states
73+
type TxAction string
74+
75+
const (
76+
// TxActionAssignNonce indicates that a nonce has been assigned to the transaction
77+
TxActionAssignNonce TxAction = "AssignNonce"
78+
// TxActionRetrieveGasPrice indicates the operation is getting a gas price
79+
TxActionRetrieveGasPrice TxAction = "RetrieveGasPrice"
80+
// TxActionTimeout indicates that the transaction has timed out may need intervention to progress it
81+
TxActionTimeout TxAction = "Timeout"
82+
// TxActionSubmitTransaction indicates that the transaction has been submitted
83+
TxActionSubmitTransaction TxAction = "SubmitTransaction"
84+
// TxActionReceiveReceipt indicates that we have received a receipt for the transaction
85+
TxActionReceiveReceipt TxAction = "ReceiveReceipt"
86+
// TxActionConfirmTransaction indicates that the transaction has been confirmed
87+
TxActionConfirmTransaction TxAction = "Confirm"
88+
)
89+
90+
// An action taken in order to progress a transaction, e.g. retrieve gas price from an oracle
91+
type TxActionEntry struct {
92+
Time *fftypes.FFTime `json:"time"`
93+
Action TxAction `json:"action"`
94+
LastOccurrence *fftypes.FFTime `json:"lastOccurrence"`
95+
Count int `json:"count"`
96+
LastError *fftypes.JSONAny `json:"lastError,omitempty"`
97+
LastErrorTime *fftypes.FFTime `json:"lastErrorTime,omitempty"`
98+
LastInfo *fftypes.JSONAny `json:"lastInfo,omitempty"`
9099
}
91100

92101
// ManagedTX is the structure stored for each new transaction request, using the external ID of the operation
@@ -124,8 +133,8 @@ type ManagedTX struct {
124133
LastSubmit *fftypes.FFTime `json:"lastSubmit,omitempty"`
125134
Receipt *ffcapi.TransactionReceiptResponse `json:"receipt,omitempty"`
126135
ErrorMessage string `json:"errorMessage,omitempty"`
127-
History []*ManagedTXUpdate `json:"history"`
128-
SubStatusHistory []*TxSubStatusEntry `json:"subStatusHistory"`
136+
History []*TxHistoryRecord `json:"history,omitempty"`
137+
HistorySummary []*TxHistorySummaryRecord `json:"historySummary,omitempty"`
129138
Confirmations []confirmations.BlockInfo `json:"confirmations,omitempty"`
130139
}
131140

@@ -154,36 +163,134 @@ type TransactionUpdateReply struct {
154163
TransactionHash string `json:"transactionHash,omitempty"`
155164
}
156165

166+
func (mtx *ManagedTX) CurrentSubStatus(ctx context.Context) *TxHistoryRecord {
167+
if len(mtx.History) > 0 {
168+
return mtx.History[len(mtx.History)-1]
169+
}
170+
return nil
171+
}
172+
157173
// Transaction sub-status entries can be added for a given transaction so a caller
158174
// can see discrete steps in a transaction moving to confirmation on the blockchain.
159-
// There only exists a single entry in the list for each unique sub-status type. If
160-
// a transaction goes through the same sub-status more than once (for example if it
161-
// has gas recalculated for it more than once) then the "Count" and "LastOccurence" fields
162-
// should be updated accordingly. This design allows a caller to see when the most recent
163-
// sub-status changes took place and if necessary, to order them by time, but prevents
164-
// the potential for the sub-status list to grow indefinitely.
175+
// For example a transaction might have a sub-status of "Stale" if a transaction has
176+
// been in pending state for a given period of time. In order to progress the transaction
177+
// while it's in a given sub-status, certain actions might be taken (such as retrieving
178+
// the latest gas price for the chain). See AddSubStatusAction(). Since a transaction
179+
// might go through many sub-status changes before being confirmed on chain the list of
180+
// entries is capped at the configured number and FIFO approach used to keep within that cap.
165181
func (mtx *ManagedTX) AddSubStatus(ctx context.Context, subStatus TxSubStatus) {
166-
// See if this status exists in the list already
167-
for _, entry := range mtx.SubStatusHistory {
168-
if entry.Status == subStatus {
169-
entry.Count++
170-
entry.LastOccurrence = fftypes.Now()
182+
// See if the status being added is the same as the current status. If so we won't create
183+
// a new record, just increment the total count
184+
if len(mtx.History) > 0 {
185+
if mtx.History[len(mtx.History)-1].Status == subStatus {
171186
return
172187
}
188+
log.L(ctx).Debugf("Entered sub-status %s", subStatus)
173189
}
174190

175-
// Prevent crazy run-away situations by limiting the number of unique sub-status
176-
// types we will keep
177-
if len(mtx.SubStatusHistory) >= 50 {
178-
log.L(ctx).Warn("Number of unique sub-status types reached. Some status detail may be lost.")
191+
// Do we need to remove the oldest entry to make space for this one?
192+
if len(mtx.History) > 50 { // TODO - get from config
193+
mtx.History = mtx.History[1:]
179194
} else {
180-
// If this is an entirely new status add it to the list
181-
newStatus := &TxSubStatusEntry{
182-
Time: fftypes.Now(),
183-
LastOccurrence: fftypes.Now(),
184-
Count: 1,
185-
Status: subStatus,
195+
// If this is a change in status add a new record
196+
newStatus := &TxHistoryRecord{
197+
Time: fftypes.Now(),
198+
Status: subStatus,
199+
Actions: make([]*TxActionEntry, 0),
200+
}
201+
mtx.History = append(mtx.History, newStatus)
202+
203+
// As was as detailed sub-status records (which might be a long list and early entries
204+
// get purged at some point) we keep a separate list of all the discrete types of sub-status
205+
// we've ever seen for this transaction along with a count of them. This means an early sub-status
206+
// (e.g. "queued") followed by 100s of different sub-status types will still be recorded
207+
newHistorySummary := true
208+
for _, statusType := range mtx.HistorySummary {
209+
if statusType.Status == subStatus {
210+
// Just increment the counter
211+
statusType.Count++
212+
newHistorySummary = false
213+
break
214+
}
215+
}
216+
217+
if newHistorySummary {
218+
if len(mtx.HistorySummary) < 50 { // TODO - get from config
219+
mtx.HistorySummary = append(mtx.HistorySummary, &TxHistorySummaryRecord{Status: subStatus, Count: 1, FirstOccurrence: fftypes.Now()})
220+
} else {
221+
log.L(ctx).Warnf("Reached maximum number of history summary records. New summary status will be not be recorded.")
222+
}
223+
}
224+
}
225+
}
226+
227+
// Make sure the provided value can be serialised to JSON
228+
func ensureValidJSON(value *fftypes.JSONAny) *fftypes.JSONAny {
229+
if json.Valid([]byte(*value)) {
230+
// Already valid
231+
return value
232+
}
233+
234+
// Convert to hex and wrap in a valid struct
235+
hex := fmt.Sprintf("%x", []byte(*value))
236+
return fftypes.JSONAnyPtr(`{"invalidJson":"` + hex + `"}`)
237+
}
238+
239+
// When a transaction is in a given sub-status (e.g. "Stale") the blockchain connector
240+
// may perform certain actions to move it out of the status. For example it might
241+
// retrieve the current gas price for the chain. TxAction's represent an action taken
242+
// while in a given sub-status. In order to limit the number of TxAction entries in
243+
// a TxSubStatusEntry each action has a count of the number of occurrences and a
244+
// latest timestamp to indicate when it was last executed. There is a last error field
245+
// which can be used to indicate the most recent error that occurred, for example an
246+
// HTTP 4xx return code from a gas oracle. There is also an information field to record
247+
// arbitrary data about the action, for example the gas price retrieved from an oracle.
248+
func (mtx *ManagedTX) AddSubStatusAction(ctx context.Context, action TxAction, info *fftypes.JSONAny, error *fftypes.JSONAny) {
249+
// An action always exists within a sub-status. If a sub-status hasn't been recorded yet we don't record the action
250+
if len(mtx.History) > 0 {
251+
252+
// See if this action exists in the list already since we only want to update the single entry, not
253+
// add a new one
254+
currentSubStatus := mtx.History[len(mtx.History)-1]
255+
for _, entry := range currentSubStatus.Actions {
256+
if entry.Action == action {
257+
entry.Count++
258+
entry.LastOccurrence = fftypes.Now()
259+
260+
if error != nil {
261+
entry.LastError = ensureValidJSON(error)
262+
entry.LastErrorTime = fftypes.Now()
263+
}
264+
265+
if info != nil {
266+
entry.LastInfo = ensureValidJSON(info)
267+
}
268+
return
269+
}
270+
}
271+
272+
// This action hasn't been recorded yet in this sub-status. Add a new entry for it.
273+
if len(currentSubStatus.Actions) >= 50 { // TODO - get from config
274+
log.L(ctx).Warn("Number of unique sub-status actions. New action detail will not be recorded.")
275+
} else {
276+
// If this is an entirely new status add it to the list
277+
newAction := &TxActionEntry{
278+
Time: fftypes.Now(),
279+
Action: action,
280+
LastOccurrence: fftypes.Now(),
281+
Count: 1,
282+
}
283+
284+
if error != nil {
285+
newAction.LastError = ensureValidJSON(error)
286+
newAction.LastErrorTime = fftypes.Now()
287+
}
288+
289+
if info != nil {
290+
newAction.LastInfo = ensureValidJSON(info)
291+
}
292+
293+
currentSubStatus.Actions = append(currentSubStatus.Actions, newAction)
186294
}
187-
mtx.SubStatusHistory = append(mtx.SubStatusHistory, newStatus)
188295
}
189296
}

0 commit comments

Comments
 (0)