word-audio.sql 3.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344
  1. -- Server-only cache. Provider work runs outside short SQL transactions.
  2. CREATE TABLE IF NOT EXISTS "WordAudioCache" (
  3. "objectId" text PRIMARY KEY, company text NOT NULL, word text NOT NULL, phonetic text NOT NULL,
  4. voice text NOT NULL, status text NOT NULL DEFAULT 'pending', url text NOT NULL DEFAULT '',
  5. audio text NOT NULL DEFAULT '', lease text, "leaseUntil" timestamptz,
  6. "retryAfter" timestamptz, "lastError" text, "createdAt" timestamptz NOT NULL DEFAULT now(),
  7. "updatedAt" timestamptz NOT NULL DEFAULT now()
  8. );
  9. CREATE TABLE IF NOT EXISTS "WordAudioAttempt" (
  10. "objectId" text PRIMARY KEY, company text NOT NULL, actor text NOT NULL, "cacheId" text NOT NULL,
  11. "providerCall" boolean NOT NULL, status text NOT NULL DEFAULT 'started',
  12. "createdAt" timestamptz NOT NULL DEFAULT now(), "updatedAt" timestamptz NOT NULL DEFAULT now()
  13. );
  14. -- Parse may have registered these tables before this migration; apply defaults there too.
  15. ALTER TABLE "WordAudioCache" ALTER COLUMN status SET DEFAULT 'pending', ALTER COLUMN url SET DEFAULT '', ALTER COLUMN audio SET DEFAULT '', ALTER COLUMN "createdAt" SET DEFAULT now(), ALTER COLUMN "updatedAt" SET DEFAULT now();
  16. ALTER TABLE "WordAudioAttempt" ALTER COLUMN status SET DEFAULT 'started', ALTER COLUMN "createdAt" SET DEFAULT now(), ALTER COLUMN "updatedAt" SET DEFAULT now();
  17. UPDATE "WordAudioCache" SET status=COALESCE(status,'pending'),url=COALESCE(url,''),audio=COALESCE(audio,'');
  18. CREATE INDEX IF NOT EXISTS word_audio_attempt_usage ON "WordAudioAttempt" (company,"createdAt",actor);
  19. CREATE INDEX IF NOT EXISTS word_audio_leases ON "WordAudioCache" (company,"leaseUntil");
  20. CREATE OR REPLACE FUNCTION xs_word_audio_claim(co text, cache_id text, actor_id text, token text,
  21. word_text text, phonetic_text text, voice_text text, daily_limit int, minute_limit int, concurrency_limit int)
  22. RETURNS jsonb LANGUAGE plpgsql AS $$
  23. DECLARE r "WordAudioCache"; n int;
  24. BEGIN
  25. PERFORM pg_advisory_xact_lock(hashtext('word-audio:'||co));
  26. INSERT INTO "WordAudioCache" ("objectId",company,word,phonetic,voice)
  27. VALUES(cache_id,co,word_text,phonetic_text,voice_text) ON CONFLICT DO NOTHING;
  28. SELECT * INTO r FROM "WordAudioCache" WHERE "objectId"=cache_id AND company=co FOR UPDATE;
  29. IF NOT FOUND THEN RAISE EXCEPTION 'audio cache tenant mismatch'; END IF;
  30. IF r.status='ready' THEN RETURN jsonb_build_object('state','ready','url',r.url); END IF;
  31. IF r."leaseUntil">now() THEN RETURN jsonb_build_object('state','pending'); END IF;
  32. IF r."retryAfter">now() THEN RETURN jsonb_build_object('state','retry'); END IF;
  33. SELECT count(*) INTO n FROM "WordAudioAttempt" WHERE company=co AND actor=actor_id AND "createdAt">now()-interval '1 minute';
  34. IF n>=minute_limit THEN RETURN jsonb_build_object('state','limited'); END IF;
  35. SELECT count(*) INTO n FROM "WordAudioCache" WHERE company=co AND "leaseUntil">now();
  36. IF n>=concurrency_limit THEN RETURN jsonb_build_object('state','pending'); END IF;
  37. IF r.audio='' THEN
  38. SELECT count(*) INTO n FROM "WordAudioAttempt" WHERE company=co AND "providerCall" AND "createdAt">=date_trunc('day',now() AT TIME ZONE 'Asia/Shanghai') AT TIME ZONE 'Asia/Shanghai';
  39. IF n>=daily_limit THEN RETURN jsonb_build_object('state','quota'); END IF;
  40. END IF;
  41. UPDATE "WordAudioCache" SET status='generating',lease=token,"leaseUntil"=now()+interval '90 seconds',"updatedAt"=now() WHERE "objectId"=cache_id;
  42. INSERT INTO "WordAudioAttempt" ("objectId",company,actor,"cacheId","providerCall") VALUES(token,co,actor_id,cache_id,r.audio='');
  43. RETURN jsonb_build_object('state','claimed','audio',r.audio);
  44. END $$;