diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 93b3d0c..50e2891 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,6 +33,9 @@ jobs: - os: linux runs-on: ubuntu-latest arch: x86_64 + - os: linux-musl + runs-on: ubuntu-latest + arch: x86_64 steps: - name: Checkout code uses: actions/checkout@v4 @@ -48,6 +51,15 @@ jobs: sudo apt-get update sudo apt-get install -y sqlite3 libsqlite3-dev build-essential + - name: Install dependencies (Alpine/musl) + if: matrix.os == 'linux-musl' + run: | + # Use Alpine container for musl builds + docker run --rm -v ${{ github.workspace }}:/workspace -w /workspace alpine:latest sh -c " + apk add --no-cache build-base sqlite-dev go curl git && + echo 'Alpine dependencies installed' + " + - name: Install dependencies (macOS) if: matrix.os == 'darwin' run: | @@ -59,7 +71,7 @@ jobs: choco install sqlite - name: Build SQLite (Unix) - if: matrix.os != 'windows' + if: matrix.os != 'windows' && matrix.os != 'linux-musl' run: | cd sqlite if [ ! -d "sqlite-latest" ]; then @@ -73,6 +85,24 @@ jobs: make make install + - name: Build SQLite (Alpine/musl) + if: matrix.os == 'linux-musl' + run: | + # Build SQLite in Alpine container for musl compatibility + docker run --rm -v ${{ github.workspace }}:/workspace -w /workspace alpine:latest sh -c " + apk add --no-cache build-base curl && + cd sqlite && + if [ ! -d 'sqlite-latest' ]; then + curl -O https://www.sqlite.org/2024/sqlite-autoconf-3450100.tar.gz + tar xzf sqlite-autoconf-3450100.tar.gz + mv sqlite-autoconf-3450100 sqlite-latest + fi && + cd sqlite-latest && + ./configure --prefix=\$(pwd)/../install --enable-static --disable-shared 'CFLAGS=-DSQLITE_ENABLE_DBPAGE_VTAB=1 -O2' && + make && + make install + " + - name: Build SQLite (Windows) if: matrix.os == 'windows' run: | @@ -89,10 +119,21 @@ jobs: bash -c "make install" - name: Build Bridge + if: matrix.os != 'linux-musl' run: | cd bridge make + - name: Build Bridge (Alpine/musl) + if: matrix.os == 'linux-musl' + run: | + # Build bridge in Alpine container + docker run --rm -v ${{ github.workspace }}:/workspace -w /workspace alpine:latest sh -c " + apk add --no-cache build-base go && + cd bridge && + make + " + - name: Set build environment (Darwin ARM64) if: matrix.os == 'darwin' && matrix.arch == 'arm64' run: | @@ -114,6 +155,13 @@ jobs: echo "GOARCH=amd64" >> $GITHUB_ENV echo "CGO_ENABLED=1" >> $GITHUB_ENV + - name: Set build environment (Linux musl x86_64) + if: matrix.os == 'linux-musl' && matrix.arch == 'x86_64' + run: | + echo "GOOS=linux" >> $GITHUB_ENV + echo "GOARCH=amd64" >> $GITHUB_ENV + echo "CGO_ENABLED=1" >> $GITHUB_ENV + - name: Set build environment (Windows) if: matrix.os == 'windows' run: | @@ -122,11 +170,21 @@ jobs: echo "CGO_ENABLED=1" >> $env:GITHUB_ENV - name: Build Client (Unix) - if: matrix.os != 'windows' + if: matrix.os != 'windows' && matrix.os != 'linux-musl' run: | cd client make build + - name: Build Client (Alpine/musl) + if: matrix.os == 'linux-musl' + run: | + # Build client in Alpine container + docker run --rm -v ${{ github.workspace }}:/workspace -w /workspace alpine:latest sh -c " + apk add --no-cache build-base go git && + cd client && + make build + " + - name: Build Client (Windows) if: matrix.os == 'windows' run: | diff --git a/client/main.go b/client/main.go index 2236700..d5bbb2e 100644 --- a/client/main.go +++ b/client/main.go @@ -15,7 +15,7 @@ import ( "github.com/sqlrsync/sqlrsync.com/sync" ) -var VERSION = "0.0.6" +var VERSION = "0.0.9" var ( serverURL string verbose bool diff --git a/client/remote/client.go b/client/remote/client.go index c1ee4a6..4e1d997 100644 --- a/client/remote/client.go +++ b/client/remote/client.go @@ -1455,14 +1455,29 @@ func (c *Client) writeLoop() { err := conn.WriteMessage(websocket.BinaryMessage, data) if err != nil { c.logger.Error("WebSocket write error", zap.Error(err)) + + // If the error indicates we already sent a close, treat this as a normal + // closure for PUSH syncs and mark the sync completed so the caller can + // finish processing. This happens when the websocket library returns + // "websocket: close sent" while attempting to write after a close. + if strings.Contains(err.Error(), "close sent") { + c.logger.Info("Write error contains 'close sent' - treating as normal closure") + c.logger.Info("Ending PUSH sync due to close-sent write error") + c.setSyncCompleted(true) + } c.setError(err) c.setConnected(false) - // Signal potential reconnection - select { - case c.reconnectChan <- struct{}{}: - default: - } + // Treat this as a true disconnection (do not trigger reconnect). + // Close the read queue so any readers observe EOF and stop. + func() { + defer func() { + if r := recover(); r != nil { + // ignore if already closed + } + }() + close(c.readQueue) + }() return } diff --git a/client/sync/coordinator.go b/client/sync/coordinator.go index 4c40637..6151280 100644 --- a/client/sync/coordinator.go +++ b/client/sync/coordinator.go @@ -361,7 +361,7 @@ func (c *Coordinator) executePull(isSubscription bool) error { AuthKey: authResult.AccessKey, ReplicaID: authResult.ReplicaID, Timeout: 8000, - PingPong: false, // No ping/pong needed for single sync + PingPong: true, // Ping/pong enabled for subscription sync Logger: c.logger.Named("remote"), Subscribe: false, // Subscription handled separately EnableTrafficInspection: c.config.Verbose,