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]
