Copilot commented on code in PR #104:
URL: 
https://github.com/apache/incubator-seata-go-samples/pull/104#discussion_r3869591986


##########
tcc/ride-order/dispatch-service/service.go:
##########
@@ -0,0 +1,105 @@
+/*
+ * 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)
+       }
+       _, 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)
+       }
+       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

Review Comment:
   DispatchService.Prepare selects an available driver and then updates it, but 
it never checks whether the UPDATE actually affected a row. Under concurrent 
requests, another transaction can reserve the same driver between the SELECT 
and UPDATE, causing RowsAffected=0 while this code still records the driverID 
in dispatchRecords and returns success (leading to a stranded reservation / 
inconsistent state).



##########
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:latest
+    ports:
+      - "8091:8091"
+      - "7091:7091"
+    environment:
+      - SEATA_PORT=8091
+      - STORE_MODE=file

Review Comment:
   docker-compose pins MySQL but uses `seataio/seata-server:latest`. Using 
`latest` makes the sample non-reproducible and can break unexpectedly when the 
upstream image changes (especially since seata-go/TCC interoperability can be 
version-sensitive). Please pin to the Seata Server version you verified 
end-to-end in this PR.



##########
tcc/ride-order/initiator/main.go:
##########
@@ -0,0 +1,145 @@
+/*
+ * 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";
+)
+
+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().
+               Post(serviceURL+"/prepare").
+               Set(constant.XidKey, tm.GetXID(ctx)).
+               Type("json").
+               Send(jsonBody).
+               End()

Review Comment:
   callService uses the default gorequest/http client without any request 
timeout. If a service is down or a connection stalls, the initiator can hang 
indefinitely inside the global transaction function, which makes the sample 
harder to run/debug and can delay rollback/cleanup.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to