Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,17 @@ func getClientConfig() ClientConfig {
type StatusConfig struct {
// ParticipantsBatchSize controls how many contacts are included per batch
// when sending a status/story to status@broadcast.
// Lowered from 5000 to 500 to avoid gRPC batch timeout errors on large contact lists.
ParticipantsBatchSize int `env:"WAHA_GOWS_STATUS_PARTICIPANTS_BATCH_SIZE" envDefault:"500"`
// Kept large on purpose: what trips WhatsApp's rate limiting is the number of
// stanzas, not the number of participants inside one. Smaller batches turned
// the ack timeouts into "server returned error 429" instead of fixing them.
// Slow acks on a big batch are handled by BatchTimeout below.
ParticipantsBatchSize int `env:"WAHA_GOWS_STATUS_PARTICIPANTS_BATCH_SIZE" envDefault:"5000"`
// BatchTimeout bounds how long we wait for the server to acknowledge one
// batch. whatsmeow defaults to 75s, which is not enough for a large status
// batch and shows up as "timed out waiting for message send response" even
// though the batch is delivered.
// Go duration format: 90s, 180s, 5m.
BatchTimeout time.Duration `env:"WAHA_GOWS_STATUS_BATCH_TIMEOUT" envDefault:"180s"`
}

func getStatusConfig() StatusConfig {
Expand Down
9 changes: 8 additions & 1 deletion src/gows/gows.go
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,14 @@ func (gows *GoWS) SendMessage(ctx context.Context, to types.JID, msg *waE2E.Mess

if to.User == "status" && to.Server == types.BroadcastServer {
// Broadcast messages (Status)
result, err := gows.SendStatusMessage(ctx, to, msg, extra)
//
// A status to a large audience can outlast the caller's deadline. Detach
// from its cancellation so a client-side timeout does not abort a send that
// is already live on WhatsApp: aborting it mid-batch leaves the remaining
// batches undelivered while the caller sees a plain failure and re-sends
// the whole status, posting it more than once.
sendCtx := context.WithoutCancel(ctx)
result, err := gows.SendStatusMessage(sendCtx, to, msg, extra)
if err != nil {
return nil, err
}
Expand Down
13 changes: 11 additions & 2 deletions src/gows/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,22 @@ func SetDeviceAndBrowser(device string, browser string) {
}

// statusParticipantsBatchSize is the number of contacts per batch when sending to status@broadcast.
// Set at startup via SetStatusParticipantsBatchSize; defaults to 500.
var statusParticipantsBatchSize = 500
// Set at startup via SetStatusParticipantsBatchSize; defaults to 5000.
var statusParticipantsBatchSize = 5000

func SetStatusParticipantsBatchSize(n int) {
statusParticipantsBatchSize = n
}

// statusBatchTimeout is how long to wait for the server to ack one status batch.
// Set at startup via SetStatusBatchTimeout; defaults to 180s. Zero leaves
// whatsmeow's own default (75s) in place.
var statusBatchTimeout = 180 * time.Second

func SetStatusBatchTimeout(d time.Duration) {
statusBatchTimeout = d
}

// SetKeepAliveInterval overrides whatsmeow's websocket keepalive ping interval.
// Returns the resulting min/max so the caller can log what is in effect.
func SetKeepAliveInterval(min time.Duration, max time.Duration) (time.Duration, time.Duration) {
Expand Down
6 changes: 6 additions & 0 deletions src/gows/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ func (gows *GoWS) SendStatusMessage(ctx context.Context, to types.JID, msg *waE2
for index, participants := range batches {
batchExtra := extra
batchExtra.Participants = participants
// Give the server room to acknowledge a large batch. Without this the
// whatsmeow default (75s) applies, and a batch that is actually delivered
// is reported as "timed out waiting for message send response".
if statusBatchTimeout > 0 {
batchExtra.Timeout = statusBatchTimeout
}

_, err := gows.Client.SendMessage(ctx, to, msg, batchExtra)
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions src/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ func main() {
statusCfg := getStatusConfig()
log.Infof("Status broadcast participants batch size: %d", statusCfg.ParticipantsBatchSize)
gows.SetStatusParticipantsBatchSize(statusCfg.ParticipantsBatchSize)
log.Infof("Status broadcast batch ack timeout: %s", statusCfg.BatchTimeout)
gows.SetStatusBatchTimeout(statusCfg.BatchTimeout)

linkPreviewCfg := getLinkPreviewConfig()
log.Infof("Link preview fetch timeout: %s", linkPreviewCfg.FetchTimeout)
Expand Down
12 changes: 11 additions & 1 deletion src/server/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,17 @@ func (s *Server) StartSession(ctx context.Context, req *__.StartSessionRequest)
case dialect == "sqlite3" || dialect == "sqlite":
// busy_timeout to prevent "database is locked" errors
// DO NOT add cache=shared, it's not safe
address = req.Config.Store.Address + "?_foreign_keys=on&_busy_timeout=30000"
//
// WAL is required here, not optional. In the default rollback-journal mode a
// single writer takes an exclusive lock that blocks every other reader and
// writer. During a status@broadcast the outgoing send and the flood of
// incoming decryptions (each a session/identity/sender-key write) contend for
// that one lock, exhaust the busy_timeout above and surface as "database is
// locked". WAL lets one writer proceed alongside readers, and
// _txlock=immediate takes the write lock up front so concurrent writers back
// off cleanly instead of deadlocking mid-transaction.
// (Mirrors the PRAGMAs already applied to the GContainer store.)
address = req.Config.Store.Address + "?_foreign_keys=on&_busy_timeout=30000&_journal_mode=WAL&_synchronous=NORMAL&_txlock=immediate"
case dialect == "postgres":
address = addApplicationName(req.Config.Store.Address, "GOWS")
default:
Expand Down