Skip to content

Commit 4323c7a

Browse files
committed
Final push.
1 parent 5e9b2ca commit 4323c7a

File tree

4 files changed

+67
-17
lines changed

4 files changed

+67
-17
lines changed

README.md

+32-3
Original file line numberDiff line numberDiff line change
@@ -339,9 +339,9 @@ Administrators should refresh these statistics manually as described above after
339339
### Deleting content (admin)
340340

341341
Administrators have the ability to delete files from the registry.
342-
This violates **gypsum**'s immutability contract and should be done sparingly.
343-
In particular, administrators must ensure that no other project links to the to-be-deleted files, otherwise those links will be invalidated.
344-
This check involves going through all the manifest files and is currently a manual process.
342+
This violates the Gobbler's immutability contract and should be done sparingly.
343+
In particular, administrators must ensure that no other project links to the to-be-deleted files, otherwise those links will be invalidated -
344+
see the ["Rerouting symlinks"](#rerouting-symlinks-admin) section for details.
345345

346346
To delete a project, create a file with the `request-delete_project-` prefix.
347347
This file should be JSON-formatted with the following properties:
@@ -379,6 +379,35 @@ On success, the version is deleted.
379379
The HTTP response will contain a JSON object with the `type` property set to `SUCCESS`.
380380
A success is still reported even if the version, its asset or its project is not present, in which case the operation is a no-op.
381381

382+
### Rerouting symlinks (admin)
383+
384+
In the (hopefully rare) scenario where one or more directories must be deleted from the registry,
385+
administrators must consider the possibility that other projects in the registry contain symbolic links to the files in the to-be-deleted directories.
386+
Deletion would result in dangling links that compromise the validity of those other projects.
387+
To avoid this, the Gobbler can reroute each affected link to a more appropriate location,
388+
either by updating the link target or replacing it with a copy of the to-be-deleted file.
389+
After successful rerouting, each project, asset or version can be safely deleted without damaging other projects.
390+
391+
To reroute links, create a file with the `request-reroute_links-` prefix.
392+
This file should be JSON-formatted with the following properties:
393+
394+
- `to_delete`: an array of JSON objects.
395+
Each object corresponds to a project, asset or version directory to be deleted.
396+
For a project directory, the object should contain a `project` string property that names the project;
397+
for an asset directory, the object should contain the `project` and `asset` string properties;
398+
and for a version directory, the object should contain the `project`, `asset` and `version` string properties.
399+
400+
On success, the Gobbler will update any links in the registry to any file in the directories corresponding to `delete`.
401+
All internal metadata files (`..manifest`, `..links`) are similarly updated to mirror the changes on the filesystem.
402+
The HTTP response will contain a JSON object with the `status` property set to `SUCCESS`.
403+
404+
Note that a rerouting request does not actually delete the directories corresponding to `to_delete`.
405+
After rerouting, administrators still need to delete each project, asset or version [as described above](#deleting-content-admin).
406+
If an administrator is sure that there are no links targeting a directory, deletion can be performed directly without the expense of rerouting.
407+
408+
We use a `to_delete` array to batch together multiple deletion tasks.
409+
This improves efficiency by amortizing the cost of a full registry scan to find links that target any of the affected directories.
410+
382411
## Parsing logs
383412

384413
For some actions, the Gobbler creates a log within the `..logs/` subdirectory of the registry.

main.go

+2
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ func main() {
144144
reportable_err = deleteAssetHandler(reqpath, &globals)
145145
} else if strings.HasPrefix(reqtype, "delete_version-") {
146146
reportable_err = deleteVersionHandler(reqpath, &globals)
147+
} else if strings.HasPrefix(reqtype, "reroute_links-") {
148+
reportable_err = rerouteLinksHandler(reqpath, &globals)
147149
} else if strings.HasPrefix(reqtype, "reindex_version-") {
148150
reportable_err = reindexHandler(reqpath, &globals)
149151
} else if strings.HasPrefix(reqtype, "health_check-") { // TO-BE-DEPRECATED, see /check below.

reroute.go

+7-3
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,9 @@ func rerouteLinksHandler(reqpath string, globals *globalConfiguration) error {
234234
}
235235

236236
// First we validate the request.
237-
all_incoming := []deleteTask{}
237+
all_incoming := struct {
238+
ToDelete []deleteTask `json:"to_delete"`
239+
}{}
238240
contents, err := os.ReadFile(reqpath)
239241
if err != nil {
240242
return fmt.Errorf("failed to read %q; %w", reqpath, err)
@@ -243,9 +245,11 @@ func rerouteLinksHandler(reqpath string, globals *globalConfiguration) error {
243245
err = json.Unmarshal(contents, &all_incoming)
244246
if err != nil {
245247
return newHttpError(http.StatusBadRequest, fmt.Errorf("failed to parse JSON from %q; %w", reqpath, err))
248+
} else if all_incoming.ToDelete == nil {
249+
return newHttpError(http.StatusBadRequest, fmt.Errorf("expected a 'to_delete' property in %q; %w", reqpath, err))
246250
}
247251

248-
for _, incoming := range all_incoming {
252+
for _, incoming := range all_incoming.ToDelete {
249253
err := isMissingOrBadName(&(incoming.Project))
250254
if err != nil {
251255
return newHttpError(http.StatusBadRequest, fmt.Errorf("invalid 'project' property in %q; %w", reqpath, err))
@@ -269,7 +273,7 @@ func rerouteLinksHandler(reqpath string, globals *globalConfiguration) error {
269273
}
270274

271275
// Then we need to reroute the links.
272-
to_delete_versions, err := listToBeDeletedVersions(globals.Registry, all_incoming)
276+
to_delete_versions, err := listToBeDeletedVersions(globals.Registry, all_incoming.ToDelete)
273277
if err != nil {
274278
return err
275279
}

reroute_test.go

+26-11
Original file line numberDiff line numberDiff line change
@@ -762,7 +762,11 @@ func TestRerouteLinksHandler(t *testing.T) {
762762
t.Fatal(err)
763763
}
764764

765-
reqpath, err := dumpRequest("reroute_links", fmt.Sprintf(`[ { "project": "%s", "asset": "%s", "version": "origination" } ]`, project, asset))
765+
reqpath, err := dumpRequest("reroute_links", fmt.Sprintf(`{
766+
"to_delete": [
767+
{ "project": "%s", "asset": "%s", "version": "origination" }
768+
]
769+
}`, project, asset))
766770
if err != nil {
767771
t.Fatalf("failed to dump a request type; %v", err)
768772
}
@@ -826,10 +830,12 @@ func TestRerouteLinksHandler(t *testing.T) {
826830
t.Fatal(err)
827831
}
828832

829-
reqpath, err := dumpRequest("reroute_links", fmt.Sprintf(`[
830-
{ "project": "%s", "asset": "%s", "version": "animation" },
831-
{ "project": "%s", "asset": "%s", "version": "natural" }
832-
]`, project, asset, project, asset))
833+
reqpath, err := dumpRequest("reroute_links", fmt.Sprintf(`{
834+
"to_delete": [
835+
{ "project": "%s", "asset": "%s", "version": "animation" },
836+
{ "project": "%s", "asset": "%s", "version": "natural" }
837+
]
838+
}`, project, asset, project, asset))
833839
if err != nil {
834840
t.Fatalf("failed to dump a request type; %v", err)
835841
}
@@ -899,7 +905,7 @@ func TestRerouteLinksHandler(t *testing.T) {
899905
t.Fatal(err)
900906
}
901907

902-
reqpath, err := dumpRequest("reroute_links", `[ { "project": "ARIA" } ]`)
908+
reqpath, err := dumpRequest("reroute_links", `{ "to_delete": [ { "project": "ARIA" } ] }`)
903909
if err != nil {
904910
t.Fatalf("failed to dump a request type; %v", err)
905911
}
@@ -924,25 +930,34 @@ func TestRerouteLinksHandler(t *testing.T) {
924930
t.Fatal(err)
925931
}
926932

927-
reqpath, err := dumpRequest("reroute_links", `[ { "project": "" } ]`)
933+
globals := newGlobalConfiguration(registry)
934+
globals.Administrators = append(globals.Administrators, self)
935+
936+
reqpath, err := dumpRequest("reroute_links", "{}")
928937
if err != nil {
929938
t.Fatalf("failed to dump a request type; %v", err)
930939
}
940+
err = rerouteLinksHandler(reqpath, &globals)
941+
if err == nil || !strings.Contains(err.Error(), "'to_delete'") {
942+
t.Error("expected failure when to_delete isn't present")
943+
}
931944

932-
globals := newGlobalConfiguration(registry)
933-
globals.Administrators = append(globals.Administrators, self)
945+
reqpath, err = dumpRequest("reroute_links", `{ "to_delete": [ { "project": "" } ] }`)
946+
if err != nil {
947+
t.Fatalf("failed to dump a request type; %v", err)
948+
}
934949
err = rerouteLinksHandler(reqpath, &globals)
935950
if err == nil || !strings.Contains(err.Error(), "invalid 'project'") {
936951
t.Error("expected failure from invalid project")
937952
}
938953

939-
reqpath, err = dumpRequest("reroute_links", `[ { "project": "ARIA", "asset": "" } ]`)
954+
reqpath, err = dumpRequest("reroute_links", `{ "to_delete": [ { "project": "ARIA", "asset": "" } ] }`)
940955
err = rerouteLinksHandler(reqpath, &globals)
941956
if err == nil || !strings.Contains(err.Error(), "invalid 'asset'") {
942957
t.Error("expected failure from invalid asset")
943958
}
944959

945-
reqpath, err = dumpRequest("reroute_links", `[ { "project": "ARIA", "asset": "anime", "version": "" } ]`)
960+
reqpath, err = dumpRequest("reroute_links", `{ "to_delete": [ { "project": "ARIA", "asset": "anime", "version": "" } ] }`)
946961
err = rerouteLinksHandler(reqpath, &globals)
947962
if err == nil || !strings.Contains(err.Error(), "invalid 'version'") {
948963
t.Error("expected failure from invalid version")

0 commit comments

Comments
 (0)