Skip to content

Commit 62ca289

Browse files
committed
Test no org emails on staging
3 parents 3fe6879 + 76fe8d8 + 1814b12 commit 62ca289

40 files changed

Lines changed: 1048 additions & 673 deletions

sql/2025-02-20_authz.sql

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,6 @@ CREATE TRIGGER users_create_subject
213213

214214
CREATE TABLE orgs (
215215
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
216-
-- There's no subject_id on the org, since the org is a user, and the user has a subject_id.
217216
user_id UUID UNIQUE NOT NULL REFERENCES users (id) ON DELETE CASCADE,
218217
-- Subject representing the org itself.
219218
-- Note that orgs also have a subject on their associated user, but since you can't log in as a subject that
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-- Drop and re-create the contribution_diff_queue table without a primary key constraint on contribution_id.
2+
-- This allows us to insert redundant rows while a diff is being computed, rather than blocking due to row lock.
3+
4+
DROP TABLE contribution_diff_queue;
5+
6+
CREATE TABLE contribution_diff_queue (
7+
contribution_id UUID REFERENCES contributions(id) ON DELETE CASCADE,
8+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
9+
);
10+
11+
-- Delete all previously-computed namespace diffs, because the diff payload is different now (we explicitly store
12+
-- errors).
13+
TRUNCATE namespace_diffs;

sql/2025-05-01_org-emails.sql

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
ALTER TABLE users
2+
-- Add is_org column
3+
ADD COLUMN is_org boolean NOT NULL DEFAULT false,
4+
-- Add check that primary_email is not null unless is_org is true
5+
ADD CONSTRAINT primary_email_not_null_unless_org CHECK (
6+
is_org OR primary_email IS NOT NULL
7+
),
8+
-- Make primary_email nullable
9+
ALTER COLUMN primary_email DROP NOT NULL;
10+
11+
-- Update the is_org column for existing users
12+
WITH org_users(user_id) AS (
13+
SELECT DISTINCT o.user_id
14+
FROM orgs o
15+
) UPDATE users
16+
SET is_org = true
17+
WHERE id IN (SELECT ou.user_id FROM org_users ou);
18+
19+
-- Add a 'creator_user_id' to orgs just to track where they came from.
20+
-- This is distinct from the owners in the auth roles.
21+
ALTER TABLE orgs
22+
ADD COLUMN creator_user_id uuid NULL REFERENCES users (id) ON DELETE SET NULL
23+
;

src/Share/Backend.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ displayType = \case
231231
pure (UserObject decl)
232232

233233
evalDocRef ::
234-
Codebase.CodebaseRuntime ->
234+
Codebase.CodebaseRuntime IO ->
235235
V2.TermReference ->
236236
Codebase.CodebaseM e (Doc.EvaluatedDoc Symbol)
237237
evalDocRef (CodebaseRuntime {codeLookup, cachedEvalResult, unisonRuntime}) termRef = do
@@ -245,6 +245,7 @@ evalDocRef (CodebaseRuntime {codeLookup, cachedEvalResult, unisonRuntime}) termR
245245

246246
typeOf :: Referent.Referent -> Codebase.CodebaseM e (Maybe (V1.Type Symbol ()))
247247
typeOf termRef = fmap void <$> Codebase.loadTypeOfReferent (Cv.referent1to2 termRef)
248+
248249
eval :: V1.Term Symbol a -> Codebase.CodebaseM e (Maybe (V1.Term Symbol ()))
249250
eval (Term.amap (const mempty) -> tm) = do
250251
-- We use an empty ppe for evalutation, it's only used for adding additional context to errors.

src/Share/BackgroundJobs/Diffs/ContributionDiffs.hs

Lines changed: 67 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import Share.BackgroundJobs.Diffs.Queries qualified as DQ
77
import Share.BackgroundJobs.Errors (reportError)
88
import Share.BackgroundJobs.Monad (Background, withTag)
99
import Share.BackgroundJobs.Workers (newWorker)
10-
import Share.Branch (Branch (..))
10+
import Share.Branch (branchCausals_)
1111
import Share.Codebase qualified as Codebase
1212
import Share.Contribution (Contribution (..))
13+
import Share.Env qualified as Env
1314
import Share.IDs
1415
import Share.IDs qualified as IDs
1516
import Share.Metrics qualified as Metrics
16-
import Share.NamespaceDiffs (NamespaceDiffError (MissingEntityError))
1717
import Share.Postgres qualified as PG
1818
import Share.Postgres.Contributions.Queries qualified as ContributionsQ
1919
import Share.Postgres.Notifications qualified as Notif
@@ -23,52 +23,81 @@ import Share.Utils.Logging qualified as Logging
2323
import Share.Web.Authorization qualified as AuthZ
2424
import Share.Web.Errors (EntityMissing (..), ErrorID (..))
2525
import Share.Web.Share.Diffs.Impl qualified as Diffs
26+
import System.Clock qualified as Clock
2627

27-
-- | Check every 10 minutes if we haven't heard on the notifications channel.
28+
-- | Check every 30 seconds if we haven't heard on the notifications channel.
2829
-- Just in case we missed a notification.
2930
maxPollingIntervalSeconds :: Int
30-
maxPollingIntervalSeconds = 10 * 60
31+
maxPollingIntervalSeconds = 30
3132

3233
worker :: Ki.Scope -> Background ()
3334
worker scope = do
3435
authZReceipt <- AuthZ.backgroundJobAuthZ
36+
badUnliftCodebaseRuntime <- Codebase.badAskUnliftCodebaseRuntime
37+
unisonRuntime <- asks Env.sandboxedRuntime
38+
let makeRuntime :: Codebase.CodebaseEnv -> IO (Codebase.CodebaseRuntime IO)
39+
makeRuntime codebase = do
40+
runtime <- Codebase.codebaseRuntime' unisonRuntime codebase
41+
pure (badUnliftCodebaseRuntime runtime)
3542
newWorker scope "diffs:contributions" $ forever do
3643
Notif.waitOnChannel Notif.ContributionDiffChannel (maxPollingIntervalSeconds * 1000000)
37-
processDiffs authZReceipt >>= \case
38-
Left (contributionId, e) ->
39-
withTag "contribution-id" (IDs.toText contributionId) $ do
40-
reportError e
41-
Right _ -> pure ()
44+
processDiffs authZReceipt makeRuntime
4245

43-
processDiffs :: AuthZ.AuthZReceipt -> Background (Either (ContributionId, NamespaceDiffError) ())
44-
processDiffs authZReceipt = Metrics.recordContributionDiffDuration . runExceptT $ do
45-
mayContributionId <- PG.runTransaction DQ.claimContributionToDiff
46-
for_ mayContributionId (diffContribution authZReceipt)
47-
case mayContributionId of
48-
Just contributionId -> do
49-
Logging.textLog ("Recomputed contribution diff: " <> tShow contributionId)
50-
& Logging.withTag ("contribution-id", tShow contributionId)
51-
& Logging.withSeverity Logging.Info
52-
& Logging.logMsg
53-
-- Keep processing releases until we run out of them.
54-
either throwError pure =<< lift (processDiffs authZReceipt)
55-
Nothing -> pure ()
46+
-- Process diffs until we run out of them. We claim a diff in a transaction, commit it, then proceed to compute the
47+
-- diff. There's therefore a chance we claim a diff and fail to compute it (due to e.g. server restart). The current
48+
-- solution to these "at most once" semantics is to simply re-enqueue a diff job if necessary; e.g. in the view diff
49+
-- endpoint handler.
50+
processDiffs :: AuthZ.AuthZReceipt -> (Codebase.CodebaseEnv -> IO (Codebase.CodebaseRuntime IO)) -> Background ()
51+
processDiffs authZReceipt makeRuntime = do
52+
let loop :: Background ()
53+
loop = do
54+
result <-
55+
PG.runTransactionMode PG.RepeatableRead PG.ReadWrite do
56+
DQ.claimContributionToDiff >>= \case
57+
Nothing -> pure Nothing
58+
Just contributionId -> do
59+
startTime <- PG.transactionUnsafeIO (Clock.getTime Clock.Monotonic)
60+
result <- PG.catchTransaction (maybeComputeAndStoreCausalDiff authZReceipt makeRuntime contributionId)
61+
pure (Just (contributionId, startTime, result))
62+
whenJust result \(contributionId, startTime, result) -> do
63+
withTag "contribution-id" (IDs.toText contributionId) do
64+
case result of
65+
Left err -> reportError err
66+
Right didWork -> do
67+
when didWork do
68+
liftIO (Metrics.recordContributionDiffDuration startTime)
69+
Logging.textLog "Computed contribution diff"
70+
& Logging.withSeverity Logging.Info
71+
& Logging.logMsg
72+
loop
73+
loop
5674

57-
diffContribution :: AuthZ.AuthZReceipt -> ContributionId -> ExceptT (ContributionId, NamespaceDiffError) Background ()
58-
diffContribution authZReceipt contributionId = withExceptT (contributionId,) . mapExceptT (withTag "contribution-id" (IDs.toText contributionId)) $ do
59-
( bestCommonAncestorCausalId,
60-
project,
61-
newBranch@Branch {causal = newBranchCausalId},
62-
oldBranch@Branch {causal = oldBranchCausalId}
63-
) <- ExceptT $ PG.tryRunTransaction $ do
64-
Contribution {bestCommonAncestorCausalId, sourceBranchId = newBranchId, targetBranchId = oldBranchId, projectId} <- ContributionsQ.contributionById contributionId `whenNothingM` throwError (MissingEntityError $ EntityMissing (ErrorID "contribution:missing") "Contribution not found")
65-
project <- Q.projectById projectId `whenNothingM` throwError (MissingEntityError $ EntityMissing (ErrorID "project:missing") "Project not found")
66-
newBranch <- Q.branchById newBranchId `whenNothingM` throwError (MissingEntityError $ EntityMissing (ErrorID "branch:missing") "Source branch not found")
67-
oldBranch <- Q.branchById oldBranchId `whenNothingM` throwError (MissingEntityError $ EntityMissing (ErrorID "branch:missing") "Target branch not found")
68-
pure (bestCommonAncestorCausalId, project, newBranch, oldBranch)
75+
-- Check whether a causal diff has already been computed, and if it hasn't, compute and store it. Otherwise, do nothing.
76+
-- Returns whether or not we did any work.
77+
maybeComputeAndStoreCausalDiff ::
78+
AuthZ.AuthZReceipt ->
79+
(Codebase.CodebaseEnv -> IO (Codebase.CodebaseRuntime IO)) ->
80+
ContributionId ->
81+
PG.Transaction EntityMissing Bool
82+
maybeComputeAndStoreCausalDiff authZReceipt makeRuntime contributionId = do
83+
Contribution {bestCommonAncestorCausalId, sourceBranchId = newBranchId, targetBranchId = oldBranchId, projectId} <-
84+
ContributionsQ.contributionById contributionId `whenNothingM` throwError (EntityMissing (ErrorID "contribution:missing") "Contribution not found")
85+
project <- Q.projectById projectId `whenNothingM` throwError (EntityMissing (ErrorID "project:missing") "Project not found")
86+
newBranch <- Q.branchById newBranchId `whenNothingM` throwError (EntityMissing (ErrorID "branch:missing") "Source branch not found")
87+
oldBranch <- Q.branchById oldBranchId `whenNothingM` throwError (EntityMissing (ErrorID "branch:missing") "Target branch not found")
6988
let oldCodebase = Codebase.codebaseForProjectBranch authZReceipt project oldBranch
7089
let newCodebase = Codebase.codebaseForProjectBranch authZReceipt project newBranch
71-
-- This method saves the diff so it'll be there when we need it, so we don't need to do anything with it.
72-
_ <-
73-
Diffs.diffCausals authZReceipt (oldCodebase, oldBranchCausalId) (newCodebase, newBranchCausalId) bestCommonAncestorCausalId
74-
pure ()
90+
let oldCausal = oldBranch ^. branchCausals_
91+
let newCausal = newBranch ^. branchCausals_
92+
ContributionsQ.existsPrecomputedNamespaceDiff (oldCodebase, oldCausal) (newCodebase, newCausal) >>= \case
93+
True -> pure False
94+
False -> do
95+
oldRuntime <- PG.transactionUnsafeIO (makeRuntime oldCodebase)
96+
newRuntime <- PG.transactionUnsafeIO (makeRuntime newCodebase)
97+
_ <-
98+
Diffs.computeAndStoreCausalDiff
99+
authZReceipt
100+
(oldCodebase, oldRuntime, oldCausal)
101+
(newCodebase, newRuntime, newCausal)
102+
bestCommonAncestorCausalId
103+
pure True
Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,33 @@
11
module Share.BackgroundJobs.Diffs.Queries
2-
( submitContributionsToBeDiffed,
2+
( submitContributionToBeDiffed,
3+
submitContributionsToBeDiffed,
34
claimContributionToDiff,
45
)
56
where
67

7-
import Data.Foldable (toList)
8-
import Data.Set (Set)
98
import Share.IDs
109
import Share.Postgres
1110
import Share.Postgres.Notifications qualified as Notif
11+
import Unison.Prelude
12+
13+
submitContributionToBeDiffed :: (QueryM m) => ContributionId -> m ()
14+
submitContributionToBeDiffed contributionId = do
15+
execute_
16+
[sql|
17+
INSERT INTO contribution_diff_queue (contribution_id)
18+
VALUES (#{contributionId})
19+
|]
20+
Notif.notifyChannel Notif.ContributionDiffChannel
1221

1322
submitContributionsToBeDiffed :: (QueryM m) => Set ContributionId -> m ()
1423
submitContributionsToBeDiffed contributions = do
1524
execute_
1625
[sql|
17-
WITH new_contributions(contribution_id) AS (
18-
SELECT * FROM ^{singleColumnTable (toList contributions)}
19-
)
20-
INSERT INTO contribution_diff_queue (contribution_id)
21-
SELECT nc.contribution_id FROM new_contributions nc
22-
ON CONFLICT DO NOTHING
26+
WITH new_contributions(contribution_id) AS (
27+
SELECT * FROM ^{singleColumnTable (toList contributions)}
28+
)
29+
INSERT INTO contribution_diff_queue (contribution_id)
30+
SELECT nc.contribution_id FROM new_contributions nc
2331
|]
2432
Notif.notifyChannel Notif.ContributionDiffChannel
2533

@@ -28,16 +36,16 @@ claimContributionToDiff :: Transaction e (Maybe ContributionId)
2836
claimContributionToDiff = do
2937
query1Col
3038
[sql|
31-
WITH chosen_contribution(contribution_id) AS (
32-
SELECT q.contribution_id
33-
FROM contribution_diff_queue q
34-
ORDER BY q.created_at ASC
35-
LIMIT 1
36-
-- Skip any that are being synced by other workers.
37-
FOR UPDATE SKIP LOCKED
38-
)
39-
DELETE FROM contribution_diff_queue
40-
USING chosen_contribution
41-
WHERE contribution_diff_queue.contribution_id = chosen_contribution.contribution_id
42-
RETURNING chosen_contribution.contribution_id
39+
WITH chosen_contribution(contribution_id) AS (
40+
SELECT q.contribution_id
41+
FROM contribution_diff_queue q
42+
ORDER BY q.created_at ASC
43+
LIMIT 1
44+
-- Skip any that are being synced by other workers.
45+
FOR UPDATE SKIP LOCKED
46+
)
47+
DELETE FROM contribution_diff_queue
48+
USING chosen_contribution
49+
WHERE contribution_diff_queue.contribution_id = chosen_contribution.contribution_id
50+
RETURNING chosen_contribution.contribution_id
4351
|]

src/Share/Branch.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ instance (Hasql.DecodeValue causal) => Hasql.DecodeRow (Branch causal) where
5555
creatorId <- PG.decodeField
5656
pure $ Branch {..}
5757

58-
branchCausals_ :: Traversal (Branch causal) (Branch causal') causal causal'
58+
branchCausals_ :: Lens (Branch causal) (Branch causal') causal causal'
5959
branchCausals_ f Branch {..} = (\causal -> Branch {causal, ..}) <$> f causal
6060

6161
branchCodebaseUser :: Branch causal -> UserId

0 commit comments

Comments
 (0)