Skip to content

Commit 5c62c72

Browse files
authored
testing: Improve lsp shutdown logic & address test race conditions (#1943)
* Improve server shutdown logic address test race conditions I have been trying to make test suite flakes less common. I am not 100% sure I have got all the issues, but these changes make the flakes and races appear much less often on my local testing. The main things done: - Add proper shutdown with timeout and worker synchronization. Refactor server test helper to manage the shutdown of the server instances centrally. This helps address issues where workers were continuing to write to pipes after closing. - Address Aggregate Data races (ast.Object sharing), uses defensive copies when setting aggregate data. ast.Object.Insert() contains non-atomic map/slice operations. - Address channel blocking issues and timeouts, e.g. in TestTemplateWorkerRaceCondition - Config access races, locking when accessing workspaceRootURI. - Fixer state mutation race, create local copy of OPAFmtOpts before mutation. Signed-off-by: Charlie Egan <charlie_egan@apple.com> * Fix race condition on client field access Add clientLock mutex to protect LanguageServer.client from concurrent access. The jsonrpc2 asyncHandler's separate goroutines for initialize and initialized requests caused a race when workers read client info after initializationGate closes. Follows same pattern as loadedConfigLock. Signed-off-by: Charlie Egan <charlie_egan@apple.com> * Replace workspace lint limit with named constant Signed-off-by: Charlie Egan <charlie_egan@apple.com> * Rename JobRouter to LintJobRouter Signed-off-by: Charlie Egan <charlie_egan@apple.com> * Load aggregates from data.workspace in ls Enable aggregate rules to work both in normal linting mode (aggregates via input) and when aggregates are provided via shared store (data.workspace.aggregates). This unifies aggregate loading in CLI and LSP by moving aggregate storage from cache to the lsp regoStore. Signed-off-by: Charlie Egan <charlie_egan@apple.com> * Refactor LSP init and parallelize file loading Most importantly, we now do a full workspace lint to avoid linting files one by one, once this is done, the single file job worker starts. Replace OverwriteAggregates/AggregateReportOnly flags with clearer IsInitialization/FullWorkspaceLint to fix init ordering and reduce races. File lint workers now wait for the initial workspace lint before processing, and workspace loading runs across 10 concurrent goroutines. Signed-off-by: Charlie Egan <charlie_egan@apple.com> * Remove StartHoverWorker call from test setup Missed after rebase updates from #1951 Signed-off-by: Charlie Egan <charlie_egan@apple.com> * Remove old waitForDiagnostics helper Signed-off-by: Charlie Egan <charlie_egan@apple.com> * Address test flakes in lsp server tests Signed-off-by: Charlie Egan <charlie_egan@apple.com> * Address data race with locking in bundle cache Signed-off-by: Charlie Egan <charlie_egan@apple.com> * Reduce test polling time for server tests This makes some of the slower polling tests faster as we learn about state changes quicker. Signed-off-by: Charlie Egan <charlie_egan@apple.com> * Update ls to use a one-at-a-time linter runner This has been done to make the server more maintainable. We have found that the partial linting with aggregates to be complex and hard to maintain. At the same time, the linter is a lot faster now so the impact is minimal. Signed-off-by: Charlie Egan <charlie_egan@apple.com> * Inline lint job runner into diagnostics worker Remove the lintJobRunner abstraction and move its logic directly into StartDiagnosticsWorker, simplifying shutdown and reducing indirection. Signed-off-by: Charlie Egan <charlie_egan@apple.com> * Remove extra workspace lint job Signed-off-by: Charlie Egan <charlie_egan@apple.com> --------- Signed-off-by: Charlie Egan <charlie_egan@apple.com>
1 parent 44ceabe commit 5c62c72

53 files changed

Lines changed: 1541 additions & 1666 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bundle/regal/main/main.rego

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ _remove_empty_aggregates(aggregates) := {"aggregate": set()} if {
233233
# - input: schema.regal.aggregate
234234
_aggregate_report_inputs[cat_title].aggregate contains formatted if {
235235
some filename, cat_title
236-
aggregate := input.aggregates_internal[filename][cat_title][_]
236+
aggregate := aggregates_internal[filename][cat_title][_]
237237
formatted := _format_aggregate(aggregate, filename)
238238
}
239239

@@ -252,3 +252,12 @@ _ignored(violation, directives) if {
252252
ignored_rules := directives[violation.location.row + 1]
253253
violation.title in ignored_rules
254254
}
255+
256+
# METADATA
257+
# description: |
258+
# returns aggregates_internal from input if available, otherwise falls back to data.workspace.aggregates
259+
# this allows aggregate rules to work both in normal linting mode and when aggregates are provided via data
260+
# scope: rule
261+
aggregates_internal := input.aggregates_internal
262+
263+
aggregates_internal := data.workspace.aggregates if not input.aggregates_internal

bundle/regal/rules/bugs/impossible-not/impossible_not.rego

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package regal.rules.bugs["impossible-not"]
77

88
import data.regal.ast
9+
import data.regal.main
910
import data.regal.result
1011
import data.regal.util
1112

@@ -144,5 +145,5 @@ _resolve(ref, pkg_path, imported_symbols) := concat(".", resolved) if {
144145
_aggregates[file] := agg if {
145146
# only one aggregate exported per file, so this is safe
146147
some file
147-
agg := input.aggregates_internal[file]["bugs/impossible-not"][_]
148+
agg := main.aggregates_internal[file]["bugs/impossible-not"][_]
148149
}

bundle/regal/rules/custom/missing-metadata/missing_metadata.rego

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package regal.rules.custom["missing-metadata"]
77

88
import data.regal.ast
99
import data.regal.config
10+
import data.regal.main
1011
import data.regal.result
1112
import data.regal.util
1213

@@ -97,7 +98,7 @@ aggregate_report contains violation if {
9798
_package_path_aggs[pkg_name] contains [file, item] if {
9899
some file, items in _aggregates
99100
some item in items
100-
pkg_name := input.aggregates_internal[file].common.package_name
101+
pkg_name := main.aggregates_internal[file].common.package_name
101102
}
102103

103104
# METADATA
@@ -122,4 +123,4 @@ _excepted_rule_pattern(cfg, value) if regex.match(cfg["except-rule-path-pattern"
122123
# METADATA
123124
# schemas:
124125
# - input: schema.regal.aggregate
125-
_aggregates[file] := input.aggregates_internal[file]["custom/missing-metadata"] if some file
126+
_aggregates[file] := main.aggregates_internal[file]["custom/missing-metadata"] if some file

bundle/regal/rules/idiomatic/no-defined-entrypoint/no_defined_entrypoint.rego

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# ref: https://www.openpolicyagent.org/projects/regal/rules/idiomatic/no-defined-entrypoint
66
package regal.rules.idiomatic["no-defined-entrypoint"]
77

8+
import data.regal.main
89
import data.regal.result
910
import data.regal.util
1011

@@ -38,4 +39,4 @@ aggregate_report contains violation if {
3839
# METADATA
3940
# schemas:
4041
# - input: schema.regal.aggregate
41-
_any_entrypoint if input.aggregates_internal[_]["idiomatic/no-defined-entrypoint"][_].entrypoint
42+
_any_entrypoint if main.aggregates_internal[_]["idiomatic/no-defined-entrypoint"][_].entrypoint

bundle/regal/rules/idiomatic/no-defined-entrypoint/no_defined_entrypoint_test.rego

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,34 @@ test_success_multiple_entrypoints_defined if {
6464

6565
r == set()
6666
}
67+
68+
# Test the fallback pattern where aggregates come from data.workspace.aggregates
69+
# instead of input.aggregates_internal - expects success (no violations)
70+
test_success_entrypoint_from_data_workspace_aggregates if {
71+
# Using with data.workspace.aggregates instead of with input.aggregates_internal
72+
r := rule.aggregate_report with data.workspace.aggregates as {"p.rego": {"idiomatic/no-defined-entrypoint": [
73+
{"entrypoint": {"col": 1, "file": "policy.rego", "row": 2}},
74+
{"entrypoint": {"col": 1, "file": "policy.rego", "row": 6}},
75+
]}}
76+
77+
r == set()
78+
}
79+
80+
# Test the fallback actually works - expects failure (violation) when no entrypoint is defined
81+
test_fail_no_entrypoint_with_data_workspace if {
82+
# Explicitly ensure input.aggregates_internal is undefined by using an empty input
83+
# No entrypoint defined, so should fail
84+
r := rule.aggregate_report with input as {}
85+
with data.workspace.aggregates as {"p.rego": {"idiomatic/no-defined-entrypoint": []}}
86+
87+
r == {{
88+
"category": "idiomatic",
89+
"description": "Missing entrypoint annotation",
90+
"level": "error",
91+
"related_resources": [{
92+
"description": "documentation",
93+
"ref": "https://www.openpolicyagent.org/projects/regal/rules/idiomatic/no-defined-entrypoint",
94+
}],
95+
"title": "no-defined-entrypoint",
96+
}}
97+
}

bundle/regal/rules/imports/circular-import/circular_import.rego

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package regal.rules.imports["circular-import"]
99

1010
import data.regal.ast
11+
import data.regal.main
1112
import data.regal.result
1213
import data.regal.util
1314

@@ -117,5 +118,5 @@ _aggregated[file] := agg if {
117118
# we know that there is only one aggregate of this type per file,
118119
# so we can simplify things some for our callers
119120
some file
120-
agg := input.aggregates_internal[file]["imports/circular-import"][_]
121+
agg := main.aggregates_internal[file]["imports/circular-import"][_]
121122
}

bundle/regal/rules/imports/prefer-package-imports/prefer_package_imports.rego

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package regal.rules.imports["prefer-package-imports"]
77

88
import data.regal.ast
99
import data.regal.config
10+
import data.regal.main
1011
import data.regal.result
1112
import data.regal.util
1213

@@ -77,5 +78,5 @@ _aggregates[file] := agg if {
7778
# we know that there is only one aggregate of this type per file,
7879
# so we can simplify things some for our callers
7980
some file
80-
agg := input.aggregates_internal[file]["imports/prefer-package-imports"][_]
81+
agg := main.aggregates_internal[file]["imports/prefer-package-imports"][_]
8182
}

bundle/regal/rules/imports/unresolved-import/unresolved_import.rego

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package regal.rules.imports["unresolved-import"]
77

88
import data.regal.ast
99
import data.regal.config
10+
import data.regal.main
1011
import data.regal.result
1112
import data.regal.util
1213

@@ -46,10 +47,10 @@ aggregate contains entry if {
4647
# schemas:
4748
# - input: schema.regal.aggregate
4849
aggregate_report contains violation if {
49-
all_known_refs := {path | path := input.aggregates_internal[_]["imports/unresolved-import"][_].exported_refs[_]}
50+
all_known_refs := {path | path := main.aggregates_internal[_]["imports/unresolved-import"][_].exported_refs[_]}
5051

5152
some file
52-
entry := input.aggregates_internal[file]["imports/unresolved-import"][_]
53+
entry := main.aggregates_internal[file]["imports/unresolved-import"][_]
5354

5455
some [path, location] in entry.imports
5556
not path in _except_imports
@@ -108,5 +109,5 @@ _aggregates[file] := agg if {
108109
# we know that there is only one aggregate of this type per file,
109110
# so we can simplify things some for our callers
110111
some file
111-
agg := input.aggregates_internal[file]["imports/unresolved-import"][_]
112+
agg := main.aggregates_internal[file]["imports/unresolved-import"][_]
112113
}

bundle/regal/rules/imports/unresolved-reference/unresolved_reference.rego

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package regal.rules.imports["unresolved-reference"]
77

88
import data.regal.ast
99
import data.regal.config
10+
import data.regal.main
1011
import data.regal.result
1112

1213
# METADATA
@@ -153,4 +154,4 @@ _to_location_object(loc, text, file) := {"location": {
153154
# METADATA
154155
# schemas:
155156
# - input: schema.regal.aggregate
156-
_aggregates[file] := input.aggregates_internal[file]["imports/unresolved-reference"] if some file
157+
_aggregates[file] := main.aggregates_internal[file]["imports/unresolved-reference"] if some file

cmd/languageserver.go

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"os/signal"
99
"syscall"
10+
"time"
1011

1112
"github.com/spf13/cobra"
1213

@@ -27,7 +28,6 @@ func init() {
2728

2829
RunE: wrapProfiling(func([]string) error {
2930
ctx, cancel := context.WithCancel(context.Background())
30-
defer cancel()
3131

3232
if exe, err := os.Executable(); err != nil {
3333
fmt.Fprintln(os.Stderr, "error getting executable:", err)
@@ -62,14 +62,14 @@ func init() {
6262

6363
ls.SetConn(conn)
6464

65-
go ls.StartDiagnosticsWorker(ctx)
66-
go ls.StartTestLocationsWorker(ctx)
67-
go ls.StartCommandWorker(ctx)
68-
go ls.StartConfigWorker(ctx)
69-
go ls.StartWorkspaceStateWorker(ctx)
70-
go ls.StartTemplateWorker(ctx)
71-
go ls.StartQueryCacheWorker(ctx)
72-
go ls.StartWebServer(ctx)
65+
ls.StartDiagnosticsWorker(ctx)
66+
ls.StartTestLocationsWorker(ctx)
67+
ls.StartCommandWorker(ctx)
68+
ls.StartConfigWorker(ctx)
69+
ls.StartWorkspaceStateWorker(ctx)
70+
ls.StartTemplateWorker(ctx)
71+
ls.StartQueryCacheWorker(ctx)
72+
ls.StartWebServer(ctx)
7373

7474
sigChan := make(chan os.Signal, 1)
7575
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
@@ -81,6 +81,15 @@ func init() {
8181
fmt.Fprintln(os.Stderr, "signal: ", sig.String())
8282
}
8383

84+
cancel()
85+
86+
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
87+
defer shutdownCancel()
88+
89+
if err := ls.Shutdown(shutdownCtx); err != nil {
90+
fmt.Fprintln(os.Stderr, "shutdown error:", err)
91+
}
92+
8493
return nil
8594
}),
8695
}

0 commit comments

Comments
 (0)