Skip to content

Commit 1e41093

Browse files
authored
Merge pull request #203 from clowder-framework/release/1.16.0
Release/1.16.0
2 parents 7d03641 + 8893116 commit 1e41093

File tree

16 files changed

+617
-78
lines changed

16 files changed

+617
-78
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/)
55
and this project adheres to [Semantic Versioning](http://semver.org/).
66

7+
## 1.16.0 - 2021-03-31
8+
9+
### Fixed
10+
- Remove the RabbitMQ plugin from the docker version of clowder
11+
12+
### Added
13+
- Added a `sort` and `order` parameter to `/api/search` endpoint that supports date and numeric field sorting.
14+
If only order is specified, created date is used. String fields are not currently supported.
15+
- Added a new `/api/deleteindex` admin endpoint that will queue an action to delete an Elasticsearch index (usually prior to a reindex).
16+
- JMeter testing suite.
17+
18+
### Changed
19+
- Consolidated field names sent by the EventSinkService to maximize reuse.
20+
- Add status column to files report to indicate if files are ARCHIVED, etc.
21+
- Reworked auto-archival configuration options to make their meanings more clear.
22+
723
## 1.15.1 - 2021-03-12
824

925
### Fixed

app/Global.scala

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -64,21 +64,18 @@ object Global extends WithFilters(new GzipFilter(), new Jsonp(), CORSFilter()) w
6464

6565
val archiveEnabled = Play.application.configuration.getBoolean("archiveEnabled", false)
6666
if (archiveEnabled && archivalTimer == null) {
67-
val archiveDebug = Play.application.configuration.getBoolean("archiveDebug", false)
68-
val interval = if (archiveDebug) { 5 minutes } else { 1 day }
69-
70-
// Determine time until next midnight
71-
val now = ZonedDateTime.now
72-
val midnight = now.truncatedTo(ChronoUnit.DAYS)
73-
val sinceLastMidnight = Duration.between(midnight, now).getSeconds
74-
val delay = if (archiveDebug) { 10 seconds } else {
75-
(Duration.ofDays(1).getSeconds - sinceLastMidnight) seconds
76-
}
77-
78-
Logger.info("Starting archival loop - first iteration in " + delay + ", next iteration after " + interval)
79-
archivalTimer = Akka.system.scheduler.schedule(delay, interval) {
80-
Logger.info("Starting auto archive process...")
81-
files.autoArchiveCandidateFiles()
67+
// Set archiveAutoInterval == 0 to disable auto archiving
68+
val archiveAutoInterval = Play.application.configuration.getLong("archiveAutoInterval", 0)
69+
if (archiveAutoInterval > 0) {
70+
val interval = FiniteDuration(archiveAutoInterval, SECONDS)
71+
val archiveAutoDelay = Play.application.configuration.getLong("archiveAutoDelay", 0)
72+
val delay = FiniteDuration(archiveAutoDelay, SECONDS)
73+
74+
Logger.info("Starting archival loop - first iteration in " + delay + ", next iteration after " + interval)
75+
archivalTimer = Akka.system.scheduler.schedule(delay, interval) {
76+
Logger.info("Starting auto archive process...")
77+
files.autoArchiveCandidateFiles()
78+
}
8279
}
8380
}
8481

app/api/Admin.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,10 @@ class Admin @Inject() (userService: UserService,
180180
if (success) Ok(toJson(Map("status" -> "reindex successfully queued")))
181181
else BadRequest(toJson(Map("status" -> "reindex queuing failed, Elasticsearch may be disabled")))
182182
}
183+
184+
def deleteIndex = ServerAdminAction { implicit request =>
185+
val success = esqueue.queue("delete_index")
186+
if (success) Ok(toJson(Map("status" -> "deindex successfully queued")))
187+
else BadRequest(toJson(Map("status" -> "deindex queuing failed, Elasticsearch may be disabled")))
188+
}
183189
}

app/api/Reporting.scala

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class Reporting @Inject()(selections: SelectionService,
3939
var headerRow = true
4040
val enum = Enumerator.generateM({
4141
val chunk = if (headerRow) {
42-
val header = "type,id,name,owner,owner_id,size_kb,uploaded,views,downloads,last_viewed,last_downloaded,location,parent_datasets,parent_collections,parent_spaces\n"
42+
val header = "type,id,name,owner,owner_id,size_kb,uploaded,views,downloads,last_viewed,last_downloaded,location,parent_datasets,parent_collections,parent_spaces,status\n"
4343
headerRow = false
4444
Some(header.getBytes("UTF-8"))
4545
} else {
@@ -137,7 +137,7 @@ class Reporting @Inject()(selections: SelectionService,
137137

138138
// TODO: This will still fail on excessively large instances without Enumerator refactor - should we maintain this endpoint or remove?
139139

140-
var contents: String = "type,id,name,owner,owner_id,size_kb,uploaded/created,views,downloads,last_viewed,last_downloaded,location,parent_datasets,parent_collections,parent_spaces\n"
140+
var contents: String = "type,id,name,owner,owner_id,size_kb,uploaded/created,views,downloads,last_viewed,last_downloaded,location,parent_datasets,parent_collections,parent_spaces,status\n"
141141

142142
collections.getMetrics().foreach(coll => {
143143
contents += _buildCollectionRow(coll, true)
@@ -288,7 +288,8 @@ class Reporting @Inject()(selections: SelectionService,
288288
contents += "\""+f.loader_id+"\","
289289
contents += "\""+ds_list+"\","
290290
contents += "\""+coll_list+"\","
291-
contents += "\""+space_list+"\""
291+
contents += "\""+space_list+"\","
292+
contents += "\""+f.status+"\""
292293
contents += "\n"
293294

294295
return contents
@@ -343,6 +344,7 @@ class Reporting @Inject()(selections: SelectionService,
343344
if (returnAllColums) contents += "," // datasets do not have parent_datasets
344345
contents += "\""+coll_list+"\","
345346
contents += "\""+space_list+"\""
347+
if (returnAllColums) contents += "," // datasets do not have status
346348
contents += "\n"
347349

348350
return contents
@@ -391,6 +393,7 @@ class Reporting @Inject()(selections: SelectionService,
391393
if (returnAllColums) contents += "," // collections do not have parent_datasets
392394
contents += "\""+coll_list+"\","
393395
contents += "\""+space_list+"\""
396+
if (returnAllColums) contents += "," // collections do not have status
394397
contents += "\n"
395398

396399
return contents

app/api/Search.scala

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class Search @Inject() (
2222
/** Search using a simple text string with filters */
2323
def search(query: String, resource_type: Option[String], datasetid: Option[String], collectionid: Option[String],
2424
spaceid: Option[String], folderid: Option[String], field: Option[String], tag: Option[String],
25-
from: Option[Int], size: Option[Int], page: Option[Int]) = PermissionAction(Permission.ViewDataset) { implicit request =>
25+
from: Option[Int], size: Option[Int], page: Option[Int], sort: Option[String], order: Option[String]) = PermissionAction(Permission.ViewDataset) { implicit request =>
2626
current.plugin[ElasticsearchPlugin] match {
2727
case Some(plugin) => {
2828
// If from is specified, use it. Otherwise use page * size of page if possible, otherwise use 0.
@@ -42,7 +42,9 @@ class Search @Inject() (
4242
(spaceid match {case Some(x) => s"&spaceid=$x" case None => ""}) +
4343
(folderid match {case Some(x) => s"&folderid=$x" case None => ""}) +
4444
(field match {case Some(x) => s"&field=$x" case None => ""}) +
45-
(tag match {case Some(x) => s"&tag=$x" case None => ""})
45+
(tag match {case Some(x) => s"&tag=$x" case None => ""}) +
46+
(sort match {case Some(x) => s"&sort=$x" case None => ""}) +
47+
(order match {case Some(x) => s"&order=$x" case None => ""})
4648

4749
// Add space filter to search here as a simple permissions check
4850
val superAdmin = request.user match {
@@ -54,7 +56,7 @@ class Search @Inject() (
5456
else
5557
spaces.listAccess(0, Set[Permission](Permission.ViewSpace), request.user, true, true, false, false).map(sp => sp.id)
5658

57-
val response = plugin.search(query, resource_type, datasetid, collectionid, spaceid, folderid, field, tag, from_index, size, permitted, request.user)
59+
val response = plugin.search(query, resource_type, datasetid, collectionid, spaceid, folderid, field, tag, from_index, size, sort, order, permitted, request.user)
5860
val result = SearchUtils.prepareSearchResponse(response, source_url, request.user)
5961
Ok(toJson(result))
6062
}

app/services/ElasticsearchPlugin.scala

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import play.api.libs.json._
2929
import _root_.util.SearchUtils
3030
import org.apache.commons.lang.StringUtils
3131
import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsRequest
32+
import org.elasticsearch.search.sort.SortOrder
3233

3334

3435
/**
@@ -130,7 +131,8 @@ class ElasticsearchPlugin(application: Application) extends Plugin {
130131
* "field_leaf_key": name of immediate field only, e.g. 'lines'
131132
*/
132133
val queryObj = prepareElasticJsonQuery(query, grouping, permitted, user)
133-
accumulatePageResult(queryObj, user, from.getOrElse(0), size.getOrElse(maxResults))
134+
// TODO: Support sorting in GUI search
135+
accumulatePageResult(queryObj, user, from.getOrElse(0), size.getOrElse(maxResults), None, None)
134136
}
135137

136138
/**
@@ -152,8 +154,8 @@ class ElasticsearchPlugin(application: Application) extends Plugin {
152154
*/
153155
def search(query: String, resource_type: Option[String], datasetid: Option[String], collectionid: Option[String],
154156
spaceid: Option[String], folderid: Option[String], field: Option[String], tag: Option[String],
155-
from: Option[Int], size: Option[Int], permitted: List[UUID], user: Option[User],
156-
index: String = nameOfIndex): ElasticsearchResult = {
157+
from: Option[Int], size: Option[Int], sort: Option[String], order: Option[String], permitted: List[UUID],
158+
user: Option[User], index: String = nameOfIndex): ElasticsearchResult = {
157159

158160
// Convert any parameters from API into the query syntax equivalent so we can parse it all together later
159161
var expanded_query = query
@@ -166,16 +168,16 @@ class ElasticsearchPlugin(application: Application) extends Plugin {
166168
folderid.foreach(fid => expanded_query += s" in:$fid")
167169

168170
val queryObj = prepareElasticJsonQuery(expanded_query.stripPrefix(" "), permitted, user)
169-
accumulatePageResult(queryObj, user, from.getOrElse(0), size.getOrElse(maxResults))
171+
accumulatePageResult(queryObj, user, from.getOrElse(0), size.getOrElse(maxResults), sort, order)
170172
}
171173

172174
/** Perform search, check permissions, and keep searching again if page isn't filled with permitted resources */
173175
def accumulatePageResult(queryObj: XContentBuilder, user: Option[User], from: Int, size: Int,
174-
index: String = nameOfIndex): ElasticsearchResult = {
176+
sort: Option[String], order: Option[String], index: String = nameOfIndex): ElasticsearchResult = {
175177
var total_results = ListBuffer.empty[ResourceRef]
176178

177179
// Fetch initial page & filter by permissions
178-
val (results, total_size) = _search(queryObj, index, Some(from), Some(size))
180+
val (results, total_size) = _search(queryObj, index, Some(from), Some(size), sort, order)
179181
Logger.debug(s"Found ${results.length} results with ${total_size} total")
180182
val filtered = checkResultPermissions(results, user)
181183
Logger.debug(s"Permission to see ${filtered.length} results")
@@ -187,7 +189,7 @@ class ElasticsearchPlugin(application: Application) extends Plugin {
187189
var exhausted = false
188190
while (total_results.length < size && !exhausted) {
189191
Logger.debug(s"Only have ${total_results.length} total results; searching for ${size*2} more from ${new_from}")
190-
val (results, total_size) = _search(queryObj, index, Some(new_from), Some(size*2))
192+
val (results, total_size) = _search(queryObj, index, Some(new_from), Some(size*2), sort, order)
191193
Logger.debug(s"Found ${results.length} results with ${total_size} total")
192194
if (results.length == 0 || new_from+results.length == total_size) exhausted = true // No more results to find
193195
val filtered = checkResultPermissions(results, user)
@@ -251,17 +253,39 @@ class ElasticsearchPlugin(application: Application) extends Plugin {
251253

252254
/*** Execute query and return list of results and total result count as tuple */
253255
def _search(queryObj: XContentBuilder, index: String = nameOfIndex,
254-
from: Option[Int] = Some(0), size: Option[Int] = Some(maxResults)): (List[ResourceRef], Long) = {
256+
from: Option[Int] = Some(0), size: Option[Int] = Some(maxResults),
257+
sort: Option[String], order: Option[String]): (List[ResourceRef], Long) = {
255258
connect()
256259
val response = client match {
257260
case Some(x) => {
258-
Logger.info("Searching Elasticsearch: "+queryObj.string())
261+
Logger.debug("Searching Elasticsearch: " + queryObj.string())
262+
263+
// Exclude _sort fields in response object
264+
var sortFilter = jsonBuilder().startObject().startArray("exclude").value("*._sort").endArray().endObject()
265+
259266
var responsePrep = x.prepareSearch(index)
260267
.setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
268+
.setSource(sortFilter)
261269
.setQuery(queryObj)
262270

263271
responsePrep = responsePrep.setFrom(from.getOrElse(0))
264272
responsePrep = responsePrep.setSize(size.getOrElse(maxResults))
273+
// Default to ascending if no order provided but a field is
274+
val searchOrder = order match {
275+
case Some("asc") => SortOrder.ASC
276+
case Some("desc") => SortOrder.DESC
277+
case Some("DESC") => SortOrder.DESC
278+
case _ => SortOrder.ASC
279+
}
280+
// Default to created field if order is provided but no field is
281+
sort match {
282+
// case Some("name") => responsePrep = responsePrep.addSort("name._sort", searchOrder) TODO: Not yet supported
283+
case Some(x) => responsePrep = responsePrep.addSort(x, searchOrder)
284+
case None => order match {
285+
case Some(o) => responsePrep = responsePrep.addSort("created", searchOrder)
286+
case None => {}
287+
}
288+
}
265289

266290
val response = responsePrep.setExplain(true).execute().actionGet()
267291
Logger.debug("Search hits: " + response.getHits().getTotalHits())
@@ -291,8 +315,7 @@ class ElasticsearchPlugin(application: Application) extends Plugin {
291315
.field("type", "custom")
292316
.field("tokenizer", "uax_url_email")
293317
.endObject()
294-
.endObject()
295-
.endObject()
318+
.endObject().endObject()
296319
.startObject("index")
297320
.startObject("mapping")
298321
.field("ignore_malformed", true)
@@ -697,10 +720,14 @@ class ElasticsearchPlugin(application: Application) extends Plugin {
697720
* as strings for datatypes besides Objects. In the future, this could
698721
* be removed, but only once the Search API better supports those data types (e.g. Date).
699722
*/
723+
724+
// TODO: With Elastic 6.8+ we can use "normalizer": "case_insensitive" for _sort fields
725+
700726
"""{"clowder_object": {
701727
|"numeric_detection": true,
702728
|"properties": {
703-
|"name": {"type": "string"},
729+
|"name": {"type": "string", "fields": {
730+
| "_sort": {"type":"string", "index": "not_analyzed"}}},
704731
|"description": {"type": "string"},
705732
|"resource_type": {"type": "string", "include_in_all": false},
706733
|"child_of": {"type": "string", "include_in_all": false},
@@ -925,7 +952,7 @@ class ElasticsearchPlugin(application: Application) extends Plugin {
925952
}
926953
}
927954

928-
// If a term is specified that isn't in this list, it's assumed to be a metadata field
955+
// If a term is specified that isn't in this list, it's assumed to be a metadata field (for sorting and filtering)
929956
val official_terms = List("name", "creator", "created", "email", "resource_type", "in", "contains", "tag", "exists", "missing")
930957

931958
// Create list of (key, operator, value) for passing to builder

app/services/mongodb/ElasticsearchQueue.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class ElasticsearchQueue @Inject() (
5353
}
5454
}
5555
case "index_all" => _indexAll()
56+
case "delete_index" => _deleteIndex()
5657
case "index_swap" => _swapIndex()
5758
case _ => throw new IllegalArgumentException(s"Unrecognized action: ${action.action}")
5859
}
@@ -63,6 +64,7 @@ class ElasticsearchQueue @Inject() (
6364
case "index_dataset" => throw new IllegalArgumentException(s"No target specified for action ${action.action}")
6465
case "index_collection" => throw new IllegalArgumentException(s"No target specified for action ${action.action}")
6566
case "index_all" => _indexAll()
67+
case "delete_index" => _deleteIndex()
6668
case "index_swap" => _swapIndex()
6769
case _ => throw new IllegalArgumentException(s"Unrecognized action: ${action.action}")
6870
}
@@ -97,6 +99,12 @@ class ElasticsearchQueue @Inject() (
9799
})
98100
}
99101

102+
def _deleteIndex() = {
103+
current.plugin[ElasticsearchPlugin].foreach(p => {
104+
p.deleteAll()
105+
})
106+
}
107+
100108
// Replace the main index with the newly reindexed temp file
101109
def _swapIndex() = {
102110
Logger.debug("Swapping temporary reindex for main index")

0 commit comments

Comments
 (0)