This is an automated email from the ASF dual-hosted git repository.
thunguo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-seata-go-samples.git
The following commit(s) were added to refs/heads/main by this push:
new 7c50b3a feat(tcc): add ride-order TCC sample with 5 services +
initiator (#104)
7c50b3a is described below
commit 7c50b3a9151be0e4504ae0de1d90c175eaba8d1a
Author: Gust wang <[email protected]>
AuthorDate: Thu Aug 27 06:06:36 2026 -0400
feat(tcc): add ride-order TCC sample with 5 services + initiator (#104)
* feat(tcc): add ride-order TCC sample with 5 services + initiator
Implements a ride-hailing order scenario demonstrating the TCC distributed
transaction pattern. Five independent Gin HTTP services (order, dispatch,
pricing, coupon, capacity) each implement Try/Confirm/Cancel with real
MySQL operations and idempotent state transitions. The initiator coordinates
the full TCC lifecycle via seata-go with XID propagation over HTTP.
Includes failure simulation where dispatch-service fails at Try phase,
triggering Cancel on all other services to restore resources.
* fix(tcc): use Request.Context() for XID propagation, add per-service
seatago.yml
- Fix critical bug: proxy.Prepare(c, req) →
proxy.Prepare(c.Request.Context(), req)
Gin middleware stores XID in Request.Context, not gin.Context values.
Without this fix, TCC branch registration silently fails and TC cannot
call Commit/Rollback on services.
- Add independent seatago.yml per service with unique application-id so
Seata TC can correctly route branch callbacks to each service process.
- Update README with per-service startup instructions.
* fix(tcc): make ride-order sample re-runnable and data-correct
- coupon-service: select an available coupon dynamically instead of a
hardcoded id, so repeated runs work (Confirm permanently consumes a
coupon). Seed data widened to 6 coupons + 6 drivers.
- order-service: return the generated order_id; the initiator now threads
the real id into pricing/dispatch instead of hardcoding order_id=1.
- initiator: drop the infinite `<-make(chan struct{})` block so it exits
after both scenarios; callService returns the response body.
- all services: defensive comma-ok type assertion on the TCC params.
- seatago.yml: disable enable-auto-data-source-proxy (AT-only flag, unused
by this pure-TCC sample).
- README: document the in-memory idempotency limitation and TCC fence
pointer, plus how to reset resources between runs.
Verified end-to-end against MySQL + Seata server: scenario 1 confirms all
five branches; scenario 2 (dispatch Try fails) cancels order/pricing/
coupon/capacity and empty-rollbacks dispatch, restoring all resources.
* fix(tcc): use US spelling "canceled" in the ride-order sample
golangci-lint runs misspell with locale: US, which rejects "cancelled".
It was not surfacing yet because the linter aborts before analysing any
package while tcc/rocketmq fails to type-check. Renamed in the SQL comment
and README too so the sample stays self-consistent.
* fix(tcc): address review feedback on the ride-order sample
- dispatch-service: check RowsAffected after reserving a driver. A
concurrent transaction can take the same driver between the SELECT and
the UPDATE, which left the branch recording a driver it never reserved
and reporting success. Mirrors the guard coupon-service already has.
- initiator: bound every /prepare call with a 10s timeout, so a hung
service cannot block the global transaction indefinitely.
- docker-compose: pin seataio/seata-server to 1.6.1 instead of latest,
matching the other per-sample compose files in the repo.
Re-verified end to end against seata-server 1.6.1 and mysql 8.0.32.
Scenario 1 commits all five branches; scenario 2 rolls back order,
pricing, coupon and capacity and empty-rollbacks dispatch. Final state:
1 order confirmed and 1 canceled, 1 driver busy and 5 available,
1 fare lock confirmed and 1 released, 1 coupon used and 5 available,
reserved_slots 1 (scenario 1 kept, scenario 2 released).
---
tcc/ride-order/README.md | 184 ++++++++++++++++++++++++++++
tcc/ride-order/capacity-service/main.go | 61 +++++++++
tcc/ride-order/capacity-service/seatago.yml | 158 ++++++++++++++++++++++++
tcc/ride-order/capacity-service/service.go | 94 ++++++++++++++
tcc/ride-order/common/db.go | 55 +++++++++
tcc/ride-order/coupon-service/main.go | 61 +++++++++
tcc/ride-order/coupon-service/seatago.yml | 158 ++++++++++++++++++++++++
tcc/ride-order/coupon-service/service.go | 104 ++++++++++++++++
tcc/ride-order/dispatch-service/main.go | 61 +++++++++
tcc/ride-order/dispatch-service/seatago.yml | 158 ++++++++++++++++++++++++
tcc/ride-order/dispatch-service/service.go | 109 ++++++++++++++++
tcc/ride-order/docker-compose.yml | 38 ++++++
tcc/ride-order/initiator/main.go | 149 ++++++++++++++++++++++
tcc/ride-order/initiator/seatago.yml | 158 ++++++++++++++++++++++++
tcc/ride-order/order-service/main.go | 69 +++++++++++
tcc/ride-order/order-service/seatago.yml | 158 ++++++++++++++++++++++++
tcc/ride-order/order-service/service.go | 92 ++++++++++++++
tcc/ride-order/pricing-service/main.go | 61 +++++++++
tcc/ride-order/pricing-service/seatago.yml | 158 ++++++++++++++++++++++++
tcc/ride-order/pricing-service/service.go | 93 ++++++++++++++
tcc/ride-order/sql/ride_order.sql | 78 ++++++++++++
21 files changed, 2257 insertions(+)
diff --git a/tcc/ride-order/README.md b/tcc/ride-order/README.md
new file mode 100644
index 0000000..106f815
--- /dev/null
+++ b/tcc/ride-order/README.md
@@ -0,0 +1,184 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You 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.
+-->
+
+# TCC Ride Order Sample
+
+A ride-hailing order scenario demonstrating the TCC (Try-Confirm-Cancel)
distributed
+transaction pattern with five independent services coordinated by seata-go.
+
+## Scenario
+
+A passenger requests a ride. Creating a valid order requires five services to
each
+reserve their own resources first. Only when all reservations succeed does the
order
+get confirmed across all services. If any one fails, all already-reserved
resources
+must be explicitly released.
+
+### Services
+
+| Service | Port | Try (Reserve) | Confirm (Commit) | Cancel (Release) |
+|---------|------|--------------|-------------------|------------------|
+| **order-service** | 8001 | Create pending order | Mark order confirmed |
Mark order canceled |
+| **dispatch-service** | 8002 | Reserve available driver | Mark driver busy |
Release driver back to pool |
+| **pricing-service** | 8003 | Lock estimated fare | Confirm fare lock |
Release fare lock |
+| **coupon-service** | 8004 | Freeze an available coupon | Mark coupon used |
Unfreeze coupon |
+| **capacity-service** | 8005 | Reserve vehicle slot (+1) | Keep reservation |
Release slot (-1) |
+
+The **initiator** (ride-order-service) acts as the Transaction Manager (TM) and
+coordinates the full TCC lifecycle via HTTP calls to each service.
+
+## Architecture
+
+```
+ Seata TC (8091)
+ ╱ │ │ │ │ ╲
+ ╱ │ │ │ │ ╲
+ initiator ──HTTP──► order dispatch pricing coupon capacity
+ (TM) :8001 :8002 :8003 :8004 :8005
+ │ │ │ │ │
+ └─────┴───────┴──────┴──────┘
+ MySQL
+```
+
+- **Initiator** starts a global transaction and calls each service's
`/prepare` endpoint via HTTP,
+ passing the XID in the request header.
+- Each **service** registers its TCC branch with Seata TC during Prepare.
+- **Seata TC** calls Commit or Rollback on each service directly via the seata
protocol (TCP).
+- Each service has its own `seatago.yml` with a unique `application-id` so
that TC can correctly
+ route branch callbacks to the right service process.
+
+## TCC Lifecycle
+
+### Success Flow
+
+```
+initiator (TM)
+ │
+ ├── POST order-service/prepare ✓ pending order created
+ ├── POST pricing-service/prepare ✓ fare locked
+ ├── POST coupon-service/prepare ✓ coupon frozen
+ ├── POST capacity-service/prepare ✓ slot reserved
+ └── POST dispatch-service/prepare ✓ driver reserved
+
+ All Try succeeded → Seata TC triggers Confirm on all branches:
+
+ ├── order-service.Confirm() → order confirmed
+ ├── pricing-service.Confirm() → fare confirmed
+ ├── coupon-service.Confirm() → coupon used
+ ├── capacity-service.Confirm() → slot confirmed
+ └── dispatch-service.Confirm() → driver assigned
+```
+
+### Failure Flow (dispatch fails)
+
+```
+initiator (TM)
+ │
+ ├── POST order-service/prepare ✓ pending order created
+ ├── POST pricing-service/prepare ✓ fare locked
+ ├── POST coupon-service/prepare ✓ coupon frozen
+ ├── POST capacity-service/prepare ✓ slot reserved
+ └── POST dispatch-service/prepare ✗ NO AVAILABLE DRIVER
+
+ Try failed → Seata TC triggers Cancel on all registered branches:
+
+ ├── order-service.Cancel() → order canceled
+ ├── pricing-service.Cancel() → fare released
+ ├── coupon-service.Cancel() → coupon unfrozen
+ └── capacity-service.Cancel() → slot released
+
+ All resources restored to original state.
+```
+
+## Idempotency
+
+All Confirm and Cancel methods are idempotent:
+
+- **Status-based SQL conditions**: Each UPDATE uses `WHERE status=<expected>`,
so repeated
+ calls on an already-committed/canceled record affect zero rows and return
success.
+- **In-memory deduplication**: Each service tracks active transactions in a
`sync.Map` keyed
+ by XID. Once a Confirm/Cancel completes, the entry is removed. Subsequent
calls for the
+ same XID find no entry and return success immediately.
+
+> **Limitation (by design, to keep the sample small):** the XID→resource
mapping lives in
+> process memory. It makes Confirm/Cancel idempotent against TC retries and
handles *empty
+> rollback* (a branch whose Try failed), but it is **not crash-durable** and
does **not** guard
+> against TCC *suspension* (a delayed Try arriving after Cancel). If a service
restarts between
+> Try and the TC callback, the mapping is lost and the reserved resource is
left stranded.
+> Production code should use the seata-go **TCC fence** (`tcc_fence_log`
table) instead — see the
+> [`tcc/fence`](../fence) sample.
+
+## How to Run
+
+### 1. Start infrastructure
+
+```bash
+cd tcc/ride-order
+docker-compose up -d
+```
+
+This starts MySQL (with auto-initialized `seata_ride` database) and Seata
Server.
+
+### 2. Start all five services (each in a separate terminal, from its own
directory)
+
+```bash
+cd tcc/ride-order/order-service && go run . # :8001
+cd tcc/ride-order/dispatch-service && go run . # :8002
+cd tcc/ride-order/pricing-service && go run . # :8003
+cd tcc/ride-order/coupon-service && go run . # :8004
+cd tcc/ride-order/capacity-service && go run . # :8005
+```
+
+Each service must be started from its own directory so it can find its local
`seatago.yml`.
+
+### 3. Run the initiator
+
+```bash
+cd tcc/ride-order/initiator
+go run .
+```
+
+### 4. Expected output
+
+**Scenario 1** (success): The initiator logs five successful Prepare calls.
Each
+service logs `[*-Try]` followed by `[*-Confirm]`.
+
+**Scenario 2** (dispatch failure): The initiator logs four successful Prepare
calls,
+then `dispatch-service prepare failed`. Each of the four services logs
`[*-Cancel]`,
+restoring all resources.
+
+### Reset between runs
+
+Each successful ride consumes real resources (a coupon becomes *used*, a
driver becomes
+*busy*) — that is the correct business outcome, so those rows are **not**
restored on Confirm.
+The seed data (6 coupons, 6 drivers) is enough for several runs. To start
completely fresh,
+recreate the database volume:
+
+```bash
+docker-compose down -v && docker-compose up -d
+```
+
+### Customize MySQL connection
+
+Override via environment variables (set on each service):
+
+```bash
+export MYSQL_HOST=127.0.0.1
+export MYSQL_PORT=3306
+export MYSQL_USERNAME=root
+export MYSQL_PASSWORD=12345678
+export MYSQL_DB=seata_ride
+```
diff --git a/tcc/ride-order/capacity-service/main.go
b/tcc/ride-order/capacity-service/main.go
new file mode 100644
index 0000000..2552721
--- /dev/null
+++ b/tcc/ride-order/capacity-service/main.go
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package main
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+
+ "seata.apache.org/seata-go/pkg/client"
+ ginmiddleware "seata.apache.org/seata-go/pkg/integration/gin"
+ "seata.apache.org/seata-go/pkg/rm/tcc"
+ "seata.apache.org/seata-go/pkg/util/log"
+
+ "seata.apache.org/seata-go-samples/tcc/ride-order/common"
+)
+
+func main() {
+ client.InitPath("seatago.yml")
+ common.InitDB()
+
+ r := gin.Default()
+ r.Use(ginmiddleware.TransactionMiddleware())
+
+ proxy, err := tcc.NewTCCServiceProxy(&CapacityService{})
+ if err != nil {
+ log.Fatalf("create CapacityService proxy error: %v", err)
+ }
+
+ r.POST("/prepare", func(c *gin.Context) {
+ var req CapacityRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error":
err.Error()})
+ return
+ }
+ if _, err := proxy.Prepare(c.Request.Context(), req); err !=
nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error":
err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{"message": "prepare ok"})
+ })
+
+ if err := r.Run(":8005"); err != nil {
+ log.Fatalf("start capacity-service error: %v", err)
+ }
+}
diff --git a/tcc/ride-order/capacity-service/seatago.yml
b/tcc/ride-order/capacity-service/seatago.yml
new file mode 100644
index 0000000..4476d43
--- /dev/null
+++ b/tcc/ride-order/capacity-service/seatago.yml
@@ -0,0 +1,158 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+# time 时间单位对应的是 time.Duration(1)
+seata:
+ enabled: true
+ # application id
+ application-id: capacity-service
+ # service group
+ tx-service-group: default_tx_group
+ access-key: aliyunAccessKey
+ secret-key: aliyunSecretKey
+ enable-auto-data-source-proxy: false
+ data-source-proxy-mode: AT
+ client:
+ rm:
+ # Maximum cache length of asynchronous queue
+ async-commit-buffer-limit: 10000
+ # The maximum number of retries when report reports the status
+ report-retry-count: 5
+ # The interval for regularly checking the metadata of the db(AT)
+ table-meta-check-enable: false
+ # Whether to report the status if the transaction is successfully
executed(AT)
+ report-success-enable: false
+ # Whether to allow regular check of db metadata(AT)
+ saga-branch-register-enable: false
+ saga-json-parser: fastjson
+ saga-retry-persist-mode-update: false
+ saga-compensate-persist-mode-update: false
+ #Ordered.HIGHEST_PRECEDENCE + 1000 #
+ tcc-action-interceptor-order: -2147482648
+ # Parse SQL parser selection
+ sql-parser-type: druid
+ lock:
+ retry-interval: 30
+ retry-times: 10
+ retry-policy-branch-rollback-on-conflict: true
+ tm:
+ commit-retry-count: 5
+ rollback-retry-count: 5
+ default-global-transaction-timeout: 60s
+ degrade-check: false
+ degrade-check-period: 2000
+ degrade-check-allow-times: 10s
+ interceptor-order: -2147482648
+ undo:
+ # Judge whether the before image and after image are the same,If it is
the same, undo will not be recorded
+ data-validation: true
+ # Serialization method
+ log-serialization: json
+ # undo log table name
+ log-table: undo_log
+ # Only store modified fields
+ only-care-update-columns: true
+ compress:
+ # Compression type. Allowed Options: None, Gzip, Zip, Sevenz, Bzip2,
Lz4, Zstd, Deflate
+ type: None
+ # Compression threshold Unit: k
+ threshold: 64k
+ load-balance:
+ type: RandomLoadBalance
+ virtual-nodes: 10
+ service:
+ vgroup-mapping:
+ # Prefix for Print Log
+ default_tx_group: default
+ grouplist:
+ default: 127.0.0.1:8091
+ enable-degrade: false
+ # close the transaction
+ disable-global-transaction: false
+ transport:
+ shutdown:
+ wait: 3s
+ # Netty related configurations
+ # type
+ type: TCP
+ server: NIO
+ heartbeat: true
+ # Encoding and decoding mode
+ serialization: seata
+ # Message compression mode
+ compressor: none
+ # Allow batch sending of requests (TM)
+ enable-tm-client-batch-send-request: false
+ # Allow batch sending of requests (RM)
+ enable-rm-client-batch-send-request: true
+ # RM send request timeout
+ rpc-rm-request-timeout: 30s
+ # TM send request timeout
+ rpc-tm-request-timeout: 30s
+ # Configuration Center
+ config:
+ type: file
+ file:
+ name: config.conf
+ nacos:
+ namespace: ""
+ server-addr: 127.0.0.1:8848
+ group: SEATA_GROUP
+ username: ""
+ password: ""
+ ##if use MSE Nacos with auth, mutex with username/password attribute
+ #access-key: ""
+ #secret-key: ""
+ data-id: seata.properties
+ # Registration Center
+ registry:
+ type: file
+ file:
+ name: registry.conf
+ nacos:
+ application: seata-server
+ server-addr: 127.0.0.1:8848
+ group: "SEATA_GROUP"
+ namespace: ""
+ username: ""
+ password: ""
+ ##if use MSE Nacos with auth, mutex with username/password attribute #
+ #access-key: "" #
+ #secret-key: "" #
+ log:
+ exception-rate: 100
+ tcc:
+ fence:
+ # Anti suspension table name
+ log-table-name: tcc_fence_log_test
+ clean-period: 60s
+ # getty configuration
+ getty:
+ reconnect-interval: 0
+ # temporary not supported connection-num
+ connection-num: 1
+ session:
+ compress-encoding: false
+ tcp-no-delay: true
+ tcp-keep-alive: true
+ keep-alive-period: 120s
+ tcp-r-buf-size: 262144
+ tcp-w-buf-size: 65536
+ tcp-read-timeout: 1s
+ tcp-write-timeout: 5s
+ wait-timeout: 1s
+ max-msg-len: 16498688
+ session-name: client_test
+ cron-period: 1s
diff --git a/tcc/ride-order/capacity-service/service.go
b/tcc/ride-order/capacity-service/service.go
new file mode 100644
index 0000000..d1968b9
--- /dev/null
+++ b/tcc/ride-order/capacity-service/service.go
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package main
+
+import (
+ "context"
+ "fmt"
+ "sync"
+
+ "seata.apache.org/seata-go/pkg/tm"
+ "seata.apache.org/seata-go/pkg/util/log"
+
+ "seata.apache.org/seata-go-samples/tcc/ride-order/common"
+)
+
+type CapacityRequest struct {
+ CapacityID int64 `json:"capacity_id"`
+}
+
+type CapacityService struct{}
+
+// capacityRecords tracks xid -> capacityID for Commit/Rollback lookup.
+var capacityRecords sync.Map
+
+func (s *CapacityService) GetActionName() string {
+ return "CapacityService"
+}
+
+// Prepare reserves a vehicle slot: reserved_slots + 1 (Try phase).
+func (s *CapacityService) Prepare(ctx context.Context, params interface{})
(bool, error) {
+ req, ok := params.(CapacityRequest)
+ if !ok {
+ return false, fmt.Errorf("invalid params type %T, want
CapacityRequest", params)
+ }
+ result, err := common.DB.ExecContext(ctx,
+ "UPDATE vehicle_capacity SET reserved_slots=reserved_slots+1
WHERE id=? AND reserved_slots<total_slots",
+ req.CapacityID)
+ if err != nil {
+ return false, fmt.Errorf("capacity prepare failed: %v", err)
+ }
+ rows, _ := result.RowsAffected()
+ if rows == 0 {
+ return false, fmt.Errorf("vehicle capacity %d full, no slots
available", req.CapacityID)
+ }
+ xid := tm.GetXID(ctx)
+ capacityRecords.Store(xid, req.CapacityID)
+ log.Infof("[Capacity-Try] reserved 1 slot on vehicle capacity %d,
xid=%s", req.CapacityID, xid)
+ return true, nil
+}
+
+// Commit keeps the reservation as-is (reservation is the final state).
+func (s *CapacityService) Commit(ctx context.Context, bac
*tm.BusinessActionContext) (bool, error) {
+ capacityID, ok := capacityRecords.Load(bac.Xid)
+ if !ok {
+ log.Infof("[Capacity-Confirm] no record for xid=%s, idempotent
skip", bac.Xid)
+ return true, nil
+ }
+ capacityRecords.Delete(bac.Xid)
+ log.Infof("[Capacity-Confirm] slot reservation confirmed on capacity
%d, xid=%s", capacityID, bac.Xid)
+ return true, nil
+}
+
+// Rollback releases the vehicle slot: reserved_slots - 1. Idempotent via
reserved_slots>0 check.
+func (s *CapacityService) Rollback(ctx context.Context, bac
*tm.BusinessActionContext) (bool, error) {
+ capacityID, ok := capacityRecords.Load(bac.Xid)
+ if !ok {
+ log.Infof("[Capacity-Cancel] no record for xid=%s, idempotent
skip", bac.Xid)
+ return true, nil
+ }
+ _, err := common.DB.Exec(
+ "UPDATE vehicle_capacity SET reserved_slots=reserved_slots-1
WHERE id=? AND reserved_slots>0",
+ capacityID)
+ if err != nil {
+ return false, fmt.Errorf("capacity cancel failed: %v", err)
+ }
+ capacityRecords.Delete(bac.Xid)
+ log.Infof("[Capacity-Cancel] slot released on capacity %d, xid=%s",
capacityID, bac.Xid)
+ return true, nil
+}
diff --git a/tcc/ride-order/common/db.go b/tcc/ride-order/common/db.go
new file mode 100644
index 0000000..a767ba5
--- /dev/null
+++ b/tcc/ride-order/common/db.go
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package common
+
+import (
+ "database/sql"
+ "os"
+
+ _ "github.com/go-sql-driver/mysql"
+)
+
+var DB *sql.DB
+
+func InitDB() {
+ defaultEnv()
+ dsn :=
os.ExpandEnv("${MYSQL_USERNAME}:${MYSQL_PASSWORD}@tcp(${MYSQL_HOST}:${MYSQL_PORT})/${MYSQL_DB}?parseTime=true")
+ var err error
+ DB, err = sql.Open("mysql", dsn)
+ if err != nil {
+ panic("open mysql failed: " + err.Error())
+ }
+ if err = DB.Ping(); err != nil {
+ panic("ping mysql failed: " + err.Error())
+ }
+}
+
+func defaultEnv() {
+ envDefaults := map[string]string{
+ "MYSQL_HOST": "127.0.0.1",
+ "MYSQL_PORT": "3306",
+ "MYSQL_USERNAME": "root",
+ "MYSQL_PASSWORD": "12345678",
+ "MYSQL_DB": "seata_ride",
+ }
+ for k, v := range envDefaults {
+ if os.Getenv(k) == "" {
+ os.Setenv(k, v)
+ }
+ }
+}
diff --git a/tcc/ride-order/coupon-service/main.go
b/tcc/ride-order/coupon-service/main.go
new file mode 100644
index 0000000..a9e24fe
--- /dev/null
+++ b/tcc/ride-order/coupon-service/main.go
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package main
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+
+ "seata.apache.org/seata-go/pkg/client"
+ ginmiddleware "seata.apache.org/seata-go/pkg/integration/gin"
+ "seata.apache.org/seata-go/pkg/rm/tcc"
+ "seata.apache.org/seata-go/pkg/util/log"
+
+ "seata.apache.org/seata-go-samples/tcc/ride-order/common"
+)
+
+func main() {
+ client.InitPath("seatago.yml")
+ common.InitDB()
+
+ r := gin.Default()
+ r.Use(ginmiddleware.TransactionMiddleware())
+
+ proxy, err := tcc.NewTCCServiceProxy(&CouponService{})
+ if err != nil {
+ log.Fatalf("create CouponService proxy error: %v", err)
+ }
+
+ r.POST("/prepare", func(c *gin.Context) {
+ var req CouponRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error":
err.Error()})
+ return
+ }
+ if _, err := proxy.Prepare(c.Request.Context(), req); err !=
nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error":
err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{"message": "prepare ok"})
+ })
+
+ if err := r.Run(":8004"); err != nil {
+ log.Fatalf("start coupon-service error: %v", err)
+ }
+}
diff --git a/tcc/ride-order/coupon-service/seatago.yml
b/tcc/ride-order/coupon-service/seatago.yml
new file mode 100644
index 0000000..53161af
--- /dev/null
+++ b/tcc/ride-order/coupon-service/seatago.yml
@@ -0,0 +1,158 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+# time 时间单位对应的是 time.Duration(1)
+seata:
+ enabled: true
+ # application id
+ application-id: coupon-service
+ # service group
+ tx-service-group: default_tx_group
+ access-key: aliyunAccessKey
+ secret-key: aliyunSecretKey
+ enable-auto-data-source-proxy: false
+ data-source-proxy-mode: AT
+ client:
+ rm:
+ # Maximum cache length of asynchronous queue
+ async-commit-buffer-limit: 10000
+ # The maximum number of retries when report reports the status
+ report-retry-count: 5
+ # The interval for regularly checking the metadata of the db(AT)
+ table-meta-check-enable: false
+ # Whether to report the status if the transaction is successfully
executed(AT)
+ report-success-enable: false
+ # Whether to allow regular check of db metadata(AT)
+ saga-branch-register-enable: false
+ saga-json-parser: fastjson
+ saga-retry-persist-mode-update: false
+ saga-compensate-persist-mode-update: false
+ #Ordered.HIGHEST_PRECEDENCE + 1000 #
+ tcc-action-interceptor-order: -2147482648
+ # Parse SQL parser selection
+ sql-parser-type: druid
+ lock:
+ retry-interval: 30
+ retry-times: 10
+ retry-policy-branch-rollback-on-conflict: true
+ tm:
+ commit-retry-count: 5
+ rollback-retry-count: 5
+ default-global-transaction-timeout: 60s
+ degrade-check: false
+ degrade-check-period: 2000
+ degrade-check-allow-times: 10s
+ interceptor-order: -2147482648
+ undo:
+ # Judge whether the before image and after image are the same,If it is
the same, undo will not be recorded
+ data-validation: true
+ # Serialization method
+ log-serialization: json
+ # undo log table name
+ log-table: undo_log
+ # Only store modified fields
+ only-care-update-columns: true
+ compress:
+ # Compression type. Allowed Options: None, Gzip, Zip, Sevenz, Bzip2,
Lz4, Zstd, Deflate
+ type: None
+ # Compression threshold Unit: k
+ threshold: 64k
+ load-balance:
+ type: RandomLoadBalance
+ virtual-nodes: 10
+ service:
+ vgroup-mapping:
+ # Prefix for Print Log
+ default_tx_group: default
+ grouplist:
+ default: 127.0.0.1:8091
+ enable-degrade: false
+ # close the transaction
+ disable-global-transaction: false
+ transport:
+ shutdown:
+ wait: 3s
+ # Netty related configurations
+ # type
+ type: TCP
+ server: NIO
+ heartbeat: true
+ # Encoding and decoding mode
+ serialization: seata
+ # Message compression mode
+ compressor: none
+ # Allow batch sending of requests (TM)
+ enable-tm-client-batch-send-request: false
+ # Allow batch sending of requests (RM)
+ enable-rm-client-batch-send-request: true
+ # RM send request timeout
+ rpc-rm-request-timeout: 30s
+ # TM send request timeout
+ rpc-tm-request-timeout: 30s
+ # Configuration Center
+ config:
+ type: file
+ file:
+ name: config.conf
+ nacos:
+ namespace: ""
+ server-addr: 127.0.0.1:8848
+ group: SEATA_GROUP
+ username: ""
+ password: ""
+ ##if use MSE Nacos with auth, mutex with username/password attribute
+ #access-key: ""
+ #secret-key: ""
+ data-id: seata.properties
+ # Registration Center
+ registry:
+ type: file
+ file:
+ name: registry.conf
+ nacos:
+ application: seata-server
+ server-addr: 127.0.0.1:8848
+ group: "SEATA_GROUP"
+ namespace: ""
+ username: ""
+ password: ""
+ ##if use MSE Nacos with auth, mutex with username/password attribute #
+ #access-key: "" #
+ #secret-key: "" #
+ log:
+ exception-rate: 100
+ tcc:
+ fence:
+ # Anti suspension table name
+ log-table-name: tcc_fence_log_test
+ clean-period: 60s
+ # getty configuration
+ getty:
+ reconnect-interval: 0
+ # temporary not supported connection-num
+ connection-num: 1
+ session:
+ compress-encoding: false
+ tcp-no-delay: true
+ tcp-keep-alive: true
+ keep-alive-period: 120s
+ tcp-r-buf-size: 262144
+ tcp-w-buf-size: 65536
+ tcp-read-timeout: 1s
+ tcp-write-timeout: 5s
+ wait-timeout: 1s
+ max-msg-len: 16498688
+ session-name: client_test
+ cron-period: 1s
diff --git a/tcc/ride-order/coupon-service/service.go
b/tcc/ride-order/coupon-service/service.go
new file mode 100644
index 0000000..2cc1c02
--- /dev/null
+++ b/tcc/ride-order/coupon-service/service.go
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package main
+
+import (
+ "context"
+ "fmt"
+ "sync"
+
+ "seata.apache.org/seata-go/pkg/tm"
+ "seata.apache.org/seata-go/pkg/util/log"
+
+ "seata.apache.org/seata-go-samples/tcc/ride-order/common"
+)
+
+type CouponRequest struct {
+ PassengerID string `json:"passenger_id"`
+}
+
+type CouponService struct{}
+
+// couponRecords tracks xid -> couponID for Commit/Rollback lookup.
+var couponRecords sync.Map
+
+func (s *CouponService) GetActionName() string {
+ return "CouponService"
+}
+
+// Prepare freezes an available coupon for the passenger: available -> frozen
(Try phase).
+// The coupon is selected dynamically so the sample stays re-runnable (no
hardcoded id
+// that gets permanently consumed after the first Confirm).
+func (s *CouponService) Prepare(ctx context.Context, params interface{})
(bool, error) {
+ req, ok := params.(CouponRequest)
+ if !ok {
+ return false, fmt.Errorf("invalid params type %T, want
CouponRequest", params)
+ }
+
+ var couponID int64
+ err := common.DB.QueryRowContext(ctx,
+ "SELECT id FROM coupons WHERE user_id=? AND status=0 ORDER BY
id LIMIT 1", req.PassengerID).Scan(&couponID)
+ if err != nil {
+ return false, fmt.Errorf("no available coupon for %s: %v",
req.PassengerID, err)
+ }
+ // Guard against a concurrent transaction grabbing the same coupon.
+ result, err := common.DB.ExecContext(ctx,
+ "UPDATE coupons SET status=1 WHERE id=? AND status=0", couponID)
+ if err != nil {
+ return false, fmt.Errorf("coupon prepare failed: %v", err)
+ }
+ if rows, _ := result.RowsAffected(); rows == 0 {
+ return false, fmt.Errorf("coupon %d no longer available",
couponID)
+ }
+ xid := tm.GetXID(ctx)
+ couponRecords.Store(xid, couponID)
+ log.Infof("[Coupon-Try] froze coupon %d for %s, xid=%s", couponID,
req.PassengerID, xid)
+ return true, nil
+}
+
+// Commit uses the coupon: frozen -> used. Idempotent via status check.
+func (s *CouponService) Commit(ctx context.Context, bac
*tm.BusinessActionContext) (bool, error) {
+ couponID, ok := couponRecords.Load(bac.Xid)
+ if !ok {
+ log.Infof("[Coupon-Confirm] no record for xid=%s, idempotent
skip", bac.Xid)
+ return true, nil
+ }
+ _, err := common.DB.Exec("UPDATE coupons SET status=2 WHERE id=? AND
status=1", couponID)
+ if err != nil {
+ return false, fmt.Errorf("coupon confirm failed: %v", err)
+ }
+ couponRecords.Delete(bac.Xid)
+ log.Infof("[Coupon-Confirm] coupon %d used, xid=%s", couponID, bac.Xid)
+ return true, nil
+}
+
+// Rollback unfreezes the coupon: frozen -> available. Idempotent via status
check.
+func (s *CouponService) Rollback(ctx context.Context, bac
*tm.BusinessActionContext) (bool, error) {
+ couponID, ok := couponRecords.Load(bac.Xid)
+ if !ok {
+ log.Infof("[Coupon-Cancel] no record for xid=%s, idempotent
skip", bac.Xid)
+ return true, nil
+ }
+ _, err := common.DB.Exec("UPDATE coupons SET status=0 WHERE id=? AND
status=1", couponID)
+ if err != nil {
+ return false, fmt.Errorf("coupon cancel failed: %v", err)
+ }
+ couponRecords.Delete(bac.Xid)
+ log.Infof("[Coupon-Cancel] coupon %d unfrozen, xid=%s", couponID,
bac.Xid)
+ return true, nil
+}
diff --git a/tcc/ride-order/dispatch-service/main.go
b/tcc/ride-order/dispatch-service/main.go
new file mode 100644
index 0000000..2cce5b8
--- /dev/null
+++ b/tcc/ride-order/dispatch-service/main.go
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package main
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+
+ "seata.apache.org/seata-go/pkg/client"
+ ginmiddleware "seata.apache.org/seata-go/pkg/integration/gin"
+ "seata.apache.org/seata-go/pkg/rm/tcc"
+ "seata.apache.org/seata-go/pkg/util/log"
+
+ "seata.apache.org/seata-go-samples/tcc/ride-order/common"
+)
+
+func main() {
+ client.InitPath("seatago.yml")
+ common.InitDB()
+
+ r := gin.Default()
+ r.Use(ginmiddleware.TransactionMiddleware())
+
+ proxy, err := tcc.NewTCCServiceProxy(&DispatchService{})
+ if err != nil {
+ log.Fatalf("create DispatchService proxy error: %v", err)
+ }
+
+ r.POST("/prepare", func(c *gin.Context) {
+ var req DispatchRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error":
err.Error()})
+ return
+ }
+ if _, err := proxy.Prepare(c.Request.Context(), req); err !=
nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error":
err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{"message": "prepare ok"})
+ })
+
+ if err := r.Run(":8002"); err != nil {
+ log.Fatalf("start dispatch-service error: %v", err)
+ }
+}
diff --git a/tcc/ride-order/dispatch-service/seatago.yml
b/tcc/ride-order/dispatch-service/seatago.yml
new file mode 100644
index 0000000..122d4e3
--- /dev/null
+++ b/tcc/ride-order/dispatch-service/seatago.yml
@@ -0,0 +1,158 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+# time 时间单位对应的是 time.Duration(1)
+seata:
+ enabled: true
+ # application id
+ application-id: dispatch-service
+ # service group
+ tx-service-group: default_tx_group
+ access-key: aliyunAccessKey
+ secret-key: aliyunSecretKey
+ enable-auto-data-source-proxy: false
+ data-source-proxy-mode: AT
+ client:
+ rm:
+ # Maximum cache length of asynchronous queue
+ async-commit-buffer-limit: 10000
+ # The maximum number of retries when report reports the status
+ report-retry-count: 5
+ # The interval for regularly checking the metadata of the db(AT)
+ table-meta-check-enable: false
+ # Whether to report the status if the transaction is successfully
executed(AT)
+ report-success-enable: false
+ # Whether to allow regular check of db metadata(AT)
+ saga-branch-register-enable: false
+ saga-json-parser: fastjson
+ saga-retry-persist-mode-update: false
+ saga-compensate-persist-mode-update: false
+ #Ordered.HIGHEST_PRECEDENCE + 1000 #
+ tcc-action-interceptor-order: -2147482648
+ # Parse SQL parser selection
+ sql-parser-type: druid
+ lock:
+ retry-interval: 30
+ retry-times: 10
+ retry-policy-branch-rollback-on-conflict: true
+ tm:
+ commit-retry-count: 5
+ rollback-retry-count: 5
+ default-global-transaction-timeout: 60s
+ degrade-check: false
+ degrade-check-period: 2000
+ degrade-check-allow-times: 10s
+ interceptor-order: -2147482648
+ undo:
+ # Judge whether the before image and after image are the same,If it is
the same, undo will not be recorded
+ data-validation: true
+ # Serialization method
+ log-serialization: json
+ # undo log table name
+ log-table: undo_log
+ # Only store modified fields
+ only-care-update-columns: true
+ compress:
+ # Compression type. Allowed Options: None, Gzip, Zip, Sevenz, Bzip2,
Lz4, Zstd, Deflate
+ type: None
+ # Compression threshold Unit: k
+ threshold: 64k
+ load-balance:
+ type: RandomLoadBalance
+ virtual-nodes: 10
+ service:
+ vgroup-mapping:
+ # Prefix for Print Log
+ default_tx_group: default
+ grouplist:
+ default: 127.0.0.1:8091
+ enable-degrade: false
+ # close the transaction
+ disable-global-transaction: false
+ transport:
+ shutdown:
+ wait: 3s
+ # Netty related configurations
+ # type
+ type: TCP
+ server: NIO
+ heartbeat: true
+ # Encoding and decoding mode
+ serialization: seata
+ # Message compression mode
+ compressor: none
+ # Allow batch sending of requests (TM)
+ enable-tm-client-batch-send-request: false
+ # Allow batch sending of requests (RM)
+ enable-rm-client-batch-send-request: true
+ # RM send request timeout
+ rpc-rm-request-timeout: 30s
+ # TM send request timeout
+ rpc-tm-request-timeout: 30s
+ # Configuration Center
+ config:
+ type: file
+ file:
+ name: config.conf
+ nacos:
+ namespace: ""
+ server-addr: 127.0.0.1:8848
+ group: SEATA_GROUP
+ username: ""
+ password: ""
+ ##if use MSE Nacos with auth, mutex with username/password attribute
+ #access-key: ""
+ #secret-key: ""
+ data-id: seata.properties
+ # Registration Center
+ registry:
+ type: file
+ file:
+ name: registry.conf
+ nacos:
+ application: seata-server
+ server-addr: 127.0.0.1:8848
+ group: "SEATA_GROUP"
+ namespace: ""
+ username: ""
+ password: ""
+ ##if use MSE Nacos with auth, mutex with username/password attribute #
+ #access-key: "" #
+ #secret-key: "" #
+ log:
+ exception-rate: 100
+ tcc:
+ fence:
+ # Anti suspension table name
+ log-table-name: tcc_fence_log_test
+ clean-period: 60s
+ # getty configuration
+ getty:
+ reconnect-interval: 0
+ # temporary not supported connection-num
+ connection-num: 1
+ session:
+ compress-encoding: false
+ tcp-no-delay: true
+ tcp-keep-alive: true
+ keep-alive-period: 120s
+ tcp-r-buf-size: 262144
+ tcp-w-buf-size: 65536
+ tcp-read-timeout: 1s
+ tcp-write-timeout: 5s
+ wait-timeout: 1s
+ max-msg-len: 16498688
+ session-name: client_test
+ cron-period: 1s
diff --git a/tcc/ride-order/dispatch-service/service.go
b/tcc/ride-order/dispatch-service/service.go
new file mode 100644
index 0000000..2cb489d
--- /dev/null
+++ b/tcc/ride-order/dispatch-service/service.go
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package main
+
+import (
+ "context"
+ "fmt"
+ "sync"
+
+ "seata.apache.org/seata-go/pkg/tm"
+ "seata.apache.org/seata-go/pkg/util/log"
+
+ "seata.apache.org/seata-go-samples/tcc/ride-order/common"
+)
+
+type DispatchRequest struct {
+ OrderID int64 `json:"order_id"`
+ SimulateFail bool `json:"simulate_fail"`
+}
+
+type DispatchService struct{}
+
+// dispatchRecords tracks xid -> driverID for Commit/Rollback lookup.
+var dispatchRecords sync.Map
+
+func (s *DispatchService) GetActionName() string {
+ return "DispatchService"
+}
+
+// Prepare reserves an available driver (Try phase).
+// When SimulateFail is true, returns an error to trigger global rollback.
+func (s *DispatchService) Prepare(ctx context.Context, params interface{})
(bool, error) {
+ req, ok := params.(DispatchRequest)
+ if !ok {
+ return false, fmt.Errorf("invalid params type %T, want
DispatchRequest", params)
+ }
+ if req.SimulateFail {
+ log.Infof("[Dispatch-Try] SIMULATED FAILURE: no available
driver, xid=%s", tm.GetXID(ctx))
+ return false, fmt.Errorf("no available driver found (simulated
failure)")
+ }
+
+ var driverID int64
+ err := common.DB.QueryRowContext(ctx,
+ "SELECT id FROM drivers WHERE status=0 LIMIT 1").Scan(&driverID)
+ if err != nil {
+ return false, fmt.Errorf("no available driver: %v", err)
+ }
+ // Guard against a concurrent transaction grabbing the same driver.
+ result, err := common.DB.ExecContext(ctx,
+ "UPDATE drivers SET status=1, reserved_order_id=? WHERE id=?
AND status=0",
+ req.OrderID, driverID)
+ if err != nil {
+ return false, fmt.Errorf("dispatch prepare failed: %v", err)
+ }
+ if rows, _ := result.RowsAffected(); rows == 0 {
+ return false, fmt.Errorf("driver %d no longer available",
driverID)
+ }
+ xid := tm.GetXID(ctx)
+ dispatchRecords.Store(xid, driverID)
+ log.Infof("[Dispatch-Try] reserved driver %d for order %d, xid=%s",
driverID, req.OrderID, xid)
+ return true, nil
+}
+
+// Commit confirms the driver: reserved -> busy. Idempotent via status check.
+func (s *DispatchService) Commit(ctx context.Context, bac
*tm.BusinessActionContext) (bool, error) {
+ driverID, ok := dispatchRecords.Load(bac.Xid)
+ if !ok {
+ log.Infof("[Dispatch-Confirm] no record for xid=%s, idempotent
skip", bac.Xid)
+ return true, nil
+ }
+ _, err := common.DB.Exec("UPDATE drivers SET status=2 WHERE id=? AND
status=1", driverID)
+ if err != nil {
+ return false, fmt.Errorf("dispatch confirm failed: %v", err)
+ }
+ dispatchRecords.Delete(bac.Xid)
+ log.Infof("[Dispatch-Confirm] driver %d now busy, xid=%s", driverID,
bac.Xid)
+ return true, nil
+}
+
+// Rollback releases the driver: reserved -> available. Idempotent via status
check.
+func (s *DispatchService) Rollback(ctx context.Context, bac
*tm.BusinessActionContext) (bool, error) {
+ driverID, ok := dispatchRecords.Load(bac.Xid)
+ if !ok {
+ log.Infof("[Dispatch-Cancel] no record for xid=%s, idempotent
skip", bac.Xid)
+ return true, nil
+ }
+ _, err := common.DB.Exec("UPDATE drivers SET status=0,
reserved_order_id=NULL WHERE id=? AND status=1", driverID)
+ if err != nil {
+ return false, fmt.Errorf("dispatch cancel failed: %v", err)
+ }
+ dispatchRecords.Delete(bac.Xid)
+ log.Infof("[Dispatch-Cancel] driver %d released, xid=%s", driverID,
bac.Xid)
+ return true, nil
+}
diff --git a/tcc/ride-order/docker-compose.yml
b/tcc/ride-order/docker-compose.yml
new file mode 100644
index 0000000..7c197db
--- /dev/null
+++ b/tcc/ride-order/docker-compose.yml
@@ -0,0 +1,38 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+
+version: '3'
+services:
+ seata-server:
+ image: seataio/seata-server:1.6.1
+ ports:
+ - "8091:8091"
+ - "7091:7091"
+ environment:
+ - SEATA_PORT=8091
+ - STORE_MODE=file
+
+ mysql:
+ image: mysql:8.0.32
+ container_name: ride-order-mysql
+ environment:
+ - MYSQL_ROOT_PASSWORD=12345678
+ command: --default-authentication-plugin=mysql_native_password
--default-time-zone='+08:00'
+ volumes:
+ - ./sql:/docker-entrypoint-initdb.d
+ ports:
+ - "3306:3306"
diff --git a/tcc/ride-order/initiator/main.go b/tcc/ride-order/initiator/main.go
new file mode 100644
index 0000000..b66aa2f
--- /dev/null
+++ b/tcc/ride-order/initiator/main.go
@@ -0,0 +1,149 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/parnurzeal/gorequest"
+
+ "seata.apache.org/seata-go/pkg/client"
+ "seata.apache.org/seata-go/pkg/constant"
+ "seata.apache.org/seata-go/pkg/tm"
+ "seata.apache.org/seata-go/pkg/util/log"
+)
+
+const (
+ orderServiceURL = "http://127.0.0.1:8001"
+ dispatchServiceURL = "http://127.0.0.1:8002"
+ pricingServiceURL = "http://127.0.0.1:8003"
+ couponServiceURL = "http://127.0.0.1:8004"
+ capacityServiceURL = "http://127.0.0.1:8005"
+
+ // Bound each Try call so a hung service cannot block the global
transaction.
+ prepareTimeout = 10 * time.Second
+)
+
+func main() {
+ client.InitPath("seatago.yml")
+ ctx := context.Background()
+
+ // ========== Scenario 1: All services succeed ==========
+ fmt.Println("\n========== Scenario 1: All services succeed ==========")
+
+ err := tm.WithGlobalTx(ctx, &tm.GtxConfig{
+ Name: "RideOrderSuccess",
+ Timeout: 60 * time.Second,
+ }, func(ctx context.Context) error {
+ return rideOrderBusiness(ctx, false)
+ })
+ if err != nil {
+ log.Errorf("Scenario 1 FAILED: %v", err)
+ } else {
+ log.Infof("Scenario 1 SUCCESS: ride order confirmed")
+ }
+
+ // Wait for Seata async Confirm callbacks
+ time.Sleep(3 * time.Second)
+
+ // ========== Scenario 2: Dispatch fails, triggers global rollback
==========
+ fmt.Println("\n========== Scenario 2: Dispatch fails, triggers global
rollback ==========")
+
+ err = tm.WithGlobalTx(ctx, &tm.GtxConfig{
+ Name: "RideOrderDispatchFail",
+ Timeout: 60 * time.Second,
+ }, func(ctx context.Context) error {
+ return rideOrderBusiness(ctx, true)
+ })
+ if err != nil {
+ log.Infof("Scenario 2 EXPECTED FAILURE: %v", err)
+ log.Infof("All resources should be released via Cancel methods")
+ } else {
+ log.Errorf("Scenario 2 should have failed but succeeded")
+ }
+
+ // Wait for Cancel callbacks
+ time.Sleep(3 * time.Second)
+ fmt.Println("\n========== Done ==========")
+}
+
+func rideOrderBusiness(ctx context.Context, simulateDispatchFail bool) error {
+ xid := tm.GetXID(ctx)
+
+ // 1. Create pending order — order-service returns the generated order
id.
+ body, err := callService(ctx, orderServiceURL,
`{"passenger_id":"passenger-001"}`)
+ if err != nil {
+ return fmt.Errorf("order-service prepare failed: %v", err)
+ }
+ var order struct {
+ OrderID int64 `json:"order_id"`
+ }
+ if err := json.Unmarshal([]byte(body), &order); err != nil ||
order.OrderID == 0 {
+ return fmt.Errorf("order-service returned no order id: body=%s
err=%v", body, err)
+ }
+ log.Infof("[Initiator] order-service prepared, order_id=%d, xid=%s",
order.OrderID, xid)
+
+ // 2. Lock estimated fare for the real order id.
+ if _, err := callService(ctx, pricingServiceURL, fmt.Sprintf(
+ `{"order_id":%d,"amount":2800}`, order.OrderID)); err != nil {
+ return fmt.Errorf("pricing-service prepare failed: %v", err)
+ }
+ log.Infof("[Initiator] pricing-service prepared, xid=%s", xid)
+
+ // 3. Freeze an available coupon for the passenger (selected
dynamically by the service).
+ if _, err := callService(ctx, couponServiceURL,
`{"passenger_id":"passenger-001"}`); err != nil {
+ return fmt.Errorf("coupon-service prepare failed: %v", err)
+ }
+ log.Infof("[Initiator] coupon-service prepared, xid=%s", xid)
+
+ // 4. Reserve vehicle slot.
+ if _, err := callService(ctx, capacityServiceURL, `{"capacity_id":1}`);
err != nil {
+ return fmt.Errorf("capacity-service prepare failed: %v", err)
+ }
+ log.Infof("[Initiator] capacity-service prepared, xid=%s", xid)
+
+ // 5. Reserve driver — THIS WILL FAIL in scenario 2.
+ if _, err := callService(ctx, dispatchServiceURL, fmt.Sprintf(
+ `{"order_id":%d,"simulate_fail":%t}`, order.OrderID,
simulateDispatchFail)); err != nil {
+ return fmt.Errorf("dispatch-service prepare failed: %v", err)
+ }
+ log.Infof("[Initiator] dispatch-service prepared, xid=%s", xid)
+
+ return nil
+}
+
+func callService(ctx context.Context, serviceURL string, jsonBody string)
(string, error) {
+ resp, body, errs := gorequest.New().
+ Timeout(prepareTimeout).
+ Post(serviceURL+"/prepare").
+ Set(constant.XidKey, tm.GetXID(ctx)).
+ Type("json").
+ Send(jsonBody).
+ End()
+ if len(errs) != 0 {
+ return "", errs[0]
+ }
+ if resp.StatusCode != http.StatusOK {
+ return "", fmt.Errorf("service returned %d: %s",
resp.StatusCode, body)
+ }
+ return body, nil
+}
diff --git a/tcc/ride-order/initiator/seatago.yml
b/tcc/ride-order/initiator/seatago.yml
new file mode 100644
index 0000000..ba1a62e
--- /dev/null
+++ b/tcc/ride-order/initiator/seatago.yml
@@ -0,0 +1,158 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+# time 时间单位对应的是 time.Duration(1)
+seata:
+ enabled: true
+ # application id
+ application-id: ride-order-initiator
+ # service group
+ tx-service-group: default_tx_group
+ access-key: aliyunAccessKey
+ secret-key: aliyunSecretKey
+ enable-auto-data-source-proxy: false
+ data-source-proxy-mode: AT
+ client:
+ rm:
+ # Maximum cache length of asynchronous queue
+ async-commit-buffer-limit: 10000
+ # The maximum number of retries when report reports the status
+ report-retry-count: 5
+ # The interval for regularly checking the metadata of the db(AT)
+ table-meta-check-enable: false
+ # Whether to report the status if the transaction is successfully
executed(AT)
+ report-success-enable: false
+ # Whether to allow regular check of db metadata(AT)
+ saga-branch-register-enable: false
+ saga-json-parser: fastjson
+ saga-retry-persist-mode-update: false
+ saga-compensate-persist-mode-update: false
+ #Ordered.HIGHEST_PRECEDENCE + 1000 #
+ tcc-action-interceptor-order: -2147482648
+ # Parse SQL parser selection
+ sql-parser-type: druid
+ lock:
+ retry-interval: 30
+ retry-times: 10
+ retry-policy-branch-rollback-on-conflict: true
+ tm:
+ commit-retry-count: 5
+ rollback-retry-count: 5
+ default-global-transaction-timeout: 60s
+ degrade-check: false
+ degrade-check-period: 2000
+ degrade-check-allow-times: 10s
+ interceptor-order: -2147482648
+ undo:
+ # Judge whether the before image and after image are the same,If it is
the same, undo will not be recorded
+ data-validation: true
+ # Serialization method
+ log-serialization: json
+ # undo log table name
+ log-table: undo_log
+ # Only store modified fields
+ only-care-update-columns: true
+ compress:
+ # Compression type. Allowed Options: None, Gzip, Zip, Sevenz, Bzip2,
Lz4, Zstd, Deflate
+ type: None
+ # Compression threshold Unit: k
+ threshold: 64k
+ load-balance:
+ type: RandomLoadBalance
+ virtual-nodes: 10
+ service:
+ vgroup-mapping:
+ # Prefix for Print Log
+ default_tx_group: default
+ grouplist:
+ default: 127.0.0.1:8091
+ enable-degrade: false
+ # close the transaction
+ disable-global-transaction: false
+ transport:
+ shutdown:
+ wait: 3s
+ # Netty related configurations
+ # type
+ type: TCP
+ server: NIO
+ heartbeat: true
+ # Encoding and decoding mode
+ serialization: seata
+ # Message compression mode
+ compressor: none
+ # Allow batch sending of requests (TM)
+ enable-tm-client-batch-send-request: false
+ # Allow batch sending of requests (RM)
+ enable-rm-client-batch-send-request: true
+ # RM send request timeout
+ rpc-rm-request-timeout: 30s
+ # TM send request timeout
+ rpc-tm-request-timeout: 30s
+ # Configuration Center
+ config:
+ type: file
+ file:
+ name: config.conf
+ nacos:
+ namespace: ""
+ server-addr: 127.0.0.1:8848
+ group: SEATA_GROUP
+ username: ""
+ password: ""
+ ##if use MSE Nacos with auth, mutex with username/password attribute
+ #access-key: ""
+ #secret-key: ""
+ data-id: seata.properties
+ # Registration Center
+ registry:
+ type: file
+ file:
+ name: registry.conf
+ nacos:
+ application: seata-server
+ server-addr: 127.0.0.1:8848
+ group: "SEATA_GROUP"
+ namespace: ""
+ username: ""
+ password: ""
+ ##if use MSE Nacos with auth, mutex with username/password attribute #
+ #access-key: "" #
+ #secret-key: "" #
+ log:
+ exception-rate: 100
+ tcc:
+ fence:
+ # Anti suspension table name
+ log-table-name: tcc_fence_log_test
+ clean-period: 60s
+ # getty configuration
+ getty:
+ reconnect-interval: 0
+ # temporary not supported connection-num
+ connection-num: 1
+ session:
+ compress-encoding: false
+ tcp-no-delay: true
+ tcp-keep-alive: true
+ keep-alive-period: 120s
+ tcp-r-buf-size: 262144
+ tcp-w-buf-size: 65536
+ tcp-read-timeout: 1s
+ tcp-write-timeout: 5s
+ wait-timeout: 1s
+ max-msg-len: 16498688
+ session-name: client_test
+ cron-period: 1s
diff --git a/tcc/ride-order/order-service/main.go
b/tcc/ride-order/order-service/main.go
new file mode 100644
index 0000000..f188aa1
--- /dev/null
+++ b/tcc/ride-order/order-service/main.go
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package main
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+
+ "seata.apache.org/seata-go/pkg/client"
+ ginmiddleware "seata.apache.org/seata-go/pkg/integration/gin"
+ "seata.apache.org/seata-go/pkg/rm/tcc"
+ "seata.apache.org/seata-go/pkg/tm"
+ "seata.apache.org/seata-go/pkg/util/log"
+
+ "seata.apache.org/seata-go-samples/tcc/ride-order/common"
+)
+
+func main() {
+ client.InitPath("seatago.yml")
+ common.InitDB()
+
+ r := gin.Default()
+ r.Use(ginmiddleware.TransactionMiddleware())
+
+ proxy, err := tcc.NewTCCServiceProxy(&OrderService{})
+ if err != nil {
+ log.Fatalf("create OrderService proxy error: %v", err)
+ }
+
+ r.POST("/prepare", func(c *gin.Context) {
+ var req OrderRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error":
err.Error()})
+ return
+ }
+ ctx := c.Request.Context()
+ if _, err := proxy.Prepare(ctx, req); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error":
err.Error()})
+ return
+ }
+ // Return the generated order id so the initiator can thread it
through
+ // the downstream service calls (pricing, dispatch) instead of
hardcoding it.
+ var orderID int64
+ if v, ok := orderRecords.Load(tm.GetXID(ctx)); ok {
+ orderID, _ = v.(int64)
+ }
+ c.JSON(http.StatusOK, gin.H{"message": "prepare ok",
"order_id": orderID})
+ })
+
+ if err := r.Run(":8001"); err != nil {
+ log.Fatalf("start order-service error: %v", err)
+ }
+}
diff --git a/tcc/ride-order/order-service/seatago.yml
b/tcc/ride-order/order-service/seatago.yml
new file mode 100644
index 0000000..71ef1bf
--- /dev/null
+++ b/tcc/ride-order/order-service/seatago.yml
@@ -0,0 +1,158 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+# time 时间单位对应的是 time.Duration(1)
+seata:
+ enabled: true
+ # application id
+ application-id: order-service
+ # service group
+ tx-service-group: default_tx_group
+ access-key: aliyunAccessKey
+ secret-key: aliyunSecretKey
+ enable-auto-data-source-proxy: false
+ data-source-proxy-mode: AT
+ client:
+ rm:
+ # Maximum cache length of asynchronous queue
+ async-commit-buffer-limit: 10000
+ # The maximum number of retries when report reports the status
+ report-retry-count: 5
+ # The interval for regularly checking the metadata of the db(AT)
+ table-meta-check-enable: false
+ # Whether to report the status if the transaction is successfully
executed(AT)
+ report-success-enable: false
+ # Whether to allow regular check of db metadata(AT)
+ saga-branch-register-enable: false
+ saga-json-parser: fastjson
+ saga-retry-persist-mode-update: false
+ saga-compensate-persist-mode-update: false
+ #Ordered.HIGHEST_PRECEDENCE + 1000 #
+ tcc-action-interceptor-order: -2147482648
+ # Parse SQL parser selection
+ sql-parser-type: druid
+ lock:
+ retry-interval: 30
+ retry-times: 10
+ retry-policy-branch-rollback-on-conflict: true
+ tm:
+ commit-retry-count: 5
+ rollback-retry-count: 5
+ default-global-transaction-timeout: 60s
+ degrade-check: false
+ degrade-check-period: 2000
+ degrade-check-allow-times: 10s
+ interceptor-order: -2147482648
+ undo:
+ # Judge whether the before image and after image are the same,If it is
the same, undo will not be recorded
+ data-validation: true
+ # Serialization method
+ log-serialization: json
+ # undo log table name
+ log-table: undo_log
+ # Only store modified fields
+ only-care-update-columns: true
+ compress:
+ # Compression type. Allowed Options: None, Gzip, Zip, Sevenz, Bzip2,
Lz4, Zstd, Deflate
+ type: None
+ # Compression threshold Unit: k
+ threshold: 64k
+ load-balance:
+ type: RandomLoadBalance
+ virtual-nodes: 10
+ service:
+ vgroup-mapping:
+ # Prefix for Print Log
+ default_tx_group: default
+ grouplist:
+ default: 127.0.0.1:8091
+ enable-degrade: false
+ # close the transaction
+ disable-global-transaction: false
+ transport:
+ shutdown:
+ wait: 3s
+ # Netty related configurations
+ # type
+ type: TCP
+ server: NIO
+ heartbeat: true
+ # Encoding and decoding mode
+ serialization: seata
+ # Message compression mode
+ compressor: none
+ # Allow batch sending of requests (TM)
+ enable-tm-client-batch-send-request: false
+ # Allow batch sending of requests (RM)
+ enable-rm-client-batch-send-request: true
+ # RM send request timeout
+ rpc-rm-request-timeout: 30s
+ # TM send request timeout
+ rpc-tm-request-timeout: 30s
+ # Configuration Center
+ config:
+ type: file
+ file:
+ name: config.conf
+ nacos:
+ namespace: ""
+ server-addr: 127.0.0.1:8848
+ group: SEATA_GROUP
+ username: ""
+ password: ""
+ ##if use MSE Nacos with auth, mutex with username/password attribute
+ #access-key: ""
+ #secret-key: ""
+ data-id: seata.properties
+ # Registration Center
+ registry:
+ type: file
+ file:
+ name: registry.conf
+ nacos:
+ application: seata-server
+ server-addr: 127.0.0.1:8848
+ group: "SEATA_GROUP"
+ namespace: ""
+ username: ""
+ password: ""
+ ##if use MSE Nacos with auth, mutex with username/password attribute #
+ #access-key: "" #
+ #secret-key: "" #
+ log:
+ exception-rate: 100
+ tcc:
+ fence:
+ # Anti suspension table name
+ log-table-name: tcc_fence_log_test
+ clean-period: 60s
+ # getty configuration
+ getty:
+ reconnect-interval: 0
+ # temporary not supported connection-num
+ connection-num: 1
+ session:
+ compress-encoding: false
+ tcp-no-delay: true
+ tcp-keep-alive: true
+ keep-alive-period: 120s
+ tcp-r-buf-size: 262144
+ tcp-w-buf-size: 65536
+ tcp-read-timeout: 1s
+ tcp-write-timeout: 5s
+ wait-timeout: 1s
+ max-msg-len: 16498688
+ session-name: client_test
+ cron-period: 1s
diff --git a/tcc/ride-order/order-service/service.go
b/tcc/ride-order/order-service/service.go
new file mode 100644
index 0000000..75420dc
--- /dev/null
+++ b/tcc/ride-order/order-service/service.go
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package main
+
+import (
+ "context"
+ "fmt"
+ "sync"
+
+ "seata.apache.org/seata-go/pkg/tm"
+ "seata.apache.org/seata-go/pkg/util/log"
+
+ "seata.apache.org/seata-go-samples/tcc/ride-order/common"
+)
+
+type OrderRequest struct {
+ PassengerID string `json:"passenger_id"`
+}
+
+type OrderService struct{}
+
+// orderRecords tracks xid -> orderID for Commit/Rollback lookup.
+var orderRecords sync.Map
+
+func (s *OrderService) GetActionName() string {
+ return "OrderService"
+}
+
+// Prepare creates a pending ride order (Try phase).
+func (s *OrderService) Prepare(ctx context.Context, params interface{}) (bool,
error) {
+ req, ok := params.(OrderRequest)
+ if !ok {
+ return false, fmt.Errorf("invalid params type %T, want
OrderRequest", params)
+ }
+ result, err := common.DB.ExecContext(ctx,
+ "INSERT INTO ride_orders (passenger_id, status) VALUES (?, 0)",
req.PassengerID)
+ if err != nil {
+ return false, fmt.Errorf("order prepare failed: %v", err)
+ }
+ orderID, _ := result.LastInsertId()
+ xid := tm.GetXID(ctx)
+ orderRecords.Store(xid, orderID)
+ log.Infof("[Order-Try] created pending order %d for passenger %s,
xid=%s", orderID, req.PassengerID, xid)
+ return true, nil
+}
+
+// Commit confirms the order: pending -> confirmed. Idempotent via status
check.
+func (s *OrderService) Commit(ctx context.Context, bac
*tm.BusinessActionContext) (bool, error) {
+ orderID, ok := orderRecords.Load(bac.Xid)
+ if !ok {
+ log.Infof("[Order-Confirm] no record for xid=%s, idempotent
skip", bac.Xid)
+ return true, nil
+ }
+ _, err := common.DB.Exec("UPDATE ride_orders SET status=1 WHERE id=?
AND status=0", orderID)
+ if err != nil {
+ return false, fmt.Errorf("order confirm failed: %v", err)
+ }
+ orderRecords.Delete(bac.Xid)
+ log.Infof("[Order-Confirm] order %d confirmed, xid=%s", orderID,
bac.Xid)
+ return true, nil
+}
+
+// Rollback cancels the order: pending -> canceled. Idempotent via status
check.
+func (s *OrderService) Rollback(ctx context.Context, bac
*tm.BusinessActionContext) (bool, error) {
+ orderID, ok := orderRecords.Load(bac.Xid)
+ if !ok {
+ log.Infof("[Order-Cancel] no record for xid=%s, idempotent
skip", bac.Xid)
+ return true, nil
+ }
+ _, err := common.DB.Exec("UPDATE ride_orders SET status=2 WHERE id=?
AND status=0", orderID)
+ if err != nil {
+ return false, fmt.Errorf("order cancel failed: %v", err)
+ }
+ orderRecords.Delete(bac.Xid)
+ log.Infof("[Order-Cancel] order %d canceled, xid=%s", orderID, bac.Xid)
+ return true, nil
+}
diff --git a/tcc/ride-order/pricing-service/main.go
b/tcc/ride-order/pricing-service/main.go
new file mode 100644
index 0000000..32bbbfd
--- /dev/null
+++ b/tcc/ride-order/pricing-service/main.go
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package main
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+
+ "seata.apache.org/seata-go/pkg/client"
+ ginmiddleware "seata.apache.org/seata-go/pkg/integration/gin"
+ "seata.apache.org/seata-go/pkg/rm/tcc"
+ "seata.apache.org/seata-go/pkg/util/log"
+
+ "seata.apache.org/seata-go-samples/tcc/ride-order/common"
+)
+
+func main() {
+ client.InitPath("seatago.yml")
+ common.InitDB()
+
+ r := gin.Default()
+ r.Use(ginmiddleware.TransactionMiddleware())
+
+ proxy, err := tcc.NewTCCServiceProxy(&PricingService{})
+ if err != nil {
+ log.Fatalf("create PricingService proxy error: %v", err)
+ }
+
+ r.POST("/prepare", func(c *gin.Context) {
+ var req PricingRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error":
err.Error()})
+ return
+ }
+ if _, err := proxy.Prepare(c.Request.Context(), req); err !=
nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error":
err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{"message": "prepare ok"})
+ })
+
+ if err := r.Run(":8003"); err != nil {
+ log.Fatalf("start pricing-service error: %v", err)
+ }
+}
diff --git a/tcc/ride-order/pricing-service/seatago.yml
b/tcc/ride-order/pricing-service/seatago.yml
new file mode 100644
index 0000000..0f43513
--- /dev/null
+++ b/tcc/ride-order/pricing-service/seatago.yml
@@ -0,0 +1,158 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+# time 时间单位对应的是 time.Duration(1)
+seata:
+ enabled: true
+ # application id
+ application-id: pricing-service
+ # service group
+ tx-service-group: default_tx_group
+ access-key: aliyunAccessKey
+ secret-key: aliyunSecretKey
+ enable-auto-data-source-proxy: false
+ data-source-proxy-mode: AT
+ client:
+ rm:
+ # Maximum cache length of asynchronous queue
+ async-commit-buffer-limit: 10000
+ # The maximum number of retries when report reports the status
+ report-retry-count: 5
+ # The interval for regularly checking the metadata of the db(AT)
+ table-meta-check-enable: false
+ # Whether to report the status if the transaction is successfully
executed(AT)
+ report-success-enable: false
+ # Whether to allow regular check of db metadata(AT)
+ saga-branch-register-enable: false
+ saga-json-parser: fastjson
+ saga-retry-persist-mode-update: false
+ saga-compensate-persist-mode-update: false
+ #Ordered.HIGHEST_PRECEDENCE + 1000 #
+ tcc-action-interceptor-order: -2147482648
+ # Parse SQL parser selection
+ sql-parser-type: druid
+ lock:
+ retry-interval: 30
+ retry-times: 10
+ retry-policy-branch-rollback-on-conflict: true
+ tm:
+ commit-retry-count: 5
+ rollback-retry-count: 5
+ default-global-transaction-timeout: 60s
+ degrade-check: false
+ degrade-check-period: 2000
+ degrade-check-allow-times: 10s
+ interceptor-order: -2147482648
+ undo:
+ # Judge whether the before image and after image are the same,If it is
the same, undo will not be recorded
+ data-validation: true
+ # Serialization method
+ log-serialization: json
+ # undo log table name
+ log-table: undo_log
+ # Only store modified fields
+ only-care-update-columns: true
+ compress:
+ # Compression type. Allowed Options: None, Gzip, Zip, Sevenz, Bzip2,
Lz4, Zstd, Deflate
+ type: None
+ # Compression threshold Unit: k
+ threshold: 64k
+ load-balance:
+ type: RandomLoadBalance
+ virtual-nodes: 10
+ service:
+ vgroup-mapping:
+ # Prefix for Print Log
+ default_tx_group: default
+ grouplist:
+ default: 127.0.0.1:8091
+ enable-degrade: false
+ # close the transaction
+ disable-global-transaction: false
+ transport:
+ shutdown:
+ wait: 3s
+ # Netty related configurations
+ # type
+ type: TCP
+ server: NIO
+ heartbeat: true
+ # Encoding and decoding mode
+ serialization: seata
+ # Message compression mode
+ compressor: none
+ # Allow batch sending of requests (TM)
+ enable-tm-client-batch-send-request: false
+ # Allow batch sending of requests (RM)
+ enable-rm-client-batch-send-request: true
+ # RM send request timeout
+ rpc-rm-request-timeout: 30s
+ # TM send request timeout
+ rpc-tm-request-timeout: 30s
+ # Configuration Center
+ config:
+ type: file
+ file:
+ name: config.conf
+ nacos:
+ namespace: ""
+ server-addr: 127.0.0.1:8848
+ group: SEATA_GROUP
+ username: ""
+ password: ""
+ ##if use MSE Nacos with auth, mutex with username/password attribute
+ #access-key: ""
+ #secret-key: ""
+ data-id: seata.properties
+ # Registration Center
+ registry:
+ type: file
+ file:
+ name: registry.conf
+ nacos:
+ application: seata-server
+ server-addr: 127.0.0.1:8848
+ group: "SEATA_GROUP"
+ namespace: ""
+ username: ""
+ password: ""
+ ##if use MSE Nacos with auth, mutex with username/password attribute #
+ #access-key: "" #
+ #secret-key: "" #
+ log:
+ exception-rate: 100
+ tcc:
+ fence:
+ # Anti suspension table name
+ log-table-name: tcc_fence_log_test
+ clean-period: 60s
+ # getty configuration
+ getty:
+ reconnect-interval: 0
+ # temporary not supported connection-num
+ connection-num: 1
+ session:
+ compress-encoding: false
+ tcp-no-delay: true
+ tcp-keep-alive: true
+ keep-alive-period: 120s
+ tcp-r-buf-size: 262144
+ tcp-w-buf-size: 65536
+ tcp-read-timeout: 1s
+ tcp-write-timeout: 5s
+ wait-timeout: 1s
+ max-msg-len: 16498688
+ session-name: client_test
+ cron-period: 1s
diff --git a/tcc/ride-order/pricing-service/service.go
b/tcc/ride-order/pricing-service/service.go
new file mode 100644
index 0000000..817c6ff
--- /dev/null
+++ b/tcc/ride-order/pricing-service/service.go
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package main
+
+import (
+ "context"
+ "fmt"
+ "sync"
+
+ "seata.apache.org/seata-go/pkg/tm"
+ "seata.apache.org/seata-go/pkg/util/log"
+
+ "seata.apache.org/seata-go-samples/tcc/ride-order/common"
+)
+
+type PricingRequest struct {
+ OrderID int64 `json:"order_id"`
+ Amount int `json:"amount"`
+}
+
+type PricingService struct{}
+
+// pricingRecords tracks xid -> priceLockID for Commit/Rollback lookup.
+var pricingRecords sync.Map
+
+func (s *PricingService) GetActionName() string {
+ return "PricingService"
+}
+
+// Prepare locks the estimated fare (Try phase).
+func (s *PricingService) Prepare(ctx context.Context, params interface{})
(bool, error) {
+ req, ok := params.(PricingRequest)
+ if !ok {
+ return false, fmt.Errorf("invalid params type %T, want
PricingRequest", params)
+ }
+ result, err := common.DB.ExecContext(ctx,
+ "INSERT INTO price_locks (order_id, amount, status) VALUES (?,
?, 0)", req.OrderID, req.Amount)
+ if err != nil {
+ return false, fmt.Errorf("pricing prepare failed: %v", err)
+ }
+ lockID, _ := result.LastInsertId()
+ xid := tm.GetXID(ctx)
+ pricingRecords.Store(xid, lockID)
+ log.Infof("[Pricing-Try] locked fare %d cents for order %d, lockID=%d,
xid=%s", req.Amount, req.OrderID, lockID, xid)
+ return true, nil
+}
+
+// Commit confirms the fare lock: locked -> confirmed. Idempotent via status
check.
+func (s *PricingService) Commit(ctx context.Context, bac
*tm.BusinessActionContext) (bool, error) {
+ lockID, ok := pricingRecords.Load(bac.Xid)
+ if !ok {
+ log.Infof("[Pricing-Confirm] no record for xid=%s, idempotent
skip", bac.Xid)
+ return true, nil
+ }
+ _, err := common.DB.Exec("UPDATE price_locks SET status=1 WHERE id=?
AND status=0", lockID)
+ if err != nil {
+ return false, fmt.Errorf("pricing confirm failed: %v", err)
+ }
+ pricingRecords.Delete(bac.Xid)
+ log.Infof("[Pricing-Confirm] fare lock %d confirmed, xid=%s", lockID,
bac.Xid)
+ return true, nil
+}
+
+// Rollback releases the fare lock: locked -> released. Idempotent via status
check.
+func (s *PricingService) Rollback(ctx context.Context, bac
*tm.BusinessActionContext) (bool, error) {
+ lockID, ok := pricingRecords.Load(bac.Xid)
+ if !ok {
+ log.Infof("[Pricing-Cancel] no record for xid=%s, idempotent
skip", bac.Xid)
+ return true, nil
+ }
+ _, err := common.DB.Exec("UPDATE price_locks SET status=2 WHERE id=?
AND status=0", lockID)
+ if err != nil {
+ return false, fmt.Errorf("pricing cancel failed: %v", err)
+ }
+ pricingRecords.Delete(bac.Xid)
+ log.Infof("[Pricing-Cancel] fare lock %d released, xid=%s", lockID,
bac.Xid)
+ return true, nil
+}
diff --git a/tcc/ride-order/sql/ride_order.sql
b/tcc/ride-order/sql/ride_order.sql
new file mode 100644
index 0000000..25aea61
--- /dev/null
+++ b/tcc/ride-order/sql/ride_order.sql
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+CREATE DATABASE IF NOT EXISTS seata_ride DEFAULT CHARACTER SET utf8mb4 COLLATE
utf8mb4_unicode_ci;
+USE seata_ride;
+
+-- ride orders
+CREATE TABLE IF NOT EXISTS ride_orders (
+ id BIGINT PRIMARY KEY AUTO_INCREMENT,
+ passenger_id VARCHAR(64) NOT NULL,
+ status TINYINT NOT NULL DEFAULT 0 COMMENT '0=pending, 1=confirmed,
2=canceled',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- drivers
+CREATE TABLE IF NOT EXISTS drivers (
+ id BIGINT PRIMARY KEY AUTO_INCREMENT,
+ name VARCHAR(64) NOT NULL,
+ status TINYINT NOT NULL DEFAULT 0 COMMENT '0=available, 1=reserved,
2=busy',
+ reserved_order_id BIGINT DEFAULT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+INSERT INTO drivers (name, status) VALUES
+ ('Driver-A', 0),
+ ('Driver-B', 0),
+ ('Driver-C', 0),
+ ('Driver-D', 0),
+ ('Driver-E', 0),
+ ('Driver-F', 0);
+
+-- price locks
+CREATE TABLE IF NOT EXISTS price_locks (
+ id BIGINT PRIMARY KEY AUTO_INCREMENT,
+ order_id BIGINT NOT NULL,
+ amount INT NOT NULL COMMENT 'fare in cents',
+ status TINYINT NOT NULL DEFAULT 0 COMMENT '0=locked, 1=confirmed,
2=released'
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- coupons
+CREATE TABLE IF NOT EXISTS coupons (
+ id BIGINT PRIMARY KEY AUTO_INCREMENT,
+ user_id VARCHAR(64) NOT NULL,
+ discount INT NOT NULL COMMENT 'discount in cents',
+ status TINYINT NOT NULL DEFAULT 0 COMMENT '0=available, 1=frozen, 2=used'
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+INSERT INTO coupons (user_id, discount, status) VALUES
+ ('passenger-001', 500, 0),
+ ('passenger-001', 300, 0),
+ ('passenger-001', 500, 0),
+ ('passenger-001', 300, 0),
+ ('passenger-001', 500, 0),
+ ('passenger-001', 300, 0);
+
+-- vehicle capacity
+CREATE TABLE IF NOT EXISTS vehicle_capacity (
+ id BIGINT PRIMARY KEY AUTO_INCREMENT,
+ vehicle_type VARCHAR(32) NOT NULL,
+ total_slots INT NOT NULL,
+ reserved_slots INT NOT NULL DEFAULT 0
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+INSERT INTO vehicle_capacity (vehicle_type, total_slots, reserved_slots) VALUES
+ ('standard', 10, 0);
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]