commit 8055aaf68b52b5d4fd2578152e6f07f0b0e102b3
parent 2eeaf9f1c11caa407bcfd474e3fc61521e591f36
Author: Joel-Haeberli <haebu@rubigen.ch>
Date: Tue, 21 May 2024 22:59:32 +0200
feat: exponential backoff for retries
Diffstat:
11 files changed, 116 insertions(+), 22 deletions(-)
diff --git a/c2ec/amount.go b/c2ec/amount.go
@@ -165,13 +165,15 @@ func ParseAmount(s string) (*Amount, error) {
return nil, fmt.Errorf("invalid amount: %s", s)
}
- fraction := 0
+ fraction := 0.0
if len(valueAndFraction) == 2 {
- fraction, err = strconv.Atoi(valueAndFraction[1])
+ divider := leadingZerosDivider(valueAndFraction[1])
+ fractionInt, err := strconv.Atoi(valueAndFraction[1])
if err != nil {
LogError("amount", err)
return nil, fmt.Errorf("invalid amount: %s", s)
}
+ fraction = float64(fractionInt) / float64(divider)
}
a := NewAmount(currency, uint64(value), uint64(fraction))
@@ -194,3 +196,17 @@ func (a *Amount) String() string {
}
return fmt.Sprintf("%s:%s", a.Currency, v)
}
+
+func leadingZerosDivider(s string) int {
+
+ rs := strings.Split(s, "")
+ leadingZeros := 0
+ for _, r := range rs {
+ if r == "0" {
+ leadingZeros += 1
+ } else {
+ break
+ }
+ }
+ return int(math.Pow10(leadingZeros))
+}
diff --git a/c2ec/amount_test.go b/c2ec/amount_test.go
@@ -194,3 +194,25 @@ func TestFormatAmountInvalid(t *testing.T) {
}
}
}
+
+func TestParseFloat(t *testing.T) {
+
+ floats := []float64{
+ 0.5,
+ 0.3,
+ 0.0004,
+ 1234.9923,
+ }
+
+ for _, f := range floats {
+
+ formatted := fmt.Sprintf("CHF:%.8f", f)
+ fmt.Println("formatted:", formatted)
+ amount, err := parseAmount(formatted)
+ if err != nil {
+ fmt.Println("failed!", err)
+ }
+
+ fmt.Println(FormatAmount(&amount))
+ }
+}
diff --git a/c2ec/proc-attestor.go b/c2ec/proc-attestor.go
@@ -86,6 +86,20 @@ func finaliseOrSetRetry(
return
}
+ if w, err := DB.GetWithdrawalById(withdrawalRowId); err != nil {
+ LogError("proc-attestor", err)
+ errs <- err
+ prepareRetryOrAbort(withdrawalRowId, errs)
+ return
+ } else {
+ if err := transaction.Attest(w); err != nil {
+ LogError("proc-attestor", err)
+ errs <- err
+ prepareRetryOrAbort(withdrawalRowId, errs)
+ return
+ }
+ }
+
completionProof := transaction.Bytes()
if len(completionProof) > 0 {
// only allow finalization operation, when the completion
diff --git a/c2ec/proc-retrier.go b/c2ec/proc-retrier.go
@@ -22,7 +22,7 @@ func RunRetrier(ctx context.Context, errs chan error) {
go func() {
for {
- time.Sleep(time.Duration(CONFIG.Server.RetryDelayMs) * time.Millisecond)
+ time.Sleep(time.Duration(1000 * time.Millisecond))
//LogInfo("proc-retrier", "attesting selected withdrawals...")
withdrawals, err := DB.GetAttestableWithdrawals()
if err != nil {
@@ -31,7 +31,13 @@ func RunRetrier(ctx context.Context, errs chan error) {
continue
}
for _, w := range withdrawals {
- attest(w, errs)
+ var lastRetryTs int64 = 0
+ if w.LastRetryTs != nil {
+ lastRetryTs = *w.LastRetryTs
+ }
+ if ShouldStartRetry(time.Unix(lastRetryTs, 0), int(w.RetryCounter), CONFIG.Server.RetryDelayMs) {
+ attest(w, errs)
+ }
}
}
}()
diff --git a/c2ec/provider.go b/c2ec/provider.go
@@ -3,6 +3,7 @@ package main
type ProviderTransaction interface {
AllowWithdrawal() bool
AbortWithdrawal() bool
+ Attest(w *Withdrawal) error
Bytes() []byte
}
diff --git a/c2ec/simulation-client.go b/c2ec/simulation-client.go
@@ -32,6 +32,11 @@ func (st *SimulationTransaction) AbortWithdrawal() bool {
return false
}
+func (st *SimulationTransaction) Attest(w *Withdrawal) error {
+
+ return nil
+}
+
func (st *SimulationTransaction) Bytes() []byte {
return bytes.NewBufferString("this is a simulated transaction and therefore has no content.").Bytes()
diff --git a/c2ec/wallee-client.go b/c2ec/wallee-client.go
@@ -54,10 +54,26 @@ func (wt *WalleeTransaction) AbortWithdrawal() bool {
strings.EqualFold(string(wt.State), string(StateDecline))
}
-// func (wt *WalleeTransaction) FormatPayto() string {
+func (wt *WalleeTransaction) Attest(w *Withdrawal) error {
-// return fmt.Sprintf("payto://wallee-transaction/%d", wt.Id)
-// }
+ amount, err := parseAmount(fmt.Sprintf("%s:%f", CONFIG.Server.Currency, wt.CompletedAmount))
+ if err != nil {
+ LogError("wallee-client", err)
+ return err
+ }
+
+ if amount.Value != uint64(w.Amount.Val) /*|| amount.Fraction != uint64(w.Amount.Frac)*/ {
+
+ return errors.New("amount does not match the withdrawal")
+ }
+
+ if wt.MerchantReference != *w.ProviderTransactionId {
+
+ return errors.New("the merchant reference does not match the withdrawal")
+ }
+
+ return nil
+}
func (wt *WalleeTransaction) Bytes() []byte {
@@ -97,6 +113,10 @@ func (w *WalleeClient) SetupClient(p *Provider) error {
func (w *WalleeClient) GetTransaction(transactionId string) (ProviderTransaction, error) {
+ if transactionId == "" {
+ return nil, errors.New("transaction id must be specified but was blank")
+ }
+
call := fmt.Sprintf("%s%s", w.baseUrl, WALLEE_SEARCH_TRANSACTION_API)
queryParams := map[string]string{
WALLEE_API_SPACEID_PARAM_NAME: strconv.Itoa(w.credentials.SpaceId),
diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/TerminalClientImplementation.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/TerminalClientImplementation.kt
@@ -95,12 +95,17 @@ class TerminalClientImplementation (
.post(reqBody)
.url(url)
.build()
- client.newCall(req).execute().use {
- if (it.isSuccessful) {
- callback(parseOrEmpty(it))
- } else {
- callback(Optional.empty())
+ try {
+ client.newCall(req).execute().use {
+ if (it.isSuccessful) {
+ callback(parseOrEmpty(it))
+ } else {
+ callback(Optional.empty())
+ }
}
+ } catch (ex: Exception) {
+ println("exception occured: $ex")
+ callback(Optional.empty())
}
}
diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AuthorizePaymentScreen.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AuthorizePaymentScreen.kt
@@ -44,7 +44,7 @@ fun AuthorizePaymentScreen(model: WithdrawalViewModel, activity: Activity, clien
val transaction = Transaction.Builder(withdrawalAmount)
.setCurrency(Currency.getInstance(uiState.currency))
.setInvoiceReference(uiState.encodedWopid)
- .setMerchantReference(uiState.transactionId)
+ .setMerchantReference(uiState.encodedWopid)
.setTransactionProcessingBehavior(TransactionProcessingBehavior.COMPLETE_IMMEDIATELY)
.build()
diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/RegisterParametersScreen.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/RegisterParametersScreen.kt
@@ -19,6 +19,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import kotlinx.coroutines.launch
+import java.util.concurrent.Executors
import kotlin.coroutines.coroutineContext
@SuppressLint("StateFlowValueCalledInComposition")
@@ -66,12 +67,9 @@ fun RegisterParametersScreen(
Text(text = "simulate registration")
}
} else {
- val s = rememberCoroutineScope()
- LaunchedEffect(key1 = uiState) {
- s.launch {
- model.startAuthorizationWhenReadyOrAbort(navigateToWhenRegistered) {
- activity.finish()
- }
+ Executors.newSingleThreadExecutor().execute {
+ model.startAuthorizationWhenReadyOrAbort(navigateToWhenRegistered) {
+ activity.finish()
}
}
}
diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalViewModel.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalViewModel.kt
@@ -237,7 +237,12 @@ class WithdrawalViewModel(
signals.doOnError(onFailure)
println("registered callbacks for parameter selection")
println("executing parameter selection long-polling request")
- recursiveRetries(uiState.value.encodedWopid)
+ try {
+ recursiveRetries(uiState.value.encodedWopid)
+ } catch (ex: Exception) {
+ println("error occured: $ex")
+ signals.setError()
+ }
println("awaiting response for parameter selection")
signals.block()
}
@@ -247,7 +252,7 @@ class WithdrawalViewModel(
terminalClient!!.sendConfirmationRequest(
_uiState.value.encodedWopid,
TerminalWithdrawalConfirmationRequest(
- _uiState.value.transactionId,
+ _uiState.value.encodedWopid,
Amount(0, 0)
)
) {
@@ -283,11 +288,13 @@ class WithdrawalViewModel(
fun withdrawalOperationFailed() {
viewModelScope.launch {
- terminalClient!!.abortWithdrawal(uiState.value.encodedWopid) {}
+ if (_uiState.value.transactionState == TransactionState.AUTHORIZATION_PENDING) {
+ terminalClient!!.abortWithdrawal(uiState.value.encodedWopid) {}
// SummaryActivity.summary.success = false
// val activity = WithdrawalActivity()
// val intent = Intent(activity, SummaryActivity::class.java)
// activity.startActivity(intent)
+ }
}
}